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}