diff --git a/core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java b/core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java index b3321732737..d85b1759fe8 100644 --- a/core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java +++ b/core/src/main/java/com/linecorp/armeria/server/DefaultServerConfig.java @@ -111,6 +111,7 @@ final class DefaultServerConfig implements ServerConfig { private final boolean enableDateHeader; private final ServerErrorHandler errorHandler; private final Http1HeaderNaming http1HeaderNaming; + private final List additionalAllowedHttpMethods; private final DependencyInjector dependencyInjector; private final Function absoluteUriTransformer; private final long unloggedExceptionsReportIntervalMillis; @@ -148,6 +149,7 @@ final class DefaultServerConfig implements ServerConfig { ServerErrorHandler errorHandler, @Nullable Mapping sslContexts, Http1HeaderNaming http1HeaderNaming, + List additionalAllowedHttpMethods, DependencyInjector dependencyInjector, Function absoluteUriTransformer, long unloggedExceptionsReportIntervalMillis, @@ -259,6 +261,8 @@ final class DefaultServerConfig implements ServerConfig { this.errorHandler = requireNonNull(errorHandler, "errorHandler"); this.sslContexts = sslContexts; this.http1HeaderNaming = requireNonNull(http1HeaderNaming, "http1HeaderNaming"); + this.additionalAllowedHttpMethods = ImmutableList.copyOf( + requireNonNull(additionalAllowedHttpMethods, "additionalAllowedHttpMethods")); this.dependencyInjector = requireNonNull(dependencyInjector, "dependencyInjector"); @SuppressWarnings("unchecked") final Function castAbsoluteUriTransformer = @@ -654,6 +658,11 @@ public Http1HeaderNaming http1HeaderNaming() { return http1HeaderNaming; } + @Override + public List additionalAllowedHttpMethods() { + return additionalAllowedHttpMethods; + } + @Override public DependencyInjector dependencyInjector() { return dependencyInjector; diff --git a/core/src/main/java/com/linecorp/armeria/server/Http1RequestDecoder.java b/core/src/main/java/com/linecorp/armeria/server/Http1RequestDecoder.java index 4311e980e19..67bee29eb64 100644 --- a/core/src/main/java/com/linecorp/armeria/server/Http1RequestDecoder.java +++ b/core/src/main/java/com/linecorp/armeria/server/Http1RequestDecoder.java @@ -197,11 +197,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception cfg, scheme.toString(), reqTarget); // Do not accept unsupported methods. final HttpMethod method = headers.method(); - switch (method) { - case CONNECT: - case UNKNOWN: - fail(id, headers, HttpStatus.METHOD_NOT_ALLOWED, "Unsupported method", null); - return; + if (method == HttpMethod.CONNECT + || (method == HttpMethod.UNKNOWN + && !cfg.additionalAllowedHttpMethods().contains(headers.get(com.linecorp.armeria.common.HttpHeaderNames.METHOD)))) { + fail(id, headers, HttpStatus.METHOD_NOT_ALLOWED, "Unsupported method", null); + return; } // Do not accept the request path '*' for a non-OPTIONS request. diff --git a/core/src/main/java/com/linecorp/armeria/server/Http2RequestDecoder.java b/core/src/main/java/com/linecorp/armeria/server/Http2RequestDecoder.java index bdfe7405be8..b1ec29f22fc 100644 --- a/core/src/main/java/com/linecorp/armeria/server/Http2RequestDecoder.java +++ b/core/src/main/java/com/linecorp/armeria/server/Http2RequestDecoder.java @@ -158,8 +158,11 @@ public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers } break; case UNKNOWN: - writeUnsupportedMethodResponse(streamId, headers); - return; + if (!cfg.additionalAllowedHttpMethods().contains(headers.get(com.linecorp.armeria.common.HttpHeaderNames.METHOD))) { + writeUnsupportedMethodResponse(streamId, headers); + return; + } + break; } // Do not accept the request path '*' for a non-OPTIONS request. diff --git a/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java b/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java index 9e77ce63c84..8c7e4c9d097 100644 --- a/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java +++ b/core/src/main/java/com/linecorp/armeria/server/ServerBuilder.java @@ -41,6 +41,7 @@ import java.security.cert.X509Certificate; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -73,6 +74,7 @@ import com.linecorp.armeria.common.Flags; import com.linecorp.armeria.common.Http1HeaderNaming; import com.linecorp.armeria.common.HttpHeaderNames; +import com.linecorp.armeria.common.HttpMethod; import com.linecorp.armeria.common.HttpRequest; import com.linecorp.armeria.common.HttpResponse; import com.linecorp.armeria.common.Request; @@ -229,6 +231,7 @@ public final class ServerBuilder implements TlsSetters, ServiceConfigsBuilder additionalAllowedHttpMethods = new ArrayList<>(); @Nullable private DependencyInjector dependencyInjector; private Function absoluteUriTransformer = Function.identity(); @@ -2220,6 +2223,17 @@ public ServerBuilder http1HeaderNaming(Http1HeaderNaming http1HeaderNaming) { return this; } + /** + * Adds additional allowed HTTP methods to the default set of allowed {@link HttpMethod}s. + * + * @param additionalAllowedHttpMethods the additional allowed HTTP methods + */ + public ServerBuilder additionalAllowedHttpMethods(String... additionalAllowedHttpMethods) { + requireNonNull(additionalAllowedHttpMethods, "additionalAllowedHttpMethods"); + this.additionalAllowedHttpMethods.addAll(Arrays.asList(additionalAllowedHttpMethods)); + return this; + } + /** * Sets the interval between reporting exceptions which is not logged * by any decorators or services such as {@link LoggingService}. @@ -2427,7 +2441,7 @@ ports, setSslContextIfAbsent(defaultVirtualHost, defaultSslContext), childChannelPipelineCustomizer, clientAddressSources, clientAddressTrustedProxyFilter, clientAddressFilter, clientAddressMapper, enableServerHeader, enableDateHeader, errorHandler, sslContexts, - http1HeaderNaming, dependencyInjector, absoluteUriTransformer, + http1HeaderNaming, additionalAllowedHttpMethods, dependencyInjector, absoluteUriTransformer, unloggedExceptionsReportIntervalMillis, ImmutableList.copyOf(shutdownSupports)); } diff --git a/core/src/main/java/com/linecorp/armeria/server/ServerConfig.java b/core/src/main/java/com/linecorp/armeria/server/ServerConfig.java index cc74c421160..bdec14e8498 100644 --- a/core/src/main/java/com/linecorp/armeria/server/ServerConfig.java +++ b/core/src/main/java/com/linecorp/armeria/server/ServerConfig.java @@ -335,6 +335,11 @@ default boolean shutdownBlockingTaskExecutorOnStop() { */ Http1HeaderNaming http1HeaderNaming(); + /** + * Returns the list of HTTP methods that are allowed to be used in addition to the standard HTTP methods. + */ + List additionalAllowedHttpMethods(); + /** * Returns the {@link DependencyInjector} that injects dependencies in annotations. */ diff --git a/core/src/main/java/com/linecorp/armeria/server/UpdatableServerConfig.java b/core/src/main/java/com/linecorp/armeria/server/UpdatableServerConfig.java index e3c327bdb64..ac9ddf4140a 100644 --- a/core/src/main/java/com/linecorp/armeria/server/UpdatableServerConfig.java +++ b/core/src/main/java/com/linecorp/armeria/server/UpdatableServerConfig.java @@ -305,6 +305,11 @@ public Http1HeaderNaming http1HeaderNaming() { return delegate.http1HeaderNaming(); } + @Override + public List additionalAllowedHttpMethods() { + return delegate.additionalAllowedHttpMethods(); + } + @Override public DependencyInjector dependencyInjector() { return delegate.dependencyInjector(); diff --git a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java index 13d3c159d61..ce03ed919a2 100644 --- a/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java +++ b/tomcat10/src/main/java/com/linecorp/armeria/server/tomcat/TomcatService.java @@ -532,8 +532,7 @@ private Request convertRequest(ServiceRequestContext ctx, String mappedPath, Agg coyoteReq.protocol().setString(ctx.sessionProtocol().isMultiplex() ? "HTTP/2.0" : "HTTP/1.1"); // Set the method. - final HttpMethod method = req.method(); - coyoteReq.method().setString(method.name()); + coyoteReq.method().setString(req.headers().get(HttpHeaderNames.METHOD)); // Set the request URI. final byte[] uriBytes = mappedPath.getBytes(StandardCharsets.US_ASCII); diff --git a/tomcat10/src/test/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatServiceTest.java b/tomcat10/src/test/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatServiceTest.java index 6d0b1e199a3..59f6565d6d4 100644 --- a/tomcat10/src/test/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatServiceTest.java +++ b/tomcat10/src/test/java/com/linecorp/armeria/server/tomcat/UnmanagedTomcatServiceTest.java @@ -26,7 +26,10 @@ import java.net.Socket; import java.nio.charset.StandardCharsets; +import org.apache.catalina.Context; +import org.apache.catalina.Wrapper; import org.apache.catalina.connector.Connector; +import org.apache.catalina.servlets.WebdavServlet; import org.apache.catalina.startup.Tomcat; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -46,6 +49,7 @@ class UnmanagedTomcatServiceTest { private static Tomcat tomcatWithWebApp; private static Tomcat tomcatWithoutWebApp; + private static Tomcat tomcatWithPropfind; @RegisterExtension static final ServerExtension server = new ServerExtension() { @@ -66,16 +70,32 @@ protected void configure(ServerBuilder sb) throws Exception { "tomcat-" + UnmanagedTomcatServiceTest.class.getSimpleName() + "-2"); assertThat(TomcatUtil.engine(tomcatWithoutWebApp.getService(), "bar")).isNotNull(); + tomcatWithPropfind = new Tomcat(); + tomcatWithPropfind.setPort(0); + + Context ctx = tomcatWithPropfind.addContext("", "build" + File.separatorChar + + "tomcat-" + UnmanagedTomcatServiceTest.class.getSimpleName() + "-3"); + + Wrapper webdavServlet = Tomcat.addServlet(ctx, "webdav", new WebdavServlet()); + webdavServlet.addInitParameter("readonly", "false"); + webdavServlet.addInitParameter("listings", "true"); + + ctx.addServletMappingDecoded("/*", "webdav"); + assertThat(TomcatUtil.engine(tomcatWithPropfind.getService(), "foobar")).isNotNull(); + // Start the Tomcats. tomcatWithWebApp.start(); tomcatWithoutWebApp.start(); + tomcatWithPropfind.start(); // Bind them to the Server. sb.serviceUnder("/empty/", TomcatService.of(new Connector(), "someHost")) .serviceUnder("/some-webapp-nohostname/", TomcatService.of(tomcatWithWebApp.getConnector())) .serviceUnder("/no-webapp/", TomcatService.of(tomcatWithoutWebApp)) - .serviceUnder("/some-webapp/", TomcatService.of(tomcatWithWebApp)); + .serviceUnder("/some-webapp/", TomcatService.of(tomcatWithWebApp)) + .serviceUnder("/propfind/", TomcatService.of(tomcatWithPropfind)) + .additionalAllowedHttpMethods("PROPFIND"); } }; @@ -89,6 +109,10 @@ static void destroyTomcat() throws Exception { tomcatWithoutWebApp.stop(); tomcatWithoutWebApp.destroy(); } + if (tomcatWithPropfind != null) { + tomcatWithPropfind.stop(); + tomcatWithPropfind.destroy(); + } } @Test @@ -147,4 +171,20 @@ void okWithoutAuthorityHeader() throws Exception { } } } + + @Test + void okWithCustomMethod() throws Exception { + final int port = server.httpPort(); + try (Socket s = new Socket(NetUtil.LOCALHOST, port)) { + final InputStream in = s.getInputStream(); + final OutputStream out = s.getOutputStream(); + out.write(("PROPFIND /propfind/ HTTP/1.1\r\n" + + "Content-Length: 0\r\n" + + "Connection: close\r\n\r\n").getBytes(StandardCharsets.US_ASCII)); + + try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) { + assertThat(br.readLine()).isEqualTo("HTTP/1.1 200 OK"); + } + } + } }