Skip to content

Commit 4509862

Browse files
author
aromanov
committed
add https and http2
1 parent 76f7539 commit 4509862

File tree

14 files changed

+411
-6
lines changed

14 files changed

+411
-6
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ext {
2222

2323
// RESTful
2424
openApiVersion = "2.1.0"
25+
logbookVersion = "3.0.0-RC.2"
2526
springCloudContractVersion = "4.0.2"
2627

2728
// SOAP

restful/README.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# RESTful
2+
3+
### SSL
4+
5+
Создание самоподписанного сертификата:
6+
7+
```shell
8+
$ openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem
9+
$ openssl pkcs12 -inkey key.pem -in certificate.pem -export -out certificate.p12
10+
```
11+
12+
```shell
13+
$ curl https://localhost:8443/api/v1/servers -v -k | jq
14+
15+
* Connected to localhost (127.0.0.1) port 8443 (#0)
16+
....
17+
* SSL connection using TLSv1.2 / AES256-SHA
18+
* ALPN: server did not agree on a protocol. Uses default.
19+
* Server certificate:
20+
* subject: C=AM; ST=Yerevan; L=Yerevan; O=IT Enduro; CN=it-endu.ro
21+
* start date: Apr 24 21:02:18 2023 GMT
22+
* expire date: Apr 23 21:02:18 2024 GMT
23+
* issuer: C=AM; ST=Yerevan; L=Yerevan; O=IT Enduro; CN=it-endu.ro
24+
* SSL certificate verify result: self signed certificate (18), continuing anyway.
25+
> GET /api/v1/servers/1 HTTP/1.1
26+
> Host: localhost:8443
27+
> User-Agent: curl/7.87.0
28+
> Accept: */*
29+
>
30+
* Mark bundle as not supporting multiuse
31+
< HTTP/1.1 200
32+
< Content-Type: application/json
33+
< Transfer-Encoding: chunked
34+
< Date: Mon, 24 Apr 2023 21:03:38 GMT
35+
36+
* Connection #0 to host localhost left intact
37+
{
38+
"id": 1,
39+
"purpose": "BACKEND",
40+
"latency": 10,
41+
"bandwidth": 10000,
42+
"state": {
43+
"id": 1,
44+
"city": "Moscow",
45+
"country": "Russia"
46+
}
47+
}
48+
```
49+
50+
### HTTP/2
51+
52+
```shell
53+
* Connected to localhost (127.0.0.1) port 8080 (#0)
54+
> GET /api/v1/servers/1 HTTP/1.1
55+
> Host: localhost:8080
56+
> User-Agent: curl/7.87.0
57+
> Accept: */*
58+
> Connection: Upgrade, HTTP2-Settings
59+
> Upgrade: h2c
60+
> HTTP2-Settings: AAMAAABkAAQCAAAAAAIAAAAA
61+
>
62+
* Mark bundle as not supporting multiuse
63+
< HTTP/1.1 101
64+
< Connection: Upgrade
65+
< Upgrade: h2c
66+
< Date: Mon, 24 Apr 2023 21:04:35 GMT
67+
* Received 101, Switching to HTTP/2
68+
* Using HTTP2, server supports multiplexing
69+
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
70+
< HTTP/2 200
71+
< content-type: application/json
72+
< date: Mon, 24 Apr 2023 21:04:35 GMT
73+
<
74+
* Connection #0 to host localhost left intact
75+
{
76+
"id": 1,
77+
"purpose": "BACKEND",
78+
"latency": 10,
79+
"bandwidth": 10000,
80+
"state": {
81+
"id": 1,
82+
"city": "Moscow",
83+
"country": "Russia"
84+
}
85+
}
86+
```
87+
88+
### NGINX кэширование
89+
90+
Запустить два instance на разных портах:
91+
92+
```shell script
93+
java -jar restful/build/libs/restful.jar --server.port=8081
94+
java -jar restful/build/libs/restful.jar --server.port=8082
95+
```
96+
97+
Конфигурация nginx:
98+
99+
```
100+
upstream backend {
101+
server 127.0.0.1:8081 max_fails=3 weight=5;
102+
server 127.0.0.1:8082 backup;
103+
}
104+
105+
server {
106+
listen 80;
107+
server_name *;
108+
109+
location / {
110+
proxy_set_header Host $host;
111+
proxy_set_header X-Real-IP $remote_addr;
112+
proxy_pass http://backend;
113+
proxy_redirect off;
114+
}
115+
}
116+
```
117+
118+
### NGINX балансировка
119+
120+
Конфигурация nginx:
121+
122+
```
123+
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=STATIC:32m max_size=1g;
124+
125+
server {
126+
listen 80;
127+
server_name *;
128+
129+
location / {
130+
proxy_cache STATIC;
131+
proxy_cache_valid any 48h;
132+
add_header X-Cached $upstream_cache_status;
133+
134+
proxy_set_header Host $host;
135+
proxy_set_header X-Real-IP $remote_addr;
136+
proxy_pass http://127.0.0.1:8080;
137+
proxy_redirect off;
138+
}
139+
}
140+
```
141+
142+
Два раза выполнить запрос через curl:
143+
144+
```shell script
145+
curl http://localhost/api/v1/servers/1 -v
146+
```
147+
148+
Второй раз в ответ получим заголовок `X-Cached: HIT`, т.е. сервер ответил 302, а тело запроса nginx достал из кэша.
149+
Для метода `http://localhost:8880/api/v1/servers` мы отдаем заголовок `Cache-Control: 60` (повторно выполнить запрос
150+
через 1 минуту) и `ETag`, на базе которого строится кэширование.
151+
152+
Для запроса `http://localhost:8880/api/v1/servers/1` устанавливается заголовок `Cache-Control: no-cahce`, который
153+
указывает промежуточным прокси, что запрос нельзя кэшировать, а нужно перезапрашивать каждый раз.
154+
155+
### Расшифровка HTTPS
156+
157+
Для расшифровки HTTPS требуется секретный ключ сервера. Запустим проект с профилем `local`, `tomcat`:
158+
159+
```shell
160+
docker compose up -d
161+
./gradlew clean build bootRun --args='--spring.profiles.active=local,tomcat'
162+
```
163+
164+
В этой конфигурации задан chipper `TLS_RSA_WITH_AES_256_CBC_SHA`, который для расшифровки требует только ключ
165+
сервера.
166+
167+
Для настройки Wireshark нужно открыть `properties` -> `protocols` -> `TLS` -> `RSA key list` с настройками:
168+
169+
* ip: `127.0.0.1`;
170+
* port: `8443`;
171+
* protocol: `http`;
172+
* keyfile: `resources/certificate.p12`;
173+
* password: `tomcat`.
174+
175+
Так же имеет смысл включить SSL debug file.
176+
177+
![wireshark ssl](images/wireshark.png)
178+
179+
В случае ошибки _ssl_restore_master_key can't find pre-master secret by Encrypted pre-master secret_, значит при обмене
180+
ключами используется алгоритм Диффи-Хеллмана, которому для расшифровки требуется не только ключ сервера, но и сессионные
181+
ключи. Для этого нужно через переменную среды `SSLKEYLOGFILE` указать путь к файлу, куда будут записываться pre-master
182+
ключи.
183+
184+
```shell
185+
SSLKEYLOGFILE=/tmp/.ssl.log curl https://localhost:8443/api/v1/state -k
186+
```

restful/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333

3434
implementation "org.springdoc:springdoc-openapi-starter-common:$openApiVersion"
3535
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:$openApiVersion"
36+
implementation "org.zalando:logbook-spring-boot-starter:$logbookVersion"
3637

3738
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml"
3839

restful/images/wireshark.png

116 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package ru.romanow.protocols.restful.config
2+
3+
import org.springframework.boot.context.properties.ConfigurationProperties
4+
5+
@ConfigurationProperties(prefix = "server.https")
6+
data class HttpsProperties(
7+
val enabled: Boolean = false,
8+
val port: Int? = null,
9+
val keyStore: String? = null,
10+
val keyStorePassword: String? = null,
11+
val keyPassword: String? = null,
12+
val ciphers: String? = null
13+
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package ru.romanow.protocols.restful.config
2+
3+
import org.apache.catalina.connector.Connector
4+
import org.apache.coyote.http11.Http11NioProtocol
5+
import org.apache.tomcat.util.net.SSLHostConfig
6+
import org.apache.tomcat.util.net.SSLHostConfigCertificate
7+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
8+
import org.springframework.boot.context.properties.EnableConfigurationProperties
9+
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
10+
import org.springframework.boot.web.server.WebServerFactoryCustomizer
11+
import org.springframework.context.annotation.Bean
12+
import org.springframework.context.annotation.Configuration
13+
import org.springframework.util.ResourceUtils
14+
15+
16+
@Configuration
17+
@EnableConfigurationProperties(HttpsProperties::class)
18+
@ConditionalOnProperty("server.https.enabled", havingValue = "true")
19+
class TomcatConfiguration(
20+
private val httpsProperties: HttpsProperties
21+
) : WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
22+
23+
override fun customize(factory: TomcatServletWebServerFactory) {
24+
factory.addAdditionalTomcatConnectors(createSslConnector())
25+
}
26+
27+
@Bean
28+
fun createSslConnector(): Connector {
29+
val connector = Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL)
30+
val protocol = connector.protocolHandler as Http11NioProtocol
31+
connector.secure = true
32+
protocol.isSSLEnabled = true
33+
connector.scheme = "https"
34+
connector.port = httpsProperties.port!!
35+
36+
val sslConfig = SSLHostConfig().also {
37+
it.hostName = protocol.defaultSSLHostConfigName
38+
it.sslProtocol = "TLS"
39+
}
40+
httpsProperties.ciphers?.let { sslConfig.ciphers = httpsProperties.ciphers }
41+
protocol.addSslHostConfig(sslConfig)
42+
43+
val certificate = SSLHostConfigCertificate(sslConfig, SSLHostConfigCertificate.Type.UNDEFINED)
44+
certificate.certificateKeystoreFile = ResourceUtils.getURL(httpsProperties.keyStore!!).toString()
45+
certificate.certificateKeystorePassword = httpsProperties.keyStorePassword
46+
47+
sslConfig.addCertificate(certificate)
48+
49+
return connector
50+
}
51+
}
Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
package ru.romanow.protocols.restful.config
22

