/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.network.internal; import com.google.common.collect.Sets; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.config.Config; import org.terasology.engine.EngineTime; import org.terasology.engine.TerasologyConstants; import org.terasology.engine.Time; import org.terasology.engine.module.ModuleManager; import org.terasology.engine.paths.PathManager; import org.terasology.module.ModuleLoader; import org.terasology.naming.Name; import org.terasology.naming.Version; import org.terasology.network.JoinStatus; import org.terasology.protobuf.NetData; import org.terasology.registry.CoreRegistry; import java.io.BufferedOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Locale; import java.util.Set; import java.util.Timer; public class ClientConnectionHandler extends SimpleChannelUpstreamHandler { private static final Logger logger = LoggerFactory.getLogger(ClientConnectionHandler.class); private final JoinStatusImpl joinStatus; private NetworkSystemImpl networkSystem; private ServerImpl server; private ModuleManager moduleManager; private Set<String> missingModules = Sets.newHashSet(); private NetData.ModuleDataHeader receivingModule; private Path tempModuleLocation; private BufferedOutputStream downloadingModule; private long lengthReceived; private Timer timeoutTimer = new Timer(); private long timeoutPoint = System.currentTimeMillis(); private final long timeoutThreshold = 10000; private Channel channel; public ClientConnectionHandler(JoinStatusImpl joinStatus, NetworkSystemImpl networkSystem) { this.networkSystem = networkSystem; this.joinStatus = joinStatus; this.moduleManager = CoreRegistry.get(ModuleManager.class); } private void scheduleTimeout(Channel inputChannel) { channel = inputChannel; timeoutPoint = System.currentTimeMillis() + timeoutThreshold; timeoutTimer.schedule(new java.util.TimerTask() { @Override public void run() { synchronized (joinStatus) { if (System.currentTimeMillis() > timeoutPoint && joinStatus.getStatus() != JoinStatus.Status.COMPLETE && joinStatus.getStatus() != JoinStatus.Status.FAILED) { joinStatus.setErrorMessage("Server stopped responding."); channel.close(); logger.error("Server timeout threshold of {} ms exceeded.", timeoutThreshold); } } } }, timeoutThreshold + 200); } @Override public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) { // If we timed out, don't handle anymore messages. if (joinStatus.getStatus() == JoinStatus.Status.FAILED) { return; } scheduleTimeout(ctx.getChannel()); // Handle message NetData.NetMessage message = (NetData.NetMessage) e.getMessage(); synchronized (joinStatus) { timeoutPoint = System.currentTimeMillis() + timeoutThreshold; if (message.hasServerInfo()) { receivedServerInfo(ctx, message.getServerInfo()); } else if (message.hasModuleDataHeader()) { receiveModuleStart(ctx, message.getModuleDataHeader()); } else if (message.hasModuleData()) { receiveModule(ctx, message.getModuleData()); } else if (message.hasJoinComplete()) { if (missingModules.size() > 0) { logger.error( "The server did not send all of the modules that were needed before ending module transmission."); } completeJoin(ctx, message.getJoinComplete()); } else { logger.error("Received unexpected message"); } } } private void receiveModuleStart(ChannelHandlerContext channelHandlerContext, NetData.ModuleDataHeader moduleDataHeader) { if (receivingModule != null) { joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); return; } String moduleId = moduleDataHeader.getId(); if (missingModules.remove(moduleId.toLowerCase(Locale.ENGLISH))) { if (moduleDataHeader.hasError()) { joinStatus.setErrorMessage("Module download error: " + moduleDataHeader.getError()); channelHandlerContext.getChannel().close(); } else { String sizeString = getSizeString(moduleDataHeader.getSize()); joinStatus.setCurrentActivity( "Downloading " + moduleDataHeader.getId() + ":" + moduleDataHeader.getVersion() + " (" + sizeString + "," + missingModules.size() + " modules remain)"); logger.info("Downloading " + moduleDataHeader.getId() + ":" + moduleDataHeader.getVersion() + " (" + sizeString + "," + missingModules.size() + " modules remain)"); receivingModule = moduleDataHeader; lengthReceived = 0; try { tempModuleLocation = Files.createTempFile("terasologyDownload", ".tmp"); tempModuleLocation.toFile().deleteOnExit(); downloadingModule = new BufferedOutputStream( Files.newOutputStream(tempModuleLocation, StandardOpenOption.WRITE)); } catch (IOException e) { logger.error("Failed to write received module", e); joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); } } } else { logger.error("Received unwanted module {}:{} from server", moduleDataHeader.getId(), moduleDataHeader.getVersion()); joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); } } private String getSizeString(long size) { if (size < 1024) { return size + " bytes"; } else if (size < 1048576) { return String.format("%.2f KB", (float) size / 1024); } else { return String.format("%.2f MB", (float) size / 1048576); } } private void receiveModule(ChannelHandlerContext channelHandlerContext, NetData.ModuleData moduleData) { if (receivingModule == null) { joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); return; } try { downloadingModule.write(moduleData.getModule().toByteArray()); lengthReceived += moduleData.getModule().size(); joinStatus.setCurrentProgress((float) lengthReceived / receivingModule.getSize()); if (lengthReceived == receivingModule.getSize()) { // finished downloadingModule.close(); String moduleName = String.format("%s-%s.jar", receivingModule.getId(), receivingModule.getVersion()); Path finalPath = PathManager.getInstance().getHomeModPath().normalize().resolve(moduleName); if (finalPath.normalize().startsWith(PathManager.getInstance().getHomeModPath())) { if (Files.exists(finalPath)) { logger.error("File already exists at {}", finalPath); joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); return; } Files.copy(tempModuleLocation, finalPath); ModuleLoader loader = new ModuleLoader(moduleManager.getModuleMetadataReader()); loader.setModuleInfoPath(TerasologyConstants.MODULE_INFO_FILENAME); moduleManager.getRegistry().add(loader.load(finalPath)); receivingModule = null; if (missingModules.isEmpty()) { sendJoin(channelHandlerContext); } } else { logger.error("Module rejected"); joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); } } } catch (IOException e) { logger.error("Error saving module", e); joinStatus.setErrorMessage("Module download error"); channelHandlerContext.getChannel().close(); } } private void completeJoin(ChannelHandlerContext channelHandlerContext, NetData.JoinCompleteMessage joinComplete) { logger.info("Join complete received"); server.setClientId(joinComplete.getClientId()); channelHandlerContext.getPipeline().remove(this); channelHandlerContext.getPipeline().get(ClientHandler.class).joinComplete(server); joinStatus.setComplete(); } private void receivedServerInfo(ChannelHandlerContext channelHandlerContext, NetData.ServerInfoMessage message) { logger.info("Received server info"); ((EngineTime) CoreRegistry.get(Time.class)).setGameTime(message.getTime()); this.server = new ServerImpl(networkSystem, channelHandlerContext.getChannel()); server.setServerInfo(message); // Request missing modules for (NetData.ModuleInfo info : message.getModuleList()) { if (null == moduleManager.getRegistry().getModule(new Name(info.getModuleId()), new Version(info.getModuleVersion()))) { missingModules.add(info.getModuleId().toLowerCase(Locale.ENGLISH)); } } if (missingModules.isEmpty()) { joinStatus.setCurrentActivity("Finalizing join"); sendJoin(channelHandlerContext); } else { joinStatus.setCurrentActivity("Requesting missing modules"); NetData.NetMessage.Builder builder = NetData.NetMessage.newBuilder(); for (String module : missingModules) { builder.addModuleRequest(NetData.ModuleRequest.newBuilder().setModuleId(module)); } channelHandlerContext.getChannel().write(builder.build()); } } private void sendJoin(ChannelHandlerContext channelHandlerContext) { Config config = CoreRegistry.get(Config.class); NetData.JoinMessage.Builder bldr = NetData.JoinMessage.newBuilder(); NetData.Color.Builder clrbldr = NetData.Color.newBuilder(); bldr.setName(config.getPlayer().getName()); bldr.setViewDistanceLevel(config.getRendering().getViewDistance().getIndex()); bldr.setColor(clrbldr.setRgba(config.getPlayer().getColor().rgba()).build()); channelHandlerContext.getChannel().write(NetData.NetMessage.newBuilder().setJoin(bldr).build()); } public JoinStatus getJoinStatus() { synchronized (joinStatus) { return joinStatus; } } }