├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── maven.yml ├── httpserver ├── src │ ├── main │ │ ├── resources │ │ │ └── META-INF │ │ │ │ └── native-image │ │ │ │ └── io.esastack │ │ │ │ └── httpserver │ │ │ │ ├── native-image.properties │ │ │ │ └── reflection-config.json │ │ └── java │ │ │ └── io │ │ │ └── esastack │ │ │ └── httpserver │ │ │ ├── metrics │ │ │ ├── RequestMetrics.java │ │ │ ├── impl │ │ │ │ ├── RequestMetricsImpl.java │ │ │ │ ├── MetricsReporter.java │ │ │ │ └── OpenSslSessionMetricsImpl.java │ │ │ ├── Metrics.java │ │ │ ├── ConnectionMetrics.java │ │ │ └── OpenSslSessionMetrics.java │ │ │ ├── impl │ │ │ ├── Http2HeadersImpl.java │ │ │ ├── OnChannelActiveHandler.java │ │ │ ├── HAProxyMessageHandler.java │ │ │ ├── RequestDecoder.java │ │ │ ├── SslCompletionHandler.java │ │ │ ├── Http1RequestHandleImpl.java │ │ │ ├── HAProxyDetector.java │ │ │ ├── MultipartFileImpl.java │ │ │ ├── H2cDetector.java │ │ │ ├── AggregationHandle.java │ │ │ ├── SslDetector.java │ │ │ ├── SslHelper.java │ │ │ ├── Http2ChunkedInput.java │ │ │ ├── AggregatedLastHttpContent.java │ │ │ ├── MultipartHandle.java │ │ │ ├── Http2ConnectionChunkHandlerBuilder.java │ │ │ ├── Http2RequestHandleImpl.java │ │ │ └── Http2ConnectionChunkHandler.java │ │ │ ├── utils │ │ │ ├── Loggers.java │ │ │ ├── Constants.java │ │ │ └── LoggedThreadFactory.java │ │ │ ├── HAProxyMode.java │ │ │ ├── core │ │ │ ├── Aggregation.java │ │ │ ├── MultiPart.java │ │ │ ├── Request.java │ │ │ ├── MultipartFile.java │ │ │ ├── RequestHandle.java │ │ │ └── BaseRequest.java │ │ │ ├── transport │ │ │ ├── Transports.java │ │ │ ├── Transport.java │ │ │ ├── EpollTransport.java │ │ │ └── NioTransport.java │ │ │ ├── H2OptionsConfigure.java │ │ │ ├── H2Options.java │ │ │ ├── MultipartOptionsConfigure.java │ │ │ ├── MultipartOptions.java │ │ │ ├── SslOptionsConfigure.java │ │ │ └── SslOptions.java │ └── test │ │ └── java │ │ └── io │ │ └── esastack │ │ └── httpserver │ │ ├── transport │ │ ├── TransportsTest.java │ │ ├── EpollTransportTest.java │ │ └── NioTransportTest.java │ │ ├── metrics │ │ └── impl │ │ │ ├── RequestMetricsImplTest.java │ │ │ └── OpenSslSessionMetricsImplTest.java │ │ ├── impl │ │ ├── Http2HeadersImplTest.java │ │ ├── OnChannelActiveHandlerTest.java │ │ ├── RequestDecoderTest.java │ │ ├── Http1RequestHandleImplTest.java │ │ ├── SslHelperTest.java │ │ ├── AggregationHandleTest.java │ │ ├── ServerRuntimeTest.java │ │ ├── SslCompletionHandlerTest.java │ │ ├── MultipartFileImplTest.java │ │ ├── AggregatedLastHttpContentTest.java │ │ ├── SslDetectorTest.java │ │ ├── MultipartHandleTest.java │ │ ├── HAProxyTest.java │ │ ├── Http2ConnectionChunkHandlerBuilderTest.java │ │ └── Http2RequestHandleImplTest.java │ │ ├── utils │ │ └── LoggedThreadFactoryTest.java │ │ ├── H2OptionsTest.java │ │ ├── it │ │ ├── DiscardTest.java │ │ └── ChunkWriteTest.java │ │ ├── MultipartOptionsTest.java │ │ └── SslOptionsTest.java └── pom.xml ├── .gitignore ├── CONTRIBUTING.md └── README.md /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Expected behavior 11 | 12 | ### Actual behavior 13 | 14 | ### Steps to reproduce 15 | 16 | ### Env 17 | - HttpServer version: 18 | - JVM version: 19 | - OS: 20 | -------------------------------------------------------------------------------- /httpserver/src/main/resources/META-INF/native-image/io.esastack/httpserver/native-image.properties: -------------------------------------------------------------------------------- 1 | Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json\ 2 | \ --initialize-at-build-time=io.esastack.httpserver\ 3 | \ --initialize-at-run-time=io.esastack.httpserver.impl.H2cDetector,\ 4 | io.esastack.httpserver.transport.NioTransport 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # maven ignore 2 | target/ 3 | *.jar 4 | # enable mvnw 5 | !.mvn/wrapper/* 6 | *.war 7 | *.zip 8 | *.tar 9 | *.tar.gz 10 | 11 | # eclipse ignore 12 | .settings/ 13 | .project 14 | .classpath 15 | 16 | # idea ignore 17 | .idea/ 18 | *.ipr 19 | *.iml 20 | *.iws 21 | 22 | # vscode files 23 | .vscode/ 24 | *.factorypath 25 | 26 | # temp ignore 27 | *.log 28 | *.cache 29 | *.diff 30 | *.patch 31 | *.tmp 32 | 33 | # system ignore 34 | .DS_Store 35 | Thumbs.db 36 | 37 | logs/ 38 | tmp/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 4 | 5 | ## Submitting a pull request 6 | 7 | 1. [Fork][fork] and clone the repository 8 | 2. Create a new branch: `git checkout -b my-branch-name` 9 | 3. Make your change and remember to add tests 10 | 4. Build the project locally and run local tests 11 | 5. Push to your fork and [submit a pull request][pr] 12 | 6. Pat your self on the back and wait for your pull request to be reviewed and merged. 13 | 14 | Thanks for your contributing! 15 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/RequestMetrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics; 17 | 18 | public interface RequestMetrics { 19 | 20 | /** 21 | * Returns the accumulated count of request. 22 | * 23 | * @return count 24 | */ 25 | long requestCount(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /httpserver/src/main/resources/META-INF/native-image/io.esastack/httpserver/reflection-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"io.esastack.httpserver.impl.Http1Handler", 4 | "methods":[ 5 | {"name":"channelInactive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, 6 | {"name":"channelReadComplete","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }, 7 | {"name":"exceptionCaught","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Throwable"] }, 8 | {"name":"userEventTriggered","parameterTypes":["io.netty.channel.ChannelHandlerContext","java.lang.Object"] } 9 | ] 10 | }, 11 | { 12 | "name":"io.esastack.httpserver.impl.HttpServerChannelInitializr" 13 | }, 14 | { 15 | "name":"io.esastack.httpserver.impl.OnChannelActiveHandler", 16 | "methods":[{"name":"channelActive","parameterTypes":["io.netty.channel.ChannelHandlerContext"] }] 17 | }, 18 | { 19 | "name":"io.esastack.httpserver.impl.RequestDecoder" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/Http2HeadersImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.netty.http.Http2HeadersAdaptor; 19 | import io.netty.handler.codec.http2.Http2Headers; 20 | 21 | final class Http2HeadersImpl extends Http2HeadersAdaptor { 22 | 23 | Http2HeadersImpl() { 24 | super(); 25 | } 26 | 27 | Http2HeadersImpl(Http2Headers headers) { 28 | super(headers); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/utils/Loggers.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.utils; 17 | 18 | import esa.commons.annotation.Internal; 19 | import esa.commons.logging.Logger; 20 | import esa.commons.logging.LoggerFactory; 21 | 22 | @Internal 23 | public final class Loggers { 24 | private static final Logger HTTP_SERVER_LOG = LoggerFactory.getLogger("io.esastack.httpserver"); 25 | 26 | public static Logger logger() { 27 | return HTTP_SERVER_LOG; 28 | } 29 | 30 | private Loggers() { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/impl/RequestMetricsImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics.impl; 17 | 18 | import io.esastack.httpserver.metrics.RequestMetrics; 19 | 20 | import java.util.concurrent.atomic.LongAdder; 21 | 22 | final class RequestMetricsImpl implements RequestMetrics { 23 | 24 | static final RequestMetrics DISABLED = () -> 0L; 25 | 26 | final LongAdder requestCount = new LongAdder(); 27 | 28 | @Override 29 | public long requestCount() { 30 | return requestCount.sum(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/utils/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.utils; 17 | 18 | import io.netty.util.AsciiString; 19 | import io.netty.util.AttributeKey; 20 | 21 | public final class Constants { 22 | 23 | /** 24 | * Time to First Byte 25 | */ 26 | public static final AsciiString TTFB = AsciiString.cached("$ttfb"); 27 | public static final AttributeKey SCHEME = AttributeKey.valueOf("$schema"); 28 | public static final String SCHEMA_HTTP = "http"; 29 | public static final String SCHEMA_HTTPS = "https"; 30 | 31 | private Constants() { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/HAProxyMode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | public enum HAProxyMode { 19 | 20 | /** 21 | * Enabled to using the HAProxy decoding, and we will NOT detect whether the HAProxy protocol is used in the coming 22 | * new connection, which means the HAProxy Protocol is required. 23 | */ 24 | ON, 25 | /** 26 | * Enabled to using the HAProxy decoding, and we will detect whether the HAProxy protocol is used in the coming new 27 | * connection. 28 | */ 29 | AUTO, 30 | /** 31 | * Disabled to using the HAProxy decoding. 32 | */ 33 | OFF 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - '**-alpha' 10 | - '**-tmp' 11 | - '**-temp' 12 | pull_request: 13 | branches-ignore: 14 | - '**-alpha' 15 | - '**-tmp' 16 | - '**-temp' 17 | workflow_dispatch: 18 | inputs: 19 | profiles: 20 | description: 'mvn -P ' 21 | required: false 22 | default: '' 23 | 24 | jobs: 25 | build: 26 | 27 | runs-on: ubuntu-latest 28 | 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Set up JDK 1.8 32 | uses: actions/setup-java@v1 33 | with: 34 | java-version: 1.8 35 | - name: Clean and Package 36 | if: github.event.inputs.profiles == '' 37 | run: mvn clean package 38 | - name: Clean and Package With Profiles 39 | if: github.event.inputs.profiles != '' 40 | run: mvn -P ${{ github.event.inputs.profiles }} clean package 41 | - name: Upload coverage to Codecov 42 | if: github.event_name == 'push' 43 | uses: codecov/codecov-action@v1.0.15 44 | with: 45 | token: ${{secrets.CODECOV_TOKEN}} 46 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/transport/TransportsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import io.netty.channel.epoll.Epoll; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertTrue; 22 | 23 | class TransportsTest { 24 | 25 | @Test 26 | void testPreferNative() { 27 | assertTrue(Transports.transport(false) instanceof NioTransport); 28 | 29 | if (Epoll.isAvailable()) { 30 | assertTrue(Transports.transport(true) instanceof EpollTransport); 31 | } else { 32 | assertTrue(Transports.transport(true) instanceof NioTransport); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/core/Aggregation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.core; 17 | 18 | import io.esastack.commons.net.http.HttpHeaders; 19 | import io.netty.buffer.ByteBuf; 20 | 21 | /** 22 | * An interface defines the body and trailing headers part of a http request. 23 | */ 24 | public interface Aggregation { 25 | 26 | /** 27 | * Gets the request body. 28 | * 29 | * @return body or {@code empty} if there's no data received. 30 | */ 31 | ByteBuf body(); 32 | 33 | /** 34 | * Gets the request trailing headers. 35 | * 36 | * @return headers or {@code empty} if there's no trailer received. 37 | */ 38 | HttpHeaders trailers(); 39 | } 40 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/metrics/impl/RequestMetricsImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics.impl; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | 22 | class RequestMetricsImplTest { 23 | 24 | @Test 25 | void testDisabled() { 26 | assertEquals(0L, RequestMetricsImpl.DISABLED.requestCount()); 27 | } 28 | 29 | @Test 30 | void testCount() { 31 | final RequestMetricsImpl metrics = new RequestMetricsImpl(); 32 | assertEquals(0, metrics.requestCount()); 33 | metrics.requestCount.increment(); 34 | assertEquals(1L, metrics.requestCount()); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/Http2HeadersImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | import static org.junit.jupiter.api.Assertions.assertNotNull; 22 | import static org.junit.jupiter.api.Assertions.assertTrue; 23 | 24 | class Http2HeadersImplTest { 25 | 26 | @Test 27 | void testUnwrap() { 28 | final Http2HeadersImpl headers = new Http2HeadersImpl(); 29 | assertNotNull(headers.unwrap()); 30 | assertTrue(headers.unwrap().isEmpty()); 31 | headers.add("a", "1"); 32 | assertEquals("1", headers.unwrap().get("a")); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/core/MultiPart.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.core; 17 | 18 | import esa.commons.collection.MultiValueMap; 19 | 20 | import java.util.List; 21 | 22 | /** 23 | * An interface defines the multipart data of a multipart request. 24 | */ 25 | public interface MultiPart { 26 | 27 | /** 28 | * Returns the uploaded files of this multipart request. 29 | * 30 | * @return files or {@code empty} if there's no files received in this request. 31 | */ 32 | List uploadFiles(); 33 | 34 | /** 35 | * Returns the attributes of this multipart request. 36 | * 37 | * @return attributes or {@code empty} if there's no attributes received in this request. 38 | */ 39 | MultiValueMap attributes(); 40 | 41 | } 42 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/transport/Transports.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import io.netty.channel.epoll.Epoll; 19 | 20 | public final class Transports { 21 | 22 | /** 23 | * Fetches an instance of {@link Transport}. 24 | * 25 | * @param preferNative is prefer to use native transports such as {@link EpollTransport} 26 | * 27 | * @return suitable instance of {@link Transport}. 28 | */ 29 | public static Transport transport(boolean preferNative) { 30 | Transport transport = null; 31 | try { 32 | if (preferNative && Epoll.isAvailable()) { 33 | transport = new EpollTransport(); 34 | } 35 | } catch (Throwable ignored) { 36 | } 37 | if (transport == null) { 38 | transport = NioTransport.INSTANCE; 39 | } 40 | return transport; 41 | } 42 | 43 | private Transports() { 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/Metrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics; 17 | 18 | /** 19 | * Metrics Interface 20 | */ 21 | public interface Metrics { 22 | 23 | /** 24 | * Indicates whether metrics is enabled. 25 | * 26 | * @return {@code true} metrics is enabled for current server, otherwise {@code false} 27 | */ 28 | boolean enabled(); 29 | 30 | /** 31 | * Gets the instance of {@link RequestMetrics}. 32 | * 33 | * @return instance of {@link RequestMetrics} 34 | */ 35 | RequestMetrics request(); 36 | 37 | /** 38 | * Gets the instance of {@link ConnectionMetrics}. 39 | * 40 | * @return instance of {@link ConnectionMetrics} 41 | */ 42 | ConnectionMetrics connection(); 43 | 44 | /** 45 | * Gets the instance of {@link OpenSslSessionMetrics}. 46 | * 47 | * @return instance of {@link OpenSslSessionMetrics} 48 | */ 49 | OpenSslSessionMetrics openSslSession(); 50 | } 51 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/utils/LoggedThreadFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.utils; 17 | 18 | import esa.commons.concurrent.InternalThread; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import static org.junit.jupiter.api.Assertions.assertFalse; 22 | import static org.junit.jupiter.api.Assertions.assertTrue; 23 | 24 | class LoggedThreadFactoryTest { 25 | 26 | @Test 27 | void testNewThread() { 28 | LoggedThreadFactory factory = new LoggedThreadFactory("foo", true); 29 | Thread thread = factory.newThread(() -> { 30 | }); 31 | assertTrue(thread.getName().startsWith("foo")); 32 | assertTrue(thread.isDaemon()); 33 | assertTrue(thread instanceof InternalThread); 34 | 35 | factory = new LoggedThreadFactory("bar"); 36 | thread = factory.newThread(() -> { 37 | }); 38 | assertTrue(thread.getName().startsWith("bar")); 39 | assertFalse(thread.isDaemon()); 40 | assertTrue(thread instanceof InternalThread); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/OnChannelActiveHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.Checks; 19 | import io.esastack.httpserver.utils.Loggers; 20 | import io.netty.channel.ChannelHandlerContext; 21 | import io.netty.channel.ChannelInboundHandlerAdapter; 22 | 23 | import java.util.function.Consumer; 24 | 25 | final class OnChannelActiveHandler extends ChannelInboundHandlerAdapter { 26 | 27 | private final Consumer onConnect; 28 | 29 | OnChannelActiveHandler(Consumer onConnect) { 30 | Checks.checkNotNull(onConnect, "onConnect"); 31 | this.onConnect = onConnect; 32 | } 33 | 34 | @Override 35 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 36 | try { 37 | onConnect.accept(ctx); 38 | } catch (Throwable t) { 39 | Loggers.logger().error("Error while processing onConnect handler.", t); 40 | } 41 | ctx.pipeline().remove(this); 42 | super.channelActive(ctx); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/HAProxyMessageHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.channel.ChannelHandler; 19 | import io.netty.channel.ChannelHandlerContext; 20 | import io.netty.channel.ChannelInboundHandlerAdapter; 21 | import io.netty.handler.codec.haproxy.HAProxyMessage; 22 | import io.netty.handler.codec.haproxy.HAProxyProxiedProtocol; 23 | 24 | import java.net.InetSocketAddress; 25 | 26 | @ChannelHandler.Sharable 27 | final class HAProxyMessageHandler extends ChannelInboundHandlerAdapter { 28 | 29 | @Override 30 | public void channelRead(ChannelHandlerContext ctx, Object o) throws Exception { 31 | if (o instanceof HAProxyMessage) { 32 | HAProxyMessage msg = (HAProxyMessage) o; 33 | if (!HAProxyProxiedProtocol.UNKNOWN.equals(msg.proxiedProtocol()) 34 | && msg.sourceAddress() != null) { 35 | ctx.channel() 36 | .attr(Utils.SOURCE_ADDRESS) 37 | .set(InetSocketAddress.createUnresolved(msg.sourceAddress(), msg.sourcePort())); 38 | } 39 | msg.release(); 40 | ctx.pipeline().remove(this); 41 | } else { 42 | super.channelRead(ctx, o); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/RequestDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.netty.http.Http1HeadersImpl; 19 | import io.esastack.httpserver.utils.Constants; 20 | import io.netty.handler.codec.http.DefaultHttpRequest; 21 | import io.netty.handler.codec.http.HttpMessage; 22 | import io.netty.handler.codec.http.HttpMethod; 23 | import io.netty.handler.codec.http.HttpRequestDecoder; 24 | import io.netty.handler.codec.http.HttpVersion; 25 | 26 | /** 27 | * This implementation of {@link HttpRequestDecoder} will set a value of timestamp into the underlying of req after 28 | * decoding req line. 29 | */ 30 | final class RequestDecoder extends HttpRequestDecoder { 31 | 32 | RequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) { 33 | super(maxInitialLineLength, maxHeaderSize, maxChunkSize); 34 | } 35 | 36 | @Override 37 | protected HttpMessage createMessage(String[] initialLine) { 38 | HttpMessage msg = new DefaultHttpRequest( 39 | HttpVersion.valueOf(initialLine[2]), 40 | HttpMethod.valueOf(initialLine[0]), 41 | initialLine[1], 42 | new Http1HeadersImpl()); 43 | msg.headers().add(Constants.TTFB, System.currentTimeMillis()); 44 | return msg; 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/impl/MetricsReporter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics.impl; 17 | 18 | import io.esastack.commons.net.http.HttpVersion; 19 | import io.esastack.httpserver.core.BaseRequest; 20 | import io.esastack.httpserver.metrics.Metrics; 21 | import io.netty.channel.Channel; 22 | import io.netty.handler.ssl.SslContext; 23 | 24 | /** 25 | * Interface defines events that may be used in metrics. 26 | */ 27 | public interface MetricsReporter extends Metrics { 28 | 29 | /** 30 | * Channel active. 31 | * 32 | * @param ch channel 33 | * @param httpVersion http version 34 | */ 35 | void reportConnect(Channel ch, HttpVersion httpVersion); 36 | 37 | /** 38 | * Channel inactive. 39 | * 40 | * @param ch channel 41 | */ 42 | void reportDisconnect(Channel ch); 43 | 44 | /** 45 | * Channel upgrade from http1 to http2. 46 | * 47 | * @param ch channel 48 | */ 49 | void reportUpgrade(Channel ch); 50 | 51 | /** 52 | * Request received. 53 | * 54 | * @param request request 55 | */ 56 | void reportRequest(BaseRequest request); 57 | 58 | /** 59 | * Server is about to starting to initialize ssl context. 60 | * 61 | * @param context ssl context 62 | */ 63 | void initSsl(SslContext context); 64 | } 65 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/transport/Transport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import io.esastack.httpserver.NetOptions; 19 | import io.netty.bootstrap.ServerBootstrap; 20 | import io.netty.channel.ChannelFactory; 21 | import io.netty.channel.EventLoopGroup; 22 | import io.netty.channel.ServerChannel; 23 | 24 | import java.net.SocketAddress; 25 | import java.util.concurrent.ThreadFactory; 26 | 27 | /** 28 | * An interface defines the traits of transport, such as NIO, Epoll. 29 | */ 30 | public interface Transport { 31 | 32 | /** 33 | * Creates a server {@link ChannelFactory}. 34 | * 35 | * @param local address to bind 36 | * 37 | * @return channel factory 38 | */ 39 | ChannelFactory serverChannelFactory(SocketAddress local); 40 | 41 | /** 42 | * Creates a {@link EventLoopGroup} 43 | * 44 | * @param nThreads threads count 45 | * @param threadFactory thread factory to use 46 | * 47 | * @return instance of {@link EventLoopGroup} 48 | */ 49 | EventLoopGroup loop(int nThreads, ThreadFactory threadFactory); 50 | 51 | /** 52 | * Applies the given {@code options} to the {@code bootstrap}. 53 | * 54 | * @param bootstrap bootstrap 55 | * @param options options 56 | * @param local address to bind 57 | */ 58 | void applyOptions(ServerBootstrap bootstrap, NetOptions options, SocketAddress local); 59 | 60 | } 61 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/H2OptionsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.junit.jupiter.api.Assertions.assertEquals; 21 | import static org.junit.jupiter.api.Assertions.assertTrue; 22 | 23 | class H2OptionsTest { 24 | 25 | @Test 26 | void testConfigure() { 27 | final H2Options options = H2OptionsConfigure.newOpts() 28 | .enabled(true) 29 | .gracefulShutdownTimeoutMillis(1L) 30 | .maxFrameSize(1) 31 | .maxReservedStreams(2) 32 | .configured(); 33 | 34 | assertTrue(options.isEnabled()); 35 | assertEquals(1L, options.getGracefulShutdownTimeoutMillis()); 36 | assertEquals(1, options.getMaxFrameSize()); 37 | assertEquals(2, options.getMaxReservedStreams()); 38 | } 39 | 40 | @Test 41 | void testCopyFromAnother() { 42 | 43 | final H2Options another = H2OptionsConfigure.newOpts() 44 | .enabled(true) 45 | .gracefulShutdownTimeoutMillis(1L) 46 | .maxFrameSize(1) 47 | .maxReservedStreams(2) 48 | .configured(); 49 | 50 | final H2Options options = new H2Options(another); 51 | assertTrue(options.isEnabled()); 52 | assertEquals(1L, options.getGracefulShutdownTimeoutMillis()); 53 | assertEquals(1, options.getMaxFrameSize()); 54 | assertEquals(2, options.getMaxReservedStreams()); 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/it/DiscardTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.it; 17 | 18 | import esa.commons.NetworkUtils; 19 | import io.esastack.httpserver.HttpServer; 20 | import io.esastack.httpserver.ServerOptionsConfigure; 21 | import io.netty.handler.logging.LogLevel; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import java.io.IOException; 25 | import java.net.HttpURLConnection; 26 | import java.net.URL; 27 | 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | 30 | class DiscardTest { 31 | 32 | @Test 33 | void testDiscard() throws IOException { 34 | final int port = NetworkUtils.selectRandomPort(); 35 | final HttpServer server = HttpServer.create(ServerOptionsConfigure.newOpts() 36 | .logging(LogLevel.INFO) 37 | .ioThreads(1) 38 | .configured()) 39 | .handle(req -> req.response().end()); 40 | 41 | server.listen(port); 42 | 43 | HttpURLConnection httpURLConnection = null; 44 | try { 45 | httpURLConnection = 46 | (HttpURLConnection) new URL("http://localhost:" + port).openConnection(); 47 | assertEquals(200, httpURLConnection.getResponseCode()); 48 | } finally { 49 | try { 50 | if (httpURLConnection != null) { 51 | httpURLConnection.disconnect(); 52 | } 53 | } catch (Exception ignored) { 54 | } 55 | server.close(); 56 | } 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/OnChannelActiveHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.ExceptionUtils; 19 | import io.netty.channel.embedded.EmbeddedChannel; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.util.concurrent.atomic.AtomicBoolean; 23 | 24 | import static org.junit.jupiter.api.Assertions.assertNull; 25 | import static org.junit.jupiter.api.Assertions.assertThrows; 26 | import static org.junit.jupiter.api.Assertions.assertTrue; 27 | 28 | class OnChannelActiveHandlerTest { 29 | 30 | @Test 31 | void testChannelActiveEvent() { 32 | assertThrows(NullPointerException.class, () -> new OnChannelActiveHandler(null)); 33 | final AtomicBoolean active = new AtomicBoolean(false); 34 | final EmbeddedChannel channel = new EmbeddedChannel(new OnChannelActiveHandler(ctx -> { 35 | active.set(true); 36 | })); 37 | assertTrue(channel.isActive()); 38 | assertTrue(active.get()); 39 | assertNull(channel.pipeline().get(OnChannelActiveHandler.class)); 40 | } 41 | 42 | @Test 43 | void testError() { 44 | assertThrows(NullPointerException.class, () -> new OnChannelActiveHandler(null)); 45 | final EmbeddedChannel channel = new EmbeddedChannel(new OnChannelActiveHandler(ctx -> { 46 | ExceptionUtils.throwException(new IllegalStateException()); 47 | })); 48 | assertTrue(channel.isActive()); 49 | assertNull(channel.pipeline().get(OnChannelActiveHandler.class)); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/H2OptionsConfigure.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | public final class H2OptionsConfigure { 19 | private boolean enabled; 20 | private int maxReservedStreams; 21 | private int maxFrameSize; 22 | private long gracefulShutdownTimeoutMillis = 60L * 1000L; 23 | 24 | private H2OptionsConfigure() { 25 | } 26 | 27 | public static H2OptionsConfigure newOpts() { 28 | return new H2OptionsConfigure(); 29 | } 30 | 31 | public H2OptionsConfigure enabled(boolean enabled) { 32 | this.enabled = enabled; 33 | return this; 34 | } 35 | 36 | public H2OptionsConfigure maxReservedStreams(int maxReservedStreams) { 37 | this.maxReservedStreams = maxReservedStreams; 38 | return this; 39 | } 40 | 41 | public H2OptionsConfigure maxFrameSize(int maxFrameSize) { 42 | this.maxFrameSize = maxFrameSize; 43 | return this; 44 | } 45 | 46 | public H2OptionsConfigure gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) { 47 | this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis; 48 | return this; 49 | } 50 | 51 | public H2Options configured() { 52 | H2Options h2Options = new H2Options(); 53 | h2Options.setEnabled(enabled); 54 | h2Options.setMaxReservedStreams(maxReservedStreams); 55 | h2Options.setMaxFrameSize(maxFrameSize); 56 | h2Options.setGracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis); 57 | return h2Options; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/RequestDecoderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.netty.http.Http1HeadersImpl; 19 | import io.esastack.httpserver.utils.Constants; 20 | import io.netty.buffer.Unpooled; 21 | import io.netty.channel.embedded.EmbeddedChannel; 22 | import io.netty.handler.codec.http.HttpRequest; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import java.nio.charset.StandardCharsets; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertNotNull; 28 | import static org.junit.jupiter.api.Assertions.assertTrue; 29 | 30 | class RequestDecoderTest { 31 | 32 | @Test 33 | void testTttfbAndHeaderType() { 34 | final EmbeddedChannel channel = 35 | new EmbeddedChannel(new RequestDecoder(1024, 36 | 1024, 37 | 1024)); 38 | final String reqStr = "GET /some/path HTTP/1.1\r\n" + 39 | "Content-Length: 8\r\n" + 40 | "\r\n" + 41 | "12345678"; 42 | final long start = System.currentTimeMillis(); 43 | assertTrue(channel.writeInbound(Unpooled.copiedBuffer(reqStr.getBytes(StandardCharsets.US_ASCII)))); 44 | final HttpRequest req = channel.readInbound(); 45 | assertNotNull(req); 46 | assertTrue(req.headers().contains(Constants.TTFB)); 47 | assertTrue(start <= Long.parseLong(req.headers().get(Constants.TTFB))); 48 | assertTrue(req.headers() instanceof Http1HeadersImpl); 49 | assertTrue(channel.finish()); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/SslCompletionHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.function.Consumer3; 19 | import io.netty.channel.Channel; 20 | import io.netty.channel.ChannelHandlerContext; 21 | import io.netty.channel.ChannelInboundHandlerAdapter; 22 | import io.netty.handler.ssl.SslHandshakeCompletionEvent; 23 | 24 | final class SslCompletionHandler extends ChannelInboundHandlerAdapter { 25 | 26 | private final Consumer3 callback; 27 | 28 | SslCompletionHandler(Consumer3 callback) { 29 | this.callback = callback; 30 | } 31 | 32 | @Override 33 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 34 | if (evt instanceof SslHandshakeCompletionEvent) { 35 | SslHandshakeCompletionEvent event = (SslHandshakeCompletionEvent) evt; 36 | if (event.isSuccess()) { 37 | ctx.pipeline().remove(this); 38 | callback.accept(true, ctx.channel(), null); 39 | } else { 40 | ctx.pipeline().remove(this); 41 | callback.accept(false, ctx.channel(), event.cause()); 42 | } 43 | } else { 44 | super.userEventTriggered(ctx, evt); 45 | } 46 | } 47 | 48 | @Override 49 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 50 | // Ignore these exception as they will be reported to the handler 51 | // the handshake will ultimately fail or succeed 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/ConnectionMetrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics; 17 | 18 | public interface ConnectionMetrics { 19 | 20 | /** 21 | * Returns the number of active http1 connections. 22 | */ 23 | long activeHttp1Connections(); 24 | 25 | /** 26 | * Returns the number of active http2 connections. 27 | */ 28 | long activeHttp2Connections(); 29 | 30 | /** 31 | * Returns the number of active https connections. 32 | */ 33 | long activeHttpsConnections(); 34 | 35 | /** 36 | * Returns the number of active non-https connections. 37 | */ 38 | long activeHttpConnections(); 39 | 40 | /** 41 | * Returns the number of all the active http1 connections and active http2 connections. 42 | */ 43 | default long activeConnections() { 44 | return activeHttp1Connections() + activeHttp2Connections(); 45 | } 46 | 47 | /** 48 | * Returns the accumulated number of http1 connections. 49 | */ 50 | long http1ConnectionCount(); 51 | 52 | /** 53 | * Returns the accumulated number of http2 connections. 54 | */ 55 | long http2ConnectionCount(); 56 | 57 | /** 58 | * Returns the accumulated number of non-https connections. 59 | */ 60 | long httpConnectionCount(); 61 | 62 | /** 63 | * Returns the accumulated number of https connections. 64 | */ 65 | long httpsConnectionCount(); 66 | 67 | /** 68 | * Returns the accumulated number of all the https and non-https connections. 69 | */ 70 | default long connectionCount() { 71 | return http1ConnectionCount() + http2ConnectionCount(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/utils/LoggedThreadFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.utils; 17 | 18 | import esa.commons.Checks; 19 | import esa.commons.concurrent.InternalThreads; 20 | 21 | import java.util.concurrent.ThreadFactory; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | 24 | /** 25 | * An {@link ThreadFactory} that prefers to create {@link esa.commons.concurrent.InternalThread} and appends a log when 26 | * {@link #newThread(Runnable)} is called. 27 | */ 28 | public class LoggedThreadFactory implements ThreadFactory { 29 | 30 | private static final AtomicInteger POOL_ID = new AtomicInteger(); 31 | 32 | private final AtomicInteger nextId = new AtomicInteger(); 33 | private final String prefix; 34 | private final boolean daemon; 35 | 36 | public LoggedThreadFactory(String prefix) { 37 | this(prefix, false); 38 | } 39 | 40 | public LoggedThreadFactory(String prefix, 41 | boolean daemon) { 42 | Checks.checkNotNull(prefix, "prefix"); 43 | this.prefix = prefix + "-" + POOL_ID.getAndIncrement() + "#"; 44 | this.daemon = daemon; 45 | } 46 | 47 | @Override 48 | public Thread newThread(Runnable r) { 49 | String name = prefix + nextId.getAndIncrement(); 50 | Thread t = InternalThreads.newThread(r, name).thread(); 51 | try { 52 | if (t.isDaemon() != daemon) { 53 | t.setDaemon(daemon); 54 | } 55 | } catch (Exception ignored) { 56 | // Doesn't matter even if failed to set. 57 | } 58 | Loggers.logger().info("Create thread of ESA HttpServer '{}'", name); 59 | return t; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/Http1RequestHandleImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.http.HttpHeaders; 19 | import io.esastack.commons.net.http.HttpMethod; 20 | import io.esastack.commons.net.http.HttpVersion; 21 | import io.esastack.httpserver.core.RequestHandle; 22 | import io.netty.channel.ChannelHandlerContext; 23 | import io.netty.handler.codec.http.HttpRequest; 24 | 25 | final class Http1RequestHandleImpl extends BaseRequestHandle implements RequestHandle { 26 | 27 | final HttpRequest req; 28 | private final Http1ResponseImpl response; 29 | 30 | Http1RequestHandleImpl(ServerRuntime runtime, 31 | ChannelHandlerContext ctx, 32 | HttpRequest req, 33 | boolean isKeepAlive) { 34 | super(runtime, ctx, HttpMethod.fastValueOf(req.method().name()), req.uri()); 35 | this.req = req; 36 | this.response = new Http1ResponseImpl(this, isKeepAlive); 37 | } 38 | 39 | @Override 40 | public HttpVersion version() { 41 | io.netty.handler.codec.http.HttpVersion version = req.protocolVersion(); 42 | if (version == io.netty.handler.codec.http.HttpVersion.HTTP_1_0) { 43 | return HttpVersion.HTTP_1_0; 44 | } else { 45 | return HttpVersion.HTTP_1_1; 46 | } 47 | } 48 | 49 | @Override 50 | public HttpHeaders headers() { 51 | return (HttpHeaders) req.headers(); 52 | } 53 | 54 | @Override 55 | public Http1ResponseImpl response() { 56 | return response; 57 | } 58 | 59 | @Override 60 | protected HttpRequest toHttpRequest() { 61 | return req; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/MultipartOptionsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import java.nio.charset.StandardCharsets; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | import static org.junit.jupiter.api.Assertions.assertTrue; 24 | 25 | class MultipartOptionsTest { 26 | 27 | @Test 28 | void testConfigure() { 29 | final MultipartOptions options = MultipartOptionsConfigure.newOpts() 30 | .charset(StandardCharsets.UTF_8) 31 | .maxSize(1L) 32 | .memoryThreshold(2L) 33 | .tempDir("/foo") 34 | .useDisk(true) 35 | .configured(); 36 | 37 | assertEquals(StandardCharsets.UTF_8, options.getCharset()); 38 | assertEquals(1L, options.getMaxSize()); 39 | assertEquals(2L, options.getMemoryThreshold()); 40 | assertEquals("/foo", options.getTempDir()); 41 | assertTrue(options.isUseDisk()); 42 | } 43 | 44 | @Test 45 | void testCopyFromAnother() { 46 | 47 | final MultipartOptions another = MultipartOptionsConfigure.newOpts() 48 | .charset(StandardCharsets.UTF_8) 49 | .maxSize(1L) 50 | .memoryThreshold(2L) 51 | .tempDir("/foo") 52 | .useDisk(true) 53 | .configured(); 54 | 55 | final MultipartOptions options = new MultipartOptions(another); 56 | assertEquals(StandardCharsets.UTF_8, options.getCharset()); 57 | assertEquals(1L, options.getMaxSize()); 58 | assertEquals(2L, options.getMemoryThreshold()); 59 | assertEquals("/foo", options.getTempDir()); 60 | assertTrue(options.isUseDisk()); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/H2Options.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import esa.commons.Checks; 19 | 20 | import java.io.Serializable; 21 | 22 | public class H2Options implements Serializable { 23 | 24 | private static final long serialVersionUID = -1962829341931588557L; 25 | 26 | private boolean enabled; 27 | private int maxReservedStreams; 28 | private int maxFrameSize; 29 | private long gracefulShutdownTimeoutMillis = 60L * 1000L; 30 | 31 | public H2Options() { 32 | } 33 | 34 | public H2Options(H2Options other) { 35 | Checks.checkNotNull(other, "other"); 36 | this.enabled = other.enabled; 37 | this.maxReservedStreams = other.maxReservedStreams; 38 | this.maxFrameSize = other.maxFrameSize; 39 | this.gracefulShutdownTimeoutMillis = other.gracefulShutdownTimeoutMillis; 40 | } 41 | 42 | public boolean isEnabled() { 43 | return enabled; 44 | } 45 | 46 | public void setEnabled(boolean enabled) { 47 | this.enabled = enabled; 48 | } 49 | 50 | public int getMaxReservedStreams() { 51 | return maxReservedStreams; 52 | } 53 | 54 | public void setMaxReservedStreams(int maxReservedStreams) { 55 | this.maxReservedStreams = maxReservedStreams; 56 | } 57 | 58 | public int getMaxFrameSize() { 59 | return maxFrameSize; 60 | } 61 | 62 | public void setMaxFrameSize(int maxFrameSize) { 63 | this.maxFrameSize = maxFrameSize; 64 | } 65 | 66 | public long getGracefulShutdownTimeoutMillis() { 67 | return gracefulShutdownTimeoutMillis; 68 | } 69 | 70 | public void setGracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) { 71 | this.gracefulShutdownTimeoutMillis = gracefulShutdownTimeoutMillis; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/transport/EpollTransportTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import io.netty.channel.ChannelFactory; 19 | import io.netty.channel.EventLoopGroup; 20 | import io.netty.channel.ServerChannel; 21 | import io.netty.channel.epoll.Epoll; 22 | import io.netty.channel.epoll.EpollEventLoopGroup; 23 | import io.netty.channel.epoll.EpollServerDomainSocketChannel; 24 | import io.netty.channel.epoll.EpollServerSocketChannel; 25 | import io.netty.channel.unix.DomainSocketAddress; 26 | import io.netty.util.concurrent.DefaultThreadFactory; 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.net.InetSocketAddress; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertTrue; 32 | import static org.junit.jupiter.api.Assumptions.assumeTrue; 33 | 34 | class EpollTransportTest { 35 | 36 | @Test 37 | void testServerChannelFactoryCreation() { 38 | final EpollTransport transport = new EpollTransport(); 39 | final ChannelFactory factory1 = transport 40 | .serverChannelFactory(new InetSocketAddress(8080)); 41 | final ChannelFactory factory2 = transport 42 | .serverChannelFactory(new DomainSocketAddress("tmp.sock")); 43 | 44 | assumeTrue(Epoll.isAvailable()); 45 | 46 | assertTrue(factory1.newChannel() instanceof EpollServerSocketChannel); 47 | assertTrue(factory2.newChannel() instanceof EpollServerDomainSocketChannel); 48 | } 49 | 50 | @Test 51 | void testEventLoopGroupCreation() { 52 | assumeTrue(Epoll.isAvailable()); 53 | final EpollTransport transport = new EpollTransport(); 54 | final EventLoopGroup loop = transport 55 | .loop(1, new DefaultThreadFactory("foo")); 56 | 57 | assertTrue(loop instanceof EpollEventLoopGroup); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/Http1RequestHandleImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.netty.http.Http1HeadersImpl; 19 | import io.netty.channel.ChannelHandlerContext; 20 | import io.netty.channel.embedded.EmbeddedChannel; 21 | import io.netty.handler.codec.http.DefaultHttpRequest; 22 | import io.netty.handler.codec.http.HttpMethod; 23 | import io.netty.handler.codec.http.HttpRequest; 24 | import io.netty.handler.codec.http.HttpVersion; 25 | import org.junit.jupiter.api.Test; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.junit.jupiter.api.Assertions.assertNotNull; 29 | import static org.junit.jupiter.api.Assertions.assertSame; 30 | import static org.mockito.Mockito.mock; 31 | import static org.mockito.Mockito.when; 32 | 33 | class Http1RequestHandleImplTest { 34 | 35 | @Test 36 | void testImplementation() { 37 | final HttpRequest request = 38 | new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/foo", new Http1HeadersImpl()); 39 | final ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); 40 | final EmbeddedChannel channel = new EmbeddedChannel(); 41 | when(ctx.channel()).thenReturn(channel); 42 | final Http1RequestHandleImpl handle = 43 | new Http1RequestHandleImpl(Helper.serverRuntime(), 44 | ctx, 45 | request, 46 | true); 47 | 48 | assertEquals(io.esastack.commons.net.http.HttpVersion.HTTP_1_1, handle.version()); 49 | 50 | assertSame(io.esastack.commons.net.http.HttpMethod.POST, handle.method()); 51 | assertSame(request.headers(), handle.headers()); 52 | 53 | assertNotNull(handle.response()); 54 | assertSame(request, handle.toHttpRequest()); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/HAProxyDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.httpserver.utils.Loggers; 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.channel.ChannelHandlerContext; 21 | import io.netty.handler.codec.ByteToMessageDecoder; 22 | import io.netty.handler.codec.ProtocolDetectionResult; 23 | import io.netty.handler.codec.ProtocolDetectionState; 24 | import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; 25 | import io.netty.handler.codec.haproxy.HAProxyProtocolVersion; 26 | 27 | import java.util.List; 28 | 29 | import static io.esastack.httpserver.impl.Utils.handleException; 30 | 31 | final class HAProxyDetector extends ByteToMessageDecoder { 32 | 33 | @Override 34 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { 35 | ProtocolDetectionResult ha = HAProxyMessageDecoder.detectProtocol(in); 36 | 37 | if (ha.state() == ProtocolDetectionState.NEEDS_MORE_DATA) { 38 | return; 39 | } 40 | 41 | if (ha.state() == ProtocolDetectionState.DETECTED) { 42 | if (Loggers.logger().isDebugEnabled()) { 43 | Loggers.logger().debug("Detected HAProxy protocol version of {} in connection {}", 44 | ha.detectedProtocol(), ctx.channel()); 45 | } 46 | 47 | ctx.pipeline() 48 | .addAfter(ctx.name(), 49 | "HAProxyMessageHandler", 50 | new HAProxyMessageHandler()); 51 | ctx.pipeline().replace(this, "HAProxyDecoder", new HAProxyMessageDecoder()); 52 | } else { 53 | ctx.pipeline().remove(this); 54 | } 55 | } 56 | 57 | @Override 58 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 59 | handleException(ctx, cause); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/MultipartOptionsConfigure.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import io.netty.handler.codec.http.HttpConstants; 19 | import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; 20 | 21 | import java.nio.charset.Charset; 22 | 23 | public final class MultipartOptionsConfigure { 24 | private long memoryThreshold = 2L * 1024L * 1024L; 25 | private long maxSize = DefaultHttpDataFactory.MAXSIZE; 26 | private Charset charset = HttpConstants.DEFAULT_CHARSET; 27 | private String tempDir; 28 | private boolean useDisk; 29 | 30 | private MultipartOptionsConfigure() { 31 | } 32 | 33 | public static MultipartOptionsConfigure newOpts() { 34 | return new MultipartOptionsConfigure(); 35 | } 36 | 37 | public MultipartOptionsConfigure memoryThreshold(long memoryThreshold) { 38 | this.memoryThreshold = memoryThreshold; 39 | return this; 40 | } 41 | 42 | public MultipartOptionsConfigure maxSize(long maxSize) { 43 | this.maxSize = maxSize; 44 | return this; 45 | } 46 | 47 | public MultipartOptionsConfigure charset(Charset charset) { 48 | this.charset = charset; 49 | return this; 50 | } 51 | 52 | public MultipartOptionsConfigure tempDir(String tempDir) { 53 | this.tempDir = tempDir; 54 | return this; 55 | } 56 | 57 | public MultipartOptionsConfigure useDisk(boolean useDisk) { 58 | this.useDisk = useDisk; 59 | return this; 60 | } 61 | 62 | public MultipartOptions configured() { 63 | MultipartOptions multipartOptions = new MultipartOptions(); 64 | multipartOptions.setUseDisk(useDisk); 65 | multipartOptions.setMemoryThreshold(memoryThreshold); 66 | multipartOptions.setMaxSize(maxSize); 67 | multipartOptions.setCharset(charset); 68 | multipartOptions.setTempDir(tempDir); 69 | return multipartOptions; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/MultipartFileImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.Checks; 19 | import io.esastack.httpserver.core.MultipartFile; 20 | import io.netty.buffer.ByteBuf; 21 | import io.netty.handler.codec.http.multipart.FileUpload; 22 | 23 | import java.io.File; 24 | import java.io.IOException; 25 | import java.nio.charset.Charset; 26 | 27 | final class MultipartFileImpl implements MultipartFile { 28 | 29 | private final FileUpload upload; 30 | 31 | MultipartFileImpl(FileUpload upload) { 32 | Checks.checkNotNull(upload, "upload"); 33 | this.upload = upload; 34 | } 35 | 36 | @Override 37 | public String name() { 38 | return upload.getName(); 39 | } 40 | 41 | @Override 42 | public String fileName() { 43 | return upload.getFilename(); 44 | } 45 | 46 | @Override 47 | public String contentType() { 48 | return upload.getContentType(); 49 | } 50 | 51 | @Override 52 | public long length() { 53 | return upload.length(); 54 | } 55 | 56 | @Override 57 | public String contentTransferEncoding() { 58 | return upload.getContentTransferEncoding(); 59 | } 60 | 61 | @Override 62 | public boolean isInMemory() { 63 | return upload.isInMemory(); 64 | } 65 | 66 | @Override 67 | public File file() throws IOException { 68 | return upload.getFile(); 69 | } 70 | 71 | @Override 72 | public ByteBuf getByteBuf() throws IOException { 73 | return upload.getByteBuf(); 74 | } 75 | 76 | @Override 77 | public void transferTo(File dest) throws IOException { 78 | upload.renameTo(dest); 79 | } 80 | 81 | @Override 82 | public String string() throws IOException { 83 | return upload.getString(); 84 | } 85 | 86 | @Override 87 | public String string(Charset charset) throws IOException { 88 | return upload.getString(charset); 89 | } 90 | 91 | @Override 92 | public void delete() { 93 | upload.delete(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/transport/EpollTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import io.esastack.httpserver.NetOptions; 19 | import io.netty.bootstrap.ServerBootstrap; 20 | import io.netty.channel.ChannelFactory; 21 | import io.netty.channel.EventLoopGroup; 22 | import io.netty.channel.ServerChannel; 23 | import io.netty.channel.epoll.EpollChannelOption; 24 | import io.netty.channel.epoll.EpollEventLoopGroup; 25 | import io.netty.channel.epoll.EpollServerDomainSocketChannel; 26 | import io.netty.channel.epoll.EpollServerSocketChannel; 27 | import io.netty.channel.unix.DomainSocketAddress; 28 | 29 | import java.net.SocketAddress; 30 | import java.util.concurrent.ThreadFactory; 31 | 32 | /** 33 | * @see io.netty.channel.epoll.Epoll 34 | */ 35 | public final class EpollTransport extends NioTransport implements Transport { 36 | 37 | @Override 38 | public ChannelFactory serverChannelFactory(SocketAddress local) { 39 | if (local instanceof DomainSocketAddress) { 40 | return EpollServerDomainSocketChannel::new; 41 | } 42 | return EpollServerSocketChannel::new; 43 | } 44 | 45 | @Override 46 | public EventLoopGroup loop(int nThreads, ThreadFactory threadFactory) { 47 | return new EpollEventLoopGroup(nThreads, threadFactory); 48 | } 49 | 50 | @Override 51 | public void applyOptions(ServerBootstrap bootstrap, 52 | NetOptions options, 53 | SocketAddress local) { 54 | final boolean isDomainSocket = local instanceof DomainSocketAddress; 55 | if (!isDomainSocket) { 56 | bootstrap.option(EpollChannelOption.SO_REUSEPORT, options.isReusePort()); 57 | } 58 | if (options.isTcpFastOpen()) { 59 | bootstrap.option(EpollChannelOption.TCP_FASTOPEN_CONNECT, true); 60 | } 61 | bootstrap.childOption(EpollChannelOption.TCP_QUICKACK, options.isTcpQuickAck()); 62 | bootstrap.childOption(EpollChannelOption.TCP_CORK, options.isTcpCork()); 63 | super.applyOptions(bootstrap, options, local); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/SslHelperTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.httpserver.SslOptions; 19 | import io.netty.handler.ssl.ClientAuth; 20 | import io.netty.handler.ssl.SslContext; 21 | import io.netty.handler.ssl.util.SelfSignedCertificate; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import java.security.cert.CertificateException; 25 | 26 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.junit.jupiter.api.Assertions.assertFalse; 29 | import static org.junit.jupiter.api.Assertions.assertNotNull; 30 | import static org.junit.jupiter.api.Assertions.assertNull; 31 | import static org.junit.jupiter.api.Assertions.assertSame; 32 | import static org.junit.jupiter.api.Assertions.assertTrue; 33 | 34 | class SslHelperTest { 35 | 36 | @Test 37 | void testNoneSsl() { 38 | final SslHelper sslHelper = new SslHelper(null, true); 39 | assertFalse(sslHelper.isSsl()); 40 | assertNull(sslHelper.options()); 41 | assertNull(sslHelper.getSslContext()); 42 | } 43 | 44 | @Test 45 | void testCreateSslContext() throws CertificateException { 46 | 47 | final SslOptions ssl = new SslOptions(); 48 | ssl.setEnabledProtocols(new String[]{"TLSv1.2"}); 49 | ssl.setCiphers(new String[]{"AES256-SHA", "AES128-SHA"}); 50 | final SelfSignedCertificate cert = new SelfSignedCertificate(); 51 | ssl.setCertificate(cert.certificate()); 52 | ssl.setPrivateKey(cert.privateKey()); 53 | ssl.setClientAuth(ClientAuth.NONE); 54 | ssl.setSessionCacheSize(10L); 55 | 56 | 57 | final SslHelper sslHelper = new SslHelper(ssl, true); 58 | assertTrue(sslHelper.isSsl()); 59 | assertSame(ssl, sslHelper.options()); 60 | 61 | final SslContext sslContext = sslHelper.getSslContext(); 62 | assertNotNull(sslContext); 63 | assertTrue(sslContext.isServer()); 64 | assertArrayEquals(ssl.getCiphers(), sslContext.cipherSuites().toArray()); 65 | assertEquals(10L, sslContext.sessionCacheSize()); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/H2cDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.Checks; 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.buffer.ByteBufUtil; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.handler.codec.ByteToMessageDecoder; 23 | 24 | import java.util.List; 25 | import java.util.function.BiConsumer; 26 | 27 | import static io.esastack.httpserver.impl.Utils.handleException; 28 | import static io.esastack.httpserver.impl.Utils.handleIdle; 29 | import static io.netty.buffer.Unpooled.unreleasableBuffer; 30 | import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf; 31 | 32 | final class H2cDetector extends ByteToMessageDecoder { 33 | 34 | private static final ByteBuf CONNECTION_PREFACE = unreleasableBuffer(connectionPrefaceBuf()); 35 | private final BiConsumer callback; 36 | 37 | H2cDetector(BiConsumer callback) { 38 | Checks.checkNotNull(callback, "callback"); 39 | this.callback = callback; 40 | } 41 | 42 | @Override 43 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { 44 | int prefaceLength = CONNECTION_PREFACE.readableBytes(); 45 | int bytesRead = Math.min(in.readableBytes(), prefaceLength); 46 | if (!ByteBufUtil.equals(CONNECTION_PREFACE, CONNECTION_PREFACE.readerIndex(), 47 | in, in.readerIndex(), bytesRead)) { 48 | callback.accept(ctx, false); 49 | ctx.pipeline().remove(this); 50 | } else if (bytesRead == prefaceLength) { 51 | // Full h2 preface match, removed source codec, using http2 codec to handle following network traffic 52 | callback.accept(ctx, true); 53 | ctx.pipeline().remove(this); 54 | } 55 | } 56 | 57 | @Override 58 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 59 | handleException(ctx, cause); 60 | } 61 | 62 | @Override 63 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 64 | if (!handleIdle(ctx, evt)) { 65 | super.userEventTriggered(ctx, evt); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/core/Request.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.core; 17 | 18 | import esa.commons.annotation.Beta; 19 | import io.netty.buffer.ByteBufAllocator; 20 | 21 | /** 22 | * An interface defines a http server request. 23 | *

