Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#1300] feat: Define allowed methods used in 'X-HTTP-Method-Override' #1301

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions documentation/manual/configuration.textile
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,15 @@ bc. http.cacheControl=0
Default: @3600@ - set cache expiry to one hour.


h3(#http.allowed.method.override). http.allowed.method.override

Define allowed methods that will be handled when defined in X-HTTP-Method-Override

bc. http.allowed.method.override=POST

Default: none


h3(#http.exposePlayServer). http.exposePlayServer

Disable the HTTP response header that identifies the HTTP server as Play. For example:
Expand Down
91 changes: 61 additions & 30 deletions framework/src/play/server/PlayHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,22 @@
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer;
import static org.jboss.netty.handler.codec.http.HttpHeaders.Names.*;

public class PlayHandler extends SimpleChannelUpstreamHandler {


private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";

/**
* If true (the default), Play will send the HTTP header
* "Server: Play! Framework; ....". This could be a security problem (old
* versions having publicly known security bugs), so you can disable the
* header in application.conf: <code>http.exposePlayServer = false</code>
* If true (the default), Play will send the HTTP header "Server: Play!
* Framework; ....". This could be a security problem (old versions having
* publicly known security bugs), so you can disable the header in
* application.conf: <code>http.exposePlayServer = false</code>
*/
private static final String signature = "Play! Framework;" + Play.version + ";" + Play.mode.name().toLowerCase();
private static final boolean exposePlayServer;
Expand All @@ -72,6 +77,13 @@ public class PlayHandler extends SimpleChannelUpstreamHandler {
public Map<String, ChannelHandler> pipelines = new HashMap<>();

private WebSocketServerHandshaker handshaker;

/**
* Define allowed methods that will be handled when defined in X-HTTP-Method-Override
* You can define allowed method in
* application.conf: <code>http.allowed.method.override=POST,PUT</code>
*/
private static final Set<String> allowedHttpMethodOverride;

static {
try {
Expand All @@ -83,6 +95,7 @@ public class PlayHandler extends SimpleChannelUpstreamHandler {

static {
exposePlayServer = !"false".equals(Play.configuration.getProperty("http.exposePlayServer"));
allowedHttpMethodOverride = Stream.of(Play.configuration.getProperty("http.allowed.method.override", "").split(",")).collect(Collectors.toSet());
}

@Override
Expand Down Expand Up @@ -168,7 +181,8 @@ public class NettyInvocation extends Invoker.Invocation {
private final HttpRequest nettyRequest;
private final MessageEvent event;

public NettyInvocation(Request request, Response response, ChannelHandlerContext ctx, HttpRequest nettyRequest, MessageEvent e) {
public NettyInvocation(Request request, Response response, ChannelHandlerContext ctx, HttpRequest nettyRequest,
MessageEvent e) {
this.ctx = ctx;
this.request = request;
this.response = response;
Expand Down Expand Up @@ -361,7 +375,8 @@ protected static void addToResponse(Response response, HttpResponse nettyRespons
nettyResponse.headers().add(SET_COOKIE, ServerCookieEncoder.STRICT.encode(c));
}

if (!response.headers.containsKey(CACHE_CONTROL) && !response.headers.containsKey(EXPIRES) && !(response.direct instanceof File)) {
if (!response.headers.containsKey(CACHE_CONTROL) && !response.headers.containsKey(EXPIRES)
&& !(response.direct instanceof File)) {
nettyResponse.headers().set(CACHE_CONTROL, "no-cache");
}

Expand Down Expand Up @@ -410,22 +425,26 @@ protected static void writeResponse(ChannelHandlerContext ctx, Response response
}
}

public void copyResponse(ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest) throws Exception {
public void copyResponse(ChannelHandlerContext ctx, Request request, Response response, HttpRequest nettyRequest)
throws Exception {
if (Logger.isTraceEnabled()) {
Logger.trace("copyResponse: begin");
}

// Decide whether to close the connection or not.

HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status));
HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(response.status));
if (exposePlayServer) {
nettyResponse.headers().set(SERVER, signature);
}

if (response.contentType != null) {
nettyResponse.headers().set(CONTENT_TYPE,
response.contentType + (response.contentType.startsWith("text/") && !response.contentType.contains("charset")
? "; charset=" + response.encoding : ""));
nettyResponse.headers()
.set(CONTENT_TYPE,
response.contentType + (response.contentType.startsWith("text/")
&& !response.contentType.contains("charset") ? "; charset=" + response.encoding
: ""));
} else {
nettyResponse.headers().set(CONTENT_TYPE, "text/plain; charset=" + response.encoding);
}
Expand Down Expand Up @@ -469,7 +488,8 @@ public void copyResponse(ChannelHandlerContext ctx, Request request, Response re
}
} else if (is != null) {
ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse);
if (!nettyRequest.getMethod().equals(HttpMethod.HEAD) && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)
&& !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
writeFuture = ctx.getChannel().write(new ChunkedStream(is));
} else {
is.close();
Expand All @@ -479,7 +499,8 @@ public void copyResponse(ChannelHandlerContext ctx, Request request, Response re
}
} else if (stream != null) {
ChannelFuture writeFuture = ctx.getChannel().write(nettyResponse);
if (!nettyRequest.getMethod().equals(HttpMethod.HEAD) && !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
if (!nettyRequest.getMethod().equals(HttpMethod.HEAD)
&& !nettyResponse.getStatus().equals(HttpResponseStatus.NOT_MODIFIED)) {
writeFuture = ctx.getChannel().write(stream);
} else {
stream.close();
Expand All @@ -506,7 +527,8 @@ static String getRemoteIPAddress(MessageEvent e) {
return fullAddress;
}

public Request parseRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest, MessageEvent messageEvent) throws Exception {
public Request parseRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest, MessageEvent messageEvent)
throws Exception {
if (Logger.isTraceEnabled()) {
Logger.trace("parseRequest: begin");
Logger.trace("parseRequest: URI = " + nettyRequest.getUri());
Expand Down Expand Up @@ -547,8 +569,9 @@ public Request parseRequest(ChannelHandlerContext ctx, HttpRequest nettyRequest,
String remoteAddress = getRemoteIPAddress(messageEvent);
String method = nettyRequest.getMethod().getName();

if (nettyRequest.headers().get("X-HTTP-Method-Override") != null) {
method = nettyRequest.headers().get("X-HTTP-Method-Override").intern();
if (nettyRequest.headers().get(X_HTTP_METHOD_OVERRIDE) != null
&& allowedHttpMethodOverride.contains(nettyRequest.headers().get(X_HTTP_METHOD_OVERRIDE).intern())) {
method = nettyRequest.headers().get(X_HTTP_METHOD_OVERRIDE).intern();
}

InputStream body = null;
Expand Down Expand Up @@ -613,8 +636,8 @@ else if (host.contains(":")) {

boolean secure = false;

Request request = Request.createRequest(remoteAddress, method, path, querystring, contentType, body, uri, host, isLoopback,
port, domain, secure, getHeaders(nettyRequest), getCookies(nettyRequest));
Request request = Request.createRequest(remoteAddress, method, path, querystring, contentType, body, uri, host,
isLoopback, port, domain, secure, getHeaders(nettyRequest), getCookies(nettyRequest));

if (Logger.isTraceEnabled()) {
Logger.trace("parseRequest: end");
Expand Down Expand Up @@ -735,7 +758,8 @@ public static void serve500(Exception e, ChannelHandlerContext ctx, HttpRequest
Logger.trace("serve500: begin");
}

HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR);
HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.INTERNAL_SERVER_ERROR);
if (exposePlayServer) {
nettyResponse.headers().set(SERVER, signature);
}
Expand Down Expand Up @@ -834,7 +858,8 @@ public void serveStatic(RenderStatic renderStatic, ChannelHandlerContext ctx, Re
Logger.trace("serveStatic: begin");
}

HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.valueOf(response.status));
HttpResponse nettyResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(response.status));
if (exposePlayServer) {
nettyResponse.headers().set(SERVER, signature);
}
Expand Down Expand Up @@ -867,15 +892,17 @@ public void serveStatic(RenderStatic renderStatic, ChannelHandlerContext ctx, Re
writeFuture.addListener(ChannelFutureListener.CLOSE);
}
} else {
FileService.serve(localFile, nettyRequest, nettyResponse, ctx, request, response, e.getChannel());
FileService.serve(localFile, nettyRequest, nettyResponse, ctx, request, response,
e.getChannel());
}
}

}
} catch (Throwable ez) {
Logger.error(ez, "serveStatic for request %s", request.method + " " + request.url);
try {
HttpResponse errorResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR);
HttpResponse errorResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.INTERNAL_SERVER_ERROR);
String errorHtml = "Internal Error (check logs)";
byte[] bytes = errorHtml.getBytes(response.encoding);
ChannelBuffer buf = ChannelBuffers.copiedBuffer(bytes);
Expand All @@ -892,7 +919,6 @@ public void serveStatic(RenderStatic renderStatic, ChannelHandlerContext ctx, Re
}
}


public static boolean isModified(String etag, long last, HttpRequest nettyRequest) {
String browserEtag = nettyRequest.headers().get(IF_NONE_MATCH);
String ifModifiedSince = nettyRequest.headers().get(IF_MODIFIED_SINCE);
Expand Down Expand Up @@ -995,7 +1021,8 @@ public void writeChunk(Object chunk) throws Exception {
}
}

public void writeChunk(Request playRequest, Response playResponse, ChannelHandlerContext ctx, HttpRequest nettyRequest, Object chunk) {
public void writeChunk(Request playRequest, Response playResponse, ChannelHandlerContext ctx,
HttpRequest nettyRequest, Object chunk) {
try {
if (playResponse.direct == null) {
playResponse.setHeader("Transfer-Encoding", "chunked");
Expand All @@ -1015,7 +1042,8 @@ public void writeChunk(Request playRequest, Response playResponse, ChannelHandle
}
}

public void closeChunked(Request playRequest, Response playResponse, ChannelHandlerContext ctx, HttpRequest nettyRequest) {
public void closeChunked(Request playRequest, Response playResponse, ChannelHandlerContext ctx,
HttpRequest nettyRequest) {
try {
((LazyChunkedInput) playResponse.direct).close();
if (this.pipelines.get("ChunkedWriteHandler") != null) {
Expand Down Expand Up @@ -1050,15 +1078,17 @@ private String getWebSocketLocation(HttpRequest req) {
return "ws://" + req.headers().get(HttpHeaders.Names.HOST) + req.getUri();
}

private void websocketHandshake(final ChannelHandlerContext ctx, HttpRequest req, MessageEvent messageEvent) throws Exception {
private void websocketHandshake(final ChannelHandlerContext ctx, HttpRequest req, MessageEvent messageEvent)
throws Exception {

Integer max = Integer.valueOf(Play.configuration.getProperty("play.netty.maxContentLength", "65345"));

// Upgrade the pipeline as the handshaker needs the HttpStream
// Aggregator
ctx.getPipeline().addLast("fake-aggregator", new HttpChunkAggregator(max));
try {
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(this.getWebSocketLocation(req), null, false);
WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory(
this.getWebSocketLocation(req), null, false);
this.handshaker = wsFactory.newHandshaker(req);
if (this.handshaker == null) {
wsFactory.sendUnsupportedWebSocketVersionResponse(ctx.getChannel());
Expand Down Expand Up @@ -1186,8 +1216,8 @@ public static class WebSocketInvocation extends Invoker.Invocation {
ChannelHandlerContext ctx;
MessageEvent e;

public WebSocketInvocation(Map<String, String> route, Http.Request request, Http.Inbound inbound, Http.Outbound outbound,
ChannelHandlerContext ctx, MessageEvent e) {
public WebSocketInvocation(Map<String, String> route, Http.Request request, Http.Inbound inbound,
Http.Outbound outbound, ChannelHandlerContext ctx, MessageEvent e) {
this.route = route;
this.request = request;
this.inbound = inbound;
Expand Down Expand Up @@ -1218,7 +1248,8 @@ public void execute() throws Exception {

@Override
public void onException(Throwable e) {
Logger.error(e, "Internal Server Error in WebSocket (closing the socket) for request %s", request.method + " " + request.url);
Logger.error(e, "Internal Server Error in WebSocket (closing the socket) for request %s",
request.method + " " + request.url);
ctx.getChannel().close();
super.onException(e);
}
Expand Down
20 changes: 18 additions & 2 deletions framework/src/play/server/ServletWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.apache.commons.io.IOUtils.closeQuietly;

Expand Down Expand Up @@ -64,6 +66,19 @@ public class ServletWrapper extends HttpServlet implements ServletContextListene
public static final String SERVLET_RES = "__SERVLET_RES";

private static boolean routerInitializedWithContext = false;


private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";

/**
* Define allowed methods that will be handled when defined in X-HTTP-Method-Override
* You can define allowed method in
* application.conf: <code>http.allowed.method.override=POST,PUT</code>
*/
private static final Set<String> allowedHttpMethodOverride;
static {
allowedHttpMethodOverride = Stream.of(Play.configuration.getProperty("http.allowed.method.override", "").split(",")).collect(Collectors.toSet());
}

@Override
public void contextInitialized(ServletContextEvent e) {
Expand Down Expand Up @@ -237,8 +252,9 @@ public static Request parseRequest(HttpServletRequest httpServletRequest) throws
contentType = "text/html".intern();
}

if (httpServletRequest.getHeader("X-HTTP-Method-Override") != null) {
method = httpServletRequest.getHeader("X-HTTP-Method-Override").intern();
if (httpServletRequest.getHeader(X_HTTP_METHOD_OVERRIDE) != null && allowedHttpMethodOverride
.contains(httpServletRequest.getHeader(X_HTTP_METHOD_OVERRIDE).intern())) {
method = httpServletRequest.getHeader(X_HTTP_METHOD_OVERRIDE).intern();
}

InputStream body = httpServletRequest.getInputStream();
Expand Down