001/*
002 * This file is part of the JDrupes non-blocking HTTP Codec
003 * Copyright (C) 2016, 2017  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 Lesser General Public License as published
007 * by 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 Lesser General Public 
013 * License for more details.
014 *
015 * You should have received a copy of the GNU Lesser General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.httpcodec.protocols.http.server;
020
021import java.io.IOException;
022import java.io.Writer;
023import java.nio.Buffer;
024import java.nio.ByteBuffer;
025import java.time.Instant;
026import java.util.List;
027import java.util.Optional;
028import java.util.ServiceLoader;
029import java.util.stream.StreamSupport;
030
031import org.jdrupes.httpcodec.Codec;
032import org.jdrupes.httpcodec.Decoder;
033import org.jdrupes.httpcodec.Encoder;
034import org.jdrupes.httpcodec.plugin.UpgradeProvider;
035
036import static org.jdrupes.httpcodec.protocols.http.HttpConstants.*;
037
038import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpProtocol;
039import org.jdrupes.httpcodec.protocols.http.HttpEncoder;
040import org.jdrupes.httpcodec.protocols.http.HttpField;
041import org.jdrupes.httpcodec.protocols.http.HttpRequest;
042import org.jdrupes.httpcodec.protocols.http.HttpResponse;
043import org.jdrupes.httpcodec.types.Converters;
044import org.jdrupes.httpcodec.types.Directive;
045
046/**
047 * An encoder for HTTP responses that accepts a header and optional
048 * payload data end encodes it into a sequence of
049 * {@link Buffer}s.
050 * 
051 * ![HttpResponseEncoder](httpresponseencoder.svg)
052 *
053 * Headers
054 * -------
055 * 
056 * ### Date ###
057 * 
058 * The encoder automatically adds a `Date` header as specified
059 * in [RFC 7231, Section 7.1.1.2](https://tools.ietf.org/html/rfc7231#section-7.1.1.2).
060 * Any existing `Date` header will be overwritten. 
061 * 
062 * ### Expires ###
063 * 
064 * If the protocol is HTTP 1.0 and the response includes a `Cache-Control`
065 * header field with a `max-age` directive, an `Expires` header field
066 * with the same information is generated (see
067 * [RFC 7234, Section 5.3](https://tools.ietf.org/html/rfc7234#section-5.3)).
068 * 
069 * @startuml httpresponseencoder.svg
070 * class HttpResponseEncoder {
071 *      +HttpResponseEncoder()
072 *      +void encode(HttpResponse messageHeader)
073 * +Result encode(Buffer in, ByteBuffer out, boolean endOfInput)
074 * }
075 * 
076 * class HttpEncoder<T extends HttpMessageHeader> {
077 * }
078 * 
079 * HttpEncoder <|-- HttpResponseEncoder : <<bind>> <T -> HttpResponse>
080 * 
081 * @enduml
082 */
083public class HttpResponseEncoder extends HttpEncoder<HttpResponse, HttpRequest>
084        implements Encoder<HttpResponse, HttpRequest> {
085
086        private static Result.Factory resultFactory = new Result.Factory() {
087        };
088        
089        private String switchingTo;
090        private UpgradeProvider protocolPlugin;
091
092        /* (non-Javadoc)
093         * @see org.jdrupes.httpcodec.protocols.http.HttpEncoder#resultFactory()
094         */
095        @Override
096        protected Result.Factory resultFactory() {
097                return resultFactory;
098        }
099
100        /* (non-Javadoc)
101         * @see org.jdrupes.httpcodec.Encoder#encoding()
102         */
103        @Override
104        public Class<HttpResponse> encoding() {
105                return HttpResponse.class;
106        }
107
108        /* (non-Javadoc)
109         * @see HttpEncoder#encode(HttpMessageHeader)
110         */
111        @Override
112        public void encode(HttpResponse messageHeader) {
113                if (messageHeader.statusCode()
114                                        == HttpStatus.SWITCHING_PROTOCOLS.statusCode()) {
115                        switchingTo = prepareSwitchProtocol(messageHeader);
116                }
117                
118                // Make sure we have an up-to-date Date, RFC 7231 7.1.1.2
119                messageHeader.setField(HttpField.DATE, Instant.now());
120
121                // ensure backward compatibility
122                if (messageHeader.protocol().compareTo(HttpProtocol.HTTP_1_1) < 0) {
123                        // Create Expires
124                        Optional<HttpField<List<Directive>>> cacheCtrl = messageHeader
125                                        .findField(HttpField.CACHE_CONTROL, Converters.DIRECTIVE_LIST);
126                        if (cacheCtrl.isPresent() 
127                                        && !messageHeader.fields().containsKey(HttpField.EXPIRES)) {
128                                Optional<Long> maxAge = cacheCtrl.get().value().stream()
129                                                .filter(d -> "max-age".equalsIgnoreCase(d.name()))
130                                                .map(d -> d.value()).map(v -> Long.parseLong(v.get()))
131                                                .findFirst();
132                                if (maxAge.isPresent()) {
133                                        messageHeader.setField(HttpField.EXPIRES, 
134                                                        Instant.now().plusSeconds(maxAge.get()));
135                                }
136                        }
137                }
138
139                // Check the content length rules
140                checkContentLength(messageHeader);
141                
142                // Finally encode
143                super.encode(messageHeader);
144        }
145
146        /* (non-Javadoc)
147         * @see org.jdrupes.httpcodec.internal.Encoder#encode(java.nio.ByteBuffer, java.nio.ByteBuffer)
148         */
149        @Override
150        public Result encode(Buffer in, ByteBuffer out, boolean endOfInput) {
151                Result result = (Result)super.encode(in, out, endOfInput);
152                if (switchingTo != null && endOfInput 
153                                && !result.isUnderflow() && !result.isOverflow()) {
154                        // Last invocation of encode
155                        return resultFactory().newResult(false, false, 
156                                        result.closeConnection(), switchingTo, 
157                                        protocolPlugin.createRequestDecoder(switchingTo), 
158                                        protocolPlugin.createResponseEncoder(switchingTo));
159                }
160                return result;
161        }
162
163        private void checkContentLength(HttpResponse messageHeader) {
164                // RFC 7230 3.3.2 
165                boolean forbidden = messageHeader.fields()
166                                .containsKey(HttpField.TRANSFER_ENCODING)
167                                || messageHeader.statusCode() % 100 == 1
168                                || messageHeader.statusCode() 
169                                                == HttpStatus.NO_CONTENT.statusCode()
170                                || (messageHeader.request().map(
171                                                r -> r.method().equalsIgnoreCase("CONNECT"))
172                                                .orElse(false)
173                                        && messageHeader.statusCode() % 100 == 2);
174                if (messageHeader.fields().containsKey(HttpField.CONTENT_LENGTH)) {
175                        if (forbidden) {
176                                messageHeader.removeField(HttpField.CONTENT_LENGTH);
177                        }
178                        return;
179                }
180                // No content length header, maybe we should add one?
181                if (forbidden || messageHeader.hasPayload()) {
182                        // Not needed or data will determine header
183                        return;
184                }
185                // Don't add header if optional
186                if (messageHeader.request().map(
187                                r -> r.method().equalsIgnoreCase("HEAD"))
188                                .orElse(false)
189                        || messageHeader.statusCode() 
190                                        == HttpStatus.NOT_MODIFIED.statusCode()) {
191                        return;
192                }
193                // Add 0 content length
194                messageHeader.setField(new HttpField<>(
195                                HttpField.CONTENT_LENGTH, 0L, Converters.LONG));
196        }
197
198        private String prepareSwitchProtocol(HttpResponse response) {
199                Optional<String> protocol = response
200                                .findField(HttpField.UPGRADE, Converters.STRING_LIST)
201                                .map(l -> l.value().get(0));
202                if (!protocol.isPresent()) {
203                        response.setStatus(HttpStatus.BAD_REQUEST)
204                                .setHasPayload(false).clearHeaders();
205                        return null;
206                }
207                // Load every time to support dynamic deployment of additional
208                // services in an OSGi environment.
209                protocolPlugin = StreamSupport.stream(
210                                ServiceLoader.load(UpgradeProvider.class)
211                                .spliterator(), false)
212                                .filter(p -> p.supportsProtocol(protocol.get()))
213                                .findFirst().orElse(null);
214                if (protocolPlugin == null) {
215                        response.setStatus(HttpStatus.BAD_REQUEST)
216                                .setHasPayload(false).clearHeaders();
217                        return null;
218                }
219                protocolPlugin.augmentInitialResponse(response);
220                if (response.statusCode() 
221                                != HttpStatus.SWITCHING_PROTOCOLS.statusCode()) {
222                        // Not switching after all
223                        return null;
224                }
225                return protocol.get();
226        }
227
228        /* (non-Javadoc)
229         * @see org.jdrupes.httpcodec.internal.Encoder#startMessage(java.io.Writer)
230         */
231        @Override
232        protected void startMessage(HttpResponse response, Writer writer)
233                throws IOException {
234                writer.write(response.protocol().toString());
235                writer.write(" ");
236                writer.write(Integer.toString(response.statusCode()));
237                writer.write(" ");
238                writer.write(response.reasonPhrase());
239                writer.write("\r\n");
240        }
241
242        /**
243         * The result from encoding a response. In addition to the usual
244         * codec result, a response encoder may signal to the invoker that the
245         * connection to the requester must be closed and that the protocol has
246         * been switched.
247         */
248        public abstract static class Result extends HttpEncoder.Result
249                implements Codec.ProtocolSwitchResult {
250
251                private String newProtocol;
252                private Decoder<?, ?> newDecoder;
253                private Encoder<?, ?> newEncoder;
254                
255                /**
256                 * Returns a new result.
257                 * 
258                 * @param overflow
259                 *            {@code true} if the data didn't fit in the out buffer
260                 * @param underflow
261                 *            {@code true} if more data is expected
262                 * @param closeConnection
263                 *            {@code true} if the connection should be closed
264                 * @param newProtocol the name of the new protocol if a switch occurred
265                 * @param newDecoder the new decoder if a switch occurred
266                 * @param newEncoder the new decoder if a switch occurred
267                 */
268                protected Result(boolean overflow, boolean underflow,
269                        boolean closeConnection, String newProtocol,
270                        Decoder<?, ?> newDecoder, Encoder<?, ?> newEncoder) {
271                        super(overflow, underflow, closeConnection);
272                        this.newProtocol = newProtocol;
273                        this.newEncoder = newEncoder;
274                        this.newDecoder = newDecoder;
275                }
276
277                @Override
278                public String newProtocol() {
279                        return newProtocol;
280                }
281                
282                @Override
283                public Encoder<?, ?> newEncoder() {
284                        return newEncoder;
285                }
286                
287                @Override
288                public Decoder<?, ?> newDecoder() {
289                        return newDecoder;
290                }
291
292                /* (non-Javadoc)
293                 * @see java.lang.Object#hashCode()
294                 */
295                @Override
296                public int hashCode() {
297                        final int prime = 31;
298                        int result = super.hashCode();
299                        result = prime * result
300                                + ((newDecoder == null) ? 0 : newDecoder.hashCode());
301                        result = prime * result
302                                + ((newEncoder == null) ? 0 : newEncoder.hashCode());
303                        result = prime * result
304                                + ((newProtocol == null) ? 0 : newProtocol.hashCode());
305                        return result;
306                }
307
308                /* (non-Javadoc)
309                 * @see java.lang.Object#equals(java.lang.Object)
310                 */
311                @Override
312                public boolean equals(Object obj) {
313                        if (this == obj) {
314                                return true;
315                        }
316                        if (!super.equals(obj)) {
317                                return false;
318                        }
319                        if (!(obj instanceof Result)) {
320                                return false;
321                        }
322                        Result other = (Result) obj;
323                        if (newDecoder == null) {
324                                if (other.newDecoder != null) {
325                                        return false;
326                                }
327                        } else if (!newDecoder.equals(other.newDecoder)) {
328                                return false;
329                        }
330                        if (newEncoder == null) {
331                                if (other.newEncoder != null) {
332                                        return false;
333                                }
334                        } else if (!newEncoder.equals(other.newEncoder)) {
335                                return false;
336                        }
337                        if (newProtocol == null) {
338                                if (other.newProtocol != null) {
339                                        return false;
340                                }
341                        } else if (!newProtocol.equals(other.newProtocol)) {
342                                return false;
343                        }
344                        return true;
345                }
346
347                /* (non-Javadoc)
348                 * @see java.lang.Object#toString()
349                 */
350                @Override
351                public String toString() {
352                        StringBuilder builder = new StringBuilder();
353                        builder.append("HttpResponseEncoder.Result [overflow=");
354                        builder.append(isOverflow());
355                        builder.append(", underflow=");
356                        builder.append(isUnderflow());
357                        builder.append(", closeConnection=");
358                        builder.append(closeConnection());
359                        builder.append(", ");
360                        if (newProtocol != null) {
361                                builder.append("newProtocol=");
362                                builder.append(newProtocol);
363                                builder.append(", ");
364                        }
365                        if (newDecoder != null) {
366                                builder.append("newDecoder=");
367                                builder.append(newDecoder);
368                                builder.append(", ");
369                        }
370                        if (newEncoder != null) {
371                                builder.append("newEncoder=");
372                                builder.append(newEncoder);
373                        }
374                        builder.append("]");
375                        return builder.toString();
376                }
377
378                /**
379                 * A factory for creating new Results.
380                 */
381                protected static class Factory extends HttpEncoder.Result.Factory {
382                        
383                        /**
384                         * Create a new result.
385                         * 
386                         * @param overflow
387                         *            {@code true} if the data didn't fit in the out buffer
388                         * @param underflow
389                         *            {@code true} if more data is expected
390                         * @param closeConnection
391                         *            {@code true} if the connection should be closed
392                         * @param newProtocol the name of the new protocol if a switch occurred
393                         * @param newDecoder the new decoder if a switch occurred
394                         * @param newEncoder the new decoder if a switch occurred
395                         * @return the result
396                         */
397                        public Result newResult(boolean overflow, boolean underflow,
398                                boolean closeConnection, String newProtocol,
399                                Decoder<?, ?> newDecoder, Encoder<?, ?> newEncoder) {
400                                return new Result(overflow, underflow, closeConnection,
401                                                newProtocol, newDecoder, newEncoder) {
402                                };
403                        }
404
405                        /**
406                         * Create a new (preliminary) result. This is invoked by the
407                         * base class. We cannot supply the missing information yet.
408                         * If necessary the result will be modified in 
409                         * {@link HttpResponseEncoder#encode(Buffer, ByteBuffer, boolean)}.
410                         * 
411                         * @param overflow
412                         *            {@code true} if the data didn't fit in the out buffer
413                         * @param underflow
414                         *            {@code true} if more data is expected
415                         * @param closeConnection
416                         *            {@code true} if the connection should be closed
417                         * @return the result
418                         */
419                        @Override
420                        public Result newResult(boolean overflow, boolean underflow,
421                                boolean closeConnection) {
422                                return newResult(overflow, underflow, closeConnection,
423                                                null, null, null);
424                        }
425                        
426                }               
427
428        }
429}