├── .gitignore ├── LICENSE ├── README ├── build.xml ├── lib └── netty-3.2.jar ├── src └── org │ └── apollo │ └── jagcached │ ├── FileServer.java │ ├── dispatch │ ├── ChannelRequest.java │ ├── HttpRequestWorker.java │ ├── JagGrabRequestWorker.java │ ├── OnDemandRequestWorker.java │ ├── RequestDispatcher.java │ ├── RequestWorker.java │ └── RequestWorkerPool.java │ ├── fs │ ├── FileDescriptor.java │ ├── FileSystemConstants.java │ ├── Index.java │ └── IndexedFileSystem.java │ ├── net │ ├── FileServerHandler.java │ ├── HttpPipelineFactory.java │ ├── JagGrabPipelineFactory.java │ ├── NetworkConstants.java │ ├── OnDemandPipelineFactory.java │ ├── jaggrab │ │ ├── JagGrabRequest.java │ │ ├── JagGrabRequestDecoder.java │ │ ├── JagGrabResponse.java │ │ └── JagGrabResponseEncoder.java │ ├── ondemand │ │ ├── OnDemandRequest.java │ │ ├── OnDemandRequestDecoder.java │ │ ├── OnDemandResponse.java │ │ └── OnDemandResponseEncoder.java │ └── service │ │ ├── ServiceRequest.java │ │ ├── ServiceRequestDecoder.java │ │ ├── ServiceResponse.java │ │ └── ServiceResponseEncoder.java │ └── resource │ ├── CombinedResourceProvider.java │ ├── HypertextResourceProvider.java │ ├── ResourceProvider.java │ └── VirtualResourceProvider.java ├── start.bat └── start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | ~* 3 | bin 4 | data/fs/* 5 | data/www/* 6 | !.gitignore 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Graham Edgecombe 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | ------------------------------------------------------------------------------- 2 | Introduction 3 | ------------------------------------------------------------------------------- 4 | 5 | jagcached is an open-source daemon designed for serving files to clients in a 6 | massively multiplayer online game environment. 7 | 8 | It can serve files using HTTP and JAGGRAB (a protocol similar to the first 9 | version of HTTP - now known as HTTP 0.9) as well as a binary streaming 10 | protocol named ondemand. JAGGRAB and ondemand are both proprietary protocols. 11 | 12 | ------------------------------------------------------------------------------- 13 | Using jagcached 14 | ------------------------------------------------------------------------------- 15 | 16 | The cache which you wish to be served should be placed in the data/fs 17 | directory, such that the paths of files look like so: 18 | 19 | /path/to/jagcached/data/fs/main_file_cache... 20 | 21 | Starting it is simple: use the start.sh shell script on Linux and the start.bat 22 | batch script on Windows. You'll need the JRE binaries in your PATH environment 23 | variable. 24 | 25 | Apache Ant can be used to build jagcached from source. You could also import 26 | the project into an IDE like Eclipse which should detect the project layout 27 | automatically (jagcached was developed and tested in Eclipse on a Linux 28 | system). 29 | 30 | Optionally, you can place additional files in the data/www folder which can be 31 | accessed over HTTP - making jagcached act as a rudimentary web server. However, 32 | the virtual path mappings (see below) cannot be overriden! jagcached will load 33 | MIME types for a few simple formats (txt, png, gif, jpeg, html, css) and 34 | otherwise assume the type "application/octect-stream". It will look for a file 35 | named "index.html" in the directory, and use that as the index page for that 36 | directory. It will not produce directory listings. 37 | 38 | You cannot configure the ports or paths that jagcached uses. You'll need to 39 | either edit the source and rebuild it, use symbolic links (for the paths) or 40 | use a different working directory. This is partly due to simplicity and partly 41 | due to laziness! 42 | 43 | This also means on Linux you will need root access so it can bind to the HTTP 44 | port. If this isn't possible, it'll just listen on 43594 and 43595 but bear in 45 | mind this isn't as efficient - clients will timeout while they try to access 46 | HTTP and then use JAGGRAB as a fallback (probably originally intended for 47 | getting around corporate firewalls). 48 | 49 | ------------------------------------------------------------------------------- 50 | Protocol and Format Documentation 51 | ------------------------------------------------------------------------------- 52 | 53 | NOTE: only JAGGRAB and the ondemand protocols are covered here. HTTP is not as 54 | it is a defined standard. All data types, unless otherwise stated, are big 55 | endian (MSB-LSB). 56 | 57 | The JAGGRAB protocol is similar to HTTP 0.9. A connection is opened on TCP port 58 | 43595 for each request. The format of a request is the following text, encoded 59 | using ASCII (and without the leading/trailing spaces): 60 | 61 | JAGGRAB / 62 | 63 | Where is the path of the resource to access, and is the line feed 64 | character in ASCII. 65 | 66 | The server then responds with the contents of the file and closes the 67 | connection. 68 | 69 | Certain paths in JAGGRAB and HTTP match files in the actual cache itself. This 70 | is the mapping: 71 | 72 | /crc => (automatically generated, see below) 73 | /title => type 0, file 1 74 | /config => type 0, file 2 75 | /interface => type 0, file 3 76 | /media => type 0, file 4 77 | /versionlist => type 0, file 5 78 | /textures => type 0, file 6 79 | /wordenc => type 0, file 7 80 | /sounds => type 0, file 8 81 | 82 | NOTE: you should only check the start of the path for these special resources. 83 | They can be postfixed with random numbers (in order to prevent caching). The 84 | CRC table will also be postfixed with the version. 85 | 86 | Hence, a typical CRC path might look like this: 87 | 88 | /crc5659473873-317 89 | 90 | Requests for other files might look like this: 91 | 92 | /title48957338448 93 | 94 | The /crc path points to an automatically generated CRC table. This is 40 bytes 95 | long. It consists of: 96 | 97 | 9x CRC32 hashes (4 bytes) 98 | a hash of the CRCs (4 bytes) 99 | 100 | The CRC32 hashes are the hashes of the files with the type 0. File 0 is unused 101 | and generally ignored by clients, so the hash could be set to anything (e.g. 102 | zero or the client version). 103 | 104 | The final hash is computed using the following algorithm: 105 | 106 | hash = 1234 107 | foreach crc hash 108 | hash = (hash << 1) + crc hash 109 | 110 | This is used to verify the integrity of the table itself. 111 | 112 | The ondemand protocol, instead of working with paths, allows you to access any 113 | file directly using its type id and file id. It runs on port 43594 (along with 114 | the game) however it has a different service id (15). 115 | 116 | Once connected, an ondemand client will send a single byte with the value 15. 117 | The server with then respond with 8 bytes which are ignored by the client and 118 | thus usually null (0). 119 | 120 | After this initial handshake, all requests are in the form: 121 | 122 | +---------------+----------------+-------------------+ 123 | | type (1 byte) | file (2 bytes) | priority (1 byte) | 124 | +---------------+----------------+-------------------+ 125 | 126 | Type typically ranges from 0-3 and is one minus the name of the index file. For 127 | example, if the client wanted a file in the index file 3, it would send the 128 | type as 2. The file id has no such transformation. 129 | 130 | The priority is 1-3, with 1 being the highest and 3 being the lowest. 131 | 132 | HIGH (1): the client needs the file urgently to continue playing the game. 133 | MEDIUM (2): the client needs the file to load the game during startup. 134 | LOW (3): the client might need the file in the future, but not now. 135 | 136 | Responses are 500 byte chunks of the file: 137 | 138 | +---------------+----------------+---------------------+-------------------+ 139 | | type (1 byte) | file (2 bytes) | file size (2 bytes) | chunk id (1 byte) | 140 | +---------------+----------------+---------------------+-------------------+ 141 | | data (n bytes, where n <= 500) | 142 | +--------------------------------+ 143 | 144 | The files are stored on the client and (generally) on the server in a 'virtual 145 | indexed file system' placed on top of the existing file system. 146 | 147 | This consists of one data file and several index files. The data file is named 148 | main_file_cache.dat or main_file_cache.dat2. The index files are named 149 | main_file_cache.idx postfixed with a number (e.g. main_file_cache.idx3). 150 | 151 | An index file is split up into several records which each points to a file. The 152 | format of each 6 byte record is: 153 | 154 | +---------------------+-----------------------+ 155 | | file size (3 bytes) | head sector (3 bytes) | 156 | +---------------------+-----------------------+ 157 | 158 | The nth file is the nth record. So, if you wanted to access file 3's record, 159 | you would read 6 bytes from (and including) the 12th byte. 160 | 161 | The data file is split up into sectors. A file can be dispersed throughout the 162 | whole file (i.e. it is fragmented) however they tend to be consecutive. The 163 | client does not currently defragment the file system, but re-downloading it 164 | from scratch has the same effect. 165 | 166 | Each sector is 520 bytes long. The format of a sector is: 167 | 168 | +------------------+------------------+ 169 | | header (8 bytes) | data (512 bytes) | 170 | +------------------+------------------+ 171 | 172 | The format of the header is: 173 | 174 | +----------------+-----------------+-----------------------+---------------+ 175 | | file (2 bytes) | chunk (2 bytes) | next sector (3 bytes) | type (1 byte) | 176 | +----------------+-----------------+-----------------------+---------------+ 177 | 178 | File and type are the file id and type id (note that if the file is in the 179 | main_file_cache.idx2 file, the type id will be 3, etc). 180 | 181 | Sectors are stored in a linked list: the index points to the head sector, and 182 | each sector points to the next sector which holds the next chunk of the file. 183 | 184 | The first sector in the list will have chunk id 0, the second will have id 1, 185 | and so on. 186 | 187 | The next sector id of the last sector in a file will be 0. 188 | 189 | ------------------------------------------------------------------------------- 190 | Licensing 191 | ------------------------------------------------------------------------------- 192 | 193 | Copyright (c) 2010 Graham Edgecombe 194 | 195 | Permission to use, copy, modify, and/or distribute this software for any 196 | purpose with or without fee is hereby granted, provided that the above 197 | copyright notice and this permission notice appear in all copies. 198 | 199 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 200 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 201 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 202 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 203 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 204 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 205 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 206 | 207 | ------------------------------------------------------------------------------- 208 | Third-Party Libraries 209 | ------------------------------------------------------------------------------- 210 | 211 | jagcached uses the Netty library (available at http://jboss.org/netty). Netty 212 | is released under the Apache License version 2.0, which the Free Software 213 | Foundation considers to be compatible with the GNU General Public License 214 | version 3 (the license jagcached is licensed under). 215 | 216 | ------------------------------------------------------------------------------- 217 | Git Repository 218 | ------------------------------------------------------------------------------- 219 | 220 | The source code is managed using git and is hosted on GitHub. The GitHub 221 | project page is located at http://github.com/apollo-rsps/jagcached and has 222 | information on accessing the repository. 223 | 224 | ------------------------------------------------------------------------------- 225 | Credits 226 | ------------------------------------------------------------------------------- 227 | 228 | jagcached would have not been possible without research, ideas and support from 229 | several people within the server emulation community. 230 | 231 | Members of the Apollo Development team: 232 | - Graham 233 | 234 | For research (protocol and formats): 235 | - wL 236 | - daiki 237 | - super_ 238 | - Tom 239 | - silabsoft 240 | 241 | For general support, ideas and discussion: 242 | - blakeman8192 243 | - Sir Sean 244 | - Raul 245 | - thiefmn6092 246 | 247 | For bug reports: 248 | - Method 249 | 250 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/netty-3.2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollo-rsps/jagcached/dab6e14e269c5d68d05f18a6139ea16e6f3beb64/lib/netty-3.2.jar -------------------------------------------------------------------------------- /src/org/apollo/jagcached/FileServer.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached; 2 | 3 | import java.net.InetSocketAddress; 4 | import java.net.SocketAddress; 5 | import java.util.concurrent.ExecutorService; 6 | import java.util.concurrent.Executors; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | 11 | import org.apollo.jagcached.dispatch.RequestWorkerPool; 12 | import org.apollo.jagcached.net.FileServerHandler; 13 | import org.apollo.jagcached.net.HttpPipelineFactory; 14 | import org.apollo.jagcached.net.JagGrabPipelineFactory; 15 | import org.apollo.jagcached.net.NetworkConstants; 16 | import org.apollo.jagcached.net.OnDemandPipelineFactory; 17 | import org.jboss.netty.bootstrap.ServerBootstrap; 18 | import org.jboss.netty.channel.ChannelPipelineFactory; 19 | import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory; 20 | import org.jboss.netty.util.HashedWheelTimer; 21 | import org.jboss.netty.util.Timer; 22 | 23 | /** 24 | * The core class of the file server. 25 | * @author Graham 26 | */ 27 | public final class FileServer { 28 | 29 | /** 30 | * The logger for this class. 31 | */ 32 | private static final Logger logger = Logger.getLogger(FileServer.class.getName()); 33 | 34 | /** 35 | * The entry point of the application. 36 | * @param args The command-line arguments. 37 | */ 38 | public static void main(String[] args) { 39 | try { 40 | new FileServer().start(); 41 | } catch (Throwable t) { 42 | logger.log(Level.SEVERE, "Error starting server.", t); 43 | } 44 | } 45 | 46 | /** 47 | * The executor service. 48 | */ 49 | private final ExecutorService service = Executors.newCachedThreadPool(); 50 | 51 | /** 52 | * The request worker pool. 53 | */ 54 | private final RequestWorkerPool pool = new RequestWorkerPool(); 55 | 56 | /** 57 | * The file server event handler. 58 | */ 59 | private final FileServerHandler handler = new FileServerHandler(); 60 | 61 | /** 62 | * The timer used for idle checking. 63 | */ 64 | private final Timer timer = new HashedWheelTimer(); 65 | 66 | /** 67 | * Starts the file server. 68 | * @throws Exception if an error occurs. 69 | */ 70 | public void start() throws Exception { 71 | logger.info("Starting workers..."); 72 | pool.start(); 73 | 74 | logger.info("Starting services..."); 75 | try { 76 | start("HTTP", new HttpPipelineFactory(handler, timer), NetworkConstants.HTTP_PORT); 77 | } catch (Throwable t) { 78 | logger.log(Level.SEVERE, "Failed to start HTTP service.", t); 79 | logger.warning("HTTP will be unavailable. JAGGRAB will be used as a fallback by clients but this isn't reccomended!"); 80 | } 81 | start("JAGGRAB", new JagGrabPipelineFactory(handler, timer), NetworkConstants.JAGGRAB_PORT); 82 | start("ondemand", new OnDemandPipelineFactory(handler, timer), NetworkConstants.SERVICE_PORT); 83 | 84 | logger.info("Ready for connections."); 85 | } 86 | 87 | /** 88 | * Starts the specified service. 89 | * @param name The name of the service. 90 | * @param pipelineFactory The pipeline factory. 91 | * @param port The port. 92 | */ 93 | private void start(String name, ChannelPipelineFactory pipelineFactory, int port) { 94 | SocketAddress address = new InetSocketAddress(port); 95 | 96 | logger.info("Binding " + name + " service to " + address + "..."); 97 | 98 | ServerBootstrap bootstrap = new ServerBootstrap(); 99 | bootstrap.setFactory(new NioServerSocketChannelFactory(service, service)); 100 | bootstrap.setPipelineFactory(pipelineFactory); 101 | bootstrap.bind(address); 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/ChannelRequest.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import org.jboss.netty.channel.Channel; 4 | 5 | /** 6 | * A specialised request which contains a channel as well as the request object 7 | * itself. 8 | * @author Graham 9 | * @param The type of request. 10 | */ 11 | public final class ChannelRequest implements Comparable> { 12 | 13 | /** 14 | * The channel. 15 | */ 16 | private final Channel channel; 17 | 18 | /** 19 | * The request. 20 | */ 21 | private final T request; 22 | 23 | /** 24 | * Creates a new channel request. 25 | * @param channel The channel. 26 | * @param request The request. 27 | */ 28 | public ChannelRequest(Channel channel, T request) { 29 | this.channel = channel; 30 | this.request = request; 31 | } 32 | 33 | /** 34 | * Gets the channel. 35 | * @return The channel. 36 | */ 37 | public Channel getChannel() { 38 | return channel; 39 | } 40 | 41 | /** 42 | * Gets the request. 43 | * @return The request. 44 | */ 45 | public T getRequest() { 46 | return request; 47 | } 48 | 49 | @SuppressWarnings("unchecked") 50 | @Override 51 | public int compareTo(ChannelRequest o) { 52 | if (request instanceof Comparable && o.request instanceof Comparable) { 53 | return ((Comparable) request).compareTo(o.request); 54 | } 55 | return 0; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/HttpRequestWorker.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.ByteBuffer; 6 | import java.nio.charset.Charset; 7 | import java.util.Date; 8 | 9 | import org.apollo.jagcached.fs.IndexedFileSystem; 10 | import org.apollo.jagcached.resource.CombinedResourceProvider; 11 | import org.apollo.jagcached.resource.HypertextResourceProvider; 12 | import org.apollo.jagcached.resource.ResourceProvider; 13 | import org.apollo.jagcached.resource.VirtualResourceProvider; 14 | import org.jboss.netty.buffer.ChannelBuffer; 15 | import org.jboss.netty.buffer.ChannelBuffers; 16 | import org.jboss.netty.channel.Channel; 17 | import org.jboss.netty.channel.ChannelFutureListener; 18 | import org.jboss.netty.handler.codec.http.DefaultHttpResponse; 19 | import org.jboss.netty.handler.codec.http.HttpRequest; 20 | import org.jboss.netty.handler.codec.http.HttpResponse; 21 | import org.jboss.netty.handler.codec.http.HttpResponseStatus; 22 | 23 | /** 24 | * A worker which services HTTP requests. 25 | * @author Graham 26 | */ 27 | public final class HttpRequestWorker extends RequestWorker { 28 | 29 | /** 30 | * The value of the server header. 31 | */ 32 | private static final String SERVER_IDENTIFIER = "JAGeX/3.1"; 33 | 34 | /** 35 | * The directory with web files. 36 | */ 37 | private static final File WWW_DIRECTORY = new File("./data/www/"); 38 | 39 | /** 40 | * The default character set. 41 | */ 42 | private static final Charset CHARACTER_SET = Charset.forName("ISO-8859-1"); 43 | 44 | /** 45 | * Creates the HTTP request worker. 46 | * @param fs The file system. 47 | */ 48 | public HttpRequestWorker(IndexedFileSystem fs) { 49 | super(new CombinedResourceProvider(new VirtualResourceProvider(fs), new HypertextResourceProvider(WWW_DIRECTORY))); 50 | } 51 | 52 | @Override 53 | protected ChannelRequest nextRequest() throws InterruptedException { 54 | return RequestDispatcher.nextHttpRequest(); 55 | } 56 | 57 | @Override 58 | protected void service(ResourceProvider provider, Channel channel, HttpRequest request) throws IOException { 59 | String path = request.getUri(); 60 | ByteBuffer buf = provider.get(path); 61 | 62 | ChannelBuffer wrappedBuf; 63 | HttpResponseStatus status = HttpResponseStatus.OK; 64 | 65 | String mimeType = getMimeType(request.getUri()); 66 | 67 | if (buf == null) { 68 | status = HttpResponseStatus.NOT_FOUND; 69 | wrappedBuf = createErrorPage(status, "The page you requested could not be found."); 70 | mimeType = "text/html"; 71 | } else { 72 | wrappedBuf = ChannelBuffers.wrappedBuffer(buf); 73 | } 74 | 75 | HttpResponse resp = new DefaultHttpResponse(request.getProtocolVersion(), status); 76 | 77 | resp.setHeader("Date", new Date()); 78 | resp.setHeader("Server", SERVER_IDENTIFIER); 79 | resp.setHeader("Content-type", mimeType + ", charset=" + CHARACTER_SET.name()); 80 | resp.setHeader("Cache-control", "no-cache"); 81 | resp.setHeader("Pragma", "no-cache"); 82 | resp.setHeader("Expires", new Date(0)); 83 | resp.setHeader("Connection", "close"); 84 | resp.setHeader("Content-length", wrappedBuf.readableBytes()); 85 | resp.setChunked(false); 86 | resp.setContent(wrappedBuf); 87 | 88 | channel.write(resp).addListener(ChannelFutureListener.CLOSE); 89 | } 90 | 91 | /** 92 | * Gets the MIME type of a file by its name. 93 | * @param name The file name. 94 | * @return The MIME type. 95 | */ 96 | private String getMimeType(String name) { 97 | if (name.endsWith(".htm") || name.endsWith(".html")) { 98 | return "text/html"; 99 | } else if (name.endsWith(".css")) { 100 | return "text/css"; 101 | } else if (name.endsWith(".js")) { 102 | return "text/javascript"; 103 | } else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) { 104 | return "image/jpeg"; 105 | } else if (name.endsWith(".gif")) { 106 | return "image/gif"; 107 | } else if (name.endsWith(".png")) { 108 | return "image/png"; 109 | } else if (name.endsWith(".txt")) { 110 | return "text/plain"; 111 | } 112 | return "application/octect-stream"; 113 | } 114 | 115 | /** 116 | * Creates an error page. 117 | * @param status The HTTP status. 118 | * @param description The error description. 119 | * @return The error page as a buffer. 120 | */ 121 | private ChannelBuffer createErrorPage(HttpResponseStatus status, String description) { 122 | String title = status.getCode() + " " + status.getReasonPhrase(); 123 | 124 | StringBuilder bldr = new StringBuilder(); 125 | 126 | bldr.append(""); 127 | bldr.append(title); 128 | bldr.append("

"); 129 | bldr.append(title); 130 | bldr.append("

"); 131 | bldr.append(description); 132 | bldr.append("


"); 133 | bldr.append(SERVER_IDENTIFIER); 134 | bldr.append(" Server
"); 135 | 136 | return ChannelBuffers.copiedBuffer(bldr.toString(), Charset.defaultCharset()); 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/JagGrabRequestWorker.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | import org.apollo.jagcached.fs.IndexedFileSystem; 7 | import org.apollo.jagcached.net.jaggrab.JagGrabRequest; 8 | import org.apollo.jagcached.net.jaggrab.JagGrabResponse; 9 | import org.apollo.jagcached.resource.ResourceProvider; 10 | import org.apollo.jagcached.resource.VirtualResourceProvider; 11 | import org.jboss.netty.buffer.ChannelBuffer; 12 | import org.jboss.netty.buffer.ChannelBuffers; 13 | import org.jboss.netty.channel.Channel; 14 | import org.jboss.netty.channel.ChannelFutureListener; 15 | 16 | /** 17 | * A worker which services JAGGRAB requests. 18 | * @author Graham 19 | */ 20 | public final class JagGrabRequestWorker extends RequestWorker { 21 | 22 | /** 23 | * Creates the JAGGRAB request worker. 24 | * @param fs The file system. 25 | */ 26 | public JagGrabRequestWorker(IndexedFileSystem fs) { 27 | super(new VirtualResourceProvider(fs)); 28 | } 29 | 30 | @Override 31 | protected ChannelRequest nextRequest() throws InterruptedException { 32 | return RequestDispatcher.nextJagGrabRequest(); 33 | } 34 | 35 | @Override 36 | protected void service(ResourceProvider provider, Channel channel, JagGrabRequest request) throws IOException { 37 | ByteBuffer buf = provider.get(request.getFilePath()); 38 | if (buf == null) { 39 | channel.close(); 40 | } else { 41 | ChannelBuffer wrapped = ChannelBuffers.wrappedBuffer(buf); 42 | channel.write(new JagGrabResponse(wrapped)).addListener(ChannelFutureListener.CLOSE); 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/OnDemandRequestWorker.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | 7 | import org.apollo.jagcached.fs.FileDescriptor; 8 | import org.apollo.jagcached.fs.IndexedFileSystem; 9 | import org.apollo.jagcached.net.ondemand.OnDemandRequest; 10 | import org.apollo.jagcached.net.ondemand.OnDemandResponse; 11 | import org.jboss.netty.buffer.ChannelBuffer; 12 | import org.jboss.netty.buffer.ChannelBuffers; 13 | import org.jboss.netty.channel.Channel; 14 | 15 | /** 16 | * A worker which services 'on-demand' requests. 17 | * @author Graham 18 | */ 19 | public final class OnDemandRequestWorker extends RequestWorker { 20 | 21 | /** 22 | * The maximum length of a chunk, in bytes. 23 | */ 24 | private static final int CHUNK_LENGTH = 500; 25 | 26 | /** 27 | * Creates the 'on-demand' request worker. 28 | * @param fs The file system. 29 | */ 30 | public OnDemandRequestWorker(IndexedFileSystem fs) { 31 | super(fs); 32 | } 33 | 34 | @Override 35 | protected ChannelRequest nextRequest() throws InterruptedException { 36 | return RequestDispatcher.nextOnDemandRequest(); 37 | } 38 | 39 | @Override 40 | protected void service(IndexedFileSystem fs, Channel channel, OnDemandRequest request) throws IOException { 41 | FileDescriptor desc = request.getFileDescriptor(); 42 | 43 | ByteBuffer buf = fs.getFile(desc); 44 | int length = buf.remaining(); 45 | 46 | for (int chunk = 0; buf.remaining() > 0; chunk++) { 47 | int chunkSize = buf.remaining(); 48 | if (chunkSize > CHUNK_LENGTH) { 49 | chunkSize = CHUNK_LENGTH; 50 | } 51 | 52 | byte[] tmp = new byte[chunkSize]; 53 | buf.get(tmp, 0, tmp.length); 54 | ChannelBuffer chunkData = ChannelBuffers.wrappedBuffer(tmp, 0, chunkSize); 55 | 56 | OnDemandResponse response = new OnDemandResponse(desc, length, chunk, chunkData); 57 | channel.write(response); 58 | } 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/RequestDispatcher.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import java.util.concurrent.BlockingQueue; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | import java.util.concurrent.PriorityBlockingQueue; 6 | 7 | 8 | import org.apollo.jagcached.net.jaggrab.JagGrabRequest; 9 | import org.apollo.jagcached.net.ondemand.OnDemandRequest; 10 | import org.jboss.netty.channel.Channel; 11 | import org.jboss.netty.handler.codec.http.HttpRequest; 12 | 13 | /** 14 | * A class which dispatches requests to worker threads. 15 | * @author Graham 16 | */ 17 | public final class RequestDispatcher { 18 | 19 | /** 20 | * The maximum size of a queue before requests are rejected. 21 | */ 22 | private static final int MAXIMUM_QUEUE_SIZE = 1024; 23 | 24 | /** 25 | * A queue for pending 'on-demand' requests. 26 | */ 27 | private static final BlockingQueue> onDemandQueue = new PriorityBlockingQueue>(); 28 | 29 | /** 30 | * A queue for pending JAGGRAB requests. 31 | */ 32 | private static final BlockingQueue> jagGrabQueue = new LinkedBlockingQueue>(); 33 | 34 | /** 35 | * A queue for pending HTTP requests. 36 | */ 37 | private static final BlockingQueue> httpQueue = new LinkedBlockingQueue>(); 38 | 39 | /** 40 | * Gets the next 'on-demand' request from the queue, blocking if none are 41 | * available. 42 | * @return The 'on-demand' request. 43 | * @throws InterruptedException if the thread is interrupted. 44 | */ 45 | static ChannelRequest nextOnDemandRequest() throws InterruptedException { 46 | return onDemandQueue.take(); 47 | } 48 | 49 | /** 50 | * Gets the next JAGGRAB request from the queue, blocking if none are 51 | * available. 52 | * @return The JAGGRAB request. 53 | * @throws InterruptedException if the thread is interrupted. 54 | */ 55 | static ChannelRequest nextJagGrabRequest() throws InterruptedException { 56 | return jagGrabQueue.take(); 57 | } 58 | 59 | /** 60 | * Gets the next HTTP request from the queue, blocking if none are 61 | * available. 62 | * @return The HTTP request. 63 | * @throws InterruptedException if the thread is interrupted. 64 | */ 65 | static ChannelRequest nextHttpRequest() throws InterruptedException { 66 | return httpQueue.take(); 67 | } 68 | 69 | /** 70 | * Dispatches an 'on-demand' request. 71 | * @param channel The channel. 72 | * @param request The request. 73 | */ 74 | public static void dispatch(Channel channel, OnDemandRequest request) { 75 | if (onDemandQueue.size() >= MAXIMUM_QUEUE_SIZE) { 76 | channel.close(); 77 | } 78 | onDemandQueue.add(new ChannelRequest(channel, request)); 79 | } 80 | 81 | /** 82 | * Dispatches a JAGGRAB request. 83 | * @param channel The channel. 84 | * @param request The request. 85 | */ 86 | public static void dispatch(Channel channel, JagGrabRequest request) { 87 | if (jagGrabQueue.size() >= MAXIMUM_QUEUE_SIZE) { 88 | channel.close(); 89 | } 90 | jagGrabQueue.add(new ChannelRequest(channel, request)); 91 | } 92 | 93 | /** 94 | * Dispatches a HTTP request. 95 | * @param channel The channel. 96 | * @param request The request. 97 | */ 98 | public static void dispatch(Channel channel, HttpRequest request) { 99 | if (httpQueue.size() >= MAXIMUM_QUEUE_SIZE) { 100 | channel.close(); 101 | } 102 | httpQueue.add(new ChannelRequest(channel, request)); 103 | } 104 | 105 | /** 106 | * Default private constructor to prevent instantiation. 107 | */ 108 | private RequestDispatcher() { 109 | 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/RequestWorker.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import java.io.IOException; 4 | 5 | import org.jboss.netty.channel.Channel; 6 | 7 | /** 8 | * The base class for request workers. 9 | * @author Graham 10 | * @param The type of request. 11 | * @param

The type of provider. 12 | */ 13 | public abstract class RequestWorker implements Runnable { 14 | 15 | /** 16 | * The resource provider. 17 | */ 18 | private final P provider; 19 | 20 | /** 21 | * An object used for locking checks to see if the worker is running. 22 | */ 23 | private final Object lock = new Object(); 24 | 25 | /** 26 | * A flag indicating if the worker should be running. 27 | */ 28 | private boolean running = true; 29 | 30 | /** 31 | * Creates the request worker with the specified file system. 32 | * @param provider The resource provider. 33 | */ 34 | public RequestWorker(P provider) { 35 | this.provider = provider; 36 | } 37 | 38 | /** 39 | * Stops this worker. The worker's thread may need to be interrupted. 40 | */ 41 | public final void stop() { 42 | synchronized (lock) { 43 | running = false; 44 | } 45 | } 46 | 47 | @Override 48 | public final void run() { 49 | while (true) { 50 | synchronized (lock) { 51 | if (!running) { 52 | break; 53 | } 54 | } 55 | 56 | ChannelRequest request; 57 | try { 58 | request = nextRequest(); 59 | } catch (InterruptedException e) { 60 | continue; 61 | } 62 | 63 | Channel channel = request.getChannel(); 64 | 65 | try { 66 | service(provider, channel, request.getRequest()); 67 | } catch (IOException e) { 68 | e.printStackTrace(); 69 | channel.close(); 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * Gets the next request. 76 | * @return The next request. 77 | * @throws InterruptedException if the thread is interrupted. 78 | */ 79 | protected abstract ChannelRequest nextRequest() throws InterruptedException; 80 | 81 | /** 82 | * Services a request. 83 | * @param provider The resource provider. 84 | * @param channel The channel. 85 | * @param request The request to service. 86 | * @throws IOException if an I/O error occurs. 87 | */ 88 | protected abstract void service(P provider, Channel channel, T request) throws IOException; 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/dispatch/RequestWorkerPool.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.dispatch; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | 9 | import org.apollo.jagcached.fs.IndexedFileSystem; 10 | 11 | 12 | /** 13 | * A class which manages the pool of request workers. 14 | * @author Graham 15 | */ 16 | public final class RequestWorkerPool { 17 | 18 | /** 19 | * The number of threads per request type. 20 | */ 21 | private static final int THREADS_PER_REQUEST_TYPE = Runtime.getRuntime().availableProcessors(); 22 | 23 | /** 24 | * The number of request types. 25 | */ 26 | private static final int REQUEST_TYPES = 3; 27 | 28 | /** 29 | * The executor service. 30 | */ 31 | private final ExecutorService service; 32 | 33 | /** 34 | * A list of request workers. 35 | */ 36 | private final List> workers = new ArrayList>(); 37 | 38 | /** 39 | * The request worker pool. 40 | */ 41 | public RequestWorkerPool() { 42 | int totalThreads = REQUEST_TYPES * THREADS_PER_REQUEST_TYPE; 43 | service = Executors.newFixedThreadPool(totalThreads); 44 | } 45 | 46 | /** 47 | * Starts the threads in the pool. 48 | * @throws Exception if the file system cannot be created. 49 | */ 50 | public void start() throws Exception { 51 | File base = new File("./data/fs/"); 52 | for (int i = 0; i < THREADS_PER_REQUEST_TYPE; i++) { 53 | workers.add(new JagGrabRequestWorker(new IndexedFileSystem(base, true))); 54 | workers.add(new OnDemandRequestWorker(new IndexedFileSystem(base, true))); 55 | workers.add(new HttpRequestWorker(new IndexedFileSystem(base, true))); 56 | } 57 | 58 | for (RequestWorker worker : workers) { 59 | service.submit(worker); 60 | } 61 | } 62 | 63 | /** 64 | * Stops the threads in the pool. 65 | */ 66 | public void stop() { 67 | for (RequestWorker worker : workers) { 68 | worker.stop(); 69 | } 70 | 71 | service.shutdownNow(); 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/fs/FileDescriptor.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.fs; 2 | 3 | /** 4 | * A class which points to a file in the cache. 5 | * @author Graham 6 | */ 7 | public final class FileDescriptor { 8 | 9 | /** 10 | * The file type. 11 | */ 12 | private final int type; 13 | 14 | /** 15 | * The file id. 16 | */ 17 | private final int file; 18 | 19 | /** 20 | * Creates the file descriptor. 21 | * @param type The file type. 22 | * @param file The file id. 23 | */ 24 | public FileDescriptor(int type, int file) { 25 | this.type = type; 26 | this.file = file; 27 | } 28 | 29 | /** 30 | * Gets the file type. 31 | * @return The file type. 32 | */ 33 | public int getType() { 34 | return type; 35 | } 36 | 37 | /** 38 | * Gets the file id. 39 | * @return The file id. 40 | */ 41 | public int getFile() { 42 | return file; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/fs/FileSystemConstants.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.fs; 2 | 3 | /** 4 | * Holds file system related constants. 5 | * @author Graham 6 | */ 7 | public final class FileSystemConstants { 8 | 9 | /** 10 | * The number of caches. 11 | */ 12 | public static final int CACHE_COUNT = 5; 13 | 14 | /** 15 | * The number of archives in cache 0. 16 | */ 17 | public static final int ARCHIVE_COUNT = 9; 18 | 19 | /** 20 | * The size of an index. 21 | */ 22 | public static final int INDEX_SIZE = 6; 23 | 24 | /** 25 | * The size of a header. 26 | */ 27 | public static final int HEADER_SIZE = 8; 28 | 29 | /** 30 | * The size of a chunk. 31 | */ 32 | public static final int CHUNK_SIZE = 512; 33 | 34 | /** 35 | * The size of a block. 36 | */ 37 | public static final int BLOCK_SIZE = HEADER_SIZE + CHUNK_SIZE; 38 | 39 | /** 40 | * Default private constructor to prevent instantiation. 41 | */ 42 | private FileSystemConstants() { 43 | 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/fs/Index.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.fs; 2 | 3 | /** 4 | * An {@link Index} points to a file in the {@code main_file_cache.dat} file. 5 | * @author Graham 6 | */ 7 | public final class Index { 8 | 9 | /** 10 | * Decodes a buffer into an index. 11 | * @param buffer The buffer. 12 | * @return The decoded {@link Index}. 13 | * @throws IllegalArgumentException if the buffer length is invalid. 14 | */ 15 | public static Index decode(byte[] buffer) { 16 | if (buffer.length != FileSystemConstants.INDEX_SIZE) { 17 | throw new IllegalArgumentException("Incorrect buffer length."); 18 | } 19 | 20 | int size = ((buffer[0] & 0xFF) << 16) | ((buffer[1] & 0xFF) << 8) | (buffer[2] & 0xFF); 21 | int block = ((buffer[3] & 0xFF) << 16) | ((buffer[4] & 0xFF) << 8) | (buffer[5] & 0xFF); 22 | 23 | return new Index(size, block); 24 | } 25 | 26 | /** 27 | * The size of the file. 28 | */ 29 | private final int size; 30 | 31 | /** 32 | * The first block of the file. 33 | */ 34 | private final int block; 35 | 36 | /** 37 | * Creates the index. 38 | * @param size The size of the file. 39 | * @param block The first block of the file. 40 | */ 41 | public Index(int size, int block) { 42 | this.size = size; 43 | this.block = block; 44 | } 45 | 46 | /** 47 | * Gets the size of the file. 48 | * @return The size of the file. 49 | */ 50 | public int getSize() { 51 | return size; 52 | } 53 | 54 | /** 55 | * Gets the first block of the file. 56 | * @return The first block of the file. 57 | */ 58 | public int getBlock() { 59 | return block; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/fs/IndexedFileSystem.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.fs; 2 | 3 | import java.io.Closeable; 4 | import java.io.File; 5 | import java.io.FileNotFoundException; 6 | import java.io.IOException; 7 | import java.io.RandomAccessFile; 8 | import java.nio.ByteBuffer; 9 | import java.util.zip.CRC32; 10 | 11 | /** 12 | * A file system based on top of the operating system's file system. It 13 | * consists of a data file and index files. Index files point to blocks in the 14 | * data file, which contains the actual data. 15 | * @author Graham 16 | */ 17 | public final class IndexedFileSystem implements Closeable { 18 | 19 | /** 20 | * Read only flag. 21 | */ 22 | private final boolean readOnly; 23 | 24 | /** 25 | * The index files. 26 | */ 27 | private RandomAccessFile[] indices = new RandomAccessFile[256]; 28 | 29 | /** 30 | * The data file. 31 | */ 32 | private RandomAccessFile data; 33 | 34 | /** 35 | * The cached CRC table. 36 | */ 37 | private ByteBuffer crcTable; 38 | 39 | /** 40 | * Creates the file system with the specified base directory. 41 | * @param base The base directory. 42 | * @param readOnly A flag indicating if the file system will be read only. 43 | * @throws Exception if the file system is invalid. 44 | */ 45 | public IndexedFileSystem(File base, boolean readOnly) throws Exception { 46 | this.readOnly = readOnly; 47 | detectLayout(base); 48 | } 49 | 50 | /** 51 | * Checks if this {@link IndexedFileSystem} is read only. 52 | * @return {@code true} if so, {@code false} if not. 53 | */ 54 | public boolean isReadOnly() { 55 | return readOnly; 56 | } 57 | 58 | /** 59 | * Automatically detect the layout of the specified directory. 60 | * @param base The base directory. 61 | * @throws Exception if the file system is invalid. 62 | */ 63 | private void detectLayout(File base) throws Exception { 64 | int indexCount = 0; 65 | for (int index = 0; index < indices.length; index++) { 66 | File f = new File(base.getAbsolutePath() + "/main_file_cache.idx" + index); 67 | if (f.exists() && !f.isDirectory()) { 68 | indexCount++; 69 | indices[index] = new RandomAccessFile(f, readOnly ? "r" : "rw"); 70 | } 71 | } 72 | if (indexCount <= 0) { 73 | throw new Exception("No index file(s) present"); 74 | } 75 | 76 | File oldEngineData = new File(base.getAbsolutePath() + "/main_file_cache.dat"); 77 | File newEngineData = new File(base.getAbsolutePath() + "/main_file_cache.dat2"); 78 | if (oldEngineData.exists() && !oldEngineData.isDirectory()) { 79 | data = new RandomAccessFile(oldEngineData, readOnly ? "r" : "rw"); 80 | } else if (newEngineData.exists() && !oldEngineData.isDirectory()) { 81 | data = new RandomAccessFile(newEngineData, readOnly ? "r" : "rw"); 82 | } else { 83 | throw new Exception("No data file present"); 84 | } 85 | } 86 | 87 | /** 88 | * Gets the index of a file. 89 | * @param fd The {@link FileDescriptor} which points to the file. 90 | * @return The {@link Index}. 91 | * @throws IOException if an I/O error occurs. 92 | */ 93 | private Index getIndex(FileDescriptor fd) throws IOException { 94 | int index = fd.getType(); 95 | if (index < 0 || index >= indices.length) { 96 | throw new IndexOutOfBoundsException(); 97 | } 98 | 99 | byte[] buffer = new byte[FileSystemConstants.INDEX_SIZE]; 100 | RandomAccessFile indexFile = indices[index]; 101 | synchronized (indexFile) { 102 | long ptr = (long) fd.getFile() * (long) FileSystemConstants.INDEX_SIZE; 103 | if (ptr >= 0 && indexFile.length() >= (ptr + FileSystemConstants.INDEX_SIZE)) { 104 | indexFile.seek(ptr); 105 | indexFile.readFully(buffer); 106 | } else { 107 | throw new FileNotFoundException(); 108 | } 109 | } 110 | 111 | return Index.decode(buffer); 112 | } 113 | 114 | /** 115 | * Gets the number of files with the specified type. 116 | * @param type The type. 117 | * @return The number of files. 118 | * @throws IOException if an I/O error occurs. 119 | */ 120 | private int getFileCount(int type) throws IOException { 121 | if (type < 0 || type >= indices.length) { 122 | throw new IndexOutOfBoundsException(); 123 | } 124 | 125 | RandomAccessFile indexFile = indices[type]; 126 | synchronized (indexFile) { 127 | return (int) (indexFile.length() / FileSystemConstants.INDEX_SIZE); 128 | } 129 | } 130 | 131 | /** 132 | * Gets the CRC table. 133 | * @return The CRC table. 134 | * @throws IOException if an I/O erorr occurs. 135 | */ 136 | public ByteBuffer getCrcTable() throws IOException { 137 | if (readOnly) { 138 | synchronized (this) { 139 | if (crcTable != null) { 140 | return crcTable.slice(); 141 | } 142 | } 143 | 144 | // the number of archives 145 | int archives = getFileCount(0); 146 | 147 | // the hash 148 | int hash = 1234; 149 | 150 | // the CRCs 151 | int[] crcs = new int[archives]; 152 | 153 | // calculate the CRCs 154 | CRC32 crc32 = new CRC32(); 155 | for (int i = 1; i < crcs.length; i++) { 156 | crc32.reset(); 157 | 158 | ByteBuffer bb = getFile(0, i); 159 | byte[] bytes = new byte[bb.remaining()]; 160 | bb.get(bytes, 0, bytes.length); 161 | crc32.update(bytes, 0, bytes.length); 162 | 163 | crcs[i] = (int) crc32.getValue(); 164 | } 165 | 166 | // hash the CRCs and place them in the buffer 167 | ByteBuffer buf = ByteBuffer.allocate(crcs.length * 4 + 4); 168 | for (int i = 0; i < crcs.length; i++) { 169 | hash = (hash << 1) + crcs[i]; 170 | buf.putInt(crcs[i]); 171 | } 172 | 173 | // place the hash into the buffer 174 | buf.putInt(hash); 175 | buf.flip(); 176 | 177 | synchronized (this) { 178 | crcTable = buf; 179 | return crcTable.slice(); 180 | } 181 | } else { 182 | throw new IOException("cannot get CRC table from a writable file system"); 183 | } 184 | } 185 | 186 | /** 187 | * Gets a file. 188 | * @param type The file type. 189 | * @param file The file id. 190 | * @return A {@link ByteBuffer} which contains the contents of the file. 191 | * @throws IOException if an I/O error occurs. 192 | */ 193 | public ByteBuffer getFile(int type, int file) throws IOException { 194 | return getFile(new FileDescriptor(type, file)); 195 | } 196 | 197 | /** 198 | * Gets a file. 199 | * @param fd The {@link FileDescriptor} which points to the file. 200 | * @return A {@link ByteBuffer} which contains the contents of the file. 201 | * @throws IOException if an I/O error occurs. 202 | */ 203 | public ByteBuffer getFile(FileDescriptor fd) throws IOException { 204 | Index index = getIndex(fd); 205 | ByteBuffer buffer = ByteBuffer.allocate(index.getSize()); 206 | 207 | // calculate some initial values 208 | long ptr = (long) index.getBlock() * (long) FileSystemConstants.BLOCK_SIZE; 209 | int read = 0; 210 | int size = index.getSize(); 211 | int blocks = size / FileSystemConstants.CHUNK_SIZE; 212 | if (size % FileSystemConstants.CHUNK_SIZE != 0) { 213 | blocks++; 214 | } 215 | 216 | for (int i = 0; i < blocks; i++) { 217 | 218 | // read header 219 | byte[] header = new byte[FileSystemConstants.HEADER_SIZE]; 220 | synchronized (data) { 221 | data.seek(ptr); 222 | data.readFully(header); 223 | } 224 | 225 | // increment pointers 226 | ptr += FileSystemConstants.HEADER_SIZE; 227 | 228 | // parse header 229 | int nextFile = ((header[0] & 0xFF) << 8) | (header[1] & 0xFF); 230 | int curChunk = ((header[2] & 0xFF) << 8) | (header[3] & 0xFF); 231 | int nextBlock = ((header[4] & 0xFF) << 16) | ((header[5] & 0xFF) << 8) | (header[6] & 0xFF); 232 | int nextType = header[7] & 0xFF; 233 | 234 | // check expected chunk id is correct 235 | if (i != curChunk) { 236 | throw new IOException("Chunk id mismatch."); 237 | } 238 | 239 | // calculate how much we can read 240 | int chunkSize = size - read; 241 | if (chunkSize > FileSystemConstants.CHUNK_SIZE) { 242 | chunkSize = FileSystemConstants.CHUNK_SIZE; 243 | } 244 | 245 | // read the next chunk and put it in the buffer 246 | byte[] chunk = new byte[chunkSize]; 247 | synchronized (data) { 248 | data.seek(ptr); 249 | data.readFully(chunk); 250 | } 251 | buffer.put(chunk); 252 | 253 | // increment pointers 254 | read += chunkSize; 255 | ptr = (long) nextBlock * (long) FileSystemConstants.BLOCK_SIZE; 256 | 257 | // if we still have more data to read, check the validity of the 258 | // header 259 | if (size > read) { 260 | if (nextType != (fd.getType() + 1)) { 261 | throw new IOException("File type mismatch."); 262 | } 263 | 264 | if (nextFile != fd.getFile()) { 265 | throw new IOException("File id mismatch."); 266 | } 267 | } 268 | } 269 | 270 | buffer.flip(); 271 | return buffer; 272 | } 273 | 274 | @Override 275 | public void close() throws IOException { 276 | if (data != null) { 277 | synchronized (data) { 278 | data.close(); 279 | } 280 | } 281 | 282 | for (RandomAccessFile index : indices) { 283 | if (index != null) { 284 | synchronized (index) { 285 | index.close(); 286 | } 287 | } 288 | } 289 | } 290 | 291 | } 292 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/FileServerHandler.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.Logger; 5 | 6 | 7 | import org.apollo.jagcached.FileServer; 8 | import org.apollo.jagcached.dispatch.RequestDispatcher; 9 | import org.apollo.jagcached.net.jaggrab.JagGrabRequest; 10 | import org.apollo.jagcached.net.ondemand.OnDemandRequest; 11 | import org.apollo.jagcached.net.service.ServiceRequest; 12 | import org.apollo.jagcached.net.service.ServiceResponse; 13 | import org.jboss.netty.channel.ChannelHandlerContext; 14 | import org.jboss.netty.channel.ExceptionEvent; 15 | import org.jboss.netty.channel.MessageEvent; 16 | import org.jboss.netty.handler.codec.http.HttpRequest; 17 | import org.jboss.netty.handler.timeout.IdleStateAwareChannelUpstreamHandler; 18 | import org.jboss.netty.handler.timeout.IdleStateEvent; 19 | 20 | /** 21 | * An {@link IdleStateAwareChannelUpstreamHandler} for the {@link FileServer}. 22 | * @author Graham 23 | */ 24 | public final class FileServerHandler extends IdleStateAwareChannelUpstreamHandler { 25 | 26 | /** 27 | * The logger for this class. 28 | */ 29 | private static final Logger logger = Logger.getLogger(FileServerHandler.class.getName()); 30 | 31 | @Override 32 | public void channelIdle(ChannelHandlerContext ctx, IdleStateEvent e) throws Exception { 33 | e.getChannel().close(); 34 | } 35 | 36 | @Override 37 | public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception { 38 | Object msg = e.getMessage(); 39 | if (msg instanceof ServiceRequest) { 40 | ServiceRequest request = (ServiceRequest) msg; 41 | if (request.getId() != ServiceRequest.SERVICE_ONDEMAND) { 42 | e.getChannel().close(); 43 | } else { 44 | e.getChannel().write(new ServiceResponse()); 45 | } 46 | } else if (msg instanceof OnDemandRequest) { 47 | RequestDispatcher.dispatch(e.getChannel(), (OnDemandRequest) msg); 48 | } else if (msg instanceof JagGrabRequest) { 49 | RequestDispatcher.dispatch(e.getChannel(), (JagGrabRequest) msg); 50 | } else if (msg instanceof HttpRequest) { 51 | RequestDispatcher.dispatch(e.getChannel(), (HttpRequest) msg); 52 | } else { 53 | throw new Exception("unknown message type"); 54 | } 55 | } 56 | 57 | @Override 58 | public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { 59 | logger.log(Level.SEVERE, "Exception occured, closing channel...", e.getCause()); 60 | e.getChannel().close(); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/HttpPipelineFactory.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net; 2 | 3 | import org.jboss.netty.channel.ChannelPipeline; 4 | import org.jboss.netty.channel.ChannelPipelineFactory; 5 | import org.jboss.netty.channel.Channels; 6 | import org.jboss.netty.handler.codec.http.HttpChunkAggregator; 7 | import org.jboss.netty.handler.codec.http.HttpRequestDecoder; 8 | import org.jboss.netty.handler.codec.http.HttpResponseEncoder; 9 | import org.jboss.netty.handler.timeout.IdleStateHandler; 10 | import org.jboss.netty.util.Timer; 11 | 12 | /** 13 | * A {@link ChannelPipelineFactory} for the HTTP protocol. 14 | * @author Graham 15 | */ 16 | public final class HttpPipelineFactory implements ChannelPipelineFactory { 17 | 18 | /** 19 | * The maximum length of a request, in bytes. 20 | */ 21 | private static final int MAX_REQUEST_LENGTH = 8192; 22 | 23 | /** 24 | * The file server event handler. 25 | */ 26 | private final FileServerHandler handler; 27 | 28 | /** 29 | * The timer used for idle checking. 30 | */ 31 | private final Timer timer; 32 | 33 | /** 34 | * Creates the HTTP pipeline factory. 35 | * @param handler The file server event handler. 36 | * @param timer The timer used for idle checking. 37 | */ 38 | public HttpPipelineFactory(FileServerHandler handler, Timer timer) { 39 | this.handler = handler; 40 | this.timer = timer; 41 | } 42 | 43 | @Override 44 | public ChannelPipeline getPipeline() throws Exception { 45 | ChannelPipeline pipeline = Channels.pipeline(); 46 | 47 | // decoders 48 | pipeline.addLast("decoder", new HttpRequestDecoder()); 49 | pipeline.addLast("chunker", new HttpChunkAggregator(MAX_REQUEST_LENGTH)); 50 | 51 | // encoders 52 | pipeline.addLast("encoder", new HttpResponseEncoder()); 53 | 54 | // handler 55 | pipeline.addLast("timeout", new IdleStateHandler(timer, NetworkConstants.IDLE_TIME, 0, 0)); 56 | pipeline.addLast("handler", handler); 57 | 58 | return pipeline; 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/JagGrabPipelineFactory.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net; 2 | 3 | import java.nio.charset.Charset; 4 | 5 | 6 | import org.apollo.jagcached.net.jaggrab.JagGrabRequestDecoder; 7 | import org.apollo.jagcached.net.jaggrab.JagGrabResponseEncoder; 8 | import org.jboss.netty.buffer.ChannelBuffer; 9 | import org.jboss.netty.buffer.ChannelBuffers; 10 | import org.jboss.netty.channel.ChannelPipeline; 11 | import org.jboss.netty.channel.ChannelPipelineFactory; 12 | import org.jboss.netty.channel.Channels; 13 | import org.jboss.netty.handler.codec.frame.DelimiterBasedFrameDecoder; 14 | import org.jboss.netty.handler.codec.string.StringDecoder; 15 | import org.jboss.netty.handler.timeout.IdleStateHandler; 16 | import org.jboss.netty.util.Timer; 17 | 18 | /** 19 | * A {@link ChannelPipelineFactory} for the JAGGRAB protocol. 20 | * @author Graham 21 | */ 22 | public final class JagGrabPipelineFactory implements ChannelPipelineFactory { 23 | 24 | /** 25 | * The maximum length of a request, in bytes. 26 | */ 27 | private static final int MAX_REQUEST_LENGTH = 8192; 28 | 29 | /** 30 | * The character set used in the request. 31 | */ 32 | private static final Charset JAGGRAB_CHARSET = Charset.forName("US-ASCII"); 33 | 34 | /** 35 | * A buffer with two line feed (LF) characters in it. 36 | */ 37 | private static final ChannelBuffer DOUBLE_LINE_FEED_DELIMITER = ChannelBuffers.buffer(2); 38 | 39 | /** 40 | * Populates the double line feed buffer. 41 | */ 42 | static { 43 | DOUBLE_LINE_FEED_DELIMITER.writeByte(10); 44 | DOUBLE_LINE_FEED_DELIMITER.writeByte(10); 45 | } 46 | 47 | /** 48 | * The file server event handler. 49 | */ 50 | private final FileServerHandler handler; 51 | 52 | /** 53 | * The timer used for idle checking. 54 | */ 55 | private final Timer timer; 56 | 57 | /** 58 | * Creates a {@code JAGGRAB} pipeline factory. 59 | * @param handler The file server event handler. 60 | * @param timer The timer used for idle checking. 61 | */ 62 | public JagGrabPipelineFactory(FileServerHandler handler, Timer timer) { 63 | this.handler = handler; 64 | this.timer = timer; 65 | } 66 | 67 | @Override 68 | public ChannelPipeline getPipeline() throws Exception { 69 | ChannelPipeline pipeline = Channels.pipeline(); 70 | 71 | // decoders 72 | pipeline.addLast("framer", new DelimiterBasedFrameDecoder(MAX_REQUEST_LENGTH, DOUBLE_LINE_FEED_DELIMITER)); 73 | pipeline.addLast("string-decoder", new StringDecoder(JAGGRAB_CHARSET)); 74 | pipeline.addLast("jaggrab-decoder", new JagGrabRequestDecoder()); 75 | 76 | // encoders 77 | pipeline.addLast("jaggrab-encoder", new JagGrabResponseEncoder()); 78 | 79 | // handler 80 | pipeline.addLast("timeout", new IdleStateHandler(timer, NetworkConstants.IDLE_TIME, 0, 0)); 81 | pipeline.addLast("handler", handler); 82 | 83 | return pipeline; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/NetworkConstants.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net; 2 | 3 | /** 4 | * A class which holds network-related constants. 5 | * @author Graham 6 | */ 7 | public final class NetworkConstants { 8 | 9 | /** 10 | * The HTTP port. 11 | */ 12 | public static final int HTTP_PORT = 80; 13 | 14 | /** 15 | * The JAGGRAB port. 16 | */ 17 | public static final int JAGGRAB_PORT = 43595; 18 | 19 | /** 20 | * The service port (which is also used for the 'on-demand' protocol). 21 | */ 22 | public static final int SERVICE_PORT = 43594; 23 | 24 | /** 25 | * The number of seconds a channel can be idle before being closed 26 | * automatically. 27 | */ 28 | public static final int IDLE_TIME = 15; 29 | 30 | /** 31 | * Default private constructor to prevent instantiaton. 32 | */ 33 | private NetworkConstants() { 34 | 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/OnDemandPipelineFactory.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net; 2 | 3 | 4 | import org.apollo.jagcached.net.ondemand.OnDemandRequestDecoder; 5 | import org.apollo.jagcached.net.ondemand.OnDemandResponseEncoder; 6 | import org.apollo.jagcached.net.service.ServiceRequestDecoder; 7 | import org.apollo.jagcached.net.service.ServiceResponseEncoder; 8 | import org.jboss.netty.channel.ChannelPipeline; 9 | import org.jboss.netty.channel.ChannelPipelineFactory; 10 | import org.jboss.netty.channel.Channels; 11 | import org.jboss.netty.handler.timeout.IdleStateHandler; 12 | import org.jboss.netty.util.Timer; 13 | 14 | /** 15 | * A {@link ChannelPipelineFactory} for the 'on-demand' protocol. 16 | * @author Graham 17 | */ 18 | public final class OnDemandPipelineFactory implements ChannelPipelineFactory { 19 | 20 | /** 21 | * The file server event handler. 22 | */ 23 | private final FileServerHandler handler; 24 | 25 | /** 26 | * The timer used for idle checking. 27 | */ 28 | private final Timer timer; 29 | 30 | /** 31 | * Creates an 'on-demand' pipeline factory. 32 | * @param handler The file server event handler. 33 | * @param timer The timer used for idle checking. 34 | */ 35 | public OnDemandPipelineFactory(FileServerHandler handler, Timer timer) { 36 | this.handler = handler; 37 | this.timer = timer; 38 | } 39 | 40 | @Override 41 | public ChannelPipeline getPipeline() throws Exception { 42 | ChannelPipeline pipeline = Channels.pipeline(); 43 | 44 | // decoders 45 | pipeline.addLast("serviceDecoder", new ServiceRequestDecoder()); 46 | pipeline.addLast("decoder", new OnDemandRequestDecoder()); 47 | 48 | // encoders 49 | pipeline.addLast("serviceEncoder", new ServiceResponseEncoder()); 50 | pipeline.addLast("encoder", new OnDemandResponseEncoder()); 51 | 52 | // handler 53 | pipeline.addLast("timeout", new IdleStateHandler(timer, NetworkConstants.IDLE_TIME, 0, 0)); 54 | pipeline.addLast("handler", handler); 55 | 56 | return pipeline; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/jaggrab/JagGrabRequest.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.jaggrab; 2 | 3 | /** 4 | * Represents the request for a single file using the JAGGRAB protocol. 5 | * @author Graham 6 | */ 7 | public final class JagGrabRequest { 8 | 9 | /** 10 | * The path to the file. 11 | */ 12 | private final String filePath; 13 | 14 | /** 15 | * Creates the request. 16 | * @param filePath The file path. 17 | */ 18 | public JagGrabRequest(String filePath) { 19 | this.filePath = filePath; 20 | } 21 | 22 | /** 23 | * Gets the file path. 24 | * @return The file path. 25 | */ 26 | public String getFilePath() { 27 | return filePath; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/jaggrab/JagGrabRequestDecoder.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.jaggrab; 2 | 3 | import org.jboss.netty.channel.Channel; 4 | import org.jboss.netty.channel.ChannelHandlerContext; 5 | import org.jboss.netty.handler.codec.oneone.OneToOneDecoder; 6 | 7 | /** 8 | * A {@link OneToOneDecoder} for the JAGGRAB protocol. 9 | * @author Graham 10 | */ 11 | public final class JagGrabRequestDecoder extends OneToOneDecoder { 12 | 13 | @Override 14 | protected Object decode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception { 15 | if (msg instanceof String) { 16 | String str = ((String) msg); 17 | if (str.startsWith("JAGGRAB /")) { 18 | String filePath = str.substring(8).trim(); 19 | return new JagGrabRequest(filePath); 20 | } else { 21 | throw new Exception("corrupted request line"); 22 | } 23 | } 24 | return msg; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/jaggrab/JagGrabResponse.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.jaggrab; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | 5 | /** 6 | * Represents a single JAGGRAB reponse. 7 | * @author Graham 8 | */ 9 | public final class JagGrabResponse { 10 | 11 | /** 12 | * The file data. 13 | */ 14 | private final ChannelBuffer fileData; 15 | 16 | /** 17 | * Creates the response. 18 | * @param fileData The file data. 19 | */ 20 | public JagGrabResponse(ChannelBuffer fileData) { 21 | this.fileData = fileData; 22 | } 23 | 24 | /** 25 | * Gets the file data. 26 | * @return The file data. 27 | */ 28 | public ChannelBuffer getFileData() { 29 | return fileData; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/jaggrab/JagGrabResponseEncoder.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.jaggrab; 2 | 3 | import org.jboss.netty.channel.Channel; 4 | import org.jboss.netty.channel.ChannelHandlerContext; 5 | import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; 6 | 7 | /** 8 | * A {@link OneToOneEncoder} for the JAGGRAB protocol. 9 | * @author Graham 10 | */ 11 | public final class JagGrabResponseEncoder extends OneToOneEncoder { 12 | 13 | @Override 14 | protected Object encode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception { 15 | if (msg instanceof JagGrabResponse) { 16 | JagGrabResponse resp = (JagGrabResponse) msg; 17 | return resp.getFileData(); 18 | } 19 | return msg; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/ondemand/OnDemandRequest.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.ondemand; 2 | 3 | import org.apollo.jagcached.fs.FileDescriptor; 4 | 5 | /** 6 | * Represents a single 'on-demand' request. 7 | * @author Graham 8 | */ 9 | public final class OnDemandRequest implements Comparable { 10 | 11 | /** 12 | * An enumeration containing the different request priorities. 13 | * @author Graham 14 | */ 15 | public enum Priority { 16 | 17 | /** 18 | * High priority - used in-game when data is required immediately but 19 | * has not yet been received. 20 | */ 21 | HIGH(1), 22 | 23 | /** 24 | * Medium priority - used while loading the 'bare minimum' required to 25 | * run the game. 26 | */ 27 | MEDIUM(2), 28 | 29 | /** 30 | * Low priority - used when a file is not required urgently. The client 31 | * login screen says "loading extra files.." when low priority loading 32 | * is being performed. 33 | */ 34 | LOW(3); 35 | 36 | /** 37 | * Converts the integer value to a priority. 38 | * @param v The integer value. 39 | * @return The priority. 40 | * @throws IllegalArgumentException if the value is outside of the 41 | * range 1-3 inclusive. 42 | */ 43 | public static Priority valueOf(int v) { 44 | switch (v) { 45 | case 1: 46 | return HIGH; 47 | case 2: 48 | return MEDIUM; 49 | case 3: 50 | return LOW; 51 | default: 52 | throw new IllegalArgumentException("priority out of range"); 53 | } 54 | } 55 | 56 | /** 57 | * The integer value. 58 | */ 59 | private final int intValue; 60 | 61 | /** 62 | * Creates a priority. 63 | * @param intValue The integer value. 64 | */ 65 | private Priority(int intValue) { 66 | this.intValue = intValue; 67 | } 68 | 69 | /** 70 | * Converts the priority to an integer. 71 | * @return The integer value. 72 | */ 73 | public int toInteger() { 74 | return intValue; 75 | } 76 | 77 | } 78 | 79 | /** 80 | * The file descriptor. 81 | */ 82 | private final FileDescriptor fileDescriptor; 83 | 84 | /** 85 | * The request priority. 86 | */ 87 | private final Priority priority; 88 | 89 | /** 90 | * Creates the 'on-demand' request. 91 | * @param fileDescriptor The file descriptor. 92 | * @param priority The priority. 93 | */ 94 | public OnDemandRequest(FileDescriptor fileDescriptor, Priority priority) { 95 | this.fileDescriptor = fileDescriptor; 96 | this.priority = priority; 97 | } 98 | 99 | /** 100 | * Gets the file descriptor. 101 | * @return The file descriptor. 102 | */ 103 | public FileDescriptor getFileDescriptor() { 104 | return fileDescriptor; 105 | } 106 | 107 | /** 108 | * Gets the priority. 109 | * @return The priority. 110 | */ 111 | public Priority getPriority() { 112 | return priority; 113 | } 114 | 115 | @Override 116 | public int compareTo(OnDemandRequest o) { 117 | int thisPriority = priority.toInteger(); 118 | int otherPriority = o.priority.toInteger(); 119 | 120 | if (thisPriority < otherPriority) { 121 | return 1; 122 | } else if (thisPriority == otherPriority) { 123 | return 0; 124 | } else { 125 | return -1; 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/ondemand/OnDemandRequestDecoder.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.ondemand; 2 | 3 | 4 | import org.apollo.jagcached.fs.FileDescriptor; 5 | import org.apollo.jagcached.net.ondemand.OnDemandRequest.Priority; 6 | import org.jboss.netty.buffer.ChannelBuffer; 7 | import org.jboss.netty.channel.Channel; 8 | import org.jboss.netty.channel.ChannelHandlerContext; 9 | import org.jboss.netty.handler.codec.frame.FrameDecoder; 10 | 11 | /** 12 | * A {@link FrameDecoder} for the 'on-demand' protocol. 13 | * @author Graham 14 | */ 15 | public final class OnDemandRequestDecoder extends FrameDecoder { 16 | 17 | @Override 18 | protected Object decode(ChannelHandlerContext ctx, Channel c, ChannelBuffer buf) throws Exception { 19 | if (buf.readableBytes() >= 4) { 20 | int type = buf.readUnsignedByte() + 1; 21 | int file = buf.readUnsignedShort(); 22 | int priority = buf.readUnsignedByte(); 23 | 24 | FileDescriptor desc = new FileDescriptor(type, file); 25 | Priority p = Priority.valueOf(priority); 26 | 27 | return new OnDemandRequest(desc, p); 28 | } 29 | return null; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/ondemand/OnDemandResponse.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.ondemand; 2 | 3 | 4 | import org.apollo.jagcached.fs.FileDescriptor; 5 | import org.jboss.netty.buffer.ChannelBuffer; 6 | 7 | /** 8 | * Represents a single 'on-demand' response. 9 | * @author Graham 10 | */ 11 | public final class OnDemandResponse { 12 | 13 | /** 14 | * The file descriptor. 15 | */ 16 | private final FileDescriptor fileDescriptor; 17 | 18 | /** 19 | * The file size. 20 | */ 21 | private final int fileSize; 22 | 23 | /** 24 | * The chunk id. 25 | */ 26 | private final int chunkId; 27 | 28 | /** 29 | * The chunk data. 30 | */ 31 | private final ChannelBuffer chunkData; 32 | 33 | /** 34 | * Creates the 'on-demand' response. 35 | * @param fileDescriptor The file descriptor. 36 | * @param fileSize The file size. 37 | * @param chunkId The chunk id. 38 | * @param chunkData The chunk data. 39 | */ 40 | public OnDemandResponse(FileDescriptor fileDescriptor, int fileSize, int chunkId, ChannelBuffer chunkData) { 41 | this.fileDescriptor = fileDescriptor; 42 | this.fileSize = fileSize; 43 | this.chunkId = chunkId; 44 | this.chunkData = chunkData; 45 | } 46 | 47 | /** 48 | * Gets the file descriptor. 49 | * @return The file descriptor. 50 | */ 51 | public FileDescriptor getFileDescriptor() { 52 | return fileDescriptor; 53 | } 54 | 55 | /** 56 | * Gets the file size. 57 | * @return The file size. 58 | */ 59 | public int getFileSize() { 60 | return fileSize; 61 | } 62 | 63 | /** 64 | * Gets the chunk id. 65 | * @return The chunk id. 66 | */ 67 | public int getChunkId() { 68 | return chunkId; 69 | } 70 | 71 | /** 72 | * Gets the chunk data. 73 | * @return The chunk data. 74 | */ 75 | public ChannelBuffer getChunkData() { 76 | return chunkData; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/ondemand/OnDemandResponseEncoder.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.ondemand; 2 | 3 | 4 | import org.apollo.jagcached.fs.FileDescriptor; 5 | import org.jboss.netty.buffer.ChannelBuffer; 6 | import org.jboss.netty.buffer.ChannelBuffers; 7 | import org.jboss.netty.channel.Channel; 8 | import org.jboss.netty.channel.ChannelHandlerContext; 9 | import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; 10 | 11 | /** 12 | * A {@link OneToOneEncoder} for the 'on-demand' protocol. 13 | * @author Graham 14 | */ 15 | public final class OnDemandResponseEncoder extends OneToOneEncoder { 16 | 17 | @Override 18 | protected Object encode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception { 19 | if (msg instanceof OnDemandResponse) { 20 | OnDemandResponse resp = (OnDemandResponse) msg; 21 | 22 | FileDescriptor fileDescriptor = resp.getFileDescriptor(); 23 | int fileSize = resp.getFileSize(); 24 | int chunkId = resp.getChunkId(); 25 | ChannelBuffer chunkData = resp.getChunkData(); 26 | 27 | ChannelBuffer buf = ChannelBuffers.buffer(6 + chunkData.readableBytes()); 28 | buf.writeByte(fileDescriptor.getType() - 1); 29 | buf.writeShort(fileDescriptor.getFile()); 30 | buf.writeShort(fileSize); 31 | buf.writeByte(chunkId); 32 | buf.writeBytes(chunkData); 33 | 34 | return buf; 35 | } 36 | return msg; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/service/ServiceRequest.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.service; 2 | 3 | /** 4 | * Represents a service request message. 5 | * @author Graham 6 | */ 7 | public final class ServiceRequest { 8 | 9 | /** 10 | * The game service id. 11 | */ 12 | public static final int SERVICE_GAME = 14; 13 | 14 | /** 15 | * The 'on-demand' service id. 16 | */ 17 | public static final int SERVICE_ONDEMAND = 15; 18 | 19 | /** 20 | * The service id. 21 | */ 22 | private final int id; 23 | 24 | /** 25 | * Creates a service request. 26 | * @param id The service id. 27 | */ 28 | public ServiceRequest(int id) { 29 | this.id = id; 30 | } 31 | 32 | /** 33 | * Gets the service id. 34 | * @return The service id. 35 | */ 36 | public int getId() { 37 | return id; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/service/ServiceRequestDecoder.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.service; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.channel.Channel; 5 | import org.jboss.netty.channel.ChannelHandlerContext; 6 | import org.jboss.netty.channel.ChannelPipeline; 7 | import org.jboss.netty.handler.codec.frame.FrameDecoder; 8 | 9 | /** 10 | * A {@link FrameDecoder} which decodes {@link ServiceRequest} messages. 11 | * @author Graham 12 | */ 13 | public final class ServiceRequestDecoder extends FrameDecoder { 14 | 15 | /** 16 | * Creates the decoder, enabling the 'unfold' mechanism. 17 | */ 18 | public ServiceRequestDecoder() { 19 | super(true); 20 | } 21 | 22 | @Override 23 | protected Object decode(ChannelHandlerContext ctx, Channel c, ChannelBuffer buf) throws Exception { 24 | if (buf.readable()) { 25 | ServiceRequest request = new ServiceRequest(buf.readUnsignedByte()); 26 | 27 | ChannelPipeline pipeline = ctx.getPipeline(); 28 | pipeline.remove(this); 29 | 30 | if (buf.readable()) { 31 | return new Object[] { request, buf.readBytes(buf.readableBytes()) }; 32 | } else { 33 | return request; 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/service/ServiceResponse.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.service; 2 | 3 | /** 4 | * Represents a response to a service request. 5 | * @author Graham 6 | */ 7 | public final class ServiceResponse { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/net/service/ServiceResponseEncoder.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.net.service; 2 | 3 | import org.jboss.netty.buffer.ChannelBuffer; 4 | import org.jboss.netty.buffer.ChannelBuffers; 5 | import org.jboss.netty.channel.Channel; 6 | import org.jboss.netty.channel.ChannelHandlerContext; 7 | import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; 8 | 9 | /** 10 | * A {@link OneToOneEncoder} which encodes {@link ServiceResponse} messages. 11 | * @author Graham 12 | */ 13 | public final class ServiceResponseEncoder extends OneToOneEncoder { 14 | 15 | @Override 16 | protected Object encode(ChannelHandlerContext ctx, Channel c, Object msg) throws Exception { 17 | if (msg instanceof ServiceResponse) { 18 | ChannelBuffer buf = ChannelBuffers.buffer(8); 19 | buf.writeLong(0); 20 | return buf; 21 | } 22 | return msg; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/resource/CombinedResourceProvider.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.resource; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A resource provider composed of multiple resource providers. 8 | * @author Graham Edgecombe 9 | */ 10 | public final class CombinedResourceProvider extends ResourceProvider { 11 | 12 | /** 13 | * An array of resource providers. 14 | */ 15 | private final ResourceProvider[] providers; 16 | 17 | /** 18 | * Creates the combined resource providers. 19 | * @param providers The providers this provider delegates to. 20 | */ 21 | public CombinedResourceProvider(ResourceProvider... providers) { 22 | this.providers = providers; 23 | } 24 | 25 | @Override 26 | public boolean accept(String path) throws IOException { 27 | return true; 28 | } 29 | 30 | @Override 31 | public ByteBuffer get(String path) throws IOException { 32 | for (ResourceProvider provider : providers) { 33 | if (provider.accept(path)) { 34 | return provider.get(path); 35 | } 36 | } 37 | return null; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/resource/HypertextResourceProvider.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.resource; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.io.RandomAccessFile; 6 | import java.net.URI; 7 | import java.nio.ByteBuffer; 8 | import java.nio.channels.FileChannel.MapMode; 9 | 10 | /** 11 | * A {@link ResourceProvider} which provides additional hypertext resources. 12 | * @author Graham Edgecombe 13 | */ 14 | public final class HypertextResourceProvider extends ResourceProvider { 15 | 16 | /** 17 | * The base directory from which documents are served. 18 | */ 19 | private final File base; 20 | 21 | /** 22 | * Creates a new hypertext resource provider with the specified base 23 | * directory. 24 | * @param base The base directory. 25 | */ 26 | public HypertextResourceProvider(File base) { 27 | this.base = base; 28 | } 29 | 30 | @Override 31 | public boolean accept(String path) throws IOException { 32 | File f = new File(base, path); 33 | URI target = f.toURI().normalize(); 34 | if (target.toASCIIString().startsWith(base.toURI().normalize().toASCIIString())) { 35 | if (f.isDirectory()) { 36 | f = new File(f, "index.html"); 37 | } 38 | return f.exists(); 39 | } 40 | return false; 41 | } 42 | 43 | @Override 44 | public ByteBuffer get(String path) throws IOException { 45 | File f = new File(base, path); 46 | if (f.isDirectory()) { 47 | f = new File(f, "index.html"); 48 | } 49 | if (!f.exists()) { 50 | return null; 51 | } 52 | 53 | RandomAccessFile raf = new RandomAccessFile(f, "r"); 54 | ByteBuffer buf; 55 | try { 56 | buf = raf.getChannel().map(MapMode.READ_ONLY, 0, raf.length()); 57 | } finally { 58 | raf.close(); 59 | } 60 | 61 | return buf; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/resource/ResourceProvider.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.resource; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | /** 7 | * A class which provides resources. 8 | * @author Graham Edgecombe 9 | */ 10 | public abstract class ResourceProvider { 11 | 12 | /** 13 | * Checks that this provider can fulfil a request to the specified 14 | * resource. 15 | * @param path The path to the resource, e.g. {@code /crc}. 16 | * @return {@code true} if the provider can fulfil a request to the 17 | * resource, {@code false} otherwise. 18 | * @throws IOException if an I/O error occurs. 19 | */ 20 | public abstract boolean accept(String path) throws IOException; 21 | 22 | /** 23 | * Gets a resource by its path. 24 | * @param path The path. 25 | * @return The resource, or {@code null} if it doesn't exist. 26 | * @throws IOException if an I/O error occurs. 27 | */ 28 | public abstract ByteBuffer get(String path) throws IOException; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/org/apollo/jagcached/resource/VirtualResourceProvider.java: -------------------------------------------------------------------------------- 1 | package org.apollo.jagcached.resource; 2 | 3 | import java.io.IOException; 4 | import java.nio.ByteBuffer; 5 | 6 | import org.apollo.jagcached.fs.IndexedFileSystem; 7 | 8 | /** 9 | * A {@link ResourceProvider} which maps virtual resources (such as 10 | * {@code /media}) to files in an {@link IndexedFileSystem}. 11 | * @author Graham Edgecombe 12 | */ 13 | public final class VirtualResourceProvider extends ResourceProvider { 14 | 15 | /** 16 | * An array of valid prefixes. 17 | */ 18 | private static final String[] VALID_PREFIXES = { 19 | "crc", "title", "config", "interface", "media", "versionlist", 20 | "textures", "wordenc", "sounds" 21 | }; 22 | 23 | /** 24 | * The file system. 25 | */ 26 | private final IndexedFileSystem fs; 27 | 28 | /** 29 | * Creates a new virtual resource provider with the specified file system. 30 | * @param fs The file system. 31 | */ 32 | public VirtualResourceProvider(IndexedFileSystem fs) { 33 | this.fs = fs; 34 | } 35 | 36 | @Override 37 | public boolean accept(String path) throws IOException { 38 | for (String prefix : VALID_PREFIXES) { 39 | if (path.startsWith("/" + prefix)) { 40 | return true; 41 | } 42 | } 43 | return false; 44 | } 45 | 46 | @Override 47 | public ByteBuffer get(String path) throws IOException { 48 | if (path.startsWith("/crc")) { 49 | return fs.getCrcTable(); 50 | } else if (path.startsWith("/title")) { 51 | return fs.getFile(0, 1); 52 | } else if (path.startsWith("/config")) { 53 | return fs.getFile(0, 2); 54 | } else if (path.startsWith("/interface")) { 55 | return fs.getFile(0, 3); 56 | } else if (path.startsWith("/media")) { 57 | return fs.getFile(0, 4); 58 | } else if (path.startsWith("/versionlist")) { 59 | return fs.getFile(0, 5); 60 | } else if (path.startsWith("/textures")) { 61 | return fs.getFile(0, 6); 62 | } else if (path.startsWith("/wordenc")) { 63 | return fs.getFile(0, 7); 64 | } else if (path.startsWith("/sounds")) { 65 | return fs.getFile(0, 8); 66 | } 67 | return null; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | java -cp bin;lib/netty-3.2.jar -server org.apollo.jagcached.FileServer 3 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sudo java -cp bin:lib/netty-3.2.jar -server org.apollo.jagcached.FileServer 3 | --------------------------------------------------------------------------------