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