24 | * Each instance of this {@link Request} is associated with a corresponding {@link #response()}. 25 | *

26 | * It would be in unfinished state which means current {@link Request} haven't been completely decoded(eg. only part of 27 | * the body is decoded, and waiting the left.) and it could be indicated by the return value of {@link #isEnded()}. So 28 | * you so would see an unfinished result by calling {@link #aggregated()} and {@link #multipart()} before {@link 29 | * #isEnded()} returns {@code true}. 30 | *

31 | * !Note: This class is not designed as thread-safe, please do not use it in multi-thread before {@link #isEnded()} 32 | * returns {@code true} 33 | */ 34 | public interface Request extends BaseRequest { 35 | 36 | /** 37 | * Returns the corresponding {@link Response}. 38 | * 39 | * @return response 40 | */ 41 | Response response(); 42 | 43 | /** 44 | * Returns the multipart result of this request. 45 | *

46 | * This is only used when current request IS a multipart request and also multipart is expected. 47 | *

48 | * !Note: You may see an unfinished result before {@link #isEnded()} returns {@code true}. 49 | * 50 | * @return multipart result or {@code empty}. 51 | */ 52 | MultiPart multipart(); 53 | 54 | /** 55 | * Returns the aggregation result of this request. 56 | *

57 | * This is only used when aggregation is expected. 58 | *

59 | * !Note: You may see an unfinished result before {@link #isEnded()} returns {@code true}. 60 | * 61 | * @return aggregation result or {@code empty}. 62 | */ 63 | Aggregation aggregated(); 64 | 65 | /** 66 | * Indicates whether current request is decoded completely. 67 | * 68 | * @return {@code true} if request is decoded completely, otherwise {@code false} 69 | */ 70 | boolean isEnded(); 71 | 72 | /** 73 | * Get current allocator. 74 | * 75 | * @return allocator 76 | */ 77 | @Beta 78 | ByteBufAllocator alloc(); 79 | 80 | } 81 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/AggregationHandle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.http.EmptyHttpHeaders; 19 | import io.esastack.commons.net.http.HttpHeaders; 20 | import io.esastack.httpserver.core.Aggregation; 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.buffer.CompositeByteBuf; 23 | import io.netty.buffer.Unpooled; 24 | import io.netty.channel.ChannelHandlerContext; 25 | 26 | import static io.esastack.httpserver.impl.Utils.tryRelease; 27 | 28 | final class AggregationHandle implements Aggregation { 29 | 30 | static final Empty EMPTY = new Empty(); 31 | private static final int MAX_COMPOSITE_BUFFER_COMPONENTS = 1024; 32 | private final ChannelHandlerContext ctx; 33 | private CompositeByteBuf content; 34 | private HttpHeaders trailers; 35 | 36 | AggregationHandle(ChannelHandlerContext ctx) { 37 | this.ctx = ctx; 38 | } 39 | 40 | @Override 41 | public ByteBuf body() { 42 | if (content == null) { 43 | return EMPTY.body(); 44 | } 45 | return content; 46 | } 47 | 48 | @Override 49 | public HttpHeaders trailers() { 50 | if (trailers == null) { 51 | return EMPTY.trailers(); 52 | } 53 | return trailers; 54 | } 55 | 56 | void setTrailers(HttpHeaders trailers) { 57 | this.trailers = trailers; 58 | } 59 | 60 | void appendPartialContent(ByteBuf partialContent) { 61 | if (content == null) { 62 | content = ctx.alloc().compositeBuffer(MAX_COMPOSITE_BUFFER_COMPONENTS); 63 | } 64 | content.addComponent(true, partialContent); 65 | } 66 | 67 | void release() { 68 | if (content != null) { 69 | // Assuming that content may have been released, so just try to release it instead calling release() 70 | // which may produce an exception if refCnt() == 0. 71 | tryRelease(content); 72 | } 73 | } 74 | 75 | private static class Empty implements Aggregation { 76 | 77 | @Override 78 | public ByteBuf body() { 79 | return Unpooled.EMPTY_BUFFER; 80 | } 81 | 82 | @Override 83 | public HttpHeaders trailers() { 84 | return EmptyHttpHeaders.INSTANCE; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/AggregationHandleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.http.EmptyHttpHeaders; 19 | import io.esastack.commons.net.http.HttpHeaders; 20 | import io.esastack.commons.net.netty.http.Http1HeadersImpl; 21 | import io.esastack.httpserver.core.Aggregation; 22 | import io.netty.buffer.ByteBuf; 23 | import io.netty.buffer.Unpooled; 24 | import io.netty.channel.ChannelHandlerContext; 25 | import io.netty.channel.ChannelInboundHandlerAdapter; 26 | import io.netty.channel.embedded.EmbeddedChannel; 27 | import org.junit.jupiter.api.Test; 28 | 29 | import java.nio.charset.StandardCharsets; 30 | 31 | import static org.junit.jupiter.api.Assertions.assertEquals; 32 | import static org.junit.jupiter.api.Assertions.assertSame; 33 | 34 | class AggregationHandleTest { 35 | 36 | @Test 37 | void testEmpty() { 38 | final Aggregation empty = AggregationHandle.EMPTY; 39 | assertSame(Unpooled.EMPTY_BUFFER, empty.body()); 40 | assertSame(EmptyHttpHeaders.INSTANCE, empty.trailers()); 41 | } 42 | 43 | @Test 44 | void testAggregateAndRelease() { 45 | final ChannelHandlerContext ctx = 46 | new EmbeddedChannel(new ChannelInboundHandlerAdapter()).pipeline().firstContext(); 47 | final AggregationHandle handle = new AggregationHandle(ctx); 48 | 49 | assertSame(Unpooled.EMPTY_BUFFER, handle.body()); 50 | assertSame(EmptyHttpHeaders.INSTANCE, handle.trailers()); 51 | 52 | final ByteBuf c1 = Unpooled.copiedBuffer("123".getBytes(StandardCharsets.UTF_8)); 53 | final ByteBuf c2 = Unpooled.copiedBuffer("456".getBytes(StandardCharsets.UTF_8)); 54 | 55 | try { 56 | handle.appendPartialContent(c1); 57 | assertEquals("123", handle.body().toString(StandardCharsets.UTF_8)); 58 | 59 | handle.appendPartialContent(c2); 60 | assertEquals("123456", handle.body().toString(StandardCharsets.UTF_8)); 61 | 62 | final HttpHeaders trailers = new Http1HeadersImpl(); 63 | handle.setTrailers(trailers); 64 | assertSame(trailers, handle.trailers()); 65 | } finally { 66 | handle.release(); 67 | assertEquals(0, handle.body().refCnt()); 68 | assertEquals(0, c1.refCnt()); 69 | assertEquals(0, c2.refCnt()); 70 | } 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/SslDetector.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.function.Consumer3; 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.channel.Channel; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.handler.codec.ByteToMessageDecoder; 23 | import io.netty.handler.ssl.SslHandler; 24 | 25 | import javax.net.ssl.SSLEngine; 26 | import java.util.List; 27 | 28 | import static io.esastack.httpserver.impl.Utils.handleException; 29 | 30 | final class SslDetector extends ByteToMessageDecoder { 31 | 32 | /** 33 | * the length of the ssl record header (in bytes) 34 | */ 35 | private static final int SSL_RECORD_HEADER_LENGTH = 5; 36 | private final SslHelper sslHelper; 37 | private final Consumer3 callback; 38 | 39 | SslDetector(SslHelper sslHelper, Consumer3 callback) { 40 | this.sslHelper = sslHelper; 41 | this.callback = callback; 42 | } 43 | 44 | @Override 45 | protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { 46 | if (in.readableBytes() < SSL_RECORD_HEADER_LENGTH) { 47 | return; 48 | } 49 | 50 | // TODO: sni support 51 | if (isSsl(in)) { 52 | SSLEngine engine = sslHelper.getSslContext().newEngine(ctx.alloc()); 53 | 54 | if (sslHelper.options().getEnabledProtocols() != null 55 | && sslHelper.options().getEnabledProtocols().length > 0) { 56 | engine.setEnabledProtocols(sslHelper.options().getEnabledProtocols()); 57 | } 58 | final SslHandler sslHandler = new SslHandler(engine); 59 | if (sslHelper.options().getHandshakeTimeoutMillis() > 0L) { 60 | sslHandler.setHandshakeTimeoutMillis(sslHelper.options().getHandshakeTimeoutMillis()); 61 | } 62 | ctx.pipeline().addAfter(ctx.name(), "SslCompletionHandler", new SslCompletionHandler(callback)); 63 | ctx.pipeline().replace(this, "SslHandler", sslHandler); 64 | } else { 65 | callback.accept(false, ctx.channel(), null); 66 | ctx.pipeline().remove(this); 67 | } 68 | } 69 | 70 | @Override 71 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 72 | handleException(ctx, cause); 73 | } 74 | 75 | private static boolean isSsl(ByteBuf in) { 76 | return SslHandler.isEncrypted(in); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/OpenSslSessionMetrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics; 17 | 18 | /** 19 | * Metrics of {@link io.netty.handler.ssl.OpenSslSessionStats} 20 | */ 21 | public interface OpenSslSessionMetrics { 22 | 23 | /** 24 | * Returns the current number of sessions in the internal session cache. 25 | */ 26 | long number(); 27 | 28 | /** 29 | * Returns the number of started SSL/TLS handshakes in server mode. 30 | */ 31 | long accept(); 32 | 33 | /** 34 | * Returns the number of successfully established SSL/TLS sessions in server mode. 35 | */ 36 | long acceptGood(); 37 | 38 | /** 39 | * Returns the number of start renegotiations in server mode. 40 | */ 41 | long acceptRenegotiate(); 42 | 43 | /** 44 | * Returns the number of successfully reused sessions. In client mode, a session set with {@code SSL_set_session} 45 | * successfully reused is counted as a hit. In server mode, a session successfully retrieved from internal or 46 | * external cache is counted as a hit. 47 | */ 48 | long hits(); 49 | 50 | /** 51 | * Returns the number of successfully retrieved sessions from the external session cache in server mode. 52 | */ 53 | long cbHits(); 54 | 55 | /** 56 | * Returns the number of sessions proposed by clients that were not found in the internal session cache in server 57 | * mode. 58 | */ 59 | long misses(); 60 | 61 | /** 62 | * Returns the number of sessions proposed by clients and either found in the internal or external session cache in 63 | * server mode, but that were invalid due to timeout. These sessions are not included in the {@link #hits()} count. 64 | */ 65 | long timeouts(); 66 | 67 | /** 68 | * Returns the number of sessions that were removed because the maximum session cache size was exceeded. 69 | */ 70 | long cacheFull(); 71 | 72 | /** 73 | * Returns the number of times a client presented a ticket that did not match any key in the list. 74 | */ 75 | long ticketKeyFail(); 76 | 77 | /** 78 | * Returns the number of times a client did not present a ticket and we issued a new one 79 | */ 80 | long ticketKeyNew(); 81 | 82 | /** 83 | * Returns the number of times a client presented a ticket derived from an older key, 84 | * and we upgraded to the primary key. 85 | */ 86 | long ticketKeyRenew(); 87 | 88 | /** 89 | * Returns the number of times a client presented a ticket derived from the primary key. 90 | */ 91 | long ticketKeyResume(); 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESA HttpServer 2 | 3 | ![Build](https://github.com/esastack/esa-httpserver/workflows/Build/badge.svg?branch=main) 4 | [![codecov](https://codecov.io/gh/esastack/esa-httpserver/branch/main/graph/badge.svg?token=C6JT3SKXX5)](https://codecov.io/gh/esastack/esa-httpserver) 5 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.esastack/httpserver/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.esastack/httpserver/) 6 | [![GitHub license](https://img.shields.io/github/license/esastack/esa-httpserver)](https://github.com/esastack/esa-httpserver/blob/main/LICENSE) 7 | 8 | ESA HttpServer is an asynchronous event-driven http server based on netty. 9 | 10 | ## Features 11 | 12 | - Asynchronous request handing 13 | - Http1/H2/H2cUpgrade 14 | - Https 15 | - HAProxy 16 | - Epoll/NIO 17 | - Chunked read/write 18 | - Body aggregation 19 | - Multipart 20 | - Metrics 21 | - more features... 22 | 23 | ## Maven Dependency 24 | 25 | ```xml 26 | 27 | io.esastack 28 | httpserver 29 | ${mvn.version} 30 | 31 | ``` 32 | 33 | ## Quick Start 34 | 35 | ```java 36 | HttpServer.create() 37 | .handle(req -> { 38 | req.onData(buf -> { 39 | // handle http content 40 | }); 41 | req.onEnd(p -> { 42 | req.response() 43 | .setStatus(200) 44 | .end("Hello ESA Http Server!".getBytes()); 45 | return p.setSuccess(null); 46 | }); 47 | }) 48 | .listen(8080) 49 | .awaitUninterruptibly(); 50 | ``` 51 | 52 | ## Performance 53 | 54 | ### Test cases 55 | 56 | - We built an echo server by ESA HttpServer and used a http client to do the requests for RPS testing with different bytes payload(16B, 128B, 512B, 1KB, 4KB, 10KB) 57 | - Also we used origin netty to build a server which is same with above for RPS testing (uses the `HttpServerCodec`, `HttpObjectAggregator` handlers directly). 58 | 59 | ### Hardware Used 60 | 61 | We used the following software for the testing: 62 | 63 | - wrk4.1.0 64 | 65 | - | | OS | CPU | Mem(G) | 66 | | ------ | ------------------------ | ---- | ------ | 67 | | server | centos:6.9-1.2.5(docker) | 4 | 8 | 68 | | client | centos:7.6-1.3.0(docker) | 16 | 3 | 69 | 70 | 71 | ### JVM Options 72 | 73 | ``` 74 | -server -Xms3072m -Xmx3072m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+PrintTenuringDistribution -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:logs/gc-${appName}-%t.log -XX:NumberOfGCLogFiles=20 -XX:GCLogFileSize=480M -XX:+UseGCLogFileRotation -XX:HeapDumpPath=. 75 | ``` 76 | 77 | ### Server Options 78 | 79 | - we set the value of IO threads to 8. 80 | 81 | ### RPS 82 | 83 | | | 16B | 128B | 512B | 1KB | 4KB | 10KB | 84 | | -------------- | --------- | --------- | --------- | --------- | -------- | -------- | 85 | | Netty | 133272.34 | 132818.53 | 132390.78 | 127366.28 | 85408.7 | 49798.84 | 86 | | ESA HttpServer | 142063.99 | 139608.23 | 139646.04 | 140159.5 | 92767.53 | 53534.21 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/MultipartOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import esa.commons.Checks; 19 | import io.netty.handler.codec.http.HttpConstants; 20 | import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory; 21 | import io.netty.handler.codec.http.multipart.DiskFileUpload; 22 | 23 | import java.nio.charset.Charset; 24 | 25 | /** 26 | * The wrapper config for {@link DefaultHttpDataFactory} 27 | */ 28 | public class MultipartOptions { 29 | 30 | /** 31 | * Save the multipart to disk no matter what size the item is when the value is true. 32 | */ 33 | private boolean useDisk; 34 | 35 | /** 36 | * Default memoryThreshold as 2MB. 37 | */ 38 | private long memoryThreshold = 2L * 1024L * 1024L; 39 | 40 | /** 41 | * Default maxSize as -1 which means disable sizeLimit 42 | */ 43 | private long maxSize = DefaultHttpDataFactory.MAXSIZE; 44 | 45 | /** 46 | * Default charset as UTF-8 47 | */ 48 | private Charset charset = HttpConstants.DEFAULT_CHARSET; 49 | 50 | /** 51 | * The directory of temp file. see {@link DiskFileUpload#baseDirectory} 52 | */ 53 | private String tempDir; 54 | 55 | public MultipartOptions() { 56 | } 57 | 58 | public MultipartOptions(MultipartOptions other) { 59 | Checks.checkNotNull(other, "other"); 60 | this.useDisk = other.useDisk; 61 | this.memoryThreshold = other.memoryThreshold; 62 | this.maxSize = other.maxSize; 63 | this.charset = other.charset; 64 | this.tempDir = other.tempDir; 65 | } 66 | 67 | public boolean isUseDisk() { 68 | return useDisk; 69 | } 70 | 71 | public void setUseDisk(boolean useDisk) { 72 | this.useDisk = useDisk; 73 | } 74 | 75 | public long getMemoryThreshold() { 76 | return memoryThreshold; 77 | } 78 | 79 | public void setMemoryThreshold(long memoryThreshold) { 80 | this.memoryThreshold = memoryThreshold; 81 | } 82 | 83 | public long getMaxSize() { 84 | return maxSize; 85 | } 86 | 87 | public void setMaxSize(long maxSize) { 88 | this.maxSize = maxSize; 89 | } 90 | 91 | public Charset getCharset() { 92 | return charset; 93 | } 94 | 95 | public void setCharset(Charset charset) { 96 | this.charset = charset; 97 | } 98 | 99 | public String getTempDir() { 100 | return tempDir; 101 | } 102 | 103 | public void setTempDir(String tempDir) { 104 | this.tempDir = tempDir; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/ServerRuntimeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.httpserver.ServerOptions; 19 | import io.esastack.httpserver.ServerOptionsConfigure; 20 | import io.netty.channel.EventLoopGroup; 21 | import io.netty.channel.local.LocalAddress; 22 | import io.netty.handler.codec.http.multipart.HttpDataFactory; 23 | import org.junit.jupiter.api.Assertions; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertFalse; 28 | import static org.junit.jupiter.api.Assertions.assertNotNull; 29 | import static org.junit.jupiter.api.Assertions.assertNotSame; 30 | import static org.junit.jupiter.api.Assertions.assertNull; 31 | import static org.junit.jupiter.api.Assertions.assertSame; 32 | import static org.junit.jupiter.api.Assertions.assertTrue; 33 | import static org.mockito.Mockito.mock; 34 | 35 | class ServerRuntimeTest { 36 | 37 | @Test 38 | void testOptions() { 39 | 40 | final HttpServerImpl.CloseFuture closeFuture = new HttpServerImpl.CloseFuture(); 41 | final ServerOptions options = ServerOptionsConfigure.newOpts() 42 | .preferNativeTransport(false) 43 | .metricsEnabled(false) 44 | .configured(); 45 | final ServerRuntime runtime = new ServerRuntime("foo", options, closeFuture); 46 | 47 | assertEquals("foo", runtime.name()); 48 | Assertions.assertFalse(runtime.options().isPreferNativeTransport()); 49 | assertNotSame(options, runtime.options()); 50 | assertFalse(runtime.shutdownStatus().get()); 51 | assertNotNull(runtime.metrics()); 52 | Assertions.assertFalse(runtime.metrics().enabled()); 53 | 54 | final HttpDataFactory httpDataFactory = runtime.multipartDataFactory(); 55 | assertNotNull(httpDataFactory); 56 | assertSame(httpDataFactory, runtime.multipartDataFactory()); 57 | 58 | assertFalse(runtime.isRunning()); 59 | assertNull(runtime.address()); 60 | assertNull(runtime.bossGroup()); 61 | assertNull(runtime.ioGroup()); 62 | assertSame(closeFuture, runtime.closeFuture()); 63 | 64 | final LocalAddress addr = new LocalAddress("foo"); 65 | final EventLoopGroup boss = mock(EventLoopGroup.class); 66 | final EventLoopGroup io = mock(EventLoopGroup.class); 67 | 68 | runtime.setStarted(addr, boss, io); 69 | 70 | assertTrue(runtime.isRunning()); 71 | assertSame(addr, runtime.address()); 72 | assertSame(boss, runtime.bossGroup()); 73 | assertSame(io, runtime.ioGroup()); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/SslCompletionHandlerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.channel.ChannelHandlerContext; 19 | import io.netty.channel.ChannelInboundHandlerAdapter; 20 | import io.netty.channel.embedded.EmbeddedChannel; 21 | import io.netty.handler.ssl.SslHandshakeCompletionEvent; 22 | import org.junit.jupiter.api.Test; 23 | 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | import java.util.concurrent.atomic.AtomicReference; 26 | 27 | import static org.junit.jupiter.api.Assertions.assertEquals; 28 | import static org.junit.jupiter.api.Assertions.assertNull; 29 | import static org.junit.jupiter.api.Assertions.assertSame; 30 | 31 | class SslCompletionHandlerTest { 32 | 33 | @Test 34 | void testHandShakeSuccess() { 35 | final AtomicInteger ret = new AtomicInteger(0); 36 | final AtomicReference err = new AtomicReference<>(); 37 | final EmbeddedChannel channel = new EmbeddedChannel(new SslCompletionHandler((r, ch, t) -> { 38 | ret.set(r ? 1 : -1); 39 | err.set(t); 40 | })); 41 | 42 | channel.pipeline().fireUserEventTriggered(SslHandshakeCompletionEvent.SUCCESS); 43 | 44 | assertEquals(1, ret.get()); 45 | assertNull(err.get()); 46 | assertNull(channel.pipeline().get(SslCompletionHandler.class)); 47 | } 48 | 49 | @Test 50 | void testHandShakeFailed() { 51 | final AtomicInteger ret = new AtomicInteger(0); 52 | final AtomicReference err = new AtomicReference<>(); 53 | final EmbeddedChannel channel = new EmbeddedChannel(new SslCompletionHandler((r, ch, t) -> { 54 | ret.set(r ? 1 : -1); 55 | err.set(t); 56 | })); 57 | 58 | final Exception ex = new IllegalStateException(); 59 | channel.pipeline().fireUserEventTriggered(new SslHandshakeCompletionEvent(ex)); 60 | 61 | assertEquals(-1, ret.get()); 62 | assertSame(ex, err.get()); 63 | assertNull(channel.pipeline().get(SslCompletionHandler.class)); 64 | } 65 | 66 | @Test 67 | void testEmptyHandingInExceptionCaught() { 68 | final AtomicReference err = new AtomicReference<>(); 69 | final EmbeddedChannel channel = new EmbeddedChannel(new SslCompletionHandler((r, ch, t) -> { 70 | }), new ChannelInboundHandlerAdapter() { 71 | @Override 72 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 73 | err.set(cause); 74 | } 75 | }); 76 | 77 | channel.pipeline().fireExceptionCaught(new IllegalStateException()); 78 | assertNull(err.get()); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/MultipartFileImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.buffer.Unpooled; 19 | import io.netty.handler.codec.http.multipart.FileUpload; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.charset.StandardCharsets; 25 | 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertSame; 28 | import static org.junit.jupiter.api.Assertions.assertTrue; 29 | import static org.mockito.ArgumentMatchers.any; 30 | import static org.mockito.ArgumentMatchers.eq; 31 | import static org.mockito.ArgumentMatchers.same; 32 | import static org.mockito.Mockito.mock; 33 | import static org.mockito.Mockito.verify; 34 | import static org.mockito.Mockito.when; 35 | 36 | class MultipartFileImplTest { 37 | 38 | @Test 39 | void testDelegate() throws IOException { 40 | final FileUpload mock = mock(FileUpload.class); 41 | final MultipartFileImpl multipart = new MultipartFileImpl(mock); 42 | 43 | when(mock.getName()).thenReturn("foo"); 44 | assertEquals("foo", multipart.name()); 45 | verify(mock).getName(); 46 | 47 | when(mock.getFilename()).thenReturn("bar"); 48 | assertEquals("bar", multipart.fileName()); 49 | verify(mock).getFilename(); 50 | 51 | when(mock.getContentType()).thenReturn("baz"); 52 | assertEquals("baz", multipart.contentType()); 53 | verify(mock).getContentType(); 54 | 55 | when(mock.length()).thenReturn(1L); 56 | assertEquals(1L, multipart.length()); 57 | verify(mock).length(); 58 | 59 | when(mock.getContentTransferEncoding()).thenReturn("qux"); 60 | assertEquals("qux", multipart.contentTransferEncoding()); 61 | verify(mock).getContentTransferEncoding(); 62 | 63 | when(mock.isInMemory()).thenReturn(true); 64 | assertTrue(multipart.isInMemory()); 65 | verify(mock).isInMemory(); 66 | 67 | final File f = new File(""); 68 | when(mock.getFile()).thenReturn(f); 69 | assertSame(f, multipart.file()); 70 | verify(mock).getFile(); 71 | 72 | when(mock.getByteBuf()).thenReturn(Unpooled.EMPTY_BUFFER); 73 | assertSame(Unpooled.EMPTY_BUFFER, multipart.getByteBuf()); 74 | verify(mock).getByteBuf(); 75 | 76 | multipart.transferTo(f); 77 | verify(mock).renameTo(same(f)); 78 | 79 | when(mock.getString()).thenReturn("x"); 80 | assertEquals("x", multipart.string()); 81 | verify(mock).getString(); 82 | 83 | when(mock.getString(any())).thenReturn("y"); 84 | assertEquals("y", multipart.string(StandardCharsets.UTF_8)); 85 | verify(mock).getString(eq(StandardCharsets.UTF_8)); 86 | 87 | multipart.delete(); 88 | verify(mock).delete(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /httpserver/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 4.0.0 19 | 20 | io.esastack 21 | httpserver-parent 22 | 0.1.2-SNAPSHOT 23 | 24 | httpserver 25 | HttpsServer :: Core 26 | 27 | 28 | 29 | io.esastack 30 | commons 31 | 32 | 33 | io.esastack 34 | commons-net-core 35 | 36 | 37 | io.esastack 38 | commons-net-netty 39 | 40 | 41 | io.netty 42 | netty-common 43 | 44 | 45 | io.netty 46 | netty-buffer 47 | 48 | 49 | io.netty 50 | netty-handler 51 | 52 | 53 | io.netty 54 | netty-codec 55 | 56 | 57 | io.netty 58 | netty-codec-http 59 | 60 | 61 | io.netty 62 | netty-codec-http2 63 | 64 | 65 | io.netty 66 | netty-codec-haproxy 67 | 68 | 69 | io.netty 70 | netty-transport 71 | 72 | 73 | io.netty 74 | netty-transport-native-epoll 75 | linux-x86_64 76 | 77 | 78 | io.netty 79 | netty-tcnative-boringssl-static 80 | provided 81 | 82 | 83 | com.jcraft 84 | jzlib 85 | provided 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/SslOptionsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import io.netty.handler.ssl.ClientAuth; 19 | import org.junit.jupiter.api.Test; 20 | 21 | import java.io.File; 22 | 23 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 24 | import static org.junit.jupiter.api.Assertions.assertEquals; 25 | import static org.junit.jupiter.api.Assertions.assertSame; 26 | 27 | class SslOptionsTest { 28 | 29 | @Test 30 | void testConfigure() { 31 | final File f = new File(""); 32 | final SslOptions ssl = SslOptionsConfigure.newOpts() 33 | .clientAuth(ClientAuth.NONE) 34 | .ciphers(new String[]{"foo"}) 35 | .enabledProtocols(new String[]{"bar"}) 36 | .certificate(f) 37 | .privateKey(f) 38 | .keyPassword("baz") 39 | .trustCertificates(f) 40 | .sessionTimeout(1L) 41 | .sessionCacheSize(2L) 42 | .handshakeTimeoutMillis(3L) 43 | .configured(); 44 | 45 | assertEquals(ClientAuth.NONE, ssl.getClientAuth()); 46 | assertArrayEquals(new String[]{"foo"}, ssl.getCiphers()); 47 | assertArrayEquals(new String[]{"bar"}, ssl.getEnabledProtocols()); 48 | assertSame(f, ssl.getCertificate()); 49 | assertSame(f, ssl.getPrivateKey()); 50 | assertEquals("baz", ssl.getKeyPassword()); 51 | assertSame(f, ssl.getTrustCertificates()); 52 | assertEquals(1L, ssl.getSessionTimeout()); 53 | assertEquals(2L, ssl.getSessionCacheSize()); 54 | assertEquals(3L, ssl.getHandshakeTimeoutMillis()); 55 | } 56 | 57 | @Test 58 | void testCopyFromAnother() { 59 | final File f = new File(""); 60 | final SslOptions another = SslOptionsConfigure.newOpts() 61 | .clientAuth(ClientAuth.NONE) 62 | .ciphers(new String[]{"foo"}) 63 | .enabledProtocols(new String[]{"bar"}) 64 | .certificate(f) 65 | .privateKey(f) 66 | .keyPassword("baz") 67 | .trustCertificates(f) 68 | .sessionTimeout(1L) 69 | .sessionCacheSize(2L) 70 | .handshakeTimeoutMillis(3L) 71 | .configured(); 72 | 73 | final SslOptions ssl = new SslOptions(another); 74 | 75 | assertEquals(ClientAuth.NONE, ssl.getClientAuth()); 76 | assertArrayEquals(new String[]{"foo"}, ssl.getCiphers()); 77 | assertArrayEquals(new String[]{"bar"}, ssl.getEnabledProtocols()); 78 | assertSame(f, ssl.getCertificate()); 79 | assertSame(f, ssl.getPrivateKey()); 80 | assertEquals("baz", ssl.getKeyPassword()); 81 | assertSame(f, ssl.getTrustCertificates()); 82 | assertEquals(1L, ssl.getSessionTimeout()); 83 | assertEquals(2L, ssl.getSessionCacheSize()); 84 | assertEquals(3L, ssl.getHandshakeTimeoutMillis()); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/core/MultipartFile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.core; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.handler.codec.http.multipart.FileUpload; 20 | 21 | import java.io.File; 22 | import java.io.IOException; 23 | import java.nio.charset.Charset; 24 | 25 | /** 26 | * A representation of uploaded file which is wrapped from {@link FileUpload}. 27 | */ 28 | public interface MultipartFile { 29 | /** 30 | * Return the name of the parameter in the multipart form. 31 | * 32 | * @return the name of the parameter (never {@code null} or empty) 33 | */ 34 | String name(); 35 | 36 | /** 37 | * Get the original file name. 38 | * 39 | * @return original file name. 40 | */ 41 | String fileName(); 42 | 43 | /** 44 | * Return the content type of the file. 45 | * 46 | * @return the content type, or {@code null} if not defined (or no file has been chosen in the multipart form) 47 | */ 48 | String contentType(); 49 | 50 | /** 51 | * Return the size of the file in bytes. 52 | * 53 | * @return the size of the file, or 0 if empty 54 | */ 55 | long length(); 56 | 57 | /** 58 | * The content transfer encoding. 59 | * 60 | * @return encoding 61 | */ 62 | String contentTransferEncoding(); 63 | 64 | /** 65 | * Whether the content is in memory. 66 | * 67 | * @return true if in memory, otherwise false 68 | */ 69 | boolean isInMemory(); 70 | 71 | /** 72 | * Get the file on disk. Note that if the {@link #isInMemory()} is true then an IOException will be thrown. 73 | * 74 | * @return file 75 | * @throws IOException ex 76 | */ 77 | File file() throws IOException; 78 | 79 | /** 80 | * Returns the content of the file item as a ByteBuf 81 | * 82 | * @return the content of the file item as a ByteBuf 83 | */ 84 | ByteBuf getByteBuf() throws IOException; 85 | 86 | /** 87 | * Transfer this to destination file. 88 | * 89 | * @param dest destination 90 | */ 91 | void transferTo(File dest) throws IOException; 92 | 93 | /** 94 | * Get file content with default charset of UTF-8. 95 | * 96 | * @return file content 97 | * @throws IOException ex 98 | */ 99 | String string() throws IOException; 100 | 101 | /** 102 | * Get file content with specified charset. 103 | * 104 | * @param charset charset 105 | * @return file content in string 106 | * @throws IOException ex 107 | */ 108 | String string(Charset charset) throws IOException; 109 | 110 | /** 111 | * Release the byteBuf and delete the temp file on disk. Note: It's important to release resource, you can get more 112 | * information from {@link FileUpload#delete()}. In fact, we'll be happy that you call this method manually. 113 | */ 114 | void delete(); 115 | } 116 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/AggregatedLastHttpContentTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.ByteBufUtil; 20 | import io.netty.buffer.Unpooled; 21 | import io.netty.handler.codec.DecoderResult; 22 | import io.netty.handler.codec.http.DefaultHttpHeaders; 23 | import io.netty.handler.codec.http.HttpHeaders; 24 | import org.junit.jupiter.api.Test; 25 | 26 | import static org.junit.jupiter.api.Assertions.assertEquals; 27 | import static org.junit.jupiter.api.Assertions.assertNotNull; 28 | import static org.junit.jupiter.api.Assertions.assertNotSame; 29 | import static org.junit.jupiter.api.Assertions.assertSame; 30 | import static org.junit.jupiter.api.Assertions.assertThrows; 31 | import static org.junit.jupiter.api.Assertions.assertTrue; 32 | 33 | class AggregatedLastHttpContentTest { 34 | 35 | @Test 36 | void testAll() { 37 | final ByteBuf c = Unpooled.copiedBuffer("foo".getBytes()); 38 | final HttpHeaders headers = new DefaultHttpHeaders(); 39 | headers.set("a", "1"); 40 | final AggregatedLastHttpContent last = new AggregatedLastHttpContent(c, headers); 41 | assertSame(c, last.content()); 42 | assertSame(headers, last.trailingHeaders()); 43 | assertSame(DecoderResult.SUCCESS, last.decoderResult()); 44 | assertSame(DecoderResult.SUCCESS, last.getDecoderResult()); 45 | assertThrows(UnsupportedOperationException.class, () -> last.setDecoderResult(DecoderResult.UNFINISHED)); 46 | assertEquals(c.refCnt(), last.refCnt()); 47 | 48 | AggregatedLastHttpContent another = last.copy(); 49 | assertNotSame(last, another); 50 | assertTrue(ByteBufUtil.equals(c, another.content())); 51 | assertNotSame(headers, another.trailingHeaders()); 52 | assertTrue(another.trailingHeaders().contains("a", "1", true)); 53 | 54 | another = last.duplicate(); 55 | assertNotSame(last, another); 56 | assertTrue(ByteBufUtil.equals(c, another.content())); 57 | assertNotSame(headers, another.trailingHeaders()); 58 | assertTrue(another.trailingHeaders().contains("a", "1", true)); 59 | 60 | another = last.retainedDuplicate(); 61 | assertNotSame(last, another); 62 | assertTrue(ByteBufUtil.equals(c, another.content())); 63 | assertNotSame(headers, another.trailingHeaders()); 64 | assertTrue(another.trailingHeaders().contains("a", "1", true)); 65 | assertEquals(2, another.refCnt()); 66 | another.release(); 67 | 68 | final ByteBuf c1 = Unpooled.copiedBuffer("bar".getBytes()); 69 | another = last.replace(c1); 70 | assertSame(c1, another.content()); 71 | assertNotSame(headers, another.trailingHeaders()); 72 | assertTrue(another.trailingHeaders().contains("a", "1", true)); 73 | 74 | assertEquals(2, last.retain().refCnt()); 75 | assertSame(last, last.touch()); 76 | assertSame(last, last.touch("baz")); 77 | assertTrue(last.release(2)); 78 | 79 | assertNotNull(last.toString()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/metrics/impl/OpenSslSessionMetricsImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics.impl; 17 | 18 | import io.esastack.httpserver.metrics.OpenSslSessionMetrics; 19 | import io.netty.handler.ssl.OpenSslSessionStats; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import static org.junit.jupiter.api.Assertions.assertEquals; 23 | import static org.mockito.Mockito.mock; 24 | import static org.mockito.Mockito.when; 25 | 26 | class OpenSslSessionMetricsImplTest { 27 | 28 | @Test 29 | void testDisabled() { 30 | final OpenSslSessionMetrics metrics = OpenSslSessionMetricsImpl.DISABLED; 31 | assertEquals(0L, metrics.number()); 32 | assertEquals(0L, metrics.accept()); 33 | assertEquals(0L, metrics.acceptGood()); 34 | assertEquals(0L, metrics.acceptRenegotiate()); 35 | assertEquals(0L, metrics.hits()); 36 | assertEquals(0L, metrics.cbHits()); 37 | assertEquals(0L, metrics.misses()); 38 | assertEquals(0L, metrics.timeouts()); 39 | assertEquals(0L, metrics.cacheFull()); 40 | assertEquals(0L, metrics.ticketKeyFail()); 41 | assertEquals(0L, metrics.ticketKeyNew()); 42 | assertEquals(0L, metrics.ticketKeyRenew()); 43 | assertEquals(0L, metrics.ticketKeyResume()); 44 | } 45 | 46 | @Test 47 | void testStats() { 48 | final OpenSslSessionStats stats = mockSessionStats(); 49 | final OpenSslSessionMetricsImpl metrics = new OpenSslSessionMetricsImpl(stats); 50 | assertSessionMetrics(metrics); 51 | } 52 | 53 | static OpenSslSessionStats mockSessionStats() { 54 | final OpenSslSessionStats stats = mock(OpenSslSessionStats.class); 55 | when(stats.number()).thenReturn(1L); 56 | when(stats.accept()).thenReturn(1L); 57 | when(stats.acceptGood()).thenReturn(1L); 58 | when(stats.acceptRenegotiate()).thenReturn(1L); 59 | when(stats.hits()).thenReturn(1L); 60 | when(stats.cbHits()).thenReturn(1L); 61 | when(stats.misses()).thenReturn(1L); 62 | when(stats.timeouts()).thenReturn(1L); 63 | when(stats.cacheFull()).thenReturn(1L); 64 | when(stats.ticketKeyFail()).thenReturn(1L); 65 | when(stats.ticketKeyNew()).thenReturn(1L); 66 | when(stats.ticketKeyRenew()).thenReturn(1L); 67 | when(stats.ticketKeyResume()).thenReturn(1L); 68 | return stats; 69 | } 70 | 71 | static void assertSessionMetrics(OpenSslSessionMetrics metrics) { 72 | assertEquals(1L, metrics.number()); 73 | assertEquals(1L, metrics.accept()); 74 | assertEquals(1L, metrics.acceptGood()); 75 | assertEquals(1L, metrics.acceptRenegotiate()); 76 | assertEquals(1L, metrics.hits()); 77 | assertEquals(1L, metrics.cbHits()); 78 | assertEquals(1L, metrics.misses()); 79 | assertEquals(1L, metrics.timeouts()); 80 | assertEquals(1L, metrics.cacheFull()); 81 | assertEquals(1L, metrics.ticketKeyFail()); 82 | assertEquals(1L, metrics.ticketKeyNew()); 83 | assertEquals(1L, metrics.ticketKeyRenew()); 84 | assertEquals(1L, metrics.ticketKeyResume()); 85 | } 86 | 87 | 88 | } 89 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/SslOptionsConfigure.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import io.netty.handler.ssl.ClientAuth; 19 | 20 | import java.io.File; 21 | 22 | public final class SslOptionsConfigure { 23 | private ClientAuth clientAuth; 24 | private String[] ciphers; 25 | private String[] enabledProtocols; 26 | private File certificate; 27 | private File privateKey; 28 | private String keyPassword; 29 | private File trustCertificates; 30 | private long sessionTimeout; 31 | private long sessionCacheSize; 32 | private long handshakeTimeoutMillis; 33 | 34 | private SslOptionsConfigure() { 35 | } 36 | 37 | public static SslOptionsConfigure newOpts() { 38 | return new SslOptionsConfigure(); 39 | } 40 | 41 | public SslOptionsConfigure clientAuth(ClientAuth clientAuth) { 42 | this.clientAuth = clientAuth; 43 | return this; 44 | } 45 | 46 | public SslOptionsConfigure ciphers(String[] ciphers) { 47 | this.ciphers = ciphers; 48 | return this; 49 | } 50 | 51 | public SslOptionsConfigure enabledProtocols(String[] enabledProtocols) { 52 | this.enabledProtocols = enabledProtocols; 53 | return this; 54 | } 55 | 56 | public SslOptionsConfigure certificate(File certificate) { 57 | this.certificate = certificate; 58 | return this; 59 | } 60 | 61 | public SslOptionsConfigure privateKey(File privateKey) { 62 | this.privateKey = privateKey; 63 | return this; 64 | } 65 | 66 | public SslOptionsConfigure keyPassword(String keyPassword) { 67 | this.keyPassword = keyPassword; 68 | return this; 69 | } 70 | 71 | public SslOptionsConfigure trustCertificates(File trustCertificates) { 72 | this.trustCertificates = trustCertificates; 73 | return this; 74 | } 75 | 76 | public SslOptionsConfigure sessionTimeout(long sessionTimeout) { 77 | this.sessionTimeout = sessionTimeout; 78 | return this; 79 | } 80 | 81 | public SslOptionsConfigure sessionCacheSize(long sessionCacheSize) { 82 | this.sessionCacheSize = sessionCacheSize; 83 | return this; 84 | } 85 | 86 | public SslOptionsConfigure handshakeTimeoutMillis(long handshakeTimeoutMillis) { 87 | this.handshakeTimeoutMillis = handshakeTimeoutMillis; 88 | return this; 89 | } 90 | 91 | public SslOptions configured() { 92 | SslOptions sslOptions = new SslOptions(); 93 | sslOptions.setClientAuth(clientAuth); 94 | sslOptions.setCiphers(ciphers); 95 | sslOptions.setEnabledProtocols(enabledProtocols); 96 | sslOptions.setCertificate(certificate); 97 | sslOptions.setPrivateKey(privateKey); 98 | sslOptions.setKeyPassword(keyPassword); 99 | sslOptions.setTrustCertificates(trustCertificates); 100 | sslOptions.setSessionTimeout(sessionTimeout); 101 | sslOptions.setSessionCacheSize(sessionCacheSize); 102 | sslOptions.setHandshakeTimeoutMillis(handshakeTimeoutMillis); 103 | return sslOptions; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/SslHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.ExceptionUtils; 19 | import io.esastack.httpserver.SslOptions; 20 | import io.netty.handler.ssl.ApplicationProtocolConfig; 21 | import io.netty.handler.ssl.ApplicationProtocolNames; 22 | import io.netty.handler.ssl.OpenSsl; 23 | import io.netty.handler.ssl.SslContext; 24 | import io.netty.handler.ssl.SslContextBuilder; 25 | import io.netty.handler.ssl.SslProvider; 26 | 27 | import javax.net.ssl.SSLException; 28 | import java.util.Arrays; 29 | 30 | final class SslHelper { 31 | 32 | private final SslOptions ssl; 33 | private final SslContext sslContext; 34 | 35 | SslHelper(SslOptions ssl, boolean isH2Enabled) { 36 | this.ssl = ssl; 37 | this.sslContext = createContext(isH2Enabled); 38 | } 39 | 40 | private SslContext createContext(boolean isH2Enabled) { 41 | if (!isSsl()) { 42 | return null; 43 | } 44 | 45 | final SslContextBuilder sslContextBuilder = 46 | SslContextBuilder.forServer(ssl.getCertificate(), 47 | ssl.getPrivateKey(), 48 | ssl.getKeyPassword()); 49 | 50 | sslContextBuilder.sslProvider(detectSslProvider()); 51 | 52 | if (isH2Enabled) { 53 | sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig( 54 | ApplicationProtocolConfig.Protocol.ALPN, 55 | // NO_ADVERTISE is currently the only mode supported by both OpenSsl and JDK providers. 56 | ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, 57 | // ACCEPT is currently the only mode supported by both OpenSsl and JDK providers. 58 | ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, 59 | ApplicationProtocolNames.HTTP_2, 60 | ApplicationProtocolNames.HTTP_1_1)); 61 | } 62 | 63 | if (ssl.getClientAuth() != null) { 64 | sslContextBuilder.clientAuth(ssl.getClientAuth()); 65 | } 66 | if (ssl.getSessionTimeout() > 0) { 67 | sslContextBuilder.sessionTimeout(ssl.getSessionTimeout()); 68 | } 69 | if (ssl.getSessionCacheSize() > 0) { 70 | sslContextBuilder.sessionCacheSize(ssl.getSessionCacheSize()); 71 | } 72 | if (ssl.getCiphers() != null && ssl.getCiphers().length > 0) { 73 | sslContextBuilder.ciphers(Arrays.asList(ssl.getCiphers())); 74 | } 75 | if (ssl.getTrustCertificates() != null) { 76 | sslContextBuilder.trustManager(ssl.getTrustCertificates()); 77 | } 78 | 79 | try { 80 | return sslContextBuilder.build(); 81 | } catch (SSLException e) { 82 | throw ExceptionUtils.asRuntime(e); 83 | } 84 | } 85 | 86 | boolean isSsl() { 87 | return ssl != null; 88 | } 89 | 90 | SslOptions options() { 91 | return ssl; 92 | } 93 | 94 | SslContext getSslContext() { 95 | return sslContext; 96 | } 97 | 98 | private static SslProvider detectSslProvider() { 99 | if (OpenSsl.isAvailable()) { 100 | return SslProvider.OPENSSL; 101 | } else { 102 | return SslProvider.JDK; 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/SslDetectorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.httpserver.SslOptions; 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.channel.embedded.EmbeddedChannel; 21 | import io.netty.handler.ssl.SslHandler; 22 | import io.netty.handler.ssl.util.SelfSignedCertificate; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import java.security.cert.CertificateException; 26 | import java.util.concurrent.atomic.AtomicInteger; 27 | import java.util.concurrent.atomic.AtomicReference; 28 | 29 | import static io.netty.buffer.Unpooled.wrappedBuffer; 30 | import static org.junit.jupiter.api.Assertions.assertEquals; 31 | import static org.junit.jupiter.api.Assertions.assertNotNull; 32 | import static org.junit.jupiter.api.Assertions.assertNull; 33 | import static org.junit.jupiter.api.Assertions.assertSame; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | 36 | class SslDetectorTest { 37 | 38 | @Test 39 | void testSslDetected() throws CertificateException { 40 | 41 | final SslOptions ssl = new SslOptions(); 42 | ssl.setEnabledProtocols(new String[]{"TLSv1.2"}); 43 | final SelfSignedCertificate certificate = new SelfSignedCertificate(); 44 | ssl.setCertificate(certificate.certificate()); 45 | ssl.setPrivateKey(certificate.privateKey()); 46 | 47 | final EmbeddedChannel channel = new EmbeddedChannel(new SslDetector(new SslHelper(ssl, false), 48 | (isSsl, ch, t) -> { 49 | })); 50 | 51 | final int handlerSize = channel.pipeline().names().size(); 52 | // Push the first part of a 5-byte handshake message. 53 | channel.writeInbound(wrappedBuffer(new byte[]{22, 3, 1})); 54 | // need more bytes 55 | assertEquals(handlerSize, channel.pipeline().names().size()); 56 | assertNull(channel.readInbound()); 57 | channel.writeInbound(wrappedBuffer(new byte[]{0, 5})); 58 | 59 | assertNotNull(channel.pipeline().get(SslHandler.class)); 60 | assertNotNull(channel.pipeline().get(SslCompletionHandler.class)); 61 | 62 | assertTrue(channel.pipeline().names().indexOf("SslHandler") 63 | < channel.pipeline().names().indexOf("SslCompletionHandler")); 64 | } 65 | 66 | @Test 67 | void testSslDetectFailed() throws CertificateException { 68 | 69 | final SslOptions ssl = new SslOptions(); 70 | ssl.setEnabledProtocols(new String[]{"TLSv1.2"}); 71 | final SelfSignedCertificate certificate = new SelfSignedCertificate(); 72 | ssl.setCertificate(certificate.certificate()); 73 | ssl.setPrivateKey(certificate.privateKey()); 74 | 75 | final AtomicInteger ret = new AtomicInteger(0); 76 | final AtomicReference err = new AtomicReference<>(); 77 | final EmbeddedChannel channel = new EmbeddedChannel(new SslDetector(new SslHelper(ssl, false), 78 | (isSsl, ch, t) -> { 79 | ret.set(isSsl ? 1 : -1); 80 | err.set(t); 81 | })); 82 | 83 | final ByteBuf buf = wrappedBuffer("12345".getBytes()); 84 | channel.writeInbound(buf); 85 | 86 | assertSame(buf, channel.readInbound()); 87 | 88 | assertNull(channel.pipeline().get(SslHandler.class)); 89 | assertNull(channel.pipeline().get(SslCompletionHandler.class)); 90 | assertEquals(-1, ret.get()); 91 | assertNull(err.get()); 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/Http2ChunkedInput.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.ByteBufAllocator; 20 | import io.netty.channel.ChannelHandlerContext; 21 | import io.netty.handler.codec.http.DefaultHttpContent; 22 | import io.netty.handler.codec.http2.Http2Headers; 23 | import io.netty.handler.stream.ChunkedInput; 24 | 25 | final class Http2ChunkedInput implements ChunkedInput { 26 | 27 | private final ChunkedInput input; 28 | private final int streamId; 29 | private final int streamDependency; 30 | private final short weight; 31 | private final boolean exclusive; 32 | private final Http2Headers trailers; 33 | 34 | Http2ChunkedInput(ChunkedInput input, 35 | Http2Headers trailers, 36 | int streamId, 37 | int streamDependency, 38 | short weight, 39 | boolean exclusive) { 40 | this.input = input; 41 | this.trailers = trailers; 42 | this.streamId = streamId; 43 | this.streamDependency = streamDependency; 44 | this.weight = weight; 45 | this.exclusive = exclusive; 46 | } 47 | 48 | @Override 49 | public boolean isEndOfInput() throws Exception { 50 | return input.isEndOfInput(); 51 | } 52 | 53 | @Override 54 | public void close() throws Exception { 55 | input.close(); 56 | } 57 | 58 | @Deprecated 59 | @Override 60 | public Content readChunk(ChannelHandlerContext ctx) throws Exception { 61 | return readChunk(ctx.alloc()); 62 | } 63 | 64 | @Override 65 | public Content readChunk(ByteBufAllocator allocator) throws Exception { 66 | ByteBuf buf = input.readChunk(allocator); 67 | if (buf == null) { 68 | return null; 69 | } 70 | 71 | if (input.isEndOfInput()) { 72 | return new LastContent(buf, trailers, streamId, streamDependency, weight, exclusive); 73 | } else { 74 | return new Content(buf, streamId); 75 | } 76 | } 77 | 78 | @Override 79 | public long length() { 80 | return input.length(); 81 | } 82 | 83 | @Override 84 | public long progress() { 85 | return input.progress(); 86 | } 87 | 88 | static class Content extends DefaultHttpContent { 89 | 90 | final int streamId; 91 | 92 | Content(ByteBuf content, 93 | int streamId) { 94 | super(content); 95 | this.streamId = streamId; 96 | } 97 | } 98 | 99 | static final class LastContent extends Content { 100 | 101 | final Http2Headers trailers; 102 | final int streamDependency; 103 | final short weight; 104 | final boolean exclusive; 105 | 106 | LastContent(ByteBuf content, 107 | Http2Headers trailers, 108 | int streamId, 109 | int streamDependency, 110 | short weight, 111 | boolean exclusive) { 112 | super(content, streamId); 113 | this.trailers = trailers; 114 | this.streamDependency = streamDependency; 115 | this.weight = weight; 116 | this.exclusive = exclusive; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/MultipartHandleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.collection.MultiMaps; 19 | import io.esastack.httpserver.core.MultiPart; 20 | import io.esastack.httpserver.core.MultipartFile; 21 | import io.netty.buffer.Unpooled; 22 | import io.netty.handler.codec.http.DefaultHttpRequest; 23 | import io.netty.handler.codec.http.HttpHeaderNames; 24 | import io.netty.handler.codec.http.HttpHeaderValues; 25 | import io.netty.handler.codec.http.HttpMethod; 26 | import io.netty.handler.codec.http.HttpRequest; 27 | import io.netty.handler.codec.http.HttpVersion; 28 | import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; 29 | import org.junit.jupiter.api.Test; 30 | 31 | import java.io.IOException; 32 | import java.nio.charset.StandardCharsets; 33 | import java.util.Collections; 34 | 35 | import static org.junit.jupiter.api.Assertions.assertEquals; 36 | import static org.junit.jupiter.api.Assertions.assertSame; 37 | 38 | class MultipartHandleTest { 39 | 40 | @Test 41 | void testEmpty() { 42 | final MultiPart empty = MultipartHandle.EMPTY; 43 | assertSame(Collections.emptyList(), empty.uploadFiles()); 44 | assertSame(MultiMaps.emptyMultiMap(), empty.attributes()); 45 | } 46 | 47 | @Test 48 | void testDecodeFilesAndAttributes() throws IOException { 49 | 50 | final HttpRequest request = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/foo"); 51 | final String boundary = "dLV9Wyq26L_-JQxk6ferf-RT153LhOO"; 52 | request.headers().add(HttpHeaderNames.CONTENT_TYPE, "multipart/form-data; boundary=" + boundary); 53 | request.headers().add(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); 54 | 55 | 56 | final HttpPostRequestDecoder decoder = new HttpPostRequestDecoder(request); 57 | final MultipartHandle handle = new MultipartHandle(decoder); 58 | assertSame(Collections.emptyList(), handle.uploadFiles()); 59 | assertSame(MultiMaps.emptyMultiMap(), handle.attributes()); 60 | 61 | final String body1 = 62 | "--" + boundary + "\r\n" + 63 | "Content-Disposition: form-data; name=\"file\"; filename=\"tmp-0.txt\"\r\n" + 64 | "Content-Type: image/gif\r\n" + 65 | "\r\n" + 66 | "foo" + "\r\n" + 67 | "--" + boundary; 68 | final String body2 = "\r\n" + 69 | "Content-Disposition: form-data; name=\"foo\"\r\n" + 70 | "\r\n" + 71 | "bar\r\n" + 72 | "--" + boundary + "--\r\n"; 73 | 74 | handle.onData(Unpooled.copiedBuffer(body1, StandardCharsets.UTF_8)); 75 | handle.onData(Unpooled.copiedBuffer(body2, StandardCharsets.UTF_8)); 76 | assertSame(Collections.emptyList(), handle.uploadFiles()); 77 | assertSame(MultiMaps.emptyMultiMap(), handle.attributes()); 78 | 79 | try { 80 | handle.end(); 81 | assertEquals(1, handle.uploadFiles().size()); 82 | final MultipartFile upload = handle.uploadFiles().get(0); 83 | assertEquals("file", upload.name()); 84 | assertEquals("tmp-0.txt", upload.fileName()); 85 | assertEquals("foo", upload.string(StandardCharsets.UTF_8)); 86 | 87 | assertEquals(1, handle.attributes().size()); 88 | assertEquals("bar", handle.attributes().getFirst("foo")); 89 | } finally { 90 | handle.release(); 91 | } 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/metrics/impl/OpenSslSessionMetricsImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.metrics.impl; 17 | 18 | import esa.commons.Checks; 19 | import io.esastack.httpserver.metrics.OpenSslSessionMetrics; 20 | import io.netty.handler.ssl.OpenSslSessionStats; 21 | 22 | final class OpenSslSessionMetricsImpl implements OpenSslSessionMetrics { 23 | 24 | static final OpenSslSessionMetrics DISABLED = new Disabled(); 25 | 26 | private final OpenSslSessionStats stats; 27 | 28 | OpenSslSessionMetricsImpl(OpenSslSessionStats stats) { 29 | Checks.checkNotNull(stats, "stats"); 30 | this.stats = stats; 31 | } 32 | 33 | @Override 34 | public long number() { 35 | return stats.number(); 36 | } 37 | 38 | @Override 39 | public long accept() { 40 | return stats.accept(); 41 | } 42 | 43 | @Override 44 | public long acceptGood() { 45 | return stats.acceptGood(); 46 | } 47 | 48 | @Override 49 | public long acceptRenegotiate() { 50 | return stats.acceptRenegotiate(); 51 | } 52 | 53 | @Override 54 | public long hits() { 55 | return stats.hits(); 56 | } 57 | 58 | @Override 59 | public long cbHits() { 60 | return stats.cbHits(); 61 | } 62 | 63 | @Override 64 | public long misses() { 65 | return stats.misses(); 66 | } 67 | 68 | @Override 69 | public long timeouts() { 70 | return stats.timeouts(); 71 | } 72 | 73 | @Override 74 | public long cacheFull() { 75 | return stats.cacheFull(); 76 | } 77 | 78 | @Override 79 | public long ticketKeyFail() { 80 | return stats.ticketKeyFail(); 81 | } 82 | 83 | @Override 84 | public long ticketKeyNew() { 85 | return stats.ticketKeyNew(); 86 | } 87 | 88 | @Override 89 | public long ticketKeyRenew() { 90 | return stats.ticketKeyRenew(); 91 | } 92 | 93 | @Override 94 | public long ticketKeyResume() { 95 | return stats.ticketKeyResume(); 96 | } 97 | 98 | private static class Disabled implements OpenSslSessionMetrics { 99 | 100 | @Override 101 | public long number() { 102 | return 0L; 103 | } 104 | 105 | @Override 106 | public long accept() { 107 | return 0L; 108 | } 109 | 110 | @Override 111 | public long acceptGood() { 112 | return 0L; 113 | } 114 | 115 | @Override 116 | public long acceptRenegotiate() { 117 | return 0L; 118 | } 119 | 120 | @Override 121 | public long hits() { 122 | return 0L; 123 | } 124 | 125 | @Override 126 | public long cbHits() { 127 | return 0L; 128 | } 129 | 130 | @Override 131 | public long misses() { 132 | return 0L; 133 | } 134 | 135 | @Override 136 | public long timeouts() { 137 | return 0L; 138 | } 139 | 140 | @Override 141 | public long cacheFull() { 142 | return 0L; 143 | } 144 | 145 | @Override 146 | public long ticketKeyFail() { 147 | return 0L; 148 | } 149 | 150 | @Override 151 | public long ticketKeyNew() { 152 | return 0L; 153 | } 154 | 155 | @Override 156 | public long ticketKeyRenew() { 157 | return 0L; 158 | } 159 | 160 | @Override 161 | public long ticketKeyResume() { 162 | return 0L; 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/AggregatedLastHttpContent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.handler.codec.DecoderResult; 20 | import io.netty.handler.codec.http.HttpHeaders; 21 | import io.netty.handler.codec.http.LastHttpContent; 22 | import io.netty.util.internal.StringUtil; 23 | 24 | import java.util.Map; 25 | 26 | final class AggregatedLastHttpContent implements LastHttpContent { 27 | 28 | private final ByteBuf content; 29 | private final HttpHeaders trailers; 30 | 31 | AggregatedLastHttpContent(ByteBuf content, HttpHeaders trailers) { 32 | this.content = content; 33 | this.trailers = trailers; 34 | } 35 | 36 | @Override 37 | public ByteBuf content() { 38 | return content; 39 | } 40 | 41 | @Override 42 | public AggregatedLastHttpContent copy() { 43 | return replace(content.copy()); 44 | } 45 | 46 | @Override 47 | public AggregatedLastHttpContent duplicate() { 48 | return replace(content.duplicate()); 49 | } 50 | 51 | @Override 52 | public AggregatedLastHttpContent replace(ByteBuf content) { 53 | return new AggregatedLastHttpContent(content, trailers.copy()); 54 | } 55 | 56 | @Override 57 | public AggregatedLastHttpContent retainedDuplicate() { 58 | return replace(content.retainedDuplicate()); 59 | } 60 | 61 | @Override 62 | public HttpHeaders trailingHeaders() { 63 | return trailers; 64 | } 65 | 66 | @Override 67 | public DecoderResult decoderResult() { 68 | return DecoderResult.SUCCESS; 69 | } 70 | 71 | @Override 72 | @Deprecated 73 | public DecoderResult getDecoderResult() { 74 | return decoderResult(); 75 | } 76 | 77 | @Override 78 | public void setDecoderResult(DecoderResult result) { 79 | throw new UnsupportedOperationException("read only"); 80 | } 81 | 82 | @Override 83 | public int refCnt() { 84 | return content.refCnt(); 85 | } 86 | 87 | @Override 88 | public AggregatedLastHttpContent retain() { 89 | content.retain(); 90 | return this; 91 | } 92 | 93 | @Override 94 | public AggregatedLastHttpContent retain(int increment) { 95 | content.retain(increment); 96 | return this; 97 | } 98 | 99 | @Override 100 | public AggregatedLastHttpContent touch() { 101 | content.touch(); 102 | return this; 103 | } 104 | 105 | @Override 106 | public AggregatedLastHttpContent touch(Object hint) { 107 | content.touch(hint); 108 | return this; 109 | } 110 | 111 | @Override 112 | public boolean release() { 113 | return content.release(); 114 | } 115 | 116 | @Override 117 | public boolean release(int decrement) { 118 | return content.release(decrement); 119 | } 120 | 121 | @Override 122 | public String toString() { 123 | StringBuilder buf = new StringBuilder(StringUtil.simpleClassName(this) + 124 | "(data: " + content() + ", decoderResult: " + decoderResult() + ')'); 125 | buf.append(StringUtil.NEWLINE); 126 | appendHeaders(buf); 127 | 128 | // Remove the last newline. 129 | buf.setLength(buf.length() - StringUtil.NEWLINE.length()); 130 | return buf.toString(); 131 | } 132 | 133 | private void appendHeaders(StringBuilder buf) { 134 | for (Map.Entry e : trailingHeaders()) { 135 | buf.append(e.getKey()); 136 | buf.append(": "); 137 | buf.append(e.getValue()); 138 | buf.append(StringUtil.NEWLINE); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/HAProxyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.NetworkUtils; 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.buffer.Unpooled; 21 | import io.netty.channel.embedded.EmbeddedChannel; 22 | import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; 23 | import org.junit.jupiter.api.Test; 24 | 25 | import java.net.SocketAddress; 26 | import java.nio.charset.StandardCharsets; 27 | 28 | import static org.junit.jupiter.api.Assertions.assertEquals; 29 | import static org.junit.jupiter.api.Assertions.assertNotNull; 30 | import static org.junit.jupiter.api.Assertions.assertNull; 31 | import static org.junit.jupiter.api.Assertions.assertSame; 32 | 33 | class HAProxyTest { 34 | 35 | @Test 36 | void testHAProxyDetected() { 37 | final EmbeddedChannel channel = new EmbeddedChannel(new HAProxyDetector()); 38 | channel.writeInbound(Unpooled.copiedBuffer("PROXY ".getBytes(StandardCharsets.US_ASCII))); 39 | // needs more bytes 40 | assertNull(channel.readInbound()); 41 | channel.writeInbound(Unpooled.copiedBuffer("TCP4 192.168.0.1 " 42 | .getBytes(StandardCharsets.US_ASCII))); 43 | // fall in HAProxyMessageDecoder 44 | assertNull(channel.readInbound()); 45 | 46 | assertNotNull(channel.pipeline().get(HAProxyMessageDecoder.class)); 47 | assertNotNull(channel.pipeline().get(HAProxyMessageHandler.class)); 48 | assertEquals(channel.pipeline().first().getClass(), HAProxyMessageDecoder.class); 49 | assertEquals(channel.pipeline().last().getClass(), HAProxyMessageHandler.class); 50 | 51 | channel.writeInbound(Unpooled.copiedBuffer("192.168.0.11 56324 443\r\n" 52 | .getBytes(StandardCharsets.US_ASCII))); 53 | 54 | assertNull(channel.pipeline().get(HAProxyMessageDecoder.class)); 55 | assertNull(channel.pipeline().get(HAProxyMessageHandler.class)); 56 | 57 | final SocketAddress sourceAddress = channel.attr(Utils.SOURCE_ADDRESS).get(); 58 | assertNotNull(sourceAddress); 59 | 60 | assertEquals("192.168.0.1:56324", NetworkUtils.parseAddress(sourceAddress)); 61 | } 62 | 63 | @Test 64 | void testHAProxyDetectFailed() { 65 | final EmbeddedChannel channel = new EmbeddedChannel(new HAProxyDetector()); 66 | final String header = "UNKNOWN UNKNOWN 192.168.0.1 192.168.0.11 56324 443\r\n"; 67 | final ByteBuf buf = Unpooled.copiedBuffer(header.getBytes(StandardCharsets.US_ASCII)); 68 | channel.writeInbound(buf); 69 | assertSame(buf, channel.readInbound()); 70 | assertNull(channel.pipeline().get(HAProxyDetector.class)); 71 | assertNull(channel.pipeline().get(HAProxyMessageDecoder.class)); 72 | assertNull(channel.pipeline().get(HAProxyMessageHandler.class)); 73 | } 74 | 75 | @Test 76 | void testHAProxyDetectedAsUnknownProtocol() { 77 | final EmbeddedChannel channel = new EmbeddedChannel(new HAProxyDetector()); 78 | final String header = "PROXY UNKNOWN 192.168.0.1 192.168.0.11 56324 443\r\n"; 79 | final ByteBuf buf = Unpooled.copiedBuffer(header.getBytes(StandardCharsets.US_ASCII)); 80 | channel.writeInbound(buf); 81 | assertNull(channel.pipeline().get(HAProxyDetector.class)); 82 | assertNull(channel.pipeline().get(HAProxyMessageDecoder.class)); 83 | assertNull(channel.pipeline().get(HAProxyMessageHandler.class)); 84 | assertNull(channel.attr(Utils.SOURCE_ADDRESS).get()); 85 | } 86 | 87 | @Test 88 | void testNoneHAPRoxyMessageReadInHAProxyMessageHandler() { 89 | final EmbeddedChannel channel = new EmbeddedChannel(new HAProxyMessageHandler()); 90 | final Object obj = new Object(); 91 | channel.writeInbound(obj); 92 | assertSame(obj, channel.readInbound()); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/it/ChunkWriteTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.it; 17 | 18 | import esa.commons.NetworkUtils; 19 | import esa.commons.io.IOUtils; 20 | import io.esastack.httpserver.HttpServer; 21 | import io.esastack.httpserver.ServerOptionsConfigure; 22 | import io.esastack.httpserver.core.Response; 23 | import io.netty.buffer.CompositeByteBuf; 24 | import io.netty.buffer.Unpooled; 25 | import io.netty.handler.logging.LogLevel; 26 | import org.junit.jupiter.api.Test; 27 | 28 | import java.io.IOException; 29 | import java.net.HttpURLConnection; 30 | import java.net.URL; 31 | import java.nio.charset.StandardCharsets; 32 | import java.util.concurrent.CompletableFuture; 33 | import java.util.concurrent.ExecutionException; 34 | import java.util.concurrent.TimeUnit; 35 | import java.util.concurrent.TimeoutException; 36 | 37 | import static org.junit.jupiter.api.Assertions.assertEquals; 38 | import static org.junit.jupiter.api.Assertions.assertTrue; 39 | 40 | class ChunkWriteTest { 41 | 42 | @Test 43 | void testWriteChunkResponse() throws IOException, InterruptedException, ExecutionException, TimeoutException { 44 | final int port = NetworkUtils.selectRandomPort(); 45 | final CompositeByteBuf buf = Unpooled.compositeBuffer(); 46 | final CompletableFuture ended = new CompletableFuture<>(); 47 | final HttpServer server = HttpServer.create(ServerOptionsConfigure.newOpts() 48 | .logging(LogLevel.INFO) 49 | .ioThreads(1) 50 | .configured()) 51 | .handle(req -> { 52 | req.onData(data -> buf.writeBytes(data.retain())) 53 | .onEnd(p -> { 54 | ended.complete(true); 55 | return p.setSuccess(null); 56 | }); 57 | final Response res = req.response(); 58 | res.headers().set("a", 1); 59 | res.write("1".getBytes()); 60 | res.write(Unpooled.copiedBuffer("2".getBytes())); 61 | res.write("3".getBytes()).addListener(f -> { 62 | res.write("4".getBytes()).addListener(f1 -> { 63 | new Thread(() -> { 64 | res.write(Unpooled.copiedBuffer("5".getBytes())).addListener(f2 -> { 65 | new Thread(() -> { 66 | res.end(Unpooled.copiedBuffer("6".getBytes())); 67 | }).start(); 68 | }); 69 | }).start(); 70 | }); 71 | }); 72 | }); 73 | 74 | server.listen(port); 75 | 76 | HttpURLConnection httpURLConnection = null; 77 | try { 78 | httpURLConnection = 79 | (HttpURLConnection) new URL("http://localhost:" + port).openConnection(); 80 | httpURLConnection.setRequestMethod("POST"); 81 | httpURLConnection.setDoOutput(true); 82 | httpURLConnection.getOutputStream().write("foo".getBytes()); 83 | assertEquals(200, httpURLConnection.getResponseCode()); 84 | assertEquals("123456", IOUtils.toString(httpURLConnection.getInputStream())); 85 | assertEquals("1", httpURLConnection.getHeaderField("a")); 86 | assertTrue(ended.get(3L, TimeUnit.SECONDS)); 87 | assertEquals("foo", buf.toString(StandardCharsets.US_ASCII)); 88 | } finally { 89 | try { 90 | if (httpURLConnection != null) { 91 | httpURLConnection.disconnect(); 92 | } 93 | } catch (Exception ignored) { 94 | } 95 | server.close(); 96 | buf.release(); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/SslOptions.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver; 17 | 18 | import esa.commons.Checks; 19 | import io.netty.handler.ssl.ClientAuth; 20 | 21 | import java.io.File; 22 | import java.io.Serializable; 23 | import java.util.Arrays; 24 | 25 | public class SslOptions implements Serializable { 26 | 27 | private static final long serialVersionUID = 6772202423835136508L; 28 | 29 | private ClientAuth clientAuth; 30 | private String[] ciphers; 31 | private String[] enabledProtocols; 32 | private File certificate; 33 | private File privateKey; 34 | private String keyPassword; 35 | private File trustCertificates; 36 | private long sessionTimeout; 37 | private long sessionCacheSize; 38 | private long handshakeTimeoutMillis; 39 | 40 | public SslOptions() { 41 | } 42 | 43 | public SslOptions(SslOptions other) { 44 | Checks.checkNotNull(other, "other"); 45 | this.clientAuth = other.clientAuth; 46 | this.ciphers = other.ciphers == null ? null : Arrays.copyOf(other.ciphers, other.ciphers.length); 47 | this.enabledProtocols = other.enabledProtocols == null 48 | ? null 49 | : Arrays.copyOf(other.enabledProtocols, other.enabledProtocols.length); 50 | this.certificate = other.certificate; 51 | this.privateKey = other.privateKey; 52 | this.keyPassword = other.keyPassword; 53 | this.trustCertificates = other.trustCertificates; 54 | this.sessionTimeout = other.sessionTimeout; 55 | this.sessionCacheSize = other.sessionCacheSize; 56 | this.handshakeTimeoutMillis = other.handshakeTimeoutMillis; 57 | } 58 | 59 | public ClientAuth getClientAuth() { 60 | return clientAuth; 61 | } 62 | 63 | public void setClientAuth(ClientAuth clientAuth) { 64 | this.clientAuth = clientAuth; 65 | } 66 | 67 | public String[] getCiphers() { 68 | return ciphers; 69 | } 70 | 71 | public void setCiphers(String[] ciphers) { 72 | this.ciphers = ciphers; 73 | } 74 | 75 | public String[] getEnabledProtocols() { 76 | return enabledProtocols; 77 | } 78 | 79 | public void setEnabledProtocols(String[] enabledProtocols) { 80 | this.enabledProtocols = enabledProtocols; 81 | } 82 | 83 | public File getCertificate() { 84 | return certificate; 85 | } 86 | 87 | public void setCertificate(File certificate) { 88 | this.certificate = certificate; 89 | } 90 | 91 | public File getPrivateKey() { 92 | return privateKey; 93 | } 94 | 95 | public void setPrivateKey(File privateKey) { 96 | this.privateKey = privateKey; 97 | } 98 | 99 | public String getKeyPassword() { 100 | return keyPassword; 101 | } 102 | 103 | public void setKeyPassword(String keyPassword) { 104 | this.keyPassword = keyPassword; 105 | } 106 | 107 | public File getTrustCertificates() { 108 | return trustCertificates; 109 | } 110 | 111 | public void setTrustCertificates(File trustCertificates) { 112 | this.trustCertificates = trustCertificates; 113 | } 114 | 115 | public long getSessionTimeout() { 116 | return sessionTimeout; 117 | } 118 | 119 | public void setSessionTimeout(long sessionTimeout) { 120 | this.sessionTimeout = sessionTimeout; 121 | } 122 | 123 | public long getSessionCacheSize() { 124 | return sessionCacheSize; 125 | } 126 | 127 | public void setSessionCacheSize(long sessionCacheSize) { 128 | this.sessionCacheSize = sessionCacheSize; 129 | } 130 | 131 | public long getHandshakeTimeoutMillis() { 132 | return handshakeTimeoutMillis; 133 | } 134 | 135 | public void setHandshakeTimeoutMillis(long handshakeTimeoutMillis) { 136 | this.handshakeTimeoutMillis = handshakeTimeoutMillis; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/MultipartHandle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.ExceptionUtils; 19 | import esa.commons.collection.LinkedMultiValueMap; 20 | import esa.commons.collection.MultiMaps; 21 | import esa.commons.collection.MultiValueMap; 22 | import io.esastack.httpserver.core.MultiPart; 23 | import io.esastack.httpserver.core.MultipartFile; 24 | import io.esastack.httpserver.utils.Loggers; 25 | import io.netty.buffer.ByteBuf; 26 | import io.netty.handler.codec.http.DefaultHttpContent; 27 | import io.netty.handler.codec.http.LastHttpContent; 28 | import io.netty.handler.codec.http.multipart.Attribute; 29 | import io.netty.handler.codec.http.multipart.FileUpload; 30 | import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; 31 | import io.netty.handler.codec.http.multipart.InterfaceHttpData; 32 | 33 | import java.io.IOException; 34 | import java.util.Collections; 35 | import java.util.LinkedList; 36 | import java.util.List; 37 | 38 | final class MultipartHandle implements MultiPart { 39 | 40 | static final Empty EMPTY = new Empty(); 41 | 42 | private final HttpPostRequestDecoder decoder; 43 | private List files; 44 | private MultiValueMap attrs; 45 | 46 | MultipartHandle(HttpPostRequestDecoder decoder) { 47 | this.decoder = decoder; 48 | } 49 | 50 | @Override 51 | public List uploadFiles() { 52 | if (files == null) { 53 | return EMPTY.uploadFiles(); 54 | } 55 | return files; 56 | } 57 | 58 | @Override 59 | public MultiValueMap attributes() { 60 | if (attrs == null) { 61 | return EMPTY.attributes(); 62 | } 63 | return attrs; 64 | } 65 | 66 | void onData(ByteBuf buf) { 67 | decoder.offer(new DefaultHttpContent(buf)); 68 | } 69 | 70 | void end() { 71 | List files = null; 72 | MultiValueMap attrs = null; 73 | try { 74 | decoder.offer(LastHttpContent.EMPTY_LAST_CONTENT); 75 | List decoded = decoder.getBodyHttpDatas(); 76 | for (InterfaceHttpData data : decoded) { 77 | if (InterfaceHttpData.HttpDataType.Attribute.equals(data.getHttpDataType())) { 78 | Attribute attr = (Attribute) data; 79 | if (attrs == null) { 80 | attrs = new LinkedMultiValueMap<>(); 81 | } 82 | attrs.add(attr.getName(), attr.getValue()); 83 | } else if (InterfaceHttpData.HttpDataType.FileUpload.equals(data.getHttpDataType())) { 84 | if (files == null) { 85 | files = new LinkedList<>(); 86 | } 87 | files.add(new MultipartFileImpl((FileUpload) data)); 88 | } 89 | } 90 | 91 | if (files != null) { 92 | this.files = files; 93 | } 94 | if (attrs != null) { 95 | this.attrs = attrs; 96 | } 97 | } catch (IOException e) { 98 | ExceptionUtils.throwException(e); 99 | } 100 | } 101 | 102 | void release() { 103 | try { 104 | decoder.destroy(); 105 | } catch (Exception e) { 106 | Loggers.logger().warn( 107 | "Unexpected error while releasing multipart resources", e); 108 | } 109 | } 110 | 111 | private static class Empty implements MultiPart { 112 | 113 | @Override 114 | public List uploadFiles() { 115 | return Collections.emptyList(); 116 | } 117 | 118 | @Override 119 | public MultiValueMap attributes() { 120 | return MultiMaps.emptyMultiMap(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/Http2ConnectionChunkHandlerBuilderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.handler.codec.http2.DefaultHttp2Connection; 19 | import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder; 20 | import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; 21 | import io.netty.handler.codec.http2.DefaultHttp2FrameReader; 22 | import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; 23 | import io.netty.handler.codec.http2.Http2Connection; 24 | import io.netty.handler.codec.http2.Http2ConnectionDecoder; 25 | import io.netty.handler.codec.http2.Http2ConnectionEncoder; 26 | import io.netty.handler.codec.http2.Http2FrameListener; 27 | import io.netty.handler.codec.http2.Http2FrameLogger; 28 | import io.netty.handler.codec.http2.Http2FrameReader; 29 | import io.netty.handler.codec.http2.Http2FrameWriter; 30 | import io.netty.handler.codec.http2.Http2HeadersEncoder; 31 | import io.netty.handler.codec.http2.Http2OutboundFrameLogger; 32 | import io.netty.handler.codec.http2.Http2Settings; 33 | import io.netty.handler.logging.LogLevel; 34 | import org.junit.jupiter.api.Test; 35 | 36 | import static org.junit.jupiter.api.Assertions.assertSame; 37 | import static org.junit.jupiter.api.Assertions.assertThrows; 38 | import static org.junit.jupiter.api.Assertions.assertTrue; 39 | import static org.mockito.Mockito.mock; 40 | 41 | class Http2ConnectionChunkHandlerBuilderTest { 42 | 43 | @Test 44 | void testBuild() { 45 | final Http2ConnectionChunkHandlerBuilder builder = new Http2ConnectionChunkHandlerBuilder(); 46 | final Http2Settings settings = Http2Settings.defaultSettings(); 47 | builder.validateHeaders(true) 48 | .initialSettings(settings); 49 | assertSame(settings, builder.initialSettings()); 50 | 51 | assertThrows(NullPointerException.class, () -> builder.frameListener(null)); 52 | assertThrows(IllegalArgumentException.class, () -> builder.gracefulShutdownTimeoutMillis(-2L)); 53 | builder.server(true); 54 | 55 | final Http2FrameListener listener = mock(Http2FrameListener.class); 56 | builder.frameListener(listener).maxReservedStreams(1) 57 | .encoderEnforceMaxConcurrentStreams(false) 58 | .encoderIgnoreMaxHeaderListSize(false) 59 | .headerSensitivityDetector(Http2HeadersEncoder.ALWAYS_SENSITIVE) 60 | .decoupleCloseAndGoAway(false) 61 | .frameLogger(new Http2FrameLogger(LogLevel.INFO)); 62 | 63 | final Http2ConnectionChunkHandler handler = builder.build(); 64 | 65 | assertTrue(handler.encoder().frameWriter() instanceof Http2OutboundFrameLogger); 66 | assertTrue(handler.connection().isServer()); 67 | } 68 | 69 | @Test 70 | void testBuildWithCodec() { 71 | final Http2Connection connection = new DefaultHttp2Connection(false); 72 | final Http2FrameWriter writer = new DefaultHttp2FrameWriter(); 73 | final Http2FrameReader reader = new DefaultHttp2FrameReader(); 74 | final Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, writer); 75 | final Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader); 76 | 77 | final Http2ConnectionChunkHandler handler = new Http2ConnectionChunkHandlerBuilder() 78 | .codec(decoder, encoder) 79 | .frameListener(mock(Http2FrameListener.class)) 80 | .build(); 81 | 82 | assertSame(encoder, handler.encoder()); 83 | } 84 | 85 | @Test 86 | void testBuildWithConnection() { 87 | final Http2Connection connection = new DefaultHttp2Connection(true); 88 | final Http2ConnectionChunkHandler handler = new Http2ConnectionChunkHandlerBuilder() 89 | .connection(connection) 90 | .frameListener(mock(Http2FrameListener.class)) 91 | .build(); 92 | 93 | assertSame(connection, handler.connection()); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/impl/Http2RequestHandleImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.esastack.commons.net.http.HttpMethod; 19 | import io.esastack.commons.net.http.HttpVersion; 20 | import io.esastack.httpserver.utils.Constants; 21 | import io.netty.channel.ChannelHandlerContext; 22 | import io.netty.channel.ChannelInboundHandlerAdapter; 23 | import io.netty.channel.embedded.EmbeddedChannel; 24 | import io.netty.handler.codec.http.HttpRequest; 25 | import io.netty.handler.codec.http2.DefaultHttp2Headers; 26 | import io.netty.handler.codec.http2.Http2CodecUtil; 27 | import io.netty.handler.codec.http2.Http2ConnectionEncoder; 28 | import io.netty.handler.codec.http2.Http2Headers; 29 | import io.netty.handler.codec.http2.Http2Stream; 30 | import org.junit.jupiter.api.Test; 31 | 32 | import static org.junit.jupiter.api.Assertions.assertEquals; 33 | import static org.junit.jupiter.api.Assertions.assertNotNull; 34 | import static org.junit.jupiter.api.Assertions.assertTrue; 35 | import static org.mockito.Mockito.mock; 36 | import static org.mockito.Mockito.when; 37 | 38 | class Http2RequestHandleImplTest { 39 | 40 | @Test 41 | void testGeneration() { 42 | 43 | final ChannelHandlerContext ctx = mock(ChannelHandlerContext.class); 44 | final EmbeddedChannel channel = new EmbeddedChannel(); 45 | when(ctx.channel()).thenReturn(channel); 46 | final Http2ConnectionEncoder encoder = mock(Http2ConnectionEncoder.class); 47 | final Http2Headers headers = new DefaultHttp2Headers() 48 | .path("/foo?a=1&b=2") 49 | .method(HttpMethod.GET.name()) 50 | .scheme(Constants.SCHEMA_HTTP) 51 | .add("a", "a"); 52 | final Http2Stream stream = mock(Http2Stream.class); 53 | final Http2RequestHandleImpl handle = 54 | Http2RequestHandleImpl.from(Helper.serverRuntime(), 55 | ctx, 56 | encoder, 57 | headers, 58 | stream, 59 | 0, 60 | Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT, 61 | false); 62 | 63 | assertEquals(Constants.SCHEMA_HTTP, handle.scheme()); 64 | assertEquals(HttpVersion.HTTP_2, handle.version()); 65 | assertEquals(HttpMethod.GET, handle.method()); 66 | assertEquals("/foo?a=1&b=2", handle.uri()); 67 | assertEquals("/foo", handle.path()); 68 | assertEquals("a=1&b=2", handle.query()); 69 | assertTrue(handle.headers() instanceof Http2HeadersImpl); 70 | assertEquals("a", handle.headers().get("a")); 71 | assertNotNull(handle.response()); 72 | 73 | final HttpRequest req = handle.toHttpRequest(); 74 | assertEquals(io.netty.handler.codec.http.HttpMethod.GET, req.method()); 75 | assertEquals("/foo?a=1&b=2", req.uri()); 76 | assertEquals("a", req.headers().get("a")); 77 | } 78 | 79 | @Test 80 | void testUsingSchemaInChannelAttribute() { 81 | final EmbeddedChannel channel = new EmbeddedChannel(new ChannelInboundHandlerAdapter()); 82 | channel.attr(Constants.SCHEME).set(Constants.SCHEMA_HTTPS); 83 | final Http2ConnectionEncoder encoder = mock(Http2ConnectionEncoder.class); 84 | final Http2Headers headers = new DefaultHttp2Headers() 85 | .path("/foo") 86 | .method(HttpMethod.GET.name()); 87 | final Http2Stream stream = mock(Http2Stream.class); 88 | final Http2RequestHandleImpl handle = 89 | Http2RequestHandleImpl.from(Helper.serverRuntime(), 90 | channel.pipeline().firstContext(), 91 | encoder, 92 | headers, 93 | stream, 94 | 0, 95 | Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT, 96 | false); 97 | 98 | assertEquals(Constants.SCHEMA_HTTPS, handle.scheme()); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/core/RequestHandle.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.core; 17 | 18 | import io.esastack.commons.net.http.HttpHeaders; 19 | import io.netty.buffer.ByteBuf; 20 | import io.netty.util.concurrent.Future; 21 | import io.netty.util.concurrent.Promise; 22 | 23 | import java.util.function.Consumer; 24 | import java.util.function.Function; 25 | 26 | /** 27 | * An interface defines a http server {@link Request} and provides the guiding to handle the coming request. 28 | *

29 | * When the {@link RequestHandle} is coming, current {@link Request} may have not been ended, which means the request 30 | * line and the request headers is coming and decoded as a instance of {@link RequestHandle} but the body(if present) 31 | * and trailing headers(if present) have not been received, and next you may want to handle them via {@link 32 | * #onData(Consumer)}, {@link #onTrailer(Consumer)}, {@link #onEnd(Function)}... 33 | *

34 | * !Note: This class is not designed as thread-safe, please handing requests in current thread(probably the EventLoop), 35 | * because it is in unfinished state before calling {@link #onEnd(Function)} method, so you can use the {@link 36 | * #aggregated()} and {@link #multipart()} in {@link #onEnd(Function)} or use it after {@link #isEnded()} returns {@code 37 | * true} 38 | */ 39 | public interface RequestHandle extends Request { 40 | 41 | /** 42 | * Sets a handler to handle the coming http body. 43 | *

44 | * The handle set would not be called if there's no http body data. 45 | * 46 | * @param h handler 47 | * @return this 48 | */ 49 | RequestHandle onData(Consumer h); 50 | 51 | /** 52 | * Sets a handler to handle the coming tailing headers. 53 | *

54 | * The handle set would not be called if there's no tailing headers. 55 | * 56 | * @param h handler 57 | * @return this 58 | */ 59 | RequestHandle onTrailer(Consumer h); 60 | 61 | /** 62 | * {@link #aggregated()} could be used after request is ended, which means you can get a completed request such as 63 | * using the {@link #aggregated()} in handle {@link #onEnd(Function)}. 64 | * 65 | * @param aggregate whether request aggregation is expected 66 | * @return this 67 | */ 68 | RequestHandle aggregate(boolean aggregate); 69 | 70 | /** 71 | * {@link #multipart()} could be used after request is ended, which means you can get a completed request that 72 | * multipart contents have already been decoded such as using the {@link #aggregated()} in handle {@link 73 | * #onEnd(Function)}. It would be useful if there's large upload files in this request. 74 | * 75 | * @param expect whether multipart is expected 76 | * @return this 77 | */ 78 | RequestHandle multipart(boolean expect); 79 | 80 | /** 81 | * Sets a handler to handle this request when current request has been decoded completely. 82 | *

83 | * Only one of the following handler will be called for each request except that there's something wrong with the 84 | * onEnd handler(eg. throw an exception) 85 | * 86 | *

    87 | *
  • onEnd handler set by {@link #onEnd(Function)}
  • 88 | *
  • onError handler set by {@link #onError(Consumer)}
  • 89 | *
90 | * 91 | * @param h handler 92 | * @return this 93 | */ 94 | RequestHandle onEnd(Function, Future> h); 95 | 96 | /** 97 | * Sets a handler to handle unexpected error. 98 | *

99 | * Only one of the following handler will be called for each request except that there's something wrong with the 100 | * onEnd handler(eg. throw an exception) 101 | * 102 | *

    103 | *
  • onEnd handler set by {@link #onEnd(Function)}
  • 104 | *
  • onError handler set by {@link #onError(Consumer)}
  • 105 | *
106 | * 107 | * @param h handler 108 | * @return this 109 | */ 110 | RequestHandle onError(Consumer h); 111 | } 112 | -------------------------------------------------------------------------------- /httpserver/src/test/java/io/esastack/httpserver/transport/NioTransportTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import io.esastack.httpserver.NetOptions; 19 | import io.netty.bootstrap.ServerBootstrap; 20 | import io.netty.channel.Channel; 21 | import io.netty.channel.ChannelFactory; 22 | import io.netty.channel.ChannelHandlerAdapter; 23 | import io.netty.channel.ChannelHandlerContext; 24 | import io.netty.channel.ChannelInboundHandlerAdapter; 25 | import io.netty.channel.ChannelOption; 26 | import io.netty.channel.EventLoopGroup; 27 | import io.netty.channel.ServerChannel; 28 | import io.netty.channel.local.LocalAddress; 29 | import io.netty.channel.nio.NioEventLoopGroup; 30 | import io.netty.channel.socket.nio.NioServerSocketChannel; 31 | import io.netty.channel.unix.DomainSocketAddress; 32 | import io.netty.util.concurrent.DefaultThreadFactory; 33 | import org.junit.jupiter.api.Test; 34 | 35 | import java.net.InetSocketAddress; 36 | import java.util.Collections; 37 | import java.util.concurrent.CountDownLatch; 38 | import java.util.concurrent.atomic.AtomicReference; 39 | 40 | import static org.junit.jupiter.api.Assertions.assertEquals; 41 | import static org.junit.jupiter.api.Assertions.assertFalse; 42 | import static org.junit.jupiter.api.Assertions.assertNotNull; 43 | import static org.junit.jupiter.api.Assertions.assertThrows; 44 | import static org.junit.jupiter.api.Assertions.assertTrue; 45 | 46 | class NioTransportTest { 47 | 48 | @Test 49 | void testServerChannelFactoryCreation() { 50 | final ChannelFactory factory = NioTransport.INSTANCE 51 | .serverChannelFactory(new InetSocketAddress(8080)); 52 | 53 | assertTrue(factory.newChannel() instanceof NioServerSocketChannel); 54 | assertThrows(IllegalArgumentException.class, () -> NioTransport.INSTANCE 55 | .serverChannelFactory(new DomainSocketAddress("/tmp.sock"))); 56 | } 57 | 58 | @Test 59 | void testEventLoopGroupCreation() { 60 | final EventLoopGroup loop = NioTransport.INSTANCE 61 | .loop(1, new DefaultThreadFactory("foo")); 62 | 63 | assertTrue(loop instanceof NioEventLoopGroup); 64 | } 65 | 66 | @Test 67 | void testApplyOptions() throws InterruptedException { 68 | final NetOptions options = new NetOptions(); 69 | options.setSoBacklog(10); 70 | options.setReuseAddress(false); 71 | options.setSoKeepalive(true); 72 | options.setTcpNoDelay(true); 73 | options.setSoRcvbuf(100); 74 | options.setSoSendbuf(100); 75 | options.setSoLinger(1); 76 | options.setOptions(Collections.singletonMap(ChannelOption.AUTO_READ, false)); 77 | final CountDownLatch latch = new CountDownLatch(1); 78 | final AtomicReference ch = new AtomicReference<>(); 79 | final EventLoopGroup group = new NioEventLoopGroup(1); 80 | try { 81 | ServerBootstrap sb = new ServerBootstrap(); 82 | NioTransport.INSTANCE.applyOptions(sb, options, new LocalAddress("foo")); 83 | sb.channel(NioServerSocketChannel.class) 84 | .group(group) 85 | .childHandler(new ChannelInboundHandlerAdapter()) 86 | .handler(new ChannelHandlerAdapter() { 87 | @Override 88 | public void handlerAdded(ChannelHandlerContext ctx) { 89 | ch.set(ctx.channel()); 90 | latch.countDown(); 91 | } 92 | }); 93 | sb.register().syncUninterruptibly(); 94 | latch.await(); 95 | final Channel channel = ch.get(); 96 | assertNotNull(channel); 97 | 98 | assertEquals(10, channel.config().getOption(ChannelOption.SO_BACKLOG)); 99 | assertFalse(channel.config().getOption(ChannelOption.SO_REUSEADDR)); 100 | assertFalse(channel.config().getOption(ChannelOption.AUTO_READ)); 101 | } finally { 102 | group.shutdownGracefully(); 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/core/BaseRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.core; 17 | 18 | import io.esastack.commons.net.http.Cookie; 19 | import io.esastack.commons.net.http.HttpHeaders; 20 | import io.esastack.commons.net.http.HttpMethod; 21 | import io.esastack.commons.net.http.HttpVersion; 22 | 23 | import java.net.SocketAddress; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | /** 28 | * Base interface of a http request. 29 | */ 30 | public interface BaseRequest { 31 | 32 | /** 33 | * HttpVersion, such as HTTP/1.1 34 | * 35 | * @return version 36 | */ 37 | HttpVersion version(); 38 | 39 | /** 40 | * HTTP or HTTPS 41 | * 42 | * @return scheme 43 | */ 44 | String scheme(); 45 | 46 | /** 47 | * Http aggregated url. For example '/foo/bar?baz=qux'. 48 | * 49 | * @return url 50 | */ 51 | String uri(); 52 | 53 | /** 54 | * Http path except parameters. For example path of uri '/foo/bar?baz=qux' is '/foo/bar'. 55 | * 56 | * @return uri 57 | */ 58 | String path(); 59 | 60 | /** 61 | * Returns the query part of the uri. For example query of uri '/foo/bar?baz=qux' is 'baz=qux'. 62 | * 63 | * @return query string 64 | */ 65 | String query(); 66 | 67 | /** 68 | * HTTP method 69 | * 70 | * @return method 71 | */ 72 | HttpMethod method(); 73 | 74 | /** 75 | * HTTP method as String type. 76 | * 77 | * @return method 78 | */ 79 | default String rawMethod() { 80 | return method().name(); 81 | } 82 | 83 | /** 84 | * Gets parameter, This pair of parameter can be from url parameters or body k-v values when Content-Type equals to 85 | * 'x-www-form-urlencoded' 86 | * 87 | * @param parName parameter name 88 | * @return value 89 | */ 90 | default String getParam(String parName) { 91 | final List params = getParams(parName); 92 | if (params != null && params.size() > 0) { 93 | return params.get(0); 94 | } 95 | return null; 96 | } 97 | 98 | /** 99 | * Get parameters This pair of parameter can be from url parameters or body k-v values when Content-Type equals to 100 | * 'x-www-form-urlencoded' 101 | * 102 | * @param parName parameter name 103 | * @return value 104 | */ 105 | default List getParams(String parName) { 106 | return paramMap().get(parName); 107 | } 108 | 109 | /** 110 | * Get parameter map This pair of parameters can be from url parameters or body k-v values when Content-Type equals 111 | * to 'x-www-form-urlencoded' 112 | * 113 | * @return map 114 | */ 115 | Map> paramMap(); 116 | 117 | /** 118 | * Gets http headers 119 | * 120 | * @return headers 121 | */ 122 | HttpHeaders headers(); 123 | 124 | /** 125 | * Returns a map containing all of the {@link Cookie} objects the client sent with this request. 126 | * 127 | * @return all of the {@link Cookie} objects the client sent with this request, returns an empty map if no cookies 128 | * were sent. 129 | */ 130 | Map cookies(); 131 | 132 | /** 133 | * Gets the {@link Cookie} with given name. 134 | * 135 | * @param name cookie name 136 | * @return cookie or {@code null} if did not find. 137 | */ 138 | default Cookie getCookie(String name) { 139 | return cookies().get(name); 140 | } 141 | 142 | /** 143 | * Returns the Internet Protocol address of the client or last proxy that sent the request. 144 | * 145 | * @return addr 146 | */ 147 | SocketAddress remoteAddress(); 148 | 149 | /** 150 | * Returns the last proxy that sent the request. 151 | * 152 | * @return addr 153 | */ 154 | SocketAddress tcpSourceAddress(); 155 | 156 | /** 157 | * Returns the Internet Protocol address of the interface on which the request was received. 158 | * 159 | * @return addr 160 | */ 161 | SocketAddress localAddress(); 162 | } 163 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/Http2ConnectionChunkHandlerBuilder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder; 19 | import io.netty.handler.codec.http2.Http2Connection; 20 | import io.netty.handler.codec.http2.Http2ConnectionDecoder; 21 | import io.netty.handler.codec.http2.Http2ConnectionEncoder; 22 | import io.netty.handler.codec.http2.Http2FrameListener; 23 | import io.netty.handler.codec.http2.Http2FrameLogger; 24 | import io.netty.handler.codec.http2.Http2HeadersEncoder; 25 | import io.netty.handler.codec.http2.Http2Settings; 26 | 27 | final class Http2ConnectionChunkHandlerBuilder 28 | extends AbstractHttp2ConnectionHandlerBuilder { 29 | 30 | @Override 31 | public Http2ConnectionChunkHandlerBuilder validateHeaders(boolean validateHeaders) { 32 | return super.validateHeaders(validateHeaders); 33 | } 34 | 35 | @Override 36 | public Http2ConnectionChunkHandlerBuilder initialSettings(Http2Settings settings) { 37 | return super.initialSettings(settings); 38 | } 39 | 40 | @Override 41 | public Http2Settings initialSettings() { 42 | return super.initialSettings(); 43 | } 44 | 45 | @Override 46 | public Http2ConnectionChunkHandlerBuilder frameListener(Http2FrameListener frameListener) { 47 | return super.frameListener(frameListener); 48 | } 49 | 50 | @Override 51 | public Http2ConnectionChunkHandlerBuilder gracefulShutdownTimeoutMillis(long gracefulShutdownTimeoutMillis) { 52 | return super.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis); 53 | } 54 | 55 | @Override 56 | public Http2ConnectionChunkHandlerBuilder server(boolean isServer) { 57 | return super.server(isServer); 58 | } 59 | 60 | @Override 61 | public Http2ConnectionChunkHandlerBuilder connection(Http2Connection connection) { 62 | return super.connection(connection); 63 | } 64 | 65 | @Override 66 | public Http2ConnectionChunkHandlerBuilder maxReservedStreams(int maxReservedStreams) { 67 | return super.maxReservedStreams(maxReservedStreams); 68 | } 69 | 70 | @Override 71 | public Http2ConnectionChunkHandlerBuilder codec(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder) { 72 | return super.codec(decoder, encoder); 73 | } 74 | 75 | @Override 76 | public Http2ConnectionChunkHandlerBuilder frameLogger(Http2FrameLogger frameLogger) { 77 | return super.frameLogger(frameLogger); 78 | } 79 | 80 | @Override 81 | public Http2ConnectionChunkHandlerBuilder encoderEnforceMaxConcurrentStreams( 82 | boolean encoderEnforceMaxConcurrentStreams) { 83 | return super.encoderEnforceMaxConcurrentStreams(encoderEnforceMaxConcurrentStreams); 84 | } 85 | 86 | @Override 87 | public Http2ConnectionChunkHandlerBuilder encoderIgnoreMaxHeaderListSize(boolean encoderIgnoreMaxHeaderListSize) { 88 | return super.encoderIgnoreMaxHeaderListSize(encoderIgnoreMaxHeaderListSize); 89 | } 90 | 91 | @Override 92 | public Http2ConnectionChunkHandlerBuilder headerSensitivityDetector(Http2HeadersEncoder.SensitivityDetector arg0) { 93 | return super.headerSensitivityDetector(arg0); 94 | } 95 | 96 | @Override 97 | @Deprecated 98 | public Http2ConnectionChunkHandlerBuilder initialHuffmanDecodeCapacity(int initialHuffmanDecodeCapacity) { 99 | return super.initialHuffmanDecodeCapacity(initialHuffmanDecodeCapacity); 100 | } 101 | 102 | @Override 103 | public Http2ConnectionChunkHandlerBuilder decoupleCloseAndGoAway(boolean decoupleCloseAndGoAway) { 104 | return super.decoupleCloseAndGoAway(decoupleCloseAndGoAway); 105 | } 106 | 107 | @Override 108 | public Http2ConnectionChunkHandler build() { 109 | return super.build(); 110 | } 111 | 112 | @Override 113 | protected Http2ConnectionChunkHandler build(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, 114 | Http2Settings initialSettings) { 115 | return new Http2ConnectionChunkHandler(decoder, encoder, initialSettings, decoupleCloseAndGoAway()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/Http2RequestHandleImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import esa.commons.Checks; 19 | import io.esastack.commons.net.http.HttpHeaders; 20 | import io.esastack.commons.net.http.HttpMethod; 21 | import io.esastack.commons.net.http.HttpVersion; 22 | import io.esastack.commons.net.netty.http.Http1HeadersImpl; 23 | import io.esastack.httpserver.core.RequestHandle; 24 | import io.esastack.httpserver.utils.Constants; 25 | import io.netty.channel.ChannelHandlerContext; 26 | import io.netty.handler.codec.http.DefaultHttpRequest; 27 | import io.netty.handler.codec.http.HttpRequest; 28 | import io.netty.handler.codec.http2.Http2ConnectionEncoder; 29 | import io.netty.handler.codec.http2.Http2Headers; 30 | import io.netty.handler.codec.http2.Http2Stream; 31 | 32 | import static io.esastack.httpserver.impl.Utils.standardHttp2Headers; 33 | import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; 34 | 35 | final class Http2RequestHandleImpl extends BaseRequestHandle implements RequestHandle { 36 | 37 | private final Http2HeadersImpl headers; 38 | private final Http2ResponseImpl response; 39 | private final String schema; 40 | final Http2Stream stream; 41 | long bytes; 42 | 43 | static Http2RequestHandleImpl from( 44 | ServerRuntime runtime, 45 | ChannelHandlerContext ctx, 46 | Http2ConnectionEncoder encoder, 47 | Http2Headers h2Headers, 48 | Http2Stream stream, 49 | int streamDependency, 50 | short weight, 51 | boolean exclusive) { 52 | 53 | final String path = 54 | Checks.checkNotNull(h2Headers.path(), "path").toString(); 55 | final HttpMethod method = 56 | HttpMethod.fastValueOf(Checks.checkNotNull(h2Headers.method(), "method").toString()); 57 | String schema; 58 | CharSequence c = h2Headers.scheme(); 59 | if (c == null) { 60 | schema = ctx.channel().attr(Constants.SCHEME).get(); 61 | } else { 62 | schema = c.toString(); 63 | } 64 | standardHttp2Headers(h2Headers); 65 | return new Http2RequestHandleImpl(runtime, 66 | ctx, 67 | encoder, 68 | path, 69 | method, 70 | schema, 71 | h2Headers, 72 | stream, 73 | streamDependency, 74 | weight, 75 | exclusive); 76 | } 77 | 78 | private Http2RequestHandleImpl(ServerRuntime runtime, 79 | ChannelHandlerContext ctx, 80 | Http2ConnectionEncoder encoder, 81 | String uri, 82 | HttpMethod method, 83 | String schema, 84 | Http2Headers headers, 85 | Http2Stream stream, 86 | int streamDependency, 87 | short weight, 88 | boolean exclusive) { 89 | super(runtime, ctx, method, uri); 90 | this.schema = schema; 91 | this.headers = new Http2HeadersImpl(headers); 92 | this.bytes = headers.getLong(CONTENT_LENGTH, -1L); 93 | this.stream = stream; 94 | this.response = new Http2ResponseImpl(this, 95 | encoder, 96 | streamDependency, 97 | weight, 98 | exclusive); 99 | } 100 | 101 | @Override 102 | public HttpVersion version() { 103 | return HttpVersion.HTTP_2; 104 | } 105 | 106 | @Override 107 | public String scheme() { 108 | return schema; 109 | } 110 | 111 | @Override 112 | public HttpHeaders headers() { 113 | return headers; 114 | } 115 | 116 | @Override 117 | public Http2ResponseImpl response() { 118 | return response; 119 | } 120 | 121 | @Override 122 | protected HttpRequest toHttpRequest() { 123 | return new DefaultHttpRequest(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, 124 | io.netty.handler.codec.http.HttpMethod.valueOf(rawMethod()), 125 | uri(), new Http1HeadersImpl().add(headers())); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/transport/NioTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.transport; 17 | 18 | import esa.commons.Checks; 19 | import io.esastack.httpserver.NetOptions; 20 | import io.esastack.httpserver.utils.Loggers; 21 | import io.netty.bootstrap.AbstractBootstrap; 22 | import io.netty.bootstrap.ServerBootstrap; 23 | import io.netty.buffer.UnpooledByteBufAllocator; 24 | import io.netty.channel.ChannelFactory; 25 | import io.netty.channel.ChannelOption; 26 | import io.netty.channel.EventLoopGroup; 27 | import io.netty.channel.ServerChannel; 28 | import io.netty.channel.nio.NioEventLoopGroup; 29 | import io.netty.channel.socket.nio.NioServerSocketChannel; 30 | import io.netty.channel.unix.DomainSocketAddress; 31 | import io.netty.util.internal.SystemPropertyUtil; 32 | 33 | import java.net.SocketAddress; 34 | import java.util.Map; 35 | import java.util.concurrent.ThreadFactory; 36 | 37 | /** 38 | * Standard JDK NIO transport. 39 | */ 40 | public class NioTransport implements Transport { 41 | 42 | static NioTransport INSTANCE = new NioTransport(); 43 | 44 | private static final boolean USE_UNPOOLED_ALLOCATOR; 45 | 46 | static { 47 | USE_UNPOOLED_ALLOCATOR = 48 | SystemPropertyUtil.getBoolean("io.esastack.httpserver.useUnpooledAllocator", false); 49 | if (Loggers.logger().isDebugEnabled()) { 50 | Loggers.logger().debug("-Dio.esastack.httpserver.useUnpooledAllocator: {}", USE_UNPOOLED_ALLOCATOR); 51 | } 52 | } 53 | 54 | @Override 55 | public ChannelFactory serverChannelFactory(SocketAddress local) { 56 | Checks.checkArg(!(local instanceof DomainSocketAddress), 57 | "Domain socket is UNSUPPORTED in NIO transport"); 58 | return NioServerSocketChannel::new; 59 | } 60 | 61 | @Override 62 | public EventLoopGroup loop(int nThreads, ThreadFactory threadFactory) { 63 | return new NioEventLoopGroup(nThreads, threadFactory); 64 | } 65 | 66 | @Override 67 | public void applyOptions(ServerBootstrap bootstrap, 68 | NetOptions options, 69 | SocketAddress local) { 70 | final boolean isDomainSocket = local instanceof DomainSocketAddress; 71 | if (!isDomainSocket) { 72 | bootstrap.childOption(ChannelOption.SO_KEEPALIVE, options.isSoKeepalive()); 73 | bootstrap.childOption(ChannelOption.TCP_NODELAY, options.isTcpNoDelay()); 74 | } 75 | 76 | if (options.getSoBacklog() > 0) { 77 | bootstrap.option(ChannelOption.SO_BACKLOG, options.getSoBacklog()); 78 | } 79 | 80 | bootstrap.option(ChannelOption.SO_REUSEADDR, options.isReuseAddress()); 81 | 82 | if (USE_UNPOOLED_ALLOCATOR) { 83 | bootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT); 84 | } 85 | 86 | if (options.getSoRcvbuf() > 0) { 87 | bootstrap.childOption(ChannelOption.SO_RCVBUF, options.getSoRcvbuf()); 88 | } 89 | 90 | if (options.getSoSendbuf() > 0) { 91 | bootstrap.childOption(ChannelOption.SO_SNDBUF, options.getSoSendbuf()); 92 | } 93 | 94 | if (options.getSoLinger() != -1) { 95 | bootstrap.childOption(ChannelOption.SO_LINGER, options.getSoLinger()); 96 | } 97 | 98 | if (options.getOptions() != null) { 99 | addOption(bootstrap, options.getOptions()); 100 | } 101 | 102 | if (options.getChildOptions() != null) { 103 | addChildOption(bootstrap, options.getChildOptions()); 104 | } 105 | } 106 | 107 | private static void addOption(AbstractBootstrap bootstrap, Map, Object> options) { 108 | if (!options.isEmpty()) { 109 | for (Map.Entry, Object> entry : options.entrySet()) { 110 | bootstrap.option(entry.getKey(), entry.getValue()); 111 | } 112 | } 113 | } 114 | 115 | @SuppressWarnings("unchecked") 116 | private static void addChildOption(ServerBootstrap bootstrap, Map, Object> options) { 117 | if (!options.isEmpty()) { 118 | for (Map.Entry, Object> entry : options.entrySet()) { 119 | bootstrap.childOption((ChannelOption) entry.getKey(), entry.getValue()); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /httpserver/src/main/java/io/esastack/httpserver/impl/Http2ConnectionChunkHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 OPPO ESA Stack Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package io.esastack.httpserver.impl; 17 | 18 | import io.netty.channel.ChannelHandlerContext; 19 | import io.netty.channel.ChannelPromise; 20 | import io.netty.handler.codec.http2.Http2ConnectionDecoder; 21 | import io.netty.handler.codec.http2.Http2ConnectionEncoder; 22 | import io.netty.handler.codec.http2.Http2ConnectionHandler; 23 | import io.netty.handler.codec.http2.Http2Settings; 24 | import io.netty.handler.stream.ChunkedWriteHandler; 25 | 26 | import static io.esastack.httpserver.impl.Utils.handleException; 27 | import static io.esastack.httpserver.impl.Utils.handleIdle; 28 | import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Exception; 29 | 30 | final class Http2ConnectionChunkHandler extends Http2ConnectionHandler { 31 | 32 | Http2ConnectionChunkHandler(Http2ConnectionDecoder decoder, 33 | Http2ConnectionEncoder encoder, 34 | Http2Settings initialSettings, 35 | boolean decoupleCloseAndGoAway) { 36 | super(decoder, encoder, initialSettings, decoupleCloseAndGoAway); 37 | } 38 | 39 | @Override 40 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 41 | super.handlerAdded(ctx); 42 | // add a ChunkedWriteHandler after Http2ConnectionChunkHandler 43 | // usually we use the Http2ConnectionChunkHandler#encoder() to write http2 response, and this 44 | // ChunkedWriteHandler we added here is used to handle the messages type of Http2ChunkedInput(eg. large file) 45 | // TODO: this should be removed in the future and try another way to write chunked input by 46 | // Http2RemoteFlowController#isWritable() and Http2RemoteFlowController$Listener#writabilityChanged(stream)) 47 | // see: https://github.com/netty/netty/issues/10801 48 | ctx.pipeline().addAfter(ctx.name(), "h2ChunkedWriter", new ChunkedWriteHandler()); 49 | } 50 | 51 | @Override 52 | public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { 53 | // Http2ChunkedInput should be closed if we failed to write(eg. stream has been removed before writing), 54 | // which is implemented in ChunkedWriteHandler#handleFuture(xxx) 55 | if (msg instanceof Http2ChunkedInput.Content) { 56 | 57 | Http2ChunkedInput.Content c = (Http2ChunkedInput.Content) msg; 58 | boolean hasBody = c.content().readableBytes() > 0; 59 | 60 | if (hasBody) { 61 | if (msg instanceof Http2ChunkedInput.LastContent) { 62 | Http2ChunkedInput.LastContent lastContent = (Http2ChunkedInput.LastContent) msg; 63 | boolean hasTrailer = lastContent.trailers != null && !lastContent.trailers.isEmpty(); 64 | 65 | encoder().writeData(ctx, 66 | c.streamId, 67 | c.content(), 68 | 0, 69 | !hasTrailer, 70 | hasTrailer ? ctx.newPromise() : promise); 71 | 72 | if (hasTrailer) { 73 | encoder().writeHeaders(ctx, 74 | c.streamId, 75 | lastContent.trailers, 76 | lastContent.streamDependency, 77 | lastContent.weight, 78 | lastContent.exclusive, 79 | 0, 80 | true, 81 | promise); 82 | } 83 | } else { 84 | encoder().writeData(ctx, 85 | c.streamId, 86 | c.content(), 87 | 0, 88 | false, 89 | promise); 90 | } 91 | } 92 | } else { 93 | super.write(ctx, msg, promise); 94 | } 95 | } 96 | 97 | @Override 98 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 99 | if (!handleIdle(ctx, evt)) { 100 | super.userEventTriggered(ctx, evt); 101 | } 102 | } 103 | 104 | @Override 105 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 106 | if (getEmbeddedHttp2Exception(cause) != null) { 107 | super.exceptionCaught(ctx, cause); 108 | } else { 109 | handleException(ctx, cause); 110 | } 111 | } 112 | } 113 | --------------------------------------------------------------------------------