001/* 002 * JGrapes Event Driven Framework 003 * Copyright (C) 2016-2018 Michael N. Lipp 004 * 005 * This program is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU Affero General Public License as published by 007 * the Free Software Foundation; either version 3 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, but 011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 013 * for more details. 014 * 015 * You should have received a copy of the GNU Affero General Public License along 016 * with this program; if not, see <http://www.gnu.org/licenses/>. 017 */ 018 019package org.jgrapes.http.events; 020 021import java.net.URI; 022import java.net.URISyntaxException; 023import java.net.URL; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Optional; 027import java.util.Stack; 028import java.util.function.BiConsumer; 029import java.util.stream.Collectors; 030import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpProtocol; 031import org.jdrupes.httpcodec.protocols.http.HttpRequest; 032import org.jgrapes.core.Channel; 033import org.jgrapes.core.CompletionEvent; 034import org.jgrapes.core.Components; 035import org.jgrapes.http.ResourcePattern; 036import org.jgrapes.http.ResourcePattern.PathSpliterator; 037import org.jgrapes.net.SocketIOChannel; 038 039/** 040 * The base class for all HTTP requests such as {@link Request.In.Get}, 041 * {@link Request.In.Post} etc. 042 * 043 * @param <R> the generic type 044 */ 045public class Request<R> extends MessageReceived<R> { 046 047 /** 048 * @param channels 049 */ 050 protected Request(Channel... channels) { 051 super(channels); 052 } 053 054 /** 055 * The base class for all incoming HTTP requests. Incoming 056 * request flow downstream and are served by the framework. 057 * 058 * A result of `true` indicates that the request has been processed, 059 * i.e. a response has been sent or will sent. 060 */ 061 @SuppressWarnings("PMD.ShortClassName") 062 public static class In extends Request<Boolean> { 063 064 @SuppressWarnings("PMD.AvoidFieldNameMatchingTypeName") 065 private final HttpRequest request; 066 private final int matchLevels; 067 private final MatchValue matchValue; 068 private final URI resourceUri; 069 private URI uri; 070 071 /** 072 * Creates a new request event with the associated {@link Completed} 073 * event. 074 * 075 * @param protocol the protocol as reported by {@link #requestUri()} 076 * @param request the request data 077 * @param matchLevels the number of elements from the request path 078 * to use in the match value (see {@link #matchValue}) 079 * @param channels the channels associated with this event 080 * @throws URISyntaxException 081 */ 082 @SuppressWarnings({ "PMD.UselessParentheses", 083 "PMD.ConstructorCallsOverridableMethod" }) 084 public In(String protocol, HttpRequest request, 085 int matchLevels, Channel... channels) 086 throws URISyntaxException { 087 super(channels); 088 new Completed(this); 089 this.request = request; 090 this.matchLevels = matchLevels; 091 092 // Do any required request specific processing of the original 093 // request URI 094 URI requestUri = effectiveRequestUri(protocol, request); 095 096 // Clean the request URI's path, keeping the segments for matchValue 097 List<String> segs = pathToSegs(requestUri); 098 requestUri = new URI(requestUri.getScheme(), 099 requestUri.getUserInfo(), requestUri.getHost(), 100 requestUri.getPort(), 101 "/" + segs.stream().collect(Collectors.joining("/")), 102 requestUri.getQuery(), null); 103 setRequestUri(requestUri); 104 105 // The URI for handler selection ignores user info and query 106 resourceUri = new URI(requestUri.getScheme(), null, 107 requestUri.getHost(), requestUri.getPort(), 108 requestUri.getPath(), null, null); 109 @SuppressWarnings("PMD.InefficientStringBuffering") 110 StringBuilder matchPath = new StringBuilder("/" + segs.stream() 111 .limit(matchLevels).collect(Collectors.joining("/"))); 112 if (segs.size() > matchLevels) { 113 if (!matchPath.toString().endsWith("/")) { 114 matchPath.append('/'); 115 } 116 matchPath.append('…'); 117 } 118 matchValue = new MatchValue(getClass(), 119 (requestUri.getScheme() == null ? "" 120 : (requestUri.getScheme() + "://")) 121 + (requestUri.getHost() == null ? "" 122 : (requestUri.getHost() 123 + (requestUri.getPort() == -1 ? "" 124 : (":" + requestUri.getPort())))) 125 + matchPath); 126 } 127 128 @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 129 private List<String> pathToSegs(URI requestUri) 130 throws URISyntaxException { 131 Iterator<String> origSegs = PathSpliterator.stream( 132 requestUri.getPath()).iterator(); 133 // Path must not be empty and must be absolute 134 if (!origSegs.hasNext() || !origSegs.next().isEmpty()) { 135 throw new URISyntaxException( 136 requestUri().getPath(), "Must be absolute"); 137 } 138 // Remove dot segments and check for "...//..." 139 @SuppressWarnings({ "PMD.LooseCoupling", 140 "PMD.ReplaceVectorWithList" }) 141 Stack<String> segs = new Stack<>(); 142 while (origSegs.hasNext()) { 143 if (!segs.isEmpty() && segs.peek().isEmpty()) { 144 // Empty segment followed by more means "//" 145 segs.clear(); 146 } 147 String seg = origSegs.next(); 148 if (".".equals(seg)) { 149 continue; 150 } 151 if ("..".equals(seg)) { 152 if (!segs.isEmpty()) { 153 segs.pop(); 154 } 155 continue; 156 } 157 segs.push(seg); 158 } 159 return segs; 160 } 161 162 /** 163 * Builds the URI that represents this request. The default 164 * implementation checks that request URI in the HTTP request 165 * is directed at this server as specified in the "Host"-header 166 * and adds the protocol, host and port if not specified 167 * in the request URI. 168 * 169 * @param protocol the protocol 170 * @param request the request 171 * @return the URI 172 * @throws URISyntaxException if the request is not acceptable 173 */ 174 protected URI effectiveRequestUri(String protocol, HttpRequest request) 175 throws URISyntaxException { 176 URI serverUri = new URI(protocol, null, 177 request.host(), request.port(), "/", null, null); 178 URI origRequest = request.requestUri(); 179 URI result = serverUri.resolve(new URI(null, null, null, -1, 180 origRequest.getPath(), origRequest.getQuery(), null)); 181 if (!result.getScheme().equals(protocol) 182 || !result.getHost().equals(request.host()) 183 || result.getPort() != request.port()) { 184 throw new URISyntaxException(origRequest.toString(), 185 "Scheme, host or port not allowed"); 186 } 187 return result; 188 } 189 190 /** 191 * Creates the appropriate derived request event type from 192 * a given {@link HttpRequest}. 193 * 194 * @param request the HTTP request 195 * @param secure whether the request was received over a secure channel 196 * @param matchLevels the match levels 197 * @return the request event 198 * @throws URISyntaxException 199 */ 200 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 201 public static In fromHttpRequest( 202 HttpRequest request, boolean secure, int matchLevels) 203 throws URISyntaxException { 204 switch (request.method()) { 205 case "OPTIONS": 206 return new Options(request, secure, matchLevels); 207 case "GET": 208 return new Get(request, secure, matchLevels); 209 case "HEAD": 210 return new Head(request, secure, matchLevels); 211 case "POST": 212 return new Post(request, secure, matchLevels); 213 case "PUT": 214 return new Put(request, secure, matchLevels); 215 case "DELETE": 216 return new Delete(request, secure, matchLevels); 217 case "TRACE": 218 return new Trace(request, secure, matchLevels); 219 case "CONNECT": 220 return new Connect(request, secure, matchLevels); 221 default: 222 return new In(secure ? "https" : "http", request, matchLevels); 223 } 224 } 225 226 /** 227 * Sets the request URI. 228 * 229 * @param uri the new request URI 230 */ 231 protected final void setRequestUri(URI uri) { 232 this.uri = uri; 233 } 234 235 /** 236 * Returns an absolute URI of the request. For incoming requests, the 237 * URI is built from the information provided by the decoder. 238 * 239 * @return the URI 240 */ 241 public final URI requestUri() { 242 return uri; 243 } 244 245 /** 246 * Returns the "raw" request as provided by the HTTP decoder. 247 * 248 * @return the request 249 */ 250 public HttpRequest httpRequest() { 251 return request; 252 } 253 254 /** 255 * The match value consists of the event class and a URI. 256 * The URI is similar to the request URI but its path elements 257 * are shortened as specified in the constructor. 258 * 259 * The match value is used as key in a map that speeds up 260 * the lookup of handlers. Having the complete URI in the match 261 * value would inflate this map. 262 * 263 * @return the object 264 * @see org.jgrapes.core.Event#defaultCriterion() 265 */ 266 @Override 267 public Object defaultCriterion() { 268 return matchValue; 269 } 270 271 /* 272 * (non-Javadoc) 273 * 274 * @see org.jgrapes.core.Event#isMatchedBy(java.lang.Object) 275 */ 276 @Override 277 public boolean isEligibleFor(Object value) { 278 if (!(value instanceof MatchValue)) { 279 return super.isEligibleFor(value); 280 } 281 MatchValue mval = (MatchValue) value; 282 if (!mval.type.isAssignableFrom(matchValue.type)) { 283 return false; 284 } 285 if (mval.resource instanceof ResourcePattern) { 286 return ((ResourcePattern) mval.resource).matches(resourceUri, 287 matchLevels) >= 0; 288 } 289 return mval.resource.equals(matchValue.resource); 290 } 291 292 /* 293 * (non-Javadoc) 294 * 295 * @see java.lang.Object#toString() 296 */ 297 @Override 298 @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") 299 public String toString() { 300 StringBuilder builder = new StringBuilder(50); 301 builder.append(Components.objectName(this)) 302 .append(" [\""); 303 String path = Optional.ofNullable(request.requestUri().getPath()) 304 .orElse(""); 305 if (path.length() > 15) { 306 builder.append("...") 307 .append(path.substring(path.length() - 12)); 308 } else { 309 builder.append(path); 310 } 311 builder.append('\"'); 312 if (channels().length > 0) { 313 builder.append(", channels="); 314 builder.append(Channel.toString(channels())); 315 } 316 builder.append(']'); 317 return builder.toString(); 318 } 319 320 /** 321 * Creates the match value. 322 * 323 * @param type the type 324 * @param resource the resource 325 * @return the object 326 */ 327 public static Object createMatchValue( 328 Class<?> type, ResourcePattern resource) { 329 return new MatchValue(type, resource); 330 } 331 332 /** 333 * Represents a match value. 334 */ 335 private static class MatchValue { 336 private final Class<?> type; 337 private final Object resource; 338 339 /** 340 * @param type 341 * @param resource 342 */ 343 public MatchValue(Class<?> type, Object resource) { 344 super(); 345 this.type = type; 346 this.resource = resource; 347 } 348 349 /* 350 * (non-Javadoc) 351 * 352 * @see java.lang.Object#hashCode() 353 */ 354 @Override 355 @SuppressWarnings("PMD.DataflowAnomalyAnalysis") 356 public int hashCode() { 357 @SuppressWarnings("PMD.AvoidFinalLocalVariable") 358 final int prime = 31; 359 int result = 1; 360 result = prime * result 361 + ((resource == null) ? 0 : resource.hashCode()); 362 result 363 = prime * result + ((type == null) ? 0 : type.hashCode()); 364 return result; 365 } 366 367 /* 368 * (non-Javadoc) 369 * 370 * @see java.lang.Object#equals(java.lang.Object) 371 */ 372 @Override 373 public boolean equals(Object obj) { 374 if (this == obj) { 375 return true; 376 } 377 if (obj == null) { 378 return false; 379 } 380 if (getClass() != obj.getClass()) { 381 return false; 382 } 383 MatchValue other = (MatchValue) obj; 384 if (resource == null) { 385 if (other.resource != null) { 386 return false; 387 } 388 } else if (!resource.equals(other.resource)) { 389 return false; 390 } 391 if (type == null) { 392 if (other.type != null) { 393 return false; 394 } 395 } else if (!type.equals(other.type)) { 396 return false; 397 } 398 return true; 399 } 400 401 } 402 403 /** 404 * The associated completion event. 405 */ 406 public static class Completed extends CompletionEvent<In> { 407 408 /** 409 * Instantiates a new event. 410 * 411 * @param monitoredEvent the monitored event 412 * @param channels the channels 413 */ 414 public Completed(In monitoredEvent, Channel... channels) { 415 super(monitoredEvent, channels); 416 } 417 } 418 419 /** 420 * Represents a HTTP CONNECT request. 421 */ 422 public static class Connect extends In { 423 424 /** 425 * Create a new event. 426 * 427 * @param request the request data 428 * @param secure indicates whether the request was received on a 429 * secure channel 430 * @param matchLevels the number of elements from the request path 431 * to use in the match value 432 * @param channels the channels on which the event is to be 433 * fired (optional) 434 * @throws URISyntaxException 435 */ 436 public Connect(HttpRequest request, boolean secure, int matchLevels, 437 Channel... channels) throws URISyntaxException { 438 super(secure ? "https" : "http", request, matchLevels, 439 channels); 440 } 441 442 /** 443 * Builds the URI that represents this request. This 444 * implementation returns the request URI without 445 * path and query component. 446 * 447 * @param protocol the protocol 448 * @param request the request 449 * @return the uri 450 * @throws URISyntaxException the URI syntax exception 451 * @see "[RFC 7230, Section 5.5](https://datatracker.ietf.org/doc/html/rfc7230#section-5.5)" 452 */ 453 @Override 454 protected URI effectiveRequestUri(String protocol, 455 HttpRequest request) throws URISyntaxException { 456 URI req = request.requestUri(); 457 return new URI(req.getScheme(), req.getUserInfo(), 458 req.getHost(), req.getPort(), null, null, null); 459 } 460 } 461 462 /** 463 * The Class Delete. 464 */ 465 public static class Delete extends In { 466 467 /** 468 * Create a new event. 469 * 470 * @param request the request data 471 * @param secure indicates whether the request was received on a 472 * secure channel 473 * @param matchLevels the number of elements from the request path 474 * to use in the match value 475 * @param channels the channels on which the event is to be 476 * fired (optional) 477 * @throws URISyntaxException 478 */ 479 public Delete(HttpRequest request, boolean secure, int matchLevels, 480 Channel... channels) throws URISyntaxException { 481 super(secure ? "https" : "http", request, matchLevels, 482 channels); 483 } 484 485 } 486 487 /** 488 * Represents a HTTP GET request. 489 */ 490 public static class Get extends In { 491 492 /** 493 * Create a new event. 494 * 495 * @param request the request data 496 * @param secure indicates whether the request was received on a 497 * secure channel 498 * @param matchLevels the number of elements from the request path 499 * to use in the match value 500 * @param channels the channels on which the event is to be 501 * fired (optional) 502 * @throws URISyntaxException 503 */ 504 public Get(HttpRequest request, boolean secure, int matchLevels, 505 Channel... channels) throws URISyntaxException { 506 super(secure ? "https" : "http", request, matchLevels, 507 channels); 508 } 509 } 510 511 /** 512 * Represents a HTTP HEAD request. 513 */ 514 public static class Head extends In { 515 516 /** 517 * Create a new event. 518 * 519 * @param request the request data 520 * @param secure indicates whether the request was received on a 521 * secure channel 522 * @param matchLevels the number of elements from the request path 523 * to use in the match value 524 * @param channels the channels on which the event is to be 525 * fired (optional) 526 * @throws URISyntaxException 527 */ 528 public Head(HttpRequest request, boolean secure, int matchLevels, 529 Channel... channels) throws URISyntaxException { 530 super(secure ? "https" : "http", request, matchLevels, 531 channels); 532 } 533 } 534 535 /** 536 * Represents a HTTP OPTIONS request. 537 */ 538 public static class Options extends In { 539 540 /** 541 * Create a new event. 542 * 543 * @param request the request data 544 * @param secure indicates whether the request was received on a 545 * secure channel 546 * @param matchLevels the number of elements from the request path 547 * to use in the match value 548 * @param channels the channels on which the event is to be 549 * fired (optional) 550 * @throws URISyntaxException 551 */ 552 public Options(HttpRequest request, boolean secure, int matchLevels, 553 Channel... channels) throws URISyntaxException { 554 super(secure ? "https" : "http", request, matchLevels, 555 channels); 556 } 557 } 558 559 /** 560 * Represents a HTTP POST request. 561 */ 562 public static class Post extends In { 563 564 /** 565 * Create a new event. 566 * 567 * @param request the request data 568 * @param secure indicates whether the request was received on a 569 * secure channel 570 * @param matchLevels the number of elements from the request path 571 * to use in the match value 572 * @param channels the channels on which the event is to be 573 * fired (optional) 574 * @throws URISyntaxException 575 */ 576 public Post(HttpRequest request, boolean secure, int matchLevels, 577 Channel... channels) throws URISyntaxException { 578 super(secure ? "https" : "http", request, matchLevels, 579 channels); 580 } 581 582 } 583 584 /** 585 * Represents a HTTP PUT request. 586 */ 587 public static class Put extends In { 588 589 /** 590 * Create a new event. 591 * 592 * @param request the request data 593 * @param secure indicates whether the request was received on a 594 * secure channel 595 * @param matchLevels the number of elements from the request path 596 * to use in the match value 597 * @param channels the channels on which the event is to be 598 * fired (optional) 599 * @throws URISyntaxException 600 */ 601 public Put(HttpRequest request, boolean secure, int matchLevels, 602 Channel... channels) throws URISyntaxException { 603 super(secure ? "https" : "http", request, matchLevels, 604 channels); 605 } 606 } 607 608 /** 609 * Represents a HTTP TRACE request. 610 */ 611 public static class Trace extends In { 612 613 /** 614 * Create a new event. 615 * 616 * @param request the request data 617 * @param secure indicates whether the request was received on a 618 * secure channel 619 * @param matchLevels the number of elements from the request path 620 * to use in the match value 621 * @param channels the channels on which the event is to be 622 * fired (optional) 623 * @throws URISyntaxException 624 */ 625 public Trace(HttpRequest request, boolean secure, int matchLevels, 626 Channel... channels) throws URISyntaxException { 627 super(secure ? "https" : "http", request, matchLevels, 628 channels); 629 } 630 } 631 } 632 633 /** 634 * The base class for all outgoing HTTP requests. Outgoing 635 * request flow upstream and are served externally. 636 * 637 * A result of `true` indicates that the request has been processed, 638 * i.e. a response has been sent or will sent. 639 */ 640 @SuppressWarnings("PMD.ShortClassName") 641 public static class Out extends Request<Void> { 642 643 private final HttpRequest request; 644 private BiConsumer<Request.Out, SocketIOChannel> connectedCallback; 645 646 /** 647 * Instantiates a new request. 648 * 649 * @param method the method 650 * @param url the url 651 */ 652 public Out(String method, URL url) { 653 try { 654 request = new HttpRequest(method, url.toURI(), 655 HttpProtocol.HTTP_1_1, false); 656 } catch (URISyntaxException e) { 657 // This should not happen because every valid URL can be 658 // converted to a URI. 659 throw new IllegalArgumentException(e); 660 } 661 } 662 663 /** 664 * Sets a "connected callback". When the {@link Out} event is 665 * created, the network connection is not yet known. Some 666 * header fields' values, however, need e.g. the port information 667 * from the connection. Therefore a callback may be set which is 668 * invoked when the connection has been obtained that will be used 669 * to send the request. 670 * 671 * @param connectedCallback the connected callback 672 * @return the out 673 */ 674 public Out setConnectedCallback( 675 BiConsumer<Request.Out, SocketIOChannel> connectedCallback) { 676 this.connectedCallback = connectedCallback; 677 return this; 678 } 679 680 /** 681 * Returns the connected callback. 682 * 683 * @return the connected callback, if set 684 */ 685 public Optional<BiConsumer<Request.Out, SocketIOChannel>> 686 connectedCallback() { 687 return Optional.ofNullable(connectedCallback); 688 } 689 690 /** 691 * The HTTP request that will be sent by the event. 692 * 693 * @return the http request 694 */ 695 public HttpRequest httpRequest() { 696 return request; 697 } 698 699 /** 700 * Returns an absolute URI of the request. 701 * 702 * @return the URI 703 */ 704 public URI requestUri() { 705 return request.requestUri(); 706 } 707 708 /* 709 * (non-Javadoc) 710 * 711 * @see java.lang.Object#toString() 712 */ 713 @Override 714 public String toString() { 715 StringBuilder builder = new StringBuilder(); 716 builder.append(Components.objectName(this)) 717 .append(" [").append(request.toString()); 718 if (channels().length > 0) { 719 builder.append(", channels="); 720 builder.append(Channel.toString(channels())); 721 } 722 builder.append(']'); 723 return builder.toString(); 724 } 725 726 /** 727 * Represents a HTTP CONNECT request. 728 */ 729 public static class Connect extends Out { 730 731 /** 732 * Instantiates a new request. 733 * 734 * @param uri the uri 735 */ 736 public Connect(URL uri) { 737 super("CONNECT", uri); 738 } 739 } 740 741 /** 742 * Represents a HTTP DELETE request. 743 */ 744 public static class Delete extends Out { 745 746 /** 747 * Instantiates a new request. 748 * 749 * @param uri the uri 750 */ 751 public Delete(URL uri) { 752 super("DELETE", uri); 753 } 754 } 755 756 /** 757 * Represents a HTTP GET request. 758 */ 759 public static class Get extends Out { 760 761 /** 762 * Instantiates a new request. 763 * 764 * @param uri the uri 765 */ 766 public Get(URL uri) { 767 super("GET", uri); 768 } 769 } 770 771 /** 772 * Represents a HTTP HEAD request. 773 */ 774 public static class Head extends Out { 775 776 /** 777 * Instantiates a new request. 778 * 779 * @param uri the uri 780 */ 781 public Head(URL uri) { 782 super("HEAD", uri); 783 } 784 } 785 786 /** 787 * Represents a HTTP OPTIONS request. 788 */ 789 public static class Options extends Out { 790 791 /** 792 * Instantiates a new request. 793 * 794 * @param uri the uri 795 */ 796 public Options(URL uri) { 797 super("OPTIONS", uri); 798 } 799 } 800 801 /** 802 * Represents a HTTP POST request. 803 */ 804 public static class Post extends Out { 805 806 /** 807 * Instantiates a new request. 808 * 809 * @param uri the uri 810 */ 811 public Post(URL uri) { 812 super("POST", uri); 813 } 814 } 815 816 /** 817 * Represents a HTTP PUT request. 818 */ 819 public static class Put extends Out { 820 821 /** 822 * Instantiates a new request. 823 * 824 * @param uri the uri 825 */ 826 public Put(URL uri) { 827 super("PUT", uri); 828 } 829 } 830 831 /** 832 * Represents a HTTP TRACE request. 833 */ 834 public static class Trace extends Out { 835 836 /** 837 * Instantiates a new request. 838 * 839 * @param uri the uri 840 */ 841 public Trace(URL uri) { 842 super("TRACE", uri); 843 } 844 } 845 } 846}