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