/* * The MIT License * * Copyright 2014 Tim Boudreau. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.mastfrog.acteur; import com.google.common.base.Objects; import com.google.inject.Key; import com.google.inject.name.Names; import com.mastfrog.acteur.ResponseWriter.AbstractOutput; import com.mastfrog.acteur.ResponseWriter.Output; import com.mastfrog.acteur.ResponseWriter.Status; import com.mastfrog.acteur.headers.HeaderValueType; import com.mastfrog.acteur.headers.Headers; import com.mastfrog.acteur.headers.Method; import com.mastfrog.acteur.server.ServerModule; import com.mastfrog.giulius.Dependencies; import com.mastfrog.guicy.scope.ReentrantScope; import com.mastfrog.util.Checks; import com.mastfrog.util.Codec; import com.mastfrog.util.Exceptions; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import static io.netty.channel.ChannelFutureListener.CLOSE; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import static io.netty.handler.codec.http.HttpResponseStatus.NOT_MODIFIED; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.handler.codec.http.cookie.Cookie; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; import org.joda.time.Duration; /** * Aggregates the set of headers and a body writer which is used to respond to * an HTTP request. Each Acteur has its own which will be merged into the one * belonging to the page if it succeeds, so Acteurs that reject a response * cannot have side-effects. * * @author Tim Boudreau */ final class ResponseImpl extends Response { private volatile boolean modified; HttpResponseStatus status; private final List<Entry<?>> headers = new ArrayList<Entry<?>>(2); private Object message; ChannelFutureListener listener; private boolean chunked; private Duration delay; ResponseImpl() { } boolean isModified() { return modified; } void modify() { this.modified = true; } void merge(ResponseImpl other) { Checks.notNull("other", other); this.modified |= other.modified; if (other.modified) { for (Entry<?> e : other.headers) { addEntry(e); } if (other.status != null) { setResponseCode(other.status); } if (other.message != null) { setMessage(other.message); } if (other.chunked) { setChunked(true); } if (other.listener != null) { setBodyWriter(other.listener); } if (other.delay != null) { this.delay = other.delay; } } } private <T> void addEntry(Entry<T> e) { add(e.decorator, e.value); } public void setMessage(Object message) { modify(); this.message = message; } public void setDelay(Duration delay) { modify(); this.delay = delay; } public void setResponseCode(HttpResponseStatus status) { modify(); this.status = status; } public HttpResponseStatus getResponseCode() { return status == null ? HttpResponseStatus.OK : status; } @Override public void setBodyWriter(ResponseWriter writer) { Page p = Page.get(); Application app = p.getApplication(); Dependencies deps = app.getDependencies(); HttpEvent evt = deps.getInstance(HttpEvent.class); Charset charset = deps.getInstance(Charset.class); ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class); Codec mapper = deps.getInstance(Codec.class); Key<ExecutorService> key = Key.get(ExecutorService.class, Names.named(ServerModule.BACKGROUND_THREAD_POOL_NAME)); ExecutorService svc = deps.getInstance(key); setWriter(writer, charset, allocator, mapper, evt, svc); } Duration getDelay() { return delay; } static class HackHttpHeaders extends HttpHeaders { private final HttpHeaders orig; public HackHttpHeaders(HttpHeaders orig, boolean chunked) { this.orig = orig; if (chunked) { orig.set(Names.TRANSFER_ENCODING, Values.CHUNKED); orig.remove(Names.CONTENT_LENGTH); } else { orig.remove(Names.TRANSFER_ENCODING); } } @Override public String get(String name) { return orig.get(name); } @Override public List<String> getAll(String name) { return orig.getAll(name); } @Override public List<Map.Entry<String, String>> entries() { return orig.entries(); } @Override public boolean contains(String name) { return orig.contains(name); } @Override public boolean isEmpty() { return orig.isEmpty(); } @Override public Set<String> names() { return orig.names(); } @Override public HttpHeaders add(String name, Object value) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } return orig.add(name, value); } @Override public HttpHeaders add(String name, Iterable<?> values) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { return this; } return orig.add(name, values); } @Override public HttpHeaders set(String name, Object value) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { return this; } // if (Names.CONTENT_ENCODING.equals(name) && !"true".equals(orig.get("X-Internal-Compress"))) { // return this; // } return orig.set(name, value); } @Override public HttpHeaders set(String name, Iterable<?> values) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { return this; } if (Names.CONTENT_ENCODING.equals(name)) { return this; } return orig.set(name, values); } @Override public HttpHeaders remove(String name) { if (Names.TRANSFER_ENCODING.equals(name)) { return this; } if (Names.CONTENT_LENGTH.equals(name)) { return this; } if (Names.CONTENT_ENCODING.equals(name)) { return this; } return orig.remove(name); } @Override public HttpHeaders clear() { return orig.clear(); } @Override public Iterator<Map.Entry<String, String>> iterator() { return orig.iterator(); } } private static class HackHttpResponse extends DefaultHttpResponse { private final HttpHeaders hdrs; // Workaround for https://github.com/netty/netty/issues/1326 HackHttpResponse(HttpResponseStatus status, boolean chunked) { super(HttpVersion.HTTP_1_1, status); hdrs = new HackHttpHeaders(super.headers(), chunked); } @Override public HttpHeaders headers() { return hdrs; } } private String cookieName(Object o) { if (o instanceof Cookie) { return ((Cookie) o).name(); } else if (o instanceof io.netty.handler.codec.http.Cookie) { return ((io.netty.handler.codec.http.Cookie) o).name(); } else { return null; } } private boolean compareCookies(Object old, Object nue) { return Objects.equal(cookieName(old), cookieName(nue)); } @SuppressWarnings("unchecked") public <T> void add(HeaderValueType<T> decorator, T value) { List<Entry<?>> old = new LinkedList<>(); // XXX set cookie! for (Iterator<Entry<?>> it = headers.iterator(); it.hasNext();) { Entry<?> e = it.next(); // Do prune setting the same cookie twice if (decorator.name().equals(HttpHeaders.Names.SET_COOKIE) && e.decorator.name().equals(HttpHeaders.Names.SET_COOKIE)) { if (compareCookies(e.value, value)) { it.remove(); continue; } else { continue; } } if (e.match(decorator) != null) { old.add(e); it.remove(); } } Entry<?> e = new Entry<>(decorator, value); // For now, special handling for Allow: // Longer term, should HeaderValueType.isArray() and a way to // coalesce if (!old.isEmpty() && decorator == Headers.ALLOW) { old.add(e); Set<Method> all = new HashSet<>(); for (Entry<?> en : old) { Method[] m = (Method[]) en.value; all.addAll(Arrays.asList(m)); } value = (T) all.toArray(new Method[0]); e = new Entry<>(decorator, value); } headers.add(e); modify(); } public <T> T get(HeaderValueType<T> decorator) { for (Entry<?> e : headers) { HeaderValueType<T> d = e.match(decorator); if (d != null) { return d.type().cast(e.value); } } return null; } public void setChunked(boolean chunked) { this.chunked = chunked; modify(); } <T extends ResponseWriter> void setWriter(T w, Dependencies deps, HttpEvent evt) { // setChunked(true); Charset charset = deps.getInstance(Charset.class); ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class); Codec mapper = deps.getInstance(Codec.class); Key<ExecutorService> key = Key.get(ExecutorService.class, Names.named(ServerModule.BACKGROUND_THREAD_POOL_NAME)); ExecutorService svc = deps.getInstance(key); setWriter(w, charset, allocator, mapper, evt, svc); } <T extends ResponseWriter> void setWriter(Class<T> w, Dependencies deps, HttpEvent evt) { // setChunked(true); Charset charset = deps.getInstance(Charset.class); ByteBufAllocator allocator = deps.getInstance(ByteBufAllocator.class); Key<ExecutorService> key = Key.get(ExecutorService.class, Names.named(ServerModule.BACKGROUND_THREAD_POOL_NAME)); ExecutorService svc = deps.getInstance(key); Codec mapper = deps.getInstance(Codec.class); setWriter(new DynResponseWriter(w, deps), charset, allocator, mapper, evt, svc); } static class DynResponseWriter extends ResponseWriter { private final AtomicReference<ResponseWriter> actual = new AtomicReference<>(); private final Callable<ResponseWriter> resp; public DynResponseWriter(final Class<? extends ResponseWriter> type, final Dependencies deps) { ReentrantScope scope = deps.getInstance(ReentrantScope.class); assert scope.inScope(); resp = scope.wrap(new Callable<ResponseWriter>() { @Override public ResponseWriter call() throws Exception { ResponseWriter w = actual.get(); if (w == null) { actual.set(w = deps.getInstance(type)); } return w; } }); } @Override public ResponseWriter.Status write(Event<?> evt, Output out) throws Exception { ResponseWriter actual = resp.call(); return actual.write(evt, out); } @Override public Status write(Event<?> evt, Output out, int iteration) throws Exception { ResponseWriter actual = resp.call(); return actual.write(evt, out, iteration); } } static boolean isKeepAlive(Event<?> evt) { return evt instanceof HttpEvent ? ((HttpEvent) evt).isKeepAlive() : false; } void setWriter(ResponseWriter w, Charset charset, ByteBufAllocator allocator, Codec mapper, Event<?> evt, ExecutorService svc) { setBodyWriter(new ResponseWriterListener(evt, w, charset, allocator, mapper, chunked, !isKeepAlive(evt), svc)); } private static final class ResponseWriterListener extends AbstractOutput implements ChannelFutureListener { private volatile ChannelFuture future; private volatile int callCount = 0; private final boolean chunked; private final ResponseWriter writer; private final boolean shouldClose; private final Event<?> evt; private final ExecutorService svc; public ResponseWriterListener(Event<?> evt, ResponseWriter writer, Charset charset, ByteBufAllocator allocator, Codec mapper, boolean chunked, boolean shouldClose, ExecutorService svc) { super(charset, allocator, mapper); this.chunked = chunked; this.writer = writer; this.shouldClose = shouldClose; this.evt = evt; this.svc = svc; } public Channel channel() { if (future == null) { throw new IllegalStateException("No future -> no channel"); } return future.channel(); } @Override public Output write(ByteBuf buf) throws IOException { assert future != null; if (chunked) { future = future.channel().writeAndFlush(new DefaultHttpContent(buf)); } else { future = future.channel().writeAndFlush(buf); } return this; } volatile boolean inOperationComplete; volatile int entryCount = 0; @Override public void operationComplete(final ChannelFuture future) throws Exception { try { // See https://github.com/netty/netty/issues/2415 for why this is needed if (entryCount > 0) { svc.submit(new Runnable() { @Override public void run() { try { operationComplete(future); } catch (Exception ex) { Exceptions.chuck(ex); } } }); return; } entryCount++; Callable<Void> c = new Callable<Void>() { @Override public Void call() throws Exception { inOperationComplete = true; try { ResponseWriterListener.this.future = future; ResponseWriter.Status status = writer.write(evt, ResponseWriterListener.this, callCount++); if (status.isCallback()) { ResponseWriterListener.this.future = ResponseWriterListener.this.future.addListener(ResponseWriterListener.this); } else if (status == Status.DONE) { if (chunked) { ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); } if (shouldClose) { ResponseWriterListener.this.future = ResponseWriterListener.this.future.addListener(CLOSE); } } } finally { inOperationComplete = false; } return null; } }; if (!inOperationComplete) { c.call(); } else { svc.submit(c); } } finally { entryCount--; } } @Override public ChannelFuture future() { return future; } @Override public Output write(HttpContent chunk) throws IOException { if (!chunked) { ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(chunk.content()); } else { ResponseWriterListener.this.future = ResponseWriterListener.this.future.channel().writeAndFlush(chunk); } return this; } } /** * Set a ChannelFutureListener which will be called after headers are * written and flushed to the socket; prefer * <code>setResponseWriter()</code> to this method unless you are not using * chunked encoding and want to stream your response (in which case, be sure * to setChunked(false) or you will have encoding errors). * * @param listener */ public void setBodyWriter(ChannelFutureListener listener) { if (this.listener != null) { throw new IllegalStateException("Listener already set to " + this.listener); } this.listener = listener; } public Object getMessage() { return message; } final boolean canHaveBody(HttpResponseStatus status) { switch (status.code()) { case 204: case 205: case 304: return false; default: return true; } } private ByteBuf writeMessage(Event<?> evt, Charset charset) { Object message = getMessage(); if (message == null) { return null; } if (message instanceof ByteBuf) { return (ByteBuf) message; } if (message instanceof byte[]) { return Unpooled.wrappedBuffer((byte[]) message); } if (message instanceof ByteBuffer) { return Unpooled.wrappedBuffer(((ByteBuffer) message)); } if (message instanceof CharSequence) { return evt.getChannel().alloc().buffer().writeBytes(message.toString().getBytes(charset)); } Page p = Page.get(); if (p == null) { throw new IllegalStateException("Call to write message outside request scope"); } Application app = p.getApplication(); Dependencies deps = app.getDependencies(); Codec codec = deps.getInstance(Codec.class); ByteBuf result = evt.getChannel().alloc().buffer(); try (OutputStream out = new ByteBufOutputStream(result)) { codec.writeValue(message, out); return result; } catch (IOException ex) { return Exceptions.chuck(ex); } } public HttpResponse toResponse(Event<?> evt, Charset charset) { if (!canHaveBody(getResponseCode()) && (message != null || listener != null)) { if (listener != ChannelFutureListener.CLOSE) { System.err.println(evt + " attempts to attach a body to " + getResponseCode() + " which cannot have one: " + message + " - " + listener); } } ByteBuf buf = writeMessage(evt, charset); HttpResponse resp; if (buf != null) { long size = buf.readableBytes(); add(Headers.CONTENT_LENGTH, size); DefaultFullHttpResponse r = new DefaultFullHttpResponse( HttpVersion.HTTP_1_1, getResponseCode(), buf); resp = r; } else { resp = new HackHttpResponse(getResponseCode(), this.status == NOT_MODIFIED ? false : chunked); } for (Entry<?> e : headers) { // Remove things which cause problems for non-modified responses - // browsers will hold the connection open regardless if (this.status == NOT_MODIFIED) { if (e.decorator == Headers.CONTENT_LENGTH) { continue; } else if (HttpHeaders.Names.CONTENT_ENCODING.equals(e.decorator.name())) { continue; } else if (Headers.TRANSFER_ENCODING.name().equals(e.decorator.name())) { continue; } } e.write(resp); } // Ensure a 0 content length is present for items with no content if (message == null && listener == null && resp.headers() instanceof HackHttpHeaders) { ((HackHttpHeaders) resp.headers()).orig.set(Headers.CONTENT_LENGTH.name(), 0); ((HackHttpHeaders) resp.headers()).remove(Headers.TRANSFER_ENCODING.name()); ((HackHttpHeaders) resp.headers()).remove(Headers.CONTENT_ENCODING.name()); } return resp; } ChannelFuture sendMessage(Event<?> evt, ChannelFuture future, HttpMessage resp) { if (listener != null) { future = future.addListener(listener); return future; } else if (!isKeepAlive(evt)) { future = future.addListener(ChannelFutureListener.CLOSE); } return future; } @Override public String toString() { return "Response{" + "modified=" + modified + ", status=" + status + ", headers=" + headers + ", message=" + message + ", listener=" + listener + ", chunked=" + chunked + " has listener " + (this.listener != null) + '}'; } private static final class Entry<T> { private final HeaderValueType<T> decorator; private final T value; Entry(HeaderValueType<T> decorator, T value) { Checks.notNull("decorator", decorator); Checks.notNull(decorator.name().toString(), value); this.decorator = decorator; this.value = value; } public void decorate(HttpMessage msg) { msg.headers().set(decorator.name(), value); } public void write(HttpMessage msg) { Headers.write(decorator, value, msg); } @Override public String toString() { return decorator.name() + ": " + decorator.toString(value); } @Override public int hashCode() { return decorator.name().hashCode(); } @Override public boolean equals(Object o) { return o instanceof Entry<?> && ((Entry<?>) o).decorator.name().equals(decorator.name()); } @SuppressWarnings({"unchecked"}) public <R> HeaderValueType<R> match(HeaderValueType<R> decorator) { if (decorator == this.decorator) { return (HeaderValueType<R>) this.decorator; } if (this.decorator.name().equals(decorator.name()) && this.decorator.type().equals(decorator.type())) { return decorator; } return null; } } }