/* This file is part of jpcsp. Jpcsp is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Jpcsp is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Jpcsp. If not, see <http://www.gnu.org/licenses/>. */ package jpcsp.network.proonline; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.LinkedList; import java.util.List; import jpcsp.HLE.kernel.types.pspNetMacAddress; import jpcsp.network.proonline.PacketFactory.SceNetAdhocctlConnectPacketS2C; import jpcsp.network.proonline.PacketFactory.SceNetAdhocctlDisconnectPacketS2C; import jpcsp.network.proonline.PacketFactory.SceNetAdhocctlPacketBaseC2S; import jpcsp.network.proonline.PacketFactory.SceNetAdhocctlPacketBaseS2C; import jpcsp.util.Utilities; import org.apache.log4j.Logger; /* * Ported from ProOnline aemu server * https://code.google.com/p/aemu/source/browse/#hg%2Fpspnet_adhocctl_server */ public class ProOnlineServer { protected static Logger log = ProOnlineNetworkAdapter.log; private static ProOnlineServer instance; private ProOnlineServerThread serverThread; private static final int port = 27312; private ServerSocket serverSocket; private List<User> users; private PacketFactory packetFactory; private User currentUser; private List<Game> games; public static ProOnlineServer getInstance() { if (instance == null) { instance = new ProOnlineServer(); } return instance; } private ProOnlineServer() { } private static class User { public Socket socket; public long lastReceiveTimestamp; public byte[] buffer = new byte[1000]; public int bufferLength = 0; public pspNetMacAddress mac; public String nickName; public Game game; public Group group; public int ip; public String ipString; public boolean isTimeout() { boolean isTimeout = System.currentTimeMillis() - lastReceiveTimestamp > 15000; if (isTimeout) { log.debug(String.format("User timed out now=%d, lastReceiveTimestamp=%d", System.currentTimeMillis(), lastReceiveTimestamp)); } return isTimeout; } @Override public String toString() { return String.format("%s (MAC: %s - IP: %s)", nickName, mac, ipString); } } private static class Game { public String name; public int playerCount; public List<Group> groups; public Game(String name) { this.name = name; groups = new LinkedList<Group>(); } } private static class Group { public String name; public Game game; public List<User> players; public Group(String name, Game game) { this.name = name; this.game = game; players = new LinkedList<User>(); if (game != null) { game.groups.add(this); } } } private class ProOnlineServerThread extends Thread { private boolean exit; @Override public void run() { log.debug(String.format("Starting ProOnlineServerThread")); while (!exit) { try { Socket socket = serverSocket.accept(); socket.setSoTimeout(1); loginUserStream(socket); } catch (SocketTimeoutException e) { // Ignore timeout } catch (IOException e) { log.debug("Accept server socket", e); } for (User user : users) { int length = 0; try { InputStream is = user.socket.getInputStream(); length = is.read(user.buffer, user.bufferLength, user.buffer.length - user.bufferLength); } catch (SocketTimeoutException e) { // Ignore timeout } catch (IOException e) { log.debug("Receive user socket", e); } if (length > 0) { user.bufferLength += length; user.lastReceiveTimestamp = System.currentTimeMillis(); processUserStream(user); } else if (length < 0 || user.isTimeout()) { logoutUser(user); } } Utilities.sleep(10); } } public void exit() { exit = true; } } public void start() { packetFactory = new PacketFactory(); try { serverSocket = new ServerSocket(port); serverSocket.setSoTimeout(1); } catch (IOException e) { log.error(String.format("Server socket at port %d not available: %s", port, e)); return; } users = new LinkedList<ProOnlineServer.User>(); games = new LinkedList<ProOnlineServer.Game>(); serverThread = new ProOnlineServerThread(); serverThread.setName("ProOnline Server Thread"); serverThread.setDaemon(true); serverThread.start(); } public void exit() { if (serverThread != null) { serverThread.exit(); serverThread = null; } if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { log.debug("Closing server socket", e); } serverSocket = null; } } private static int convertIp(byte[] bytes) { int ip = 0; for (int i = 0; i < bytes.length; i++) { ip |= bytes[i] << (i * 8); } return ip; } private void sendToUser(User user, SceNetAdhocctlPacketBaseS2C packet) throws IOException { OutputStream os = user.socket.getOutputStream(); os.write(packet.getBytes()); os.flush(); } private void loginUserStream(Socket socket) throws IOException { String ip = socket.getInetAddress().getHostAddress(); // Check for duplicated user // for (User user : users) { // if (user.ipString.equals(ip)) { // // Duplicate user (same IP & same port) // log.debug(String.format("Duplicate user IP: %s", ip)); // socket.close(); // return; // } // } User user = new User(); user.ip = convertIp(socket.getInetAddress().getAddress()); user.ipString = ip; user.socket = socket; user.lastReceiveTimestamp = System.currentTimeMillis(); users.add(user); log.info(String.format("New Connection from %s", user.ipString)); } private void logoutUser(User user) { if (user.group != null) { disconnectUser(user); } try { user.socket.close(); } catch (IOException e) { // Ignore exception } if (user.game != null) { log.info(String.format("%s stopped playing %s.", user, user.game.name)); user.game.playerCount--; // Empty game if (user.game.playerCount <= 0) { games.remove(user.game); } user.game = null; } else { log.info(String.format("Dropped Connection %s.", user)); } users.remove(user); } private void processUserStream(User user) { if (user.bufferLength <= 0) { return; } int consumed = 0; SceNetAdhocctlPacketBaseC2S packet = packetFactory.createPacketC2S(null, this, user.buffer, user.bufferLength); if (packet == null) { // Skip the unknown code consumed = 1; } else if (user.bufferLength >= packet.getLength()) { if (log.isDebugEnabled()) { log.debug(String.format("Incoming client packet %s", packet)); } currentUser = user; packet.process(); currentUser = null; consumed = packet.getLength(); } if (consumed >= user.bufferLength) { user.bufferLength = 0; } else { // Removed consumed bytes from the buffer user.bufferLength -= consumed; System.arraycopy(user.buffer, consumed, user.buffer, 0, user.bufferLength); } } public void processLogin(pspNetMacAddress mac, String nickName, String gameName) { if (gameName.matches("[A-Z0-9]{9}")) { currentUser.game = null; for (Game game : games) { if (game.name.equals(gameName)) { currentUser.game = game; break; } } if (currentUser.game == null) { currentUser.game = new Game(gameName); games.add(currentUser.game); } currentUser.game.playerCount++; currentUser.mac = mac; currentUser.nickName = nickName; log.info(String.format("%s started playing %s.", currentUser, currentUser.game.name)); } else { log.info(String.format("Invalid login for game '%s'", gameName)); } } private void disconnectUser(User user) { if (user.group != null) { Group group = user.group; group.players.remove(user); for (User groupUser : group.players) { SceNetAdhocctlDisconnectPacketS2C packet = new SceNetAdhocctlDisconnectPacketS2C(user.ip); try { sendToUser(groupUser, packet); } catch (IOException e) { log.debug("disconnectUser", e); } } log.info(String.format("%s left %s group %s.", user, user.game.name, group.name)); user.group = null; // Empty group if (group.players.isEmpty()) { group.game.groups.remove(group); } } else { log.info(String.format("%s attempted to leave %s without joining one first.", user, user.game.name)); logoutUser(user); } } public void processDisconnect() { disconnectUser(currentUser); } public void processScan() { // User is disconnected if (currentUser.group == null) { // Iterate game groups for (Group group : currentUser.game.groups) { pspNetMacAddress mac = new pspNetMacAddress(); if (!group.players.isEmpty()) { // Founder of the group is the first player mac = group.players.get(0).mac; } try { sendToUser(currentUser, new PacketFactory.SceNetAdhocctlScanPacketS2C(group.name, mac)); } catch (IOException e) { log.debug("processScan", e); } } } else { log.info(String.format("%s attempted to scan for %s groups without disconnecting from %s first.", currentUser, currentUser.game.name, currentUser.group.name)); logoutUser(currentUser); } } private void spreadMessage(User fromUser, String message) { // Global notice if (fromUser == null) { // Iterate players for (User user : users) { // User has access to chat if (user.group != null) { try { sendToUser(user, new PacketFactory.SceNetAdhocctlChatPacketS2C(message, "")); } catch (IOException e) { log.debug("spreadMessage global notice", e); } } } } else if (fromUser.group != null) { // User is connected int messageCount = 0; for (User user : fromUser.group.players) { // Skip self if (user != fromUser) { try { sendToUser(user, new PacketFactory.SceNetAdhocctlChatPacketS2C(message, fromUser.nickName)); messageCount++; } catch (IOException e) { log.debug("spreadMessage", e); } } } if (messageCount > 0) { log.info(String.format("%s sent '%s' to %d players in %s group %s", fromUser, message, messageCount, fromUser.game.name, fromUser.group.name)); } } else { // User is disconnected log.info(String.format("%s attempted to send a text message without joining a %s group first", fromUser, fromUser.game.name)); } } public void processChat(String message) { spreadMessage(currentUser, message); } public void processConnect(String groupName) { if (groupName.matches("[A-Za-z0-9]*")) { // User is disconnected if (currentUser.group == null) { for (Group group : currentUser.game.groups) { if (group.name.equals(groupName)) { currentUser.group = group; break; } } // New group if (currentUser.group == null) { currentUser.group = new Group(groupName, currentUser.game); } for (User user : currentUser.group.players) { SceNetAdhocctlConnectPacketS2C packet = new SceNetAdhocctlConnectPacketS2C(currentUser.nickName, currentUser.mac, currentUser.ip); try { sendToUser(user, packet); } catch (IOException e) { log.debug("processConnect", e); } packet = new SceNetAdhocctlConnectPacketS2C(user.nickName, user.mac, user.ip); try { sendToUser(currentUser, packet); } catch (IOException e) { log.debug("processConnect", e); } } currentUser.group.players.add(currentUser); try { sendToUser(currentUser, new PacketFactory.SceNetAdhocctlConnectBSSIDPacketS2C(currentUser.group.players.get(0).mac)); } catch (IOException e) { log.debug("processConnect", e); } log.info(String.format("%s joined %s group '%s'.", currentUser, currentUser.game == null ? "" : currentUser.game.name, currentUser.group.name)); } else { // Already connected to another group log.info(String.format("%s attempted to join %s group '%s' without disconnecting from %s first.", currentUser, currentUser.game == null ? "" : currentUser.game.name, groupName, currentUser.group.name)); logoutUser(currentUser); } } else { log.info(String.format("%s attempted to join invalid %s group '%s'.", currentUser, currentUser.game == null ? "" : currentUser.game.name, groupName)); logoutUser(currentUser); } } }