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