Improve the HTTP implementation

This commit adds support for:

* Version negotiation.
* HEAD requests.
* Friendly error messages.
* More robust reference counting.

Signed-off-by: Graham <gpe@openrs2.org>
Graham 4 years ago
parent 92a01b6262
commit 620808cb97
  1. 13
      game/src/main/kotlin/org/openrs2/game/net/crossdomain/CrossDomainChannelHandler.kt
  2. 81
      game/src/main/kotlin/org/openrs2/game/net/http/Http.kt
  3. 17
      game/src/main/kotlin/org/openrs2/game/net/http/HttpChannelHandler.kt

@ -1,14 +1,13 @@
package org.openrs2.game.net.crossdomain package org.openrs2.game.net.crossdomain
import io.netty.buffer.Unpooled import io.netty.buffer.Unpooled
import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpMethod import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import org.openrs2.buffer.use
import org.openrs2.game.net.http.Http import org.openrs2.game.net.http.Http
@ChannelHandler.Sharable @ChannelHandler.Sharable
@ -29,15 +28,13 @@ public object CrossDomainChannelHandler : SimpleChannelInboundHandler<HttpReques
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpRequest) { override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpRequest) {
if (msg.method() != HttpMethod.GET || msg.uri() != ENDPOINT) { if (msg.method() != HttpMethod.GET || msg.uri() != ENDPOINT) {
ctx.write(Http.createResponse(HttpResponseStatus.BAD_REQUEST)).addListener(ChannelFutureListener.CLOSE) Http.writeResponse(ctx, msg, HttpResponseStatus.BAD_REQUEST)
return return
} }
val response = Http.createResponse(HttpResponseStatus.OK) Unpooled.wrappedBuffer(POLICY).use { buf ->
response.headers().add(HttpHeaderNames.CONTENT_TYPE, Http.TEXT_X_CROSS_DOMAIN_POLICY) Http.writeResponse(ctx, msg, buf, Http.TEXT_X_CROSS_DOMAIN_POLICY)
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, POLICY.size) }
ctx.write(response, ctx.voidPromise())
ctx.write(Unpooled.wrappedBuffer(POLICY)).addListener(ChannelFutureListener.CLOSE)
} }
override fun channelReadComplete(ctx: ChannelHandlerContext) { override fun channelReadComplete(ctx: ChannelHandlerContext) {

@ -1,20 +1,91 @@
package org.openrs2.game.net.http package org.openrs2.game.net.http
import io.netty.buffer.ByteBuf
import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandlerContext
import io.netty.channel.FileRegion
import io.netty.handler.codec.http.DefaultHttpResponse import io.netty.handler.codec.http.DefaultHttpResponse
import io.netty.handler.codec.http.HttpHeaderNames import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpResponse import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpVersion import io.netty.handler.codec.http.HttpVersion
import io.netty.util.ReferenceCounted
import org.openrs2.buffer.copiedBuffer
import org.openrs2.buffer.use
public object Http { public object Http {
public const val MAX_CONTENT_LENGTH: Int = 65536 public const val MAX_CONTENT_LENGTH: Int = 65536
public const val TEXT_X_CROSS_DOMAIN_POLICY: String = "text/x-cross-domain-policy" public const val TEXT_X_CROSS_DOMAIN_POLICY: String = "text/x-cross-domain-policy"
private const val BANNER = "OpenRS2"
public fun createResponse(status: HttpResponseStatus): HttpResponse { private fun writeResponse(
val response = DefaultHttpResponse(HttpVersion.HTTP_1_1, status) ctx: ChannelHandlerContext,
request: HttpRequest,
status: HttpResponseStatus,
content: ReferenceCounted,
contentType: CharSequence,
contentLength: Long
) {
val version = if (request.protocolVersion() == HttpVersion.HTTP_1_0) {
HttpVersion.HTTP_1_0
} else {
HttpVersion.HTTP_1_1
}
val response = DefaultHttpResponse(version, status)
response.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE) response.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE)
response.headers().add(HttpHeaderNames.SERVER, "OpenRS2") response.headers().add(HttpHeaderNames.SERVER, BANNER)
return response response.headers().add(HttpHeaderNames.CONTENT_TYPE, contentType)
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, contentLength)
if (request.method() == HttpMethod.HEAD) {
ctx.write(response).addListener(ChannelFutureListener.CLOSE)
} else {
ctx.write(response, ctx.voidPromise())
ctx.write(content.retain()).addListener(ChannelFutureListener.CLOSE)
}
}
public fun writeResponse(
ctx: ChannelHandlerContext,
request: HttpRequest,
content: ByteBuf,
contentType: CharSequence
) {
writeResponse(ctx, request, HttpResponseStatus.OK, content, contentType, content.readableBytes().toLong())
}
public fun writeResponse(
ctx: ChannelHandlerContext,
request: HttpRequest,
content: FileRegion,
contentType: CharSequence
) {
writeResponse(ctx, request, HttpResponseStatus.OK, content, contentType, content.count())
}
public fun writeResponse(ctx: ChannelHandlerContext, request: HttpRequest, status: HttpResponseStatus) {
copiedBuffer(
"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>$status</title>
</head>
<body>
<center>
<h1>$status</h1>
</center>
<hr />
<center>$BANNER</center>
</body>
</html>
""".trimIndent().plus("\n"), Charsets.UTF_8
).use { buf ->
writeResponse(ctx, request, status, buf, HttpHeaderValues.TEXT_HTML, buf.readableBytes().toLong())
}
} }
} }

@ -1,13 +1,12 @@
package org.openrs2.game.net.http package org.openrs2.game.net.http
import io.netty.channel.ChannelFutureListener
import io.netty.channel.ChannelHandler import io.netty.channel.ChannelHandler
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelHandlerContext
import io.netty.channel.SimpleChannelInboundHandler import io.netty.channel.SimpleChannelInboundHandler
import io.netty.handler.codec.http.HttpHeaderNames
import io.netty.handler.codec.http.HttpHeaderValues import io.netty.handler.codec.http.HttpHeaderValues
import io.netty.handler.codec.http.HttpRequest import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus import io.netty.handler.codec.http.HttpResponseStatus
import org.openrs2.buffer.use
import org.openrs2.game.net.FileProvider import org.openrs2.game.net.FileProvider
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -24,22 +23,18 @@ public class HttpChannelHandler @Inject constructor(
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpRequest) { override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpRequest) {
val uri = msg.uri() val uri = msg.uri()
if (!uri.startsWith("/")) { if (!uri.startsWith("/")) {
ctx.write(Http.createResponse(HttpResponseStatus.BAD_REQUEST)).addListener(ChannelFutureListener.CLOSE) Http.writeResponse(ctx, msg, HttpResponseStatus.BAD_REQUEST)
return return
} }
val file = fileProvider.get(uri.substring(1)) fileProvider.get(uri.substring(1)).use { file ->
if (file == null) { if (file == null) {
ctx.write(Http.createResponse(HttpResponseStatus.NOT_FOUND)).addListener(ChannelFutureListener.CLOSE) Http.writeResponse(ctx, msg, HttpResponseStatus.NOT_FOUND)
return return
} }
val response = Http.createResponse(HttpResponseStatus.OK) Http.writeResponse(ctx, msg, file, HttpHeaderValues.APPLICATION_OCTET_STREAM)
response.headers().add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM) }
response.headers().add(HttpHeaderNames.CONTENT_LENGTH, file.count())
ctx.write(response, ctx.voidPromise())
ctx.write(file).addListener(ChannelFutureListener.CLOSE)
} }
override fun channelReadComplete(ctx: ChannelHandlerContext) { override fun channelReadComplete(ctx: ChannelHandlerContext) {

Loading…
Cancel
Save