Skip to content

Commit 9ee00fe

Browse files
Fixed handling of CORS within Vert.x (#4232)
1 parent 916d9ff commit 9ee00fe

File tree

7 files changed

+149
-7
lines changed

7 files changed

+149
-7
lines changed

build.sbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,6 +2112,7 @@ lazy val examples: ProjectMatrix = (projectMatrix in file("examples"))
21122112
sttpStubServer,
21132113
swaggerUiBundle,
21142114
redocBundle,
2115+
vertxServer,
21152116
zioHttpServer,
21162117
zioJson,
21172118
zioMetrics
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// {cat=Security; effects=Future; server=Vert.x}: CORS interceptor
2+
3+
//> using dep com.softwaremill.sttp.tapir::tapir-vertx-server:1.11.11
4+
//> using dep com.softwaremill.sttp.client3::core:3.10.2
5+
6+
package sttp.tapir.examples.security
7+
8+
import io.vertx.core.Vertx
9+
import io.vertx.ext.web.*
10+
import sttp.client3.*
11+
import sttp.model.headers.Origin
12+
import sttp.model.{Header, HeaderNames, Method, StatusCode}
13+
import sttp.tapir.*
14+
import sttp.tapir.server.interceptor.cors.{CORSConfig, CORSInterceptor}
15+
import sttp.tapir.server.vertx.VertxFutureServerInterpreter.*
16+
import sttp.tapir.server.vertx.{VertxFutureServerInterpreter, VertxFutureServerOptions}
17+
18+
import scala.concurrent.duration.*
19+
import scala.concurrent.{Await, ExecutionContext, Future}
20+
21+
@main def corsInterceptorVertxServer() =
22+
given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
23+
val vertx = Vertx.vertx()
24+
25+
val server = vertx.createHttpServer()
26+
val router = Router.router(vertx)
27+
28+
val myEndpoint = endpoint.get
29+
.in("path")
30+
.out(plainBody[String])
31+
.serverLogic(_ => Future(Right("OK")))
32+
33+
val corsInterceptor = VertxFutureServerOptions.customiseInterceptors
34+
.corsInterceptor(
35+
CORSInterceptor.customOrThrow(
36+
CORSConfig.default
37+
.allowOrigin(Origin.Host("http", "my.origin"))
38+
.allowMethods(Method.GET)
39+
)
40+
)
41+
.options
42+
43+
val attach = VertxFutureServerInterpreter(corsInterceptor).route(myEndpoint)
44+
attach(router)
45+
46+
// starting the server
47+
val bindAndCheck = server.requestHandler(router).listen(9000).asScala.map { binding =>
48+
val backend = HttpClientSyncBackend()
49+
50+
// Sending preflight request with allowed origin
51+
val preflightResponse = basicRequest
52+
.options(uri"http://localhost:9000/path")
53+
.headers(
54+
Header.origin(Origin.Host("http", "my.origin")),
55+
Header.accessControlRequestMethod(Method.GET)
56+
)
57+
.send(backend)
58+
59+
assert(preflightResponse.code == StatusCode.NoContent)
60+
assert(preflightResponse.headers.contains(Header.accessControlAllowOrigin("http://my.origin")))
61+
assert(preflightResponse.headers.contains(Header.accessControlAllowMethods(Method.GET)))
62+
63+
println("Got expected response for preflight request")
64+
65+
// Sending preflight request with disallowed origin
66+
val preflightResponseForDisallowedOrigin = basicRequest
67+
.options(uri"http://localhost:9000/path")
68+
.headers(
69+
Header.origin(Origin.Host("http", "disallowed.com")),
70+
Header.accessControlRequestMethod(Method.GET)
71+
)
72+
.send(backend)
73+
74+
// Check response does not contain allowed origin header
75+
assert(preflightResponseForDisallowedOrigin.code == StatusCode.NoContent)
76+
assert(!preflightResponseForDisallowedOrigin.headers.contains(Header.accessControlAllowOrigin("http://example.com")))
77+
78+
println("Got expected response for preflight request for wrong origin. No allowed origin header in response")
79+
80+
// Sending regular request from allowed origin
81+
val requestResponse = basicRequest
82+
.response(asStringAlways)
83+
.get(uri"http://localhost:9000/path")
84+
.headers(Header.origin(Origin.Host("http", "my.origin")))
85+
.send(backend)
86+
87+
assert(requestResponse.code == StatusCode.Ok)
88+
assert(requestResponse.body == "OK")
89+
assert(requestResponse.headers.contains(Header.vary(HeaderNames.Origin)))
90+
assert(requestResponse.headers.contains(Header.accessControlAllowOrigin("http://my.origin")))
91+
92+
println("Got expected response for regular request")
93+
94+
binding
95+
}
96+
97+
Await.result(bindAndCheck.flatMap(_.close().asScala), 1.minute)

server/vertx-server/cats/src/main/scala/sttp/tapir/server/vertx/cats/VertxCatsServerInterpreter.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ trait VertxCatsServerInterpreter[F[_]] extends CommonServerInterpreter with Vert
3535
def route(
3636
e: ServerEndpoint[Fs2Streams[F] with WebSockets, F]
3737
): Router => Route = { router =>
38+
val routeDef = extractRouteDefinition(e.endpoint)
3839
val readStreamCompatible = fs2ReadStreamCompatible(vertxCatsServerOptions)
39-
mountWithDefaultHandlers(e)(router, extractRouteDefinition(e.endpoint), vertxCatsServerOptions)
40+
optionsRouteIfCORSDefined(e)(router, routeDef, vertxCatsServerOptions)
41+
.foreach(_.handler(endpointHandler(e, readStreamCompatible)))
42+
mountWithDefaultHandlers(e)(router, routeDef, vertxCatsServerOptions)
4043
.handler(endpointHandler(e, readStreamCompatible))
4144
}
4245

server/vertx-server/src/main/scala/sttp/tapir/server/vertx/VertxFutureServerInterpreter.scala

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ package sttp.tapir.server.vertx
22

33
import io.vertx.core.{Handler, Future => VFuture}
44
import io.vertx.ext.web.{Route, Router, RoutingContext}
5-
import sttp.monad.FutureMonad
65
import sttp.capabilities.WebSockets
6+
import sttp.monad.FutureMonad
77
import sttp.tapir.server.ServerEndpoint
88
import sttp.tapir.server.interceptor.RequestResult
99
import sttp.tapir.server.interpreter.{BodyListener, ServerInterpreter}
@@ -26,7 +26,10 @@ trait VertxFutureServerInterpreter extends CommonServerInterpreter with VertxErr
2626
* A function, that given a router, will attach this endpoint to it
2727
*/
2828
def route[A, U, I, E, O](e: ServerEndpoint[VertxStreams with WebSockets, Future]): Router => Route = { router =>
29-
mountWithDefaultHandlers(e)(router, extractRouteDefinition(e.endpoint), vertxFutureServerOptions)
29+
val routeDef = extractRouteDefinition(e.endpoint)
30+
optionsRouteIfCORSDefined(e)(router, routeDef, vertxFutureServerOptions)
31+
.foreach(_.handler(endpointHandler(e)))
32+
mountWithDefaultHandlers(e)(router, routeDef, vertxFutureServerOptions)
3033
.handler(endpointHandler(e))
3134
}
3235

@@ -37,7 +40,10 @@ trait VertxFutureServerInterpreter extends CommonServerInterpreter with VertxErr
3740
* A function, that given a router, will attach this endpoint to it
3841
*/
3942
def blockingRoute(e: ServerEndpoint[VertxStreams with WebSockets, Future]): Router => Route = { router =>
40-
mountWithDefaultHandlers(e)(router, extractRouteDefinition(e.endpoint), vertxFutureServerOptions)
43+
val routeDef = extractRouteDefinition(e.endpoint)
44+
optionsRouteIfCORSDefined(e)(router, routeDef, vertxFutureServerOptions)
45+
.foreach(_.handler(endpointHandler(e)))
46+
mountWithDefaultHandlers(e)(router, routeDef, vertxFutureServerOptions)
4147
.blockingHandler(endpointHandler(e))
4248
}
4349

server/vertx-server/src/main/scala/sttp/tapir/server/vertx/interpreters/CommonServerInterpreter.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
11
package sttp.tapir.server.vertx.interpreters
22

3+
import io.vertx.core.http.HttpMethod._
34
import io.vertx.ext.web.{Route, Router}
45
import sttp.tapir.server.ServerEndpoint
6+
import sttp.tapir.server.interceptor.Interceptor
7+
import sttp.tapir.server.interceptor.cors.CORSInterceptor
58
import sttp.tapir.server.vertx.VertxServerOptions
69
import sttp.tapir.server.vertx.handlers.attachDefaultHandlers
710
import sttp.tapir.server.vertx.routing.PathMapping.{RouteDefinition, createRoute}
811

912
trait CommonServerInterpreter {
13+
14+
/** Checks if a CORS interceptor is defined in the server options and creates an OPTIONS route if it is.
15+
*
16+
* Vert.x will signal a 405 error if a route matches the path, but doesn’t match the HTTP Method. So if CORS is defined, we additionally
17+
* register OPTIONS route which accepts the preflight requests.
18+
*
19+
* @return
20+
* An optional Route. If a CORS interceptor is defined, an OPTIONS route is created and returned. Otherwise, None is returned.
21+
*/
22+
protected def optionsRouteIfCORSDefined[C, F[_]](
23+
e: ServerEndpoint[C, F]
24+
)(router: Router, routeDef: RouteDefinition, serverOptions: VertxServerOptions[F]): Option[Route] = {
25+
def isCORSInterceptorDefined(interceptors: List[Interceptor[F]]): Boolean = {
26+
interceptors.collectFirst { case ci: CORSInterceptor[F] => ci }.nonEmpty
27+
}
28+
29+
def createOptionsRoute(router: Router, route: RouteDefinition): Option[Route] =
30+
route match {
31+
case (Some(method), path) if Set(GET, HEAD, POST, PUT, DELETE).contains(method) =>
32+
Some(router.options(path))
33+
case (None, path) => Some(router.options(path))
34+
case _ => None
35+
}
36+
37+
if (isCORSInterceptorDefined(serverOptions.interceptors)) {
38+
createOptionsRoute(router, routeDef)
39+
} else
40+
None
41+
}
42+
1043
protected def mountWithDefaultHandlers[C, F[_]](e: ServerEndpoint[C, F])(
1144
router: Router,
1245
routeDef: RouteDefinition,

server/vertx-server/src/main/scala/sttp/tapir/server/vertx/routing/PathMapping.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package sttp.tapir.server.vertx.routing
22

33
import io.vertx.core.http.HttpMethod
44
import io.vertx.ext.web.{Route, Router}
5-
import sttp.tapir.{AnyEndpoint, EndpointInput}
65
import sttp.tapir.EndpointInput.PathCapture
76
import sttp.tapir.internal._
7+
import sttp.tapir.{AnyEndpoint, EndpointInput}
88

99
object PathMapping {
1010

@@ -49,5 +49,4 @@ object PathMapping {
4949
.mkString
5050
if (path.isEmpty) "/*" else path
5151
}
52-
5352
}

server/vertx-server/zio/src/main/scala/sttp/tapir/server/vertx/zio/VertxZioServerInterpreter.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ trait VertxZioServerInterpreter[R] extends CommonServerInterpreter with VertxErr
2525
def route[R2](e: ZServerEndpoint[R2, ZioStreams with WebSockets])(implicit
2626
runtime: Runtime[R & R2]
2727
): Router => Route = { router =>
28-
mountWithDefaultHandlers(e.widen)(router, extractRouteDefinition(e.endpoint), vertxZioServerOptions)
28+
val routeDef = extractRouteDefinition(e.endpoint)
29+
optionsRouteIfCORSDefined(e.widen)(router, routeDef, vertxZioServerOptions)
30+
.foreach(_.handler(endpointHandler(e)))
31+
mountWithDefaultHandlers(e.widen)(router, routeDef, vertxZioServerOptions)
2932
.handler(endpointHandler(e))
3033
}
3134

0 commit comments

Comments
 (0)