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}