33
import org.springframework.context.annotation.Configuration
4+
import org.springframework.http.HttpMethod
45
import org.springframework.http.MediaType
56
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer
7+
import org.springframework.web.servlet.config.annotation.CorsRegistry
68
import org.springframework.web.servlet.config.annotation.EnableWebMvc
79
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
810

911
@EnableWebMvc
1012
@Configuration
1113
class WebConfiguration : WebMvcConfigurationSupport() {
14+
override fun addCorsMappings(registry: CorsRegistry) {
15+
registry.addMapping("/**")
16+
.allowedOrigins("*")
17+
.allowedMethods(
18+
HttpMethod.GET.name(),
19+
HttpMethod.POST.name(),
20+
HttpMethod.PUT.name(),
21+
HttpMethod.PATCH.name(),
22+
HttpMethod.DELETE.name()
23+
)
24+
}
1225

1326
public override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
1427
configurer.defaultContentType(MediaType.APPLICATION_JSON)
15-
.mediaType("json", MediaType.APPLICATION_JSON)
1628
.mediaType("xml", MediaType.APPLICATION_XML)
29+
.mediaType("json", MediaType.APPLICATION_JSON)
1730
}
1831
}

restful/src/main/java/ru/romanow/protocols/restful/web/ServerController.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,46 @@
11
package ru.romanow.protocols.restful.web
22

3+
import com.fasterxml.jackson.databind.ObjectMapper
34
import io.swagger.v3.oas.annotations.Operation
45
import io.swagger.v3.oas.annotations.tags.Tag
56
import jakarta.validation.Valid
7+
import org.springframework.http.CacheControl
68
import org.springframework.http.HttpStatus
79
import org.springframework.http.MediaType
810
import org.springframework.http.ResponseEntity
11+
import org.springframework.http.ResponseEntity.ok
12+
import org.springframework.util.DigestUtils
913
import org.springframework.web.bind.annotation.*
1014
import org.springframework.web.servlet.support.ServletUriComponentsBuilder.fromCurrentRequest
1115
import ru.romanow.protocols.api.model.CreateServerRequest
16+
import ru.romanow.protocols.api.model.ServerResponse
1217
import ru.romanow.protocols.api.model.ServersResponse
1318
import ru.romanow.protocols.common.server.service.ServerService
14-
import java.net.URI
15-
import java.nio.charset.Charset
19+
import java.util.concurrent.TimeUnit
1620

1721
@Tag(name = "Server API")
1822
@RestController
1923
@RequestMapping("/api/v1/servers")
2024
class ServerController(
21-
private val serverService: ServerService
25+
private val serverService: ServerService,
26+
private val objectMapper: ObjectMapper
2227
) {
2328

2429
@Operation(summary = "Get server by Id")
2530
@GetMapping(value = ["/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE])
26-
fun getById(@PathVariable id: Int) = serverService.getById(id)
31+
fun getById(@PathVariable id: Int): ResponseEntity<ServerResponse> = ok()
32+
.cacheControl(CacheControl.noCache())
33+
.body(serverService.getById(id))
2734

2835
@Operation(summary = "Find all servers")
2936
@GetMapping(produces = [MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE])
30-
fun all() = ServersResponse(serverService.all())
37+
fun all(): ResponseEntity<ServersResponse> {
38+
val servers = serverService.all()
39+
return ok()
40+
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES))
41+
.eTag(DigestUtils.md5DigestAsHex(objectMapper.writeValueAsBytes(servers)))
42+
.body(ServersResponse(servers))
43+
}
3144

3245
@Operation(summary = "Find servers in city")
3346
@GetMapping(

0 commit comments

Comments
 (0)