forked from openrs2/openrs2
parent
65b3e1315a
commit
bc018a3b0f
@ -1,9 +1,24 @@ |
||||
package org.openrs2.game |
||||
|
||||
import com.google.common.util.concurrent.Service |
||||
import com.google.inject.AbstractModule |
||||
import com.google.inject.multibindings.Multibinder |
||||
import org.openrs2.buffer.BufferModule |
||||
import org.openrs2.cache.CacheModule |
||||
import org.openrs2.conf.ConfigModule |
||||
import org.openrs2.game.net.NetworkService |
||||
import org.openrs2.game.net.js5.Js5Service |
||||
import org.openrs2.net.NetworkModule |
||||
|
||||
public object GameModule : AbstractModule() { |
||||
override fun configure() { |
||||
// empty |
||||
install(BufferModule) |
||||
install(CacheModule) |
||||
install(ConfigModule) |
||||
install(NetworkModule) |
||||
|
||||
val binder = Multibinder.newSetBinder(binder(), Service::class.java) |
||||
binder.addBinding().to(Js5Service::class.java) |
||||
binder.addBinding().to(NetworkService::class.java) |
||||
} |
||||
} |
||||
|
@ -1,10 +1,18 @@ |
||||
package org.openrs2.game |
||||
|
||||
import com.google.common.util.concurrent.Service |
||||
import com.google.common.util.concurrent.ServiceManager |
||||
import javax.inject.Inject |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
public class GameServer { |
||||
public class GameServer @Inject constructor( |
||||
services: Set<Service> |
||||
) { |
||||
private val serviceManager = ServiceManager(services) |
||||
|
||||
public fun run() { |
||||
TODO() |
||||
serviceManager.startAsync().awaitHealthy() |
||||
serviceManager.awaitStopped() |
||||
} |
||||
} |
||||
|
@ -0,0 +1,53 @@ |
||||
package org.openrs2.game.net |
||||
|
||||
import io.netty.channel.DefaultFileRegion |
||||
import io.netty.channel.FileRegion |
||||
import java.nio.channels.FileChannel |
||||
import java.nio.file.Files |
||||
import java.nio.file.Path |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
public class FileProvider { |
||||
public fun get(uri: String): FileRegion? { |
||||
if (!uri.startsWith("/")) { |
||||
return null |
||||
} |
||||
|
||||
var path = ROOT.resolve(uri.substring(1)).toAbsolutePath().normalize() |
||||
if (!path.startsWith(ROOT)) { |
||||
return null |
||||
} |
||||
|
||||
if (!Files.exists(path)) { |
||||
path = stripChecksum(path) |
||||
} |
||||
|
||||
if (!Files.isRegularFile(path)) { |
||||
return null |
||||
} |
||||
|
||||
val channel = FileChannel.open(path) |
||||
return DefaultFileRegion(channel, 0, channel.size()) |
||||
} |
||||
|
||||
private fun stripChecksum(path: Path): Path { |
||||
val name = path.fileName.toString() |
||||
|
||||
val extensionIndex = name.lastIndexOf('.') |
||||
if (extensionIndex == -1) { |
||||
return path |
||||
} |
||||
|
||||
val checksumIndex = name.lastIndexOf('_', extensionIndex) |
||||
if (checksumIndex == -1) { |
||||
return path |
||||
} |
||||
|
||||
return path.resolveSibling(name.substring(0, checksumIndex) + name.substring(extensionIndex)) |
||||
} |
||||
|
||||
private companion object { |
||||
private val ROOT = Path.of("nonfree/var/cache/client").toAbsolutePath().normalize() |
||||
} |
||||
} |
@ -0,0 +1,62 @@ |
||||
package org.openrs2.game.net |
||||
|
||||
import com.google.common.util.concurrent.AbstractService |
||||
import io.netty.channel.EventLoopGroup |
||||
import org.openrs2.game.net.http.HttpChannelInitializer |
||||
import org.openrs2.net.BootstrapFactory |
||||
import org.openrs2.net.asCompletableFuture |
||||
import java.util.concurrent.CompletableFuture |
||||
import javax.inject.Inject |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
public class NetworkService @Inject constructor( |
||||
private val bootstrapFactory: BootstrapFactory, |
||||
private val httpInitializer: HttpChannelInitializer, |
||||
private val rs2Initializer: Rs2ChannelInitializer |
||||
) : AbstractService() { |
||||
private lateinit var group: EventLoopGroup |
||||
|
||||
override fun doStart() { |
||||
group = bootstrapFactory.createEventLoopGroup() |
||||
|
||||
val httpFuture = bootstrapFactory.createServerBootstrap(group) |
||||
.childHandler(httpInitializer) |
||||
.bind(HTTP_PORT) |
||||
.asCompletableFuture() |
||||
|
||||
val rs2Initializer = bootstrapFactory.createServerBootstrap(group) |
||||
.childHandler(rs2Initializer) |
||||
|
||||
val rs2PrimaryFuture = rs2Initializer.bind(RS2_PRIMARY_PORT) |
||||
.asCompletableFuture() |
||||
|
||||
val rs2SecondaryFuture = rs2Initializer.bind(RS2_SECONDARY_PORT) |
||||
.asCompletableFuture() |
||||
|
||||
CompletableFuture.allOf(httpFuture, rs2PrimaryFuture, rs2SecondaryFuture).handle { _, ex -> |
||||
if (ex != null) { |
||||
notifyFailed(ex) |
||||
} else { |
||||
notifyStarted() |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun doStop() { |
||||
group.shutdownGracefully().addListener { future -> |
||||
if (future.isSuccess) { |
||||
notifyStopped() |
||||
} else { |
||||
notifyFailed(future.cause()) |
||||
} |
||||
} |
||||
} |
||||
|
||||
private companion object { |
||||
// TODO(gpe): make these configurable |
||||
private const val RS2_PRIMARY_PORT = 40001 |
||||
private const val RS2_SECONDARY_PORT = 50001 |
||||
private const val HTTP_PORT = 7001 |
||||
} |
||||
} |
@ -0,0 +1,24 @@ |
||||
package org.openrs2.game.net |
||||
|
||||
import io.netty.channel.Channel |
||||
import io.netty.channel.ChannelInitializer |
||||
import org.openrs2.game.net.login.LoginChannelHandler |
||||
import org.openrs2.protocol.Protocol |
||||
import org.openrs2.protocol.Rs2Decoder |
||||
import org.openrs2.protocol.Rs2Encoder |
||||
import javax.inject.Inject |
||||
import javax.inject.Provider |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
public class Rs2ChannelInitializer @Inject constructor( |
||||
private val handlerProvider: Provider<LoginChannelHandler> |
||||
) : ChannelInitializer<Channel>() { |
||||
override fun initChannel(ch: Channel) { |
||||
ch.pipeline().addLast( |
||||
Rs2Decoder(Protocol.LOGIN_UPSTREAM), |
||||
Rs2Encoder(Protocol.LOGIN_DOWNSTREAM), |
||||
handlerProvider.get() |
||||
) |
||||
} |
||||
} |
@ -0,0 +1,52 @@ |
||||
package org.openrs2.game.net.http |
||||
|
||||
import io.netty.channel.ChannelFutureListener |
||||
import io.netty.channel.ChannelHandler |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.channel.SimpleChannelInboundHandler |
||||
import io.netty.handler.codec.http.DefaultHttpResponse |
||||
import io.netty.handler.codec.http.HttpHeaderNames |
||||
import io.netty.handler.codec.http.HttpHeaderValues |
||||
import io.netty.handler.codec.http.HttpRequest |
||||
import io.netty.handler.codec.http.HttpResponse |
||||
import io.netty.handler.codec.http.HttpResponseStatus |
||||
import io.netty.handler.codec.http.HttpVersion |
||||
import org.openrs2.game.net.FileProvider |
||||
import javax.inject.Inject |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
@ChannelHandler.Sharable |
||||
public class HttpChannelHandler @Inject constructor( |
||||
private val fileProvider: FileProvider |
||||
) : SimpleChannelInboundHandler<HttpRequest>() { |
||||
override fun channelActive(ctx: ChannelHandlerContext) { |
||||
ctx.read() |
||||
} |
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: HttpRequest) { |
||||
val file = fileProvider.get(msg.uri()) |
||||
if (file == null) { |
||||
ctx.write(createResponse(HttpResponseStatus.NOT_FOUND)).addListener(ChannelFutureListener.CLOSE) |
||||
return |
||||
} |
||||
|
||||
val response = createResponse(HttpResponseStatus.OK) |
||||
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) { |
||||
ctx.flush() |
||||
} |
||||
|
||||
private fun createResponse(status: HttpResponseStatus): HttpResponse { |
||||
val response = DefaultHttpResponse(HttpVersion.HTTP_1_1, status) |
||||
response.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE) |
||||
response.headers().add(HttpHeaderNames.SERVER, "OpenRS2") |
||||
return response |
||||
} |
||||
} |
@ -0,0 +1,27 @@ |
||||
package org.openrs2.game.net.http |
||||
|
||||
import io.netty.channel.Channel |
||||
import io.netty.channel.ChannelInitializer |
||||
import io.netty.handler.codec.http.HttpObjectAggregator |
||||
import io.netty.handler.codec.http.HttpRequestDecoder |
||||
import io.netty.handler.codec.http.HttpResponseEncoder |
||||
import javax.inject.Inject |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
public class HttpChannelInitializer @Inject constructor( |
||||
private val handler: HttpChannelHandler |
||||
) : ChannelInitializer<Channel>() { |
||||
override fun initChannel(ch: Channel) { |
||||
ch.pipeline().addLast( |
||||
HttpRequestDecoder(), |
||||
HttpResponseEncoder(), |
||||
HttpObjectAggregator(MAX_CONTENT_LENGTH), |
||||
handler |
||||
) |
||||
} |
||||
|
||||
private companion object { |
||||
private const val MAX_CONTENT_LENGTH = 65536 |
||||
} |
||||
} |
@ -0,0 +1,34 @@ |
||||
package org.openrs2.game.net.jaggrab |
||||
|
||||
import io.netty.channel.ChannelFutureListener |
||||
import io.netty.channel.ChannelHandler |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.channel.SimpleChannelInboundHandler |
||||
import org.openrs2.game.net.FileProvider |
||||
import org.openrs2.protocol.jaggrab.JaggrabRequest |
||||
import javax.inject.Inject |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
@ChannelHandler.Sharable |
||||
public class JaggrabChannelHandler @Inject constructor( |
||||
private val fileProvider: FileProvider |
||||
) : SimpleChannelInboundHandler<JaggrabRequest>() { |
||||
override fun handlerAdded(ctx: ChannelHandlerContext) { |
||||
ctx.read() |
||||
} |
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: JaggrabRequest) { |
||||
val file = fileProvider.get(msg.path) |
||||
if (file == null) { |
||||
ctx.close() |
||||
return |
||||
} |
||||
|
||||
ctx.write(file).addListener(ChannelFutureListener.CLOSE) |
||||
} |
||||
|
||||
override fun channelReadComplete(ctx: ChannelHandlerContext) { |
||||
ctx.flush() |
||||
} |
||||
} |
@ -0,0 +1,40 @@ |
||||
package org.openrs2.game.net.js5 |
||||
|
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.channel.SimpleChannelInboundHandler |
||||
import org.openrs2.protocol.js5.Js5Request |
||||
import org.openrs2.protocol.js5.XorEncoder |
||||
import javax.inject.Inject |
||||
|
||||
public class Js5ChannelHandler @Inject constructor( |
||||
private val service: Js5Service |
||||
) : SimpleChannelInboundHandler<Js5Request>() { |
||||
private lateinit var client: Js5Client |
||||
|
||||
override fun handlerAdded(ctx: ChannelHandlerContext) { |
||||
client = Js5Client(ctx.read()) |
||||
} |
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: Js5Request) { |
||||
when (msg) { |
||||
is Js5Request.Group -> service.push(client, msg) |
||||
is Js5Request.Rekey -> handleRekey(ctx, msg) |
||||
is Js5Request.Disconnect -> ctx.close() |
||||
} |
||||
} |
||||
|
||||
private fun handleRekey(ctx: ChannelHandlerContext, msg: Js5Request.Rekey) { |
||||
val encoder = ctx.pipeline().get(XorEncoder::class.java) |
||||
encoder.key = msg.key |
||||
} |
||||
|
||||
override fun channelReadComplete(ctx: ChannelHandlerContext) { |
||||
service.readIfNotFull(client) |
||||
} |
||||
|
||||
override fun channelWritabilityChanged(ctx: ChannelHandlerContext) { |
||||
if (ctx.channel().isWritable) { |
||||
service.notifyIfNotEmpty(client) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,43 @@ |
||||
package org.openrs2.game.net.js5 |
||||
|
||||
import io.netty.channel.ChannelHandlerContext |
||||
import org.openrs2.protocol.js5.Js5Request |
||||
|
||||
public class Js5Client( |
||||
public val ctx: ChannelHandlerContext |
||||
) { |
||||
private val urgent = ArrayDeque<Js5Request.Group>() |
||||
private val prefetch = ArrayDeque<Js5Request.Group>() |
||||
|
||||
public fun push(request: Js5Request.Group) { |
||||
if (request.prefetch) { |
||||
prefetch += request |
||||
} else { |
||||
urgent += request |
||||
} |
||||
} |
||||
|
||||
public fun pop(): Js5Request.Group? { |
||||
val request = urgent.removeFirstOrNull() |
||||
if (request != null) { |
||||
return request |
||||
} |
||||
return prefetch.removeFirstOrNull() |
||||
} |
||||
|
||||
public fun isNotFull(): Boolean { |
||||
return urgent.size < MAX_QUEUE_SIZE && prefetch.size < MAX_QUEUE_SIZE |
||||
} |
||||
|
||||
public fun isNotEmpty(): Boolean { |
||||
return urgent.isNotEmpty() || prefetch.isNotEmpty() |
||||
} |
||||
|
||||
public fun isReady(): Boolean { |
||||
return ctx.channel().isWritable && isNotEmpty() |
||||
} |
||||
|
||||
private companion object { |
||||
private const val MAX_QUEUE_SIZE = 20 |
||||
} |
||||
} |
@ -0,0 +1,128 @@ |
||||
package org.openrs2.game.net.js5 |
||||
|
||||
import com.google.common.util.concurrent.AbstractExecutionThreadService |
||||
import io.netty.buffer.ByteBufAllocator |
||||
import org.openrs2.buffer.use |
||||
import org.openrs2.cache.Js5Archive |
||||
import org.openrs2.cache.Js5Compression |
||||
import org.openrs2.cache.Js5CompressionType |
||||
import org.openrs2.cache.Js5MasterIndex |
||||
import org.openrs2.cache.Store |
||||
import org.openrs2.cache.VersionTrailer |
||||
import org.openrs2.protocol.js5.Js5Request |
||||
import org.openrs2.protocol.js5.Js5Response |
||||
import org.openrs2.util.collect.UniqueQueue |
||||
import javax.inject.Inject |
||||
import javax.inject.Singleton |
||||
|
||||
@Singleton |
||||
public class Js5Service @Inject constructor( |
||||
private val store: Store, |
||||
private val masterIndex: Js5MasterIndex, |
||||
private val alloc: ByteBufAllocator |
||||
) : AbstractExecutionThreadService() { |
||||
private val lock = Object() |
||||
private val clients = UniqueQueue<Js5Client>() |
||||
|
||||
override fun run() { |
||||
while (true) { |
||||
var client: Js5Client |
||||
var request: Js5Request.Group |
||||
|
||||
synchronized(lock) { |
||||
while (true) { |
||||
if (!isRunning) { |
||||
return |
||||
} |
||||
|
||||
val next = clients.poll() |
||||
if (next == null) { |
||||
lock.wait() |
||||
continue |
||||
} |
||||
|
||||
client = next |
||||
request = client.pop() ?: continue |
||||
break |
||||
} |
||||
} |
||||
|
||||
serve(client, request) |
||||
} |
||||
} |
||||
|
||||
private fun serve(client: Js5Client, request: Js5Request.Group) { |
||||
val ctx = client.ctx |
||||
if (!ctx.channel().isActive) { |
||||
return |
||||
} |
||||
|
||||
val buf = if (request.archive == Js5Archive.ARCHIVESET && request.group == Js5Archive.ARCHIVESET) { |
||||
alloc.buffer().use { uncompressed -> |
||||
masterIndex.write(uncompressed) |
||||
|
||||
Js5Compression.compress(uncompressed, Js5CompressionType.UNCOMPRESSED).use { compressed -> |
||||
compressed.retain() |
||||
} |
||||
} |
||||
} else { |
||||
store.read(request.archive, request.group).use { buf -> |
||||
if (request.archive != Js5Archive.ARCHIVESET) { |
||||
VersionTrailer.strip(buf) |
||||
} |
||||
|
||||
buf.retain() |
||||
} |
||||
} |
||||
|
||||
val response = Js5Response(request.prefetch, request.archive, request.group, buf) |
||||
ctx.writeAndFlush(response, ctx.voidPromise()) |
||||
|
||||
synchronized(lock) { |
||||
if (client.isReady()) { |
||||
clients.add(client) |
||||
} |
||||
|
||||
if (client.isNotFull()) { |
||||
ctx.read() |
||||
} |
||||
} |
||||
} |
||||
|
||||
public fun push(client: Js5Client, request: Js5Request.Group) { |
||||
synchronized(lock) { |
||||
client.push(request) |
||||
|
||||
if (client.isReady()) { |
||||
clients.add(client) |
||||
lock.notifyAll() |
||||
} |
||||
|
||||
if (client.isNotFull()) { |
||||
client.ctx.read() |
||||
} |
||||
} |
||||
} |
||||
|
||||
public fun readIfNotFull(client: Js5Client) { |
||||
synchronized(lock) { |
||||
if (client.isNotFull()) { |
||||
client.ctx.read() |
||||
} |
||||
} |
||||
} |
||||
|
||||
public fun notifyIfNotEmpty(client: Js5Client) { |
||||
synchronized(lock) { |
||||
if (client.isNotEmpty()) { |
||||
lock.notifyAll() |
||||
} |
||||
} |
||||
} |
||||
|
||||
override fun triggerShutdown() { |
||||
synchronized(lock) { |
||||
lock.notifyAll() |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,79 @@ |
||||
package org.openrs2.game.net.login |
||||
|
||||
import io.netty.buffer.Unpooled |
||||
import io.netty.channel.ChannelFutureListener |
||||
import io.netty.channel.ChannelHandlerContext |
||||
import io.netty.channel.SimpleChannelInboundHandler |
||||
import io.netty.handler.codec.DelimiterBasedFrameDecoder |
||||
import io.netty.handler.codec.string.StringDecoder |
||||
import org.openrs2.buffer.copiedBuffer |
||||
import org.openrs2.game.net.jaggrab.JaggrabChannelHandler |
||||
import org.openrs2.game.net.js5.Js5ChannelHandler |
||||
import org.openrs2.protocol.Rs2Decoder |
||||
import org.openrs2.protocol.Rs2Encoder |
||||
import org.openrs2.protocol.jaggrab.JaggrabRequestDecoder |
||||
import org.openrs2.protocol.js5.Js5RequestDecoder |
||||
import org.openrs2.protocol.js5.Js5ResponseEncoder |
||||
import org.openrs2.protocol.js5.XorDecoder |
||||
import org.openrs2.protocol.login.LoginRequest |
||||
import org.openrs2.protocol.login.LoginResponse |
||||
import javax.inject.Inject |
||||
import javax.inject.Provider |
||||
|
||||
public class LoginChannelHandler @Inject constructor( |
||||
private val js5HandlerProvider: Provider<Js5ChannelHandler>, |
||||
private val jaggrabHandler: JaggrabChannelHandler |
||||
) : SimpleChannelInboundHandler<LoginRequest>() { |
||||
override fun channelActive(ctx: ChannelHandlerContext) { |
||||
ctx.read() |
||||
} |
||||
|
||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: LoginRequest) { |
||||
when (msg) { |
||||
is LoginRequest.InitJs5RemoteConnection -> handleInitJs5RemoteConnection(ctx, msg) |
||||
is LoginRequest.InitJaggrabConnection -> handleInitJaggrabConnection(ctx) |
||||
} |
||||
} |
||||
|
||||
private fun handleInitJs5RemoteConnection(ctx: ChannelHandlerContext, msg: LoginRequest.InitJs5RemoteConnection) { |
||||
if (msg.build != BUILD) { |
||||
ctx.write(LoginResponse.ClientOutOfDate).addListener(ChannelFutureListener.CLOSE) |
||||
return |
||||
} |
||||
|
||||
ctx.pipeline().addLast( |
||||
XorDecoder(), |
||||
Js5RequestDecoder(), |
||||
Js5ResponseEncoder, |
||||
js5HandlerProvider.get() |
||||
) |
||||
ctx.pipeline().remove(Rs2Decoder::class.java) |
||||
|
||||
ctx.write(LoginResponse.Js5Ok).addListener { |
||||
ctx.pipeline().remove(Rs2Encoder::class.java) |
||||
ctx.pipeline().remove(this) |
||||
} |
||||
} |
||||
|
||||
private fun handleInitJaggrabConnection(ctx: ChannelHandlerContext) { |
||||
ctx.pipeline().addLast( |
||||
DelimiterBasedFrameDecoder(JAGGRAB_MAX_FRAME_LENGTH, JAGGRAB_DELIMITER), |
||||
StringDecoder(Charsets.UTF_8), |
||||
JaggrabRequestDecoder, |
||||
jaggrabHandler |
||||
) |
||||
ctx.pipeline().remove(Rs2Decoder::class.java) |
||||
ctx.pipeline().remove(Rs2Encoder::class.java) |
||||
ctx.pipeline().remove(this) |
||||
} |
||||
|
||||
override fun channelReadComplete(ctx: ChannelHandlerContext) { |
||||
ctx.flush() |
||||
} |
||||
|
||||
private companion object { |
||||
private const val BUILD = 550 |
||||
private const val JAGGRAB_MAX_FRAME_LENGTH = 4096 |
||||
private val JAGGRAB_DELIMITER = Unpooled.unreleasableBuffer(copiedBuffer("\n\n")) |
||||
} |
||||
} |
Loading…
Reference in new issue