/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.internal.netty; import static io.netty.channel.ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE; import java.io.IOException; import java.io.InputStream; import java.net.InetSocketAddress; import java.net.URLDecoder; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.concurrent.Executor; import java.util.function.Function; import java.util.stream.Collectors; import org.jooby.MediaType; import org.jooby.Sse; import org.jooby.spi.NativePushPromise; import org.jooby.spi.NativeRequest; import org.jooby.spi.NativeUpload; import org.jooby.spi.NativeWebSocket; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.Multimap; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.buffer.ByteBufInputStream; import io.netty.channel.Channel; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieDecoder; import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; import io.netty.handler.codec.http.multipart.FileUpload; import io.netty.handler.codec.http.multipart.HttpData; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; import io.netty.handler.codec.http.multipart.InterfaceHttpData.HttpDataType; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.util.AttributeKey; import io.netty.util.ReferenceCounted; public class NettyRequest implements NativeRequest { public static final AttributeKey<String> PROTOCOL = AttributeKey .newInstance(NettyRequest.class.getName() + ".protol"); public static final AttributeKey<Boolean> NEED_FLUSH = AttributeKey .newInstance(NettyRequest.class.getName() + ".needFlush"); public static final AttributeKey<Boolean> ASYNC = AttributeKey .newInstance(NettyRequest.class.getName() + ".async"); public static final AttributeKey<Boolean> SECURE = AttributeKey .newInstance(NettyRequest.class.getName() + ".secure");; private HttpRequest req; private QueryStringDecoder query; private List<org.jooby.Cookie> cookies; private Multimap<String, String> params; private Multimap<String, NativeUpload> files; private String tmpdir; private String path; private ChannelHandlerContext ctx; private int wsMaxMessageSize; public NettyRequest(final ChannelHandlerContext ctx, final HttpRequest req, final String tmpdir, final int wsMaxMessageSize) throws IOException { this.ctx = ctx; this.req = req; this.tmpdir = tmpdir; this.query = new QueryStringDecoder(req.uri()); this.path = URLDecoder.decode(query.path(), "UTF-8"); this.wsMaxMessageSize = wsMaxMessageSize; Channel channel = ctx.channel(); channel.attr(ASYNC).set(false); } @Override public Optional<String> queryString() { String uri = req.uri(); int at = uri.indexOf('?') + 1; return at > 0 && at < uri.length() ? Optional.of(uri.substring(at)) : Optional.empty(); } @Override public String method() { return req.method().name(); } @Override public String rawPath() { String uri = req.uri(); int at = uri.indexOf('?'); return at > 0 ? uri.substring(0, at) : uri; } @Override public String path() { return path; } @Override public List<String> paramNames() throws IOException { return ImmutableList.copyOf(decodeParams().keySet()); } @Override public List<String> params(final String name) throws Exception { return (List<String>) decodeParams().get(name); } @Override public List<String> headers(final String name) { return req.headers().getAll(name); } @Override public Optional<String> header(final String name) { String value = req.headers().get(name); return Optional.ofNullable(value); } @Override public List<String> headerNames() { ImmutableList.Builder<String> builder = ImmutableList.builder(); req.headers().names().forEach(it -> builder.add(it.toString())); return builder.build(); } @Override public List<org.jooby.Cookie> cookies() { if (this.cookies == null) { String cookieString = req.headers().get(HttpHeaderNames.COOKIE); if (cookieString != null) { this.cookies = ServerCookieDecoder.STRICT.decode(cookieString).stream() .map(this::cookie) .collect(Collectors.toList()); } else { this.cookies = Collections.emptyList(); } } return this.cookies; } @Override public List<NativeUpload> files(final String name) throws IOException { decodeParams(); return ImmutableList.copyOf(this.files.get(name)); } @Override public InputStream in() throws IOException { ByteBuf content = ((HttpContent) req).content(); return new ByteBufInputStream(content); } @Override public String ip() { InetSocketAddress remoteAddress = (InetSocketAddress) ctx.channel().remoteAddress(); return remoteAddress.getAddress().getHostAddress(); } @Override public String protocol() { return ctx.pipeline().get("h2") == null ? req.protocolVersion().text() : "HTTP/2.0"; } @Override public boolean secure() { return ifSecure(Boolean.TRUE, Boolean.FALSE).booleanValue(); } @SuppressWarnings("unchecked") @Override public <T> T upgrade(final Class<T> type) throws Exception { if (type == NativeWebSocket.class) { String protocol = ifSecure("wss", "ws"); String webSocketURL = protocol + "://" + req.headers().get(HttpHeaderNames.HOST) + path; WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( webSocketURL, null, true, wsMaxMessageSize); WebSocketServerHandshaker handshaker = wsFactory.newHandshaker(req); NettyWebSocket result = new NettyWebSocket(ctx, handshaker, (ws) -> { handshaker.handshake(ctx.channel(), (FullHttpRequest) req) .addListener(FIRE_EXCEPTION_ON_FAILURE) .addListener(payload -> ws.connect()) .addListener(FIRE_EXCEPTION_ON_FAILURE); }); ctx.channel().attr(NettyWebSocket.KEY).set(result); return (T) result; } else if (type == Sse.class) { NettySse sse = new NettySse(ctx); return (T) sse; } else if (type == NativePushPromise.class) { return (T) new NettyPush(ctx, req.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()), header("host").orElse(ip()), ifSecure("https", "http")); } throw new UnsupportedOperationException("Not Supported: " + type); } @Override public void startAsync(final Executor executor, final Runnable runnable) { Channel channel = ctx.channel(); channel.attr(NEED_FLUSH).set(false); channel.attr(ASYNC).set(true); ReferenceCounted body = ((ByteBufHolder) req).content(); body.retain(); executor.execute(() -> { try { runnable.run(); } finally { body.release(); } }); } private org.jooby.Cookie cookie(final Cookie c) { org.jooby.Cookie.Definition cookie = new org.jooby.Cookie.Definition(c.name(), c.value()); Optional.ofNullable(c.domain()).ifPresent(cookie::domain); Optional.ofNullable(c.path()).ifPresent(cookie::path); return cookie.toCookie(); } private Multimap<String, String> decodeParams() throws IOException { if (params == null) { params = ArrayListMultimap.create(); files = ArrayListMultimap.create(); query.parameters() .forEach((name, values) -> values.forEach(value -> params.put(name, value))); HttpMethod method = req.method(); boolean hasBody = method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH); boolean formLike = false; if (req.headers().contains("Content-Type")) { String contentType = req.headers().get("Content-Type").toLowerCase(); formLike = (contentType.startsWith(MediaType.multipart.name()) || contentType.startsWith(MediaType.form.name())); } if (hasBody && formLike) { HttpPostRequestDecoder decoder = new HttpPostRequestDecoder( new DefaultHttpDataFactory(), req); try { Function<HttpPostRequestDecoder, Boolean> hasNext = it -> { try { return it.hasNext(); } catch (HttpPostRequestDecoder.EndOfDataDecoderException ex) { return false; } }; while (hasNext.apply(decoder)) { HttpData field = (HttpData) decoder.next(); try { String name = field.getName(); if (field.getHttpDataType() == HttpDataType.FileUpload) { files.put(name, new NettyUpload((FileUpload) field, tmpdir)); } else { params.put(name, field.getString()); } } finally { field.release(); } } } finally { decoder.destroy(); } } } return params; } private <T> T ifSecure(final T then, final T otherwise) { return ctx.pipeline().get("ssl") != null ? then : otherwise; } }