Skip to content

Commit

Permalink
Merge pull request #1301 from xael-fry/1300_x-http-method-override_al…
Browse files Browse the repository at this point in the history
…lowed_method_config

[#1300] feat: Define allowed methods used in 'X-HTTP-Method-Override'
  • Loading branch information
xael-fry authored Mar 19, 2019
2 parents 43de51c + 184c4af commit 7efdc6e
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 32 deletions.
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

0 comments on commit 7efdc6e

Please sign in to comment.