Skip to content

Commit 68a1c47

Browse files
committed
Add file ext to media type
1 parent bb7c09c commit 68a1c47

File tree

19 files changed

+1271
-88
lines changed

19 files changed

+1271
-88
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ dependencies {
2626

2727
## TBD
2828

29-
- Handle head
3029
- Handle 405 response - Method not allowed
3130
- Handle 406 response - Not Acceptable
3231
- Upload file

kttp-api/src/main/kotlin/com/coditory/kttp/headers/MediaType.kt

Lines changed: 911 additions & 74 deletions
Large diffs are not rendered by default.

kttp-api/src/test/kotlin/com/coditory/kttp/headers/MediaTypeSpec.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,28 @@ class MediaTypeSpec : FunSpec({
115115
}
116116
}
117117
}
118+
119+
context("should match MediaType with file ext") {
120+
listOf(
121+
tuple("image/png", listOf("png")),
122+
tuple("application/json", listOf("json")),
123+
tuple("application/vnd.ms-excel", listOf("xls", "xlm", "xla", "xlc", "xlt", "xlw")),
124+
).forEach { t ->
125+
val exts = t.b
126+
val firstExt = exts.first()
127+
val mediaTypeValue = t.a
128+
val mediaType = MediaType.parse(mediaTypeValue)!!
129+
test("$mediaType -> $exts") {
130+
MediaType.getAllFileExtByMediaType(mediaType) shouldBe exts
131+
MediaType.getAllFileExtByMediaTypeValue(mediaTypeValue) shouldBe exts
132+
MediaType.getExtByMediaTypeValue(mediaTypeValue) shouldBe firstExt
133+
MediaType.getExtByMediaType(mediaType) shouldBe firstExt
134+
mediaType.fileExt() shouldBe firstExt
135+
exts.forEach { ext ->
136+
MediaType.getMediaTypeByExt(ext) shouldBe mediaType
137+
MediaType.getMediaTypeValueByExt(ext) shouldBe mediaTypeValue
138+
}
139+
}
140+
}
141+
}
118142
})

kttp-server/api/src/main/kotlin/com/coditory/kttp/server/HttpRequest.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ class HttpRequest(
1616
private val deserializer: Deserializer,
1717
val source: Source,
1818
) {
19+
fun copy(
20+
method: HttpRequestMethod = this.method,
21+
uri: URI = this.uri,
22+
headers: HttpHeaders = this.headers,
23+
source: Source = this.source,
24+
): HttpRequest {
25+
return HttpRequest(
26+
method = method,
27+
uri = uri,
28+
headers = headers,
29+
source = source,
30+
deserializer = this.deserializer,
31+
)
32+
}
33+
1934
suspend fun readBodyAsString() = source.readString()
2035

2136
suspend fun <T> readBodyAs(strategy: DeserializationStrategy<T>): T {

kttp-server/api/src/main/kotlin/com/coditory/kttp/server/HttpResponse.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.coditory.kttp.server
33
import com.coditory.kttp.HttpResponseHead
44
import com.coditory.kttp.HttpStatus
55
import com.coditory.kttp.headers.HttpHeaders
6+
import com.coditory.kttp.headers.MediaType
67
import kotlinx.serialization.SerializationStrategy
78

89
sealed interface HttpResponse {
@@ -27,7 +28,7 @@ sealed interface HttpResponse {
2728
data class TextResponse(
2829
val body: String,
2930
override val status: HttpStatus = HttpStatus.OK,
30-
override val headers: HttpHeaders = HttpHeaders.empty(),
31+
override val headers: HttpHeaders = HttpHeaders.from(HttpHeaders.ContentType to MediaType.Text.Plain.value),
3132
) : HttpResponse
3233

3334
data class SerializableResponse<T>(

kttp-server/api/src/main/kotlin/com/coditory/kttp/server/HttpRoute.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import com.coditory.kttp.HttpRequestMethod
44
import com.coditory.kttp.headers.MediaType
55

66
interface HttpRoute {
7+
fun routing(config: HttpRoute.() -> Unit) = routing(HttpRequestMatcher.matchingAll(), config)
8+
79
fun routing(matcher: HttpRequestMatcher, config: HttpRoute.() -> Unit)
810

911
fun routing(
@@ -24,6 +26,8 @@ interface HttpRoute {
2426
routing(matcher, config)
2527
}
2628

29+
fun filter(filter: HttpFilter) = filter(HttpRequestMatcher.matchingAll(), filter)
30+
2731
fun filter(matcher: HttpRequestMatcher, filter: HttpFilter)
2832

2933
fun filter(path: String? = null, method: HttpRequestMethod? = null, produces: MediaType? = null, consumes: MediaType? = null, predicate: HttpRequestPredicate? = null, filter: HttpFilter) {
@@ -37,6 +41,8 @@ interface HttpRoute {
3741
filter(matcher, filter)
3842
}
3943

44+
fun handler(handler: HttpHandler) = handler(HttpRequestMatcher.matchingAll(), handler)
45+
4046
fun handler(matcher: HttpRequestMatcher, handler: HttpHandler)
4147

4248
fun handler(path: String? = null, method: HttpRequestMethod? = null, produces: MediaType? = null, consumes: MediaType? = null, predicate: HttpRequestPredicate? = null, action: HttpHandler) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package com.coditory.kttp.server
22

33
import com.coditory.kttp.HttpRequestHead
4+
import kotlin.reflect.KClass
45

56
interface HttpRouter : HttpRoute {
7+
fun hasHandler(request: HttpRequestHead): Boolean
8+
fun getRequestMatchers(path: String): List<HttpRequestMatcher>
9+
fun removeHandler(handler: KClass<HttpHandler>)
610
fun removeHandler(handler: HttpHandler)
11+
fun removeFilter(filter: KClass<HttpFilter>)
712
fun removeFilter(filter: HttpFilter)
813
fun chain(request: HttpRequestHead): HttpChain
914
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.coditory.kttp.server.filter
2+
3+
import com.coditory.kttp.HttpRequestMethod
4+
import com.coditory.kttp.HttpStatus
5+
import com.coditory.kttp.headers.HttpHeaders
6+
import com.coditory.kttp.headers.MutableHttpHeaders
7+
import com.coditory.kttp.server.HttpChain
8+
import com.coditory.kttp.server.HttpExchange
9+
import com.coditory.kttp.server.HttpFilter
10+
import com.coditory.kttp.server.HttpRequest
11+
import com.coditory.kttp.server.HttpRequestMatcher
12+
import com.coditory.kttp.server.HttpResponse
13+
import com.coditory.kttp.server.HttpRouter
14+
import kotlin.time.Duration
15+
16+
class CorsHttpFilter(
17+
private val router: HttpRouter,
18+
private val additionalMethods: Set<HttpRequestMethod> = emptySet(),
19+
methods: Set<HttpRequestMethod> = emptySet(),
20+
headers: Set<String> = emptySet(),
21+
private val allowOriginAny: Boolean = false,
22+
private val allowOrigins: Set<String>? = null,
23+
private val maxAge: Duration? = null,
24+
) : HttpFilter {
25+
private val allowMethods = CorsSafelist.methods.plus(methods)
26+
private val allowHeaders = CorsSafelist.headers.plus(headers).map { it.lowercase() }.toSet()
27+
28+
override suspend fun doFilter(exchange: HttpExchange, chain: HttpChain): HttpResponse {
29+
if (exchange.request.method != HttpRequestMethod.OPTIONS) {
30+
return chain.doFilter(exchange)
31+
}
32+
if (!exchange.request.headers.contains(HttpHeaders.AccessControlRequestMethod)) {
33+
return chain.doFilter(exchange)
34+
}
35+
val matchers = router.getRequestMatchers(exchange.request.uri.path)
36+
if (matchers.isEmpty()) {
37+
HttpResponse.StatusResponse(HttpStatus.NotFound)
38+
}
39+
val headers = MutableHttpHeaders.empty()
40+
handleOrigin(exchange.request, headers)
41+
handleMethods(matchers, headers)
42+
handleHeaders(exchange.request, headers)
43+
handleMaxAge(headers)
44+
return HttpResponse.StatusResponse(HttpStatus.OK, headers)
45+
}
46+
47+
private fun handleOrigin(request: HttpRequest, headers: MutableHttpHeaders) {
48+
val origin = request.headers[HttpHeaders.Origin]
49+
if (allowOriginAny) {
50+
headers.add(HttpHeaders.AccessControlAllowOrigin, "*")
51+
} else if (origin != null && allowOrigins?.contains(origin) ?: true) {
52+
headers.add(HttpHeaders.AccessControlAllowOrigin, origin)
53+
} else if (!allowOrigins.isNullOrEmpty()) {
54+
headers.add(HttpHeaders.AccessControlAllowOrigin, allowOrigins.first())
55+
}
56+
}
57+
58+
private fun handleMethods(matchers: List<HttpRequestMatcher>, headers: MutableHttpHeaders) {
59+
val methods = matchers.map { it.methods }
60+
.flatMap { it }
61+
.plus(additionalMethods)
62+
.filter { allowMethods.contains(it) }
63+
.toSet()
64+
headers.add(HttpHeaders.AccessControlAllowMethods, methods.joinToString(", "))
65+
}
66+
67+
private fun handleHeaders(request: HttpRequest, headers: MutableHttpHeaders) {
68+
val controlHeaders = request.headers[HttpHeaders.AccessControlAllowHeaders]?.split(" *, *")
69+
if (controlHeaders.isNullOrEmpty()) {
70+
return
71+
}
72+
val allowed = controlHeaders
73+
.map { it.lowercase() }
74+
.filter { allowHeaders.contains(it.lowercase()) }
75+
.toList()
76+
headers.add(HttpHeaders.AccessControlAllowHeaders, allowed)
77+
}
78+
79+
private fun handleMaxAge(headers: MutableHttpHeaders) {
80+
if (maxAge == null) return
81+
headers.add(HttpHeaders.AccessControlMaxAge, maxAge.inWholeSeconds.toString())
82+
}
83+
84+
private object CorsSafelist {
85+
val headers = setOf(
86+
HttpHeaders.Accept,
87+
HttpHeaders.AcceptLanguage,
88+
HttpHeaders.ContentLanguage,
89+
HttpHeaders.ContentType,
90+
HttpHeaders.Range,
91+
).map { it.lowercase() }.toSet()
92+
val methods = setOf(
93+
HttpRequestMethod.GET,
94+
HttpRequestMethod.POST,
95+
HttpRequestMethod.HEAD,
96+
)
97+
}
98+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.coditory.kttp.server.filter
2+
3+
import com.coditory.kttp.HttpRequestMethod
4+
import com.coditory.kttp.server.HttpChain
5+
import com.coditory.kttp.server.HttpExchange
6+
import com.coditory.kttp.server.HttpFilter
7+
import com.coditory.kttp.server.HttpResponse
8+
import com.coditory.kttp.server.HttpRouter
9+
10+
class HeadHttpFilter(
11+
private val router: HttpRouter,
12+
) : HttpFilter {
13+
override suspend fun doFilter(exchange: HttpExchange, chain: HttpChain): HttpResponse {
14+
if (exchange.request.method != HttpRequestMethod.HEAD) {
15+
return chain.doFilter(exchange)
16+
}
17+
if (router.hasHandler(exchange.request.toHead())) {
18+
return chain.doFilter(exchange)
19+
}
20+
val getRequest = exchange.request.copy(
21+
method = HttpRequestMethod.GET,
22+
)
23+
val exchangeWithGet = exchange.copy(
24+
request = getRequest,
25+
)
26+
val getChain = router.chain(getRequest.toHead())
27+
val getResponse = getChain.doFilter(exchangeWithGet)
28+
if (getResponse is HttpResponse.SentResponse) {
29+
return getResponse
30+
}
31+
return HttpResponse.StatusResponse(getResponse.status, getResponse.headers)
32+
}
33+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.coditory.kttp.server.filter
2+
3+
import com.coditory.kttp.HttpStatus
4+
import com.coditory.kttp.server.HttpChain
5+
import com.coditory.kttp.server.HttpExchange
6+
import com.coditory.kttp.server.HttpFilter
7+
import com.coditory.kttp.server.HttpResponse
8+
import com.coditory.kttp.server.HttpRouter
9+
10+
class NotAcceptableHttpFilter(
11+
private val router: HttpRouter,
12+
) : HttpFilter {
13+
override suspend fun doFilter(
14+
exchange: HttpExchange,
15+
chain: HttpChain,
16+
): HttpResponse {
17+
if (router.hasHandler(exchange.request.toHead())) {
18+
return chain.doFilter(exchange)
19+
}
20+
val matchers = router.getRequestMatchers(exchange.request.uri.path)
21+
if (matchers.isEmpty()) {
22+
return chain.doFilter(exchange)
23+
}
24+
val methods = matchers.map { it.methods }
25+
.flatMap { it }
26+
.toSet()
27+
if (!methods.contains(exchange.request.method)) {
28+
return HttpResponse.StatusResponse(HttpStatus.MethodNotAllowed)
29+
}
30+
val accept = exchange.request.headers.accept()
31+
if (accept == null) {
32+
return chain.doFilter(exchange)
33+
}
34+
if (matchers.any { it.matchesAccept(accept) }) {
35+
return chain.doFilter(exchange)
36+
}
37+
return HttpResponse.StatusResponse(HttpStatus.NotAcceptable)
38+
}
39+
}

0 commit comments

Comments
 (0)