/* 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.remote; import static jpcsp.HLE.modules.sceNpAuth.STATUS_ACCOUNT_PARENTAL_CONTROL_ENABLED; import static jpcsp.HLE.modules.sceNpAuth.addTicketDateParam; import static jpcsp.HLE.modules.sceNpAuth.addTicketLongParam; import static jpcsp.HLE.modules.sceNpAuth.addTicketParam; import static jpcsp.filesystems.umdiso.UmdIsoFile.sectorLength; import static jpcsp.util.Utilities.getDefaultPortForProtocol; import java.awt.AWTException; import java.awt.Rectangle; import java.awt.Robot; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Authenticator; import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; import java.net.Proxy; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URL; import java.nio.ByteBuffer; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import javax.imageio.ImageIO; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLServerSocketFactory; import jpcsp.Controller.keyCode; import jpcsp.Emulator; import jpcsp.MainGUI; import jpcsp.State; import jpcsp.HLE.Modules; import jpcsp.HLE.kernel.types.IAction; import jpcsp.HLE.kernel.types.SceNpTicket; import jpcsp.HLE.kernel.types.SceNpTicket.TicketParam; import jpcsp.HLE.modules.sceNp; import jpcsp.filesystems.umdiso.UmdIsoFile; import jpcsp.filesystems.umdiso.UmdIsoReader; import jpcsp.format.Elf32Header; import jpcsp.remote.HTTPConfiguration.HttpServerConfiguration; import jpcsp.settings.Settings; import jpcsp.util.Utilities; import org.apache.log4j.Logger; public class HTTPServer { private static Logger log = Logger.getLogger("http"); private static HTTPServer instance; private static final HTTPServerDescriptor[] serverDescriptors = new HTTPServerDescriptor[] { new HTTPServerDescriptor(0, 80, false), new HTTPServerDescriptor(1, 443, true) }; public static boolean processProxyRequestLocally = false; private static final String method = "method"; private static final String path = "path"; private static final String host = "host"; private static final String parameters = "parameters"; private static final String version = "version"; private static final String data = "data"; private static final String contentLength = "content-length"; private static final String eol = "\r\n"; private static final String boundary = "--boundarybetweensingleimages"; private static final String isoDirectory = "/iso/"; private static final String iconDirectory = "/icon/"; private static final String rootDirectory = "root"; private static final String widgetDirectory = "Widget"; private static final String widgetPath = rootDirectory + "/" + widgetDirectory; private static final String indexFile = "index.html"; private static final String naclDirectory = "nacl"; private static final String widgetlistFile = "/widgetlist.xml"; private HTTPServerThread[] serverThreads; private Robot captureRobot; private UmdIsoReader previousUmdIsoReader; private String previousIsoFilename; private HashMap<Integer, keyCode> keyMapping; private int runMapping = -1; private int pauseMapping = -1; private int resetMapping = -1; private static final int MAX_COMPRESSED_COUNT = 0x7F; private DisplayAction displayAction; private int displayActionUsageCount = 0; private BufferedImage currentDisplayImage; private boolean currentDisplayImageHasAlpha = false; private Proxy proxy; private int proxyPort; private int proxyAddress; private SceNpTicket ticket; public static HTTPServer getInstance() { if (instance == null) { Utilities.disableSslCertificateChecks(); instance = new HTTPServer(); } return instance; } private static class HTTPServerDescriptor { private int index; private int port; private boolean ssl; public HTTPServerDescriptor(int index, int port, boolean ssl) { this.index = index; this.port = port; this.ssl = ssl; } public int getIndex() { return index; } public int getPort() { return port; } public boolean isSsl() { return ssl; } } private class HTTPServerThread extends Thread { private boolean exit; private ServerSocket serverSocket; private HTTPServerDescriptor descriptor; public HTTPServerThread(HTTPServerDescriptor descriptor) { this.descriptor = descriptor; } @Override public void run() { try { if (descriptor.isSsl()) { SSLServerSocketFactory factory = getSSLServerSocketFactory(); if (factory != null) { serverSocket = factory.createServerSocket(descriptor.getPort()); } } else { serverSocket = new ServerSocket(descriptor.getPort()); } if (serverSocket != null) { serverSocket.setSoTimeout(1); } } catch (IOException e) { log.error(String.format("Server socket at port %d not available: %s", descriptor.getPort(), e)); } catch (KeyStoreException e) { log.error(String.format("SSL Server socket at port %d not available: %s", descriptor.getPort(), e)); } catch (NoSuchAlgorithmException e) { log.error(String.format("SSL Server socket at port %d not available: %s", descriptor.getPort(), e)); } catch (CertificateException e) { log.error(String.format("SSL Server socket at port %d not available: %s", descriptor.getPort(), e)); } catch (UnrecoverableKeyException e) { log.error(String.format("SSL Server socket at port %d not available: %s", descriptor.getPort(), e)); } catch (KeyManagementException e) { log.error(String.format("SSL Server socket at port %d not available: %s", descriptor.getPort(), e)); } if (serverSocket == null) { exit(); } while (!exit) { try { Socket socket = serverSocket.accept(); socket.setSoTimeout(1); HTTPSocketHandlerThread handlerThread = new HTTPSocketHandlerThread(descriptor, socket); handlerThread.setName(String.format("HTTP Handler %d/%d", descriptor.getPort(), socket.getPort())); handlerThread.setDaemon(true); handlerThread.start(); } catch (SocketTimeoutException e) { // Ignore timeout } catch (IOException e) { log.debug("Accept server socket", e); } Utilities.sleep(10); } if (serverSocket != null) { try { serverSocket.close(); } catch (IOException e) { // Ignore exception } } serverThreads[descriptor.getIndex()] = null; } private SSLServerSocketFactory getSSLServerSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, KeyManagementException { String jksFileName = "jpcsp.jks"; if (!new File(jksFileName).canRead()) { if (log.isDebugEnabled()) { log.debug(String.format("getSSLServerSocketFactory cannot read the file '%s'", jksFileName)); } return null; } char[] password = "changeit".toCharArray(); FileInputStream keyStoreInputStream = new FileInputStream(jksFileName); KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(keyStoreInputStream, password); String defaultAlgorithm = KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(defaultAlgorithm); keyManagerFactory.init(keyStore, password); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), null, null); return sslContext.getServerSocketFactory(); } public void exit() { exit = true; } } private class HTTPSocketHandlerThread extends Thread { private HTTPServerDescriptor descriptor; private Socket socket; public HTTPSocketHandlerThread(HTTPServerDescriptor descriptor, Socket socket) { this.descriptor = descriptor; this.socket = socket; } @Override public void run() { process(descriptor, socket); } } private class DisplayAction implements IAction { @Override public void execute() { currentDisplayImage = Modules.sceDisplayModule.getCurrentDisplayAsBufferedImage(false); currentDisplayImageHasAlpha = false; } } private HTTPServer() { keyMapping = new HashMap<Integer, keyCode>(); serverThreads = new HTTPServerThread[serverDescriptors.length]; for (HTTPServerDescriptor descriptor : serverDescriptors) { if (descriptor.getIndex() == 0) { String addressName = "localhost"; proxyPort = descriptor.getPort(); InetSocketAddress socketAddress = new InetSocketAddress(addressName, proxyPort); proxy = new Proxy(Proxy.Type.HTTP, socketAddress); byte addrBytes[] = socketAddress.getAddress().getAddress(); proxyAddress = (addrBytes[0] & 0xFF) | ((addrBytes[1] & 0xFF) << 8) | ((addrBytes[2] & 0xFF) << 16) | ((addrBytes[3] & 0xFF) << 24); } HTTPServerThread serverThread = new HTTPServerThread(descriptor); serverThreads[descriptor.getIndex()] = serverThread; serverThread.setDaemon(true); serverThread.setName("HTTP Server"); serverThread.start(); } Authenticator.setDefault(new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { if (log.isDebugEnabled()) { log.debug(String.format("getPasswordAuthentication called for scheme='%s', prompt='%s'", getRequestingScheme(), getRequestingPrompt())); } if ("digest".equals(getRequestingScheme())) { return new PasswordAuthentication("c7y-basic01", "A9QTbosh0W0D^{7467l-n_>2Y%JG^v>o".toCharArray()); } else if ("c7y-basic".equals(getRequestingPrompt())) { // This is the PSP authentication, but it seems to no longer be accepted... char[] pwd = new char[] { (char) 0x35, (char) 0x03, (char) 0x0f, (char) 0x19, (char) 0x40, (char) 0x16, (char) 0x49, (char) 0x04, (char) 0x1c, (char) 0x35, (char) 0x03, (char) 0x1e, (char) 0x21, (char) 0x48, (char) 0x2d, (char) 0x4e, (char) 0x07, (char) 0x1c, (char) 0x5a, (char) 0x36, (char) 0x0e, (char) 0x3f, (char) 0x0c, (char) 0x18, (char) 0x49, (char) 0x15, (char) 0x4e, (char) 0x21, (char) 0x14, (char) 0x36, (char) 0x1d, (char) 0x16 }; return new PasswordAuthentication("c7y-basic02", pwd); } else if ("c7y-ranking".equals(getRequestingPrompt())) { // This is the PSP authentication, but it seems to no longer be accepted... char[] pwd = new char[] { (char) 0x21, (char) 0x2D, (char) 0x18, (char) 0x1B, (char) 0x1D, (char) 0x0E, (char) 0x2A, (char) 0x23, (char) 0x04, (char) 0x4C, (char) 0x4B, (char) 0x19, (char) 0x4F, (char) 0x25, (char) 0x26, (char) 0x3F, (char) 0x4B, (char) 0x4D, (char) 0x4C, (char) 0x44, (char) 0x58, (char) 0x3C, (char) 0x31, (char) 0x4C, (char) 0x15, (char) 0x4C, (char) 0x5C, (char) 0x41, (char) 0x32, (char) 0x38, (char) 0x1E, (char) 0x08 }; return new PasswordAuthentication("c7y-ranking01", pwd); } return super.getPasswordAuthentication(); } }); try { captureRobot = new Robot(); captureRobot.setAutoDelay(0); } catch (AWTException e) { log.error("Create captureRobot", e); } } private static String decodePath(String path) { StringBuilder decoded = new StringBuilder(); for (int i = 0; i < path.length(); i++) { char c = path.charAt(i); if (c == '+') { decoded.append(' '); } else if (c == '%') { int hex = Integer.parseInt(path.substring(i + 1, i + 3), 16); i += 2; decoded.append((char) hex); } else { decoded.append(c); } } return decoded.toString(); } private void process(HTTPServerDescriptor descriptor, Socket socket) { InputStream is = null; try { is = socket.getInputStream(); } catch (IOException e) { log.error("process InputStream", e); } OutputStream os = null; try { os = socket.getOutputStream(); } catch (IOException e) { log.error("process OutputStream", e); } byte[] buffer = new byte[10000]; int bufferLength = 0; while (is != null && os != null) { try { int length = is.read(buffer, bufferLength, buffer.length - bufferLength); if (length < 0) { break; } if (length > 0) { bufferLength += length; String request = new String(buffer, 0, bufferLength); HashMap<String, String> requestHeaders = parseRequest(request); if (requestHeaders != null) { if (log.isDebugEnabled()) { log.debug(String.format("Received request: '%s', headers: %s", request, requestHeaders)); } boolean keepAlive = process(descriptor, requestHeaders, os); os.flush(); if (keepAlive) { bufferLength = 0; } else { break; } } } } catch (SocketTimeoutException e) { // Ignore timeout } catch (IOException e) { // Do not log the exception when the remote client has closed the connection if (!(e.getCause() instanceof EOFException)) { if (log.isDebugEnabled()) { log.debug("Receive socket", e); } } break; } } try { socket.close(); } catch (IOException e) { // Ignore exception } } private HashMap<String, String> parseRequest(String request) { HashMap<String, String> headers = new HashMap<String, String>(); String[] lines = request.split(eol, -1); // Do not loose trailing empty strings boolean header = true; for (int i = 0; i < lines.length; i++) { String line = lines[i]; if (i == 0) { // Parse e.g. "GET / HTTP/1.1" into 3 words: "GET", "/" and "HTTP/1.1" String[] words = line.split(" "); if (words.length >= 1) { headers.put(method, words[0]); } if (words.length >= 2) { String completePath = words[1]; int parametersIndex = completePath.indexOf("?"); if (parametersIndex >= 0) { headers.put(path, decodePath(completePath.substring(0, parametersIndex))); headers.put(parameters, completePath.substring(parametersIndex + 1)); } else { headers.put(path, decodePath(completePath)); } } if (words.length >= 3) { headers.put(version, words[2]); } } else if (header) { if (line.length() == 0) { // End of header header = false; } else { // Parse e.g. "Host: localhost:30005" into 2 words: "Host" and "localhost:30005" String[] words = line.split(": *", 2); if (words.length >= 2) { headers.put(words[0].toLowerCase(), words[1]); } } } else if (line.length() > 0) { String previousData = headers.get(data); if (previousData != null) { headers.put(data, previousData + "\n" + line); } else { headers.put(data, line); } } } if (header) { return null; } if (headers.get(contentLength) != null) { int headerContentLength = Integer.parseInt(headers.get(contentLength)); if (headerContentLength > 0) { String additionalData = headers.get(data); if (additionalData == null || additionalData.length() < headerContentLength) { return null; } } } return headers; } private boolean doProxy(HttpServerConfiguration httpServerConfiguration, HTTPServerDescriptor descriptor, HashMap<String, String> request, OutputStream os, String pathValue) throws IOException { int forcedPort = 0; if (httpServerConfiguration.serverPort != descriptor.port) { forcedPort = httpServerConfiguration.serverPort; } boolean keepAlive = doProxy(descriptor, request, os, pathValue, forcedPort); if (!httpServerConfiguration.doKeepAlive) { keepAlive = false; } return keepAlive; } private boolean doProxy(HTTPServerDescriptor descriptor, HashMap<String, String> request, OutputStream os, String pathValue, int forcedPort) throws IOException { boolean keepAlive = false; String remoteUrl = getUrl(descriptor, request, pathValue, forcedPort); if (log.isDebugEnabled()) { log.debug(String.format("doProxy connecting to '%s'", remoteUrl)); } HttpURLConnection connection = (HttpURLConnection) new URL(remoteUrl).openConnection(); for (String key : request.keySet()) { if (!data.equals(key) && !method.equals(key) && !version.equals(key) && !path.equals(key) && !parameters.equals(key)) { connection.setRequestProperty(key, request.get(key)); } } // Do not follow HTTP redirects connection.setInstanceFollowRedirects(false); connection.setRequestMethod(request.get(method)); String additionalData = request.get(data); if (additionalData != null) { if ("/nav/auth".equals(pathValue) && additionalData.contains("&consoleid=")) { // Remove the "consoleid" parameter as it is recognized as invalid. // The dummy value returned by sceOpenPSIDGetPSID is not valid. additionalData = additionalData.replaceAll("\\&consoleid=[0-9a-fA-F]*", ""); } connection.setDoOutput(true); OutputStream dataStream = connection.getOutputStream(); dataStream.write(additionalData.getBytes()); dataStream.close(); } connection.connect(); int dataLength = connection.getContentLength(); byte[] buffer = new byte[100000]; int length = 0; boolean endOfInputReached = false; InputStream in = null; try { in = connection.getInputStream(); while (length < buffer.length) { int l = in.read(buffer, length, buffer.length - length); if (l < 0) { endOfInputReached = true; break; } length += l; } } catch (IOException e) { log.debug("doProxy", e); } String bufferString = new String(buffer, 0, length); boolean bufferPatched = false; if (bufferString.contains("https://legaldoc.dl.playstation.net")) { bufferString = bufferString.replace("https://legaldoc.dl.playstation.net", "http://legaldoc.dl.playstation.net"); bufferPatched = true; } if (bufferPatched) { buffer = bufferString.getBytes(); length = buffer.length; // Also update the "Content-Length" header if it was specified if (dataLength >= 0) { dataLength = length; } } sendHTTPResponseCode(os, connection.getResponseCode(), connection.getResponseMessage()); // Only send a "Content-Length" header if the remote server did send it if (dataLength >= 0) { sendResponseHeader(os, contentLength, dataLength); } for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) { String key = entry.getKey(); if (key != null && !"transfer-encoding".equals(key.toLowerCase())) { for (String value : entry.getValue()) { // Ignore "Set-Cookie" with an empty value if ("Set-Cookie".equalsIgnoreCase(key) && value.length() == 0) { continue; } // If we changed "https" into "http", remove the information that the cookie can // only be sent over https, otherwise, it will be lost. if (forcedPort == 443 && "Set-Cookie".equalsIgnoreCase(key)) { value = value.replace("; Secure", ""); } // If we changed "https" into "http", keep redirecting to the // http address instead of https. if (forcedPort == 443 && "Location".equalsIgnoreCase(key)) { if (value.startsWith("https:")) { value = value.replaceFirst("https:", "http:"); } } sendResponseHeader(os, key, value); if ("connection".equalsIgnoreCase(key) && "keep-alive".equalsIgnoreCase(value)) { keepAlive = true; } if ("content-type".equalsIgnoreCase(key) && "application/x-i-5-ticket".equalsIgnoreCase(value) && length > 0) { ticket = new SceNpTicket(); ticket.read(buffer, 0, length); } } } } sendEndOfHeaders(os); if (log.isDebugEnabled()) { log.debug(String.format("doProxy%s:\n%s", (bufferPatched ? " (response patched)" : ""), Utilities.getMemoryDump(buffer, 0, length))); } os.write(buffer, 0, length); if (in != null) { while (!endOfInputReached) { length = 0; try { while (length < buffer.length) { int l = in.read(buffer, length, buffer.length - length); if (l < 0) { endOfInputReached = true; break; } length += l; } } catch (IOException e) { log.debug("doProxy", e); } os.write(buffer, 0, length); } in.close(); } return keepAlive; } private HttpServerConfiguration getHttpServerConfiguration(String serverName, String pathValue) { if (serverName != null) { for (HttpServerConfiguration httpServerConfiguration : HTTPConfiguration.doProxyServers) { if (httpServerConfiguration.serverName.equals(serverName)) { boolean found = true; if (httpServerConfiguration.fakedPaths != null) { for (String fakedPath : httpServerConfiguration.fakedPaths) { if (fakedPath.equals(pathValue)) { found = false; break; } } } if (found) { return httpServerConfiguration; } } } } return null; } private boolean process(HTTPServerDescriptor descriptor, HashMap<String, String> request, OutputStream os) throws IOException { boolean keepAlive = false; try { String pathValue = request.get(path); String baseUrl = getBaseUrl(descriptor, request, 0); if (pathValue.startsWith(baseUrl)) { pathValue = pathValue.substring(baseUrl.length() - 1); } HttpServerConfiguration httpServerConfiguration = getHttpServerConfiguration(request.get(host), pathValue); if (httpServerConfiguration != null) { keepAlive = doProxy(httpServerConfiguration, descriptor, request, os, pathValue); // } else if ("auth.np.ac.playstation.net".equals(request.get(host)) && "/nav/auth".equals(pathValue)) { // sendNpNavAuth(request.get(data), os); // } else if ("getprof.gb.np.community.playstation.net".equals(request.get(host)) && "/basic_view/sec/get_self_profile".equals(pathValue)) { // sendNpGetSelfProfile(request.get(data), os); } else if ("commerce.np.ac.playstation.net".equals(request.get(host)) && "/cap.m".equals(pathValue)) { sendCapM(request.get(data), os); } else if ("commerce.np.ac.playstation.net".equals(request.get(host)) && "/kdp.m".equals(pathValue)) { sendKdpM(request.get(data), os); } else if ("video.dl.playstation.net".equals(request.get(host)) && pathValue.matches("/cdn/video/[A-Z][A-Z]/g")) { sendVideoStore(os); } else if ("GET".equals(request.get(method))) { if ("/".equals(pathValue)) { sendResponseFile(os, rootDirectory + "/" + indexFile); } else if ("/screen.png".equals(pathValue)) { sendScreenImage(os, "png"); } else if ("/screen.jpg".equals(pathValue)) { sendScreenImage(os, "jpg"); } else if ("/screen.mjpg".equals(pathValue)) { sendVideoMJPG(os); } else if ("/screen.raw".equals(pathValue)) { sendVideoRAW(os); } else if ("/screen.craw".equals(pathValue)) { sendVideoCompressedRAW(os); } else if ("/audio.wav".equals(pathValue)) { sendAudioWAV(os); } else if ("/audio.raw".equals(pathValue)) { sendAudioRAW(os); } else if ("/controls".equals(pathValue)) { processControls(os, request.get(parameters)); } else if (pathValue.startsWith(iconDirectory)) { sendIcon(os, pathValue); } else if (pathValue.startsWith(isoDirectory)) { sendIso(request, os, pathValue, true); } else if (widgetlistFile.equals(pathValue)) { sendWidgetlist(descriptor, request, os, pathValue); } else if (pathValue.startsWith("/" + widgetDirectory + "/")) { sendWidget(os, request.get(parameters), rootDirectory + pathValue); } else if (pathValue.startsWith("/" + naclDirectory + "/")) { sendNaClResponse(os, pathValue.substring(6)); } else if (pathValue.endsWith(".html")) { sendResponseFile(os, rootDirectory + pathValue); } else if (pathValue.endsWith(".txt")) { sendResponseFile(os, rootDirectory + pathValue); } else if (pathValue.endsWith(".xml")) { sendResponseFile(os, rootDirectory + pathValue); } else { sendErrorNotFound(os); } } else if ("HEAD".equals(request.get(method))) { if (pathValue.startsWith(isoDirectory)) { sendIso(request, os, pathValue, false); } else { sendErrorNotFound(os); } } else { sendError(os, 405); } } catch (SocketException e) { // Ignore exception (e.g. Connection reset by peer) keepAlive = false; } return keepAlive; } private static String guessMimeType(String fileName) { if (fileName != null) { if (fileName.endsWith(".js")) { return "application/javascript"; } else if (fileName.endsWith(".html")) { return "text/html"; } else if (fileName.endsWith(".css")) { return "text/css"; } else if (fileName.endsWith(".png")) { return "image/png"; } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) { return "image/jpeg"; } else if (fileName.endsWith(".xml")) { return "text/xml"; } else if (fileName.endsWith(".zip")) { return "application/zip"; } } return "application/octet-stream"; } private void sendRedirect(OutputStream os, String redirect) throws IOException { sendHTTPResponseCode(os, 302); sendResponseHeader(os, "Location", redirect); sendEndOfHeaders(os); } private void sendResponseFile(OutputStream os, String fileName) throws IOException { sendResponseFile(os, getClass().getResourceAsStream(fileName), guessMimeType(fileName)); } private void sendResponseFile(OutputStream os, InputStream is, String contentType) throws IOException { byte[] buffer = new byte[1000]; int contentLength = 0; if (is != null) { while (true) { if (buffer.length - contentLength < 1000) { buffer = Utilities.extendArray(buffer, 1000); } int length = is.read(buffer, contentLength, buffer.length - contentLength); if (length < 0) { break; } contentLength += length; } is.close(); } sendOK(os); if (contentType != null) { sendResponseHeader(os, "Content-Type", contentType); } if (contentLength > 0) { sendResponseHeader(os, "Content-Length", contentLength); } sendEndOfHeaders(os); if (contentLength > 0) { os.write(buffer, 0, contentLength); } } private void sendResponseLine(OutputStream os, String line) throws IOException { if (log.isDebugEnabled()) { log.debug(String.format("Response: %s", line)); } os.write(line.getBytes()); os.write(eol.getBytes()); } private static String guessHTTPResponseCodeMsg(int code) { switch (code) { case 200: return "OK"; case 206: return "Partial Content"; case 302: return "Found"; case 404: return "Not Found"; case 405: return "Method Not Allowed"; } return ""; } private void sendHTTPResponseCode(OutputStream os, int code, String msg) throws IOException { sendResponseLine(os, String.format("HTTP/1.1 %d %s", code, msg)); } private void sendHTTPResponseCode(OutputStream os, int code) throws IOException { sendHTTPResponseCode(os, code, guessHTTPResponseCodeMsg(code)); } private void sendOK(OutputStream os) throws IOException { sendHTTPResponseCode(os, 200); } private void sendResponseHeader(OutputStream os, String name, String value) throws IOException { sendResponseLine(os, String.format("%s: %s", name, value)); } private void sendResponseHeader(OutputStream os, String name, int value) throws IOException { sendResponseHeader(os, name, String.valueOf(value)); } private void sendResponseHeader(OutputStream os, String name, long value) throws IOException { sendResponseHeader(os, name, String.valueOf(value)); } private void sendNoCache(OutputStream os) throws IOException { sendResponseHeader(os, "Cache-Control", "no-cache"); sendResponseHeader(os, "Cache-Control", "private"); } private void sendEndOfHeaders(OutputStream os) throws IOException { sendResponseLine(os, ""); } private void sendError(OutputStream os, int code) throws IOException { sendHTTPResponseCode(os, code); sendEndOfHeaders(os); } private void sendErrorNotFound(OutputStream os) throws IOException { sendError(os, 404); } private void sendScreenImage(OutputStream os, String fileFormat) throws IOException { String fileName = String.format("%s%cscreen.%s", Settings.getInstance().readString("emu.tmppath"), File.separatorChar, fileFormat); File file = new File(fileName); file.deleteOnExit(); Rectangle rect = Emulator.getMainGUI().getCaptureRectangle(); if (log.isDebugEnabled()) { log.debug(String.format("Capturing screen from %s to %s", rect, fileName)); } BufferedImage img = captureRobot.createScreenCapture(rect); try { file.delete(); ImageIO.write(img, fileFormat, file); img.flush(); } catch (IOException e) { log.error("Error saving screenshot", e); } if (file.canRead()) { int length = (int) file.length(); sendOK(os); sendNoCache(os); sendResponseHeader(os, "Content-Type", String.format("image/%s", fileFormat)); sendResponseHeader(os, "Content-Length", length); sendEndOfHeaders(os); byte[] buffer = new byte[length]; InputStream is = new FileInputStream(file); length = is.read(buffer); is.close(); file.delete(); os.write(buffer, 0, length); } else { sendErrorNotFound(os); } } private void sendVideoMJPG(OutputStream os) throws IOException { String fileFormat = "jpg"; String fileName = String.format("%s%cscreen.%s", Settings.getInstance().readString("emu.tmppath"), File.separatorChar, fileFormat); File file = new File(fileName); file.deleteOnExit(); Rectangle rect = Emulator.getMainGUI().getCaptureRectangle(); if (log.isDebugEnabled()) { log.debug(String.format("Capturing screen from %s to %s", rect, fileName)); } startDisplayAction(); try { sendOK(os); sendNoCache(os); sendResponseHeader(os, "Content-Type", String.format("multipart/x-mixed-replace; boundary=%s", boundary)); sendEndOfHeaders(os); while (true) { BufferedImage img = getScreenImage(rect); try { file.delete(); ImageIO.write(img, fileFormat, file); img.flush(); } catch (IOException e) { log.error("Error saving screenshot", e); } if (file.canRead()) { int length = (int) file.length(); if (log.isDebugEnabled()) { log.debug(String.format("Sending video image length=%d", length)); } byte[] buffer = new byte[length]; InputStream is = new FileInputStream(file); length = is.read(buffer); is.close(); sendResponseLine(os, boundary); sendResponseHeader(os, "Content-Type", "image/jpeg"); sendResponseHeader(os, "Content-Length", length); sendEndOfHeaders(os); os.write(buffer, 0, length); sendEndOfHeaders(os); os.flush(); } else { if (log.isDebugEnabled()) { log.debug(String.format("Cannot read capture file %s", file)); } break; } } } finally { stopDisplayAction(); file.delete(); } } private void sendAudioWAV(OutputStream os) throws IOException { if (log.isDebugEnabled()) { log.debug(String.format("sendAudioWAV")); } sendOK(os); sendResponseHeader(os, "Content-Type", "audio/wav"); sendEndOfHeaders(os); int channels = 2; int sampleRate = 44100; byte[] silenceBuffer = new byte[1024 * channels * 2]; byte[] header = new byte[100]; int n = 0; // "RIFF" header[n++] = 'R'; header[n++] = 'I'; header[n++] = 'F'; header[n++] = 'F'; // Total file size header[n++] = 0; header[n++] = 0; header[n++] = 0; header[n++] = 0x7F; // "WAVE" header[n++] = 'W'; header[n++] = 'A'; header[n++] = 'V'; header[n++] = 'E'; // "fmt " tag header[n++] = 'f'; header[n++] = 'm'; header[n++] = 't'; header[n++] = ' '; // length of "fmt " tag header[n++] = 16; header[n++] = 0; header[n++] = 0; header[n++] = 0; // format tag (1 == PCM) header[n++] = 1; header[n++] = 0; // channels header[n++] = (byte) channels; header[n++] = 0; // sample rate header[n++] = (byte) ((sampleRate ) & 0xFF); header[n++] = (byte) ((sampleRate >> 8) & 0xFF); header[n++] = 0; header[n++] = 0; // bytes per second int bytesPerSecond = 2 * channels * sampleRate; header[n++] = (byte) ((bytesPerSecond ) & 0xFF); header[n++] = (byte) ((bytesPerSecond >> 8) & 0xFF); header[n++] = (byte) ((bytesPerSecond >> 16) & 0xFF); header[n++] = (byte) ((bytesPerSecond >> 24) & 0xFF); // block align header[n++] = (byte) (2 * channels); header[n++] = 0; // bits per sample header[n++] = 16; header[n++] = 0; os.write(header, 0, n); byte[] dataHeader = new byte[8]; dataHeader[0] = 'd'; dataHeader[1] = 'a'; dataHeader[2] = 't'; dataHeader[3] = 'a'; long start = System.currentTimeMillis(); while (true) { long now = System.currentTimeMillis(); while (now < start) { Utilities.sleep(1, 0); now = System.currentTimeMillis(); } byte[] buffer = Modules.sceAudioModule.audioData; if (buffer == null) { buffer = silenceBuffer; } int length = buffer.length; dataHeader[4] = (byte) ((length ) & 0xFF); dataHeader[5] = (byte) ((length >> 8) & 0xFF); dataHeader[6] = (byte) ((length >> 16) & 0xFF); dataHeader[7] = (byte) ((length >> 24) & 0xFF); os.write(dataHeader); os.write(buffer, 0, length); if (log.isDebugEnabled()) { log.debug(String.format("sendAudioWAV sent %d bytes", length)); } start += 1000 * length / (2 * channels * sampleRate); } } private void sendAudioRAW(OutputStream os) throws IOException { if (log.isDebugEnabled()) { log.debug(String.format("sendAudioRAW")); } sendOK(os); sendResponseHeader(os, "Content-Type", "audio/raw"); sendEndOfHeaders(os); int channels = 2; int sampleRate = 44100; byte[] silenceBuffer = new byte[1024 * channels * 2]; long start = System.currentTimeMillis(); while (true) { long now = System.currentTimeMillis(); while (now < start) { Utilities.sleep(1, 0); now = System.currentTimeMillis(); } byte[] buffer = Modules.sceAudioModule.audioData; if (buffer == null) { buffer = silenceBuffer; } int length = buffer.length; os.write(buffer, 0, length); if (log.isDebugEnabled()) { log.debug(String.format("sendAudioRAW sent %d bytes", length)); } start += 1000 * length / (2 * channels * sampleRate); } } private BufferedImage getScreenImage(Rectangle rect) { if (currentDisplayImage != null) { return currentDisplayImage; } currentDisplayImageHasAlpha = true; return captureRobot.createScreenCapture(rect); } private void sendVideoRAW(OutputStream os) throws IOException { Rectangle rect = Emulator.getMainGUI().getCaptureRectangle(); if (log.isDebugEnabled()) { log.debug(String.format("Capturing RAW screen from %s", rect)); } startDisplayAction(); try { sendOK(os); sendNoCache(os); sendResponseHeader(os, "Content-Type", "video/raw"); sendEndOfHeaders(os); byte[] pixels = new byte[rect.width * rect.height * 3]; while (true) { BufferedImage img = getScreenImage(rect); int i = 0; for (int y = 0; y < img.getHeight(); y++) { for (int x = 0; x < img.getWidth(); x++, i+= 3) { int color = img.getRGB(x, y); pixels[i + 0] = (byte) ((color >> 16) & 0xFF); pixels[i + 1] = (byte) ((color >> 8) & 0xFF); pixels[i + 2] = (byte) ((color >> 0) & 0xFF); } } os.write(pixels); if (log.isDebugEnabled()) { log.debug(String.format("sendVideoRAW sent %dx%d image (%d bytes)", rect.width, rect.height, pixels.length)); } os.flush(); } } finally { stopDisplayAction(); } } private int storeCompressedPixel(int color, byte[] buffer, int compressedLength, boolean rle, int count) { if (!rle) { count |= 0x80; } buffer[compressedLength++] = (byte) count; buffer[compressedLength++] = (byte) ((color >> 16) & 0xFF); buffer[compressedLength++] = (byte) ((color >> 8) & 0xFF); buffer[compressedLength++] = (byte) ((color >> 0) & 0xFF); return compressedLength; } private int compressImage(int width, int height, int[] image, int[] previousImage, byte[] buffer, int compressedLength) { int i = 0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; ) { int color = image[i]; int previousColor = previousImage[i]; i++; x++; // RLE? if (x < width && color == image[i]) { if (color == previousColor) { // Both methods apply: RLE and matching previous video. // Choose the one matching the most pixels. boolean rleFailed = false; boolean previousFailed = false; int count; for (count = 0; x < width && count < MAX_COMPRESSED_COUNT; count++) { boolean rleMatch = !rleFailed && image[i] == color; boolean previousMatch = !previousFailed && image[i] == previousImage[i]; if (rleMatch) { if (previousMatch) { // OK, both still matching } else { // Continue RLE, previous image no longer matching previousFailed = true; } } else { if (previousMatch) { // Continue testing previous image, RLE no longer matching rleFailed = true; } else { // Both tests failed, abort break; } } i++; x++; } // If none failed, prefer RLE encoding (because faster decoding) if (!rleFailed) { compressedLength = storeCompressedPixel(color, buffer, compressedLength, true, count); } else { // Encode to match the previous image if (x < width) { color = image[i++]; x++; } else if (count > 0) { // Past screen width, take previous pixel color = image[i - 1]; count--; } compressedLength = storeCompressedPixel(color, buffer, compressedLength, false, count); } } else { // Only RLE, not matching previous image i++; x++; int count; for (count = 1; x < width; count++) { if (color != image[i] || count >= MAX_COMPRESSED_COUNT) { break; } i++; x++; } compressedLength = storeCompressedPixel(color, buffer, compressedLength, true, count); } } else if (x < width && color == previousColor) { // No RLE, only matching previous image int count; for (count = 0; x < width; count++) { color = image[i]; previousColor = previousImage[i]; i++; x++; if (color != previousColor || count >= MAX_COMPRESSED_COUNT || x >= width) { break; } } compressedLength = storeCompressedPixel(color, buffer, compressedLength, false, count); } else { // No RLE, not matching previous image compressedLength = storeCompressedPixel(color, buffer, compressedLength, true, 0); } } } return compressedLength; } private static void write32(byte[] buffer, int offset, int value) { buffer[offset + 0] = (byte) ((value >> 0) & 0xFF); buffer[offset + 1] = (byte) ((value >> 8) & 0xFF); buffer[offset + 2] = (byte) ((value >> 16) & 0xFF); buffer[offset + 3] = (byte) ((value >> 24) & 0xFF); } private void sendVideoCompressedRAW(OutputStream os) throws IOException { Rectangle rect = Emulator.getMainGUI().getCaptureRectangle(); if (log.isDebugEnabled()) { log.debug(String.format("Capturing compressed RAW screen from %s", rect)); } startDisplayAction(); try { sendOK(os); sendNoCache(os); sendResponseHeader(os, "Content-Type", "video/compressed-raw"); sendEndOfHeaders(os); int[] image = new int[0]; int[] previousImage = null; byte[] buffer = null; while (true) { BufferedImage img = getScreenImage(rect); if (img != null) { int width = img.getWidth(); int height = img.getHeight(); int imageSize = width * height; // Is the image now larger? if (image.length < imageSize) { // Resize the buffers image = new int[imageSize]; previousImage = new int[imageSize]; buffer = new byte[imageSize * 4 + 12]; } img.getRGB(0, 0, width, height, image, 0, width); if (currentDisplayImageHasAlpha) { for (int i = 0; i < imageSize; i++) { image[i] &= 0x00FFFFFF; } } // The first 12 bytes of the buffer will contain // - the length of the compressed image (including the 12 bytes header) // - the image width in pixels // - the image height in pixels int compressedLength = compressImage(width, height, image, previousImage, buffer, 12); // Store the length of the compressed image and its size write32(buffer, 0, compressedLength); write32(buffer, 4, width); write32(buffer, 8, height); os.write(buffer, 0, compressedLength); if (log.isDebugEnabled()) { log.debug(String.format("sendVideoCompressedRAW sent %dx%d image (%d bytes, compression rate %.1f%%)", width, height, compressedLength, 100f * compressedLength / (image.length * 3))); } os.flush(); // Swap previous and current image buffers int[] swapImage = image; image = previousImage; previousImage = swapImage; } else { Utilities.sleep(10, 0); } } } finally { stopDisplayAction(); } } private void sendIso(HashMap<String, String> request, OutputStream os, String pathValue, boolean sendContent) throws IOException { String isoFileName = pathValue.substring(isoDirectory.length()); if (log.isDebugEnabled()) { log.debug(String.format("sendIso '%s'", isoFileName)); } boolean contentSent = false; try { UmdIsoReader iso = getIso(isoFileName); if (iso != null) { if (sendContent) { String range = request.get("range"); if (range != null) { if (range.startsWith("bytes=")) { String rangeValues = range.substring(6); String[] ranges = rangeValues.split("-"); if (ranges != null && ranges.length == 2) { long from = Long.parseLong(ranges[0]); long to = Long.parseLong(ranges[1]); if (log.isDebugEnabled()) { log.debug(String.format("sendIso bytes from=0x%X, to=0x%X, length=0x%X", from, to, to - from + 1)); } sendHTTPResponseCode(os, 206); sendResponseHeader(os, "Content-Range", String.format("bytes %d-%d", from, to)); sendEndOfHeaders(os); sendIsoContent(os, iso, from, to); contentSent = true; } else { log.warn(String.format("sendIso: unsupported range format '%s'", range)); } } else { log.warn(String.format("sendIso: unsupported range format '%s'", range)); } } } else { sendOK(os); long isoLength = iso.getNumSectors() * (long) sectorLength; if (log.isDebugEnabled()) { log.debug(String.format("sendIso returning content-length=0x%X", isoLength)); } sendResponseHeader(os, "Content-Length", isoLength); sendResponseHeader(os, "Accept-Ranges", "bytes"); sendEndOfHeaders(os); contentSent = true; } } } catch (IOException e) { contentSent = false; } if (!contentSent) { sendErrorNotFound(os); } } private UmdIsoReader getIso(String isoFileName) throws FileNotFoundException, IOException { UmdIsoReader iso = null; if (isoFileName.equals(previousIsoFilename)) { iso = previousUmdIsoReader; if (log.isDebugEnabled()) { log.debug(String.format("Reusing previous UmdIsoReader for '%s'", isoFileName)); } } else { if ("umdbuffer.iso".equals(isoFileName)) { iso = new UmdIsoReader((String) null, true); } else { File[] umdPaths = MainGUI.getUmdPaths(false); for (int i = 0; i < umdPaths.length; i++) { File isoPath = new File(String.format("%s%s%s", umdPaths[i], File.separator, isoFileName)); if (isoPath.exists()) { iso = new UmdIsoReader(isoPath.getPath(), false); break; } } } if (previousUmdIsoReader != null) { previousUmdIsoReader.close(); } previousIsoFilename = isoFileName; previousUmdIsoReader = iso; } return iso; } private void sendIsoContent(OutputStream os, UmdIsoReader iso, long from, long to) throws IOException { int startSector = (int) (from / sectorLength); int endSector = (int) ((to + sectorLength) / sectorLength); int numberSectors = endSector - startSector; byte[] buffer = new byte[numberSectors * UmdIsoFile.sectorLength]; iso.readSectors(startSector, numberSectors, buffer, 0); int startSectorOffset = (int) (from - startSector * (long) sectorLength); int length = (int) (to - from + 1); os.write(buffer, startSectorOffset, length); } private static Map<String, String> parseParameters(String parameters) { Map<String, String> result = new HashMap<String, String>(); String[] nvpairs = parameters.split("&"); for (String nvpair : nvpairs) { String[] nv = nvpair.split("=", 2); if (nv != null && nv.length >= 2) { String name = nv[0]; String value = decodePath(nv[1]); result.put(name, value); } } return result; } private void processControls(OutputStream os, String parameters) throws IOException { if (parameters != null) { if (log.isDebugEnabled()) { log.debug(String.format("processControls %s", parameters)); } Map<String, String> event = parseParameters(parameters); String type = event.get("type"); if ("keyup".equals(type)) { int code = Integer.parseInt(event.get("keyCode")); if (code == runMapping) { Emulator.getMainGUI().run(); } else if (code == pauseMapping) { Emulator.getMainGUI().pause(); } else if (code == resetMapping) { Emulator.getMainGUI().reset(); } else if (keyMapping.containsKey(code)) { State.controller.keyReleased(keyMapping.get(code)); } else { State.controller.keyReleased(code); } } else if ("keydown".equals(type)) { int code = Integer.parseInt(event.get("keyCode")); if (keyMapping.containsKey(code)) { State.controller.keyPressed(keyMapping.get(code)); } else { State.controller.keyPressed(code); } } else if ("run".equals(type)) { Emulator.getMainGUI().run(); } else if ("pause".equals(type)) { Emulator.getMainGUI().pause(); } else if ("reset".equals(type)) { Emulator.getMainGUI().reset(); } else if ("mapping".equals(type)) { processKeyMapping(event); } else { log.warn(String.format("processControls unknown type '%s'", type)); } } sendOK(os); sendEndOfHeaders(os); } private void processKeyMapping(Map<String, String> event) { for (String key : event.keySet()) { String value = event.get(key); if (value.length() == 0) { // Silently ignore empty values } else if ("run".equals(key)) { runMapping = Integer.parseInt(value); } else if ("pause".equals(key)) { pauseMapping = Integer.parseInt(value); } else if ("reset".equals(key)) { resetMapping = Integer.parseInt(value); } else if (!"type".equals(key)) { try { keyCode code = keyCode.valueOf(key); keyMapping.put(Integer.parseInt(value), code); } catch (IllegalArgumentException e) { // Ignore exception } } } } private void sendIcon(OutputStream os, String pathValue) throws IOException { sendResponseFile(os, "/jpcsp/icons/" + pathValue.substring(iconDirectory.length())); } private String readInputStream(InputStream is) throws IOException { byte[] buffer = new byte[100000]; int length = is.read(buffer); return new String(buffer, 0, length); } private String readResource(String name) throws IOException { InputStream is = getClass().getResourceAsStream(name); return readInputStream(is); } private String extractTemplateRepeat(String template) { int repeat = template.indexOf("$REPEAT"); int end = template.indexOf("$END"); if (repeat < 0 || end < 0 || end < repeat) { return ""; } return template.substring(repeat + 7, end); } private String replaceTemplate(String template, String name, String value) { return template.replace(name, value); } private String replaceTemplateRepeat(String template, String value) { int repeat = template.indexOf("$REPEAT"); int end = template.indexOf("$END"); if (repeat < 0 || end < 0 || end < repeat) { return template; } return template.substring(0, repeat) + value + template.substring(end + 4); } private String[] getWidgetList() throws IOException { List<String> list = new LinkedList<String>(); BufferedReader dir = null; try { dir = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(widgetPath))); while (true) { String entry = dir.readLine(); if (entry == null) { break; } list.add(entry); } } finally { if (dir != null) { dir.close(); } } return list.toArray(new String[list.size()]); } private InputStream getFileFromZip(String zipFileName, String fileName) throws IOException { if (File.separatorChar != '/') { fileName = fileName.replace('/', File.separatorChar); } InputStream zipInput = getClass().getResourceAsStream(zipFileName); if (zipInput != null) { ZipInputStream zipContent = new ZipInputStream(zipInput); while (true) { ZipEntry entry = zipContent.getNextEntry(); if (entry == null) { break; } if (fileName.equalsIgnoreCase(entry.getName())) { return zipContent; } } } return null; } private void sendNaClResponse(OutputStream os, String pathValue) throws IOException { int sepIndex = pathValue.indexOf("/"); if (sepIndex < 0) { if (pathValue.isEmpty() || indexFile.equals(pathValue)) { String template = readResource(rootDirectory + "/" + naclDirectory + "/" + indexFile); String repeat = extractTemplateRepeat(template); StringBuilder lines = new StringBuilder(); for (String widget : getWidgetList()) { lines.append(replaceTemplate(repeat, "$NAME", widget.replace(".zip", ""))); } String html = replaceTemplateRepeat(template, lines.toString()); if (log.isDebugEnabled()) { log.debug(String.format("sendNaClResponse returning:\n%s", html)); } sendResponseFile(os, new ByteArrayInputStream(html.getBytes()), guessMimeType(indexFile)); } else { sendRedirect(os, pathValue + "/" + indexFile); } return; } String zipFileName = pathValue.substring(0, sepIndex); String resourceFileName = pathValue.substring(sepIndex + 1); if (resourceFileName.startsWith("$MANAGER_WIDGET/")) { // Sending dummy Widget.js and TVKeyValue.js sendResponseFile(os, rootDirectory + "/" + resourceFileName.substring(1)); } else { zipFileName = String.format("%s/%s.zip", widgetPath, zipFileName); InputStream zipContent = getFileFromZip(zipFileName, resourceFileName); if (zipContent != null) { sendResponseFile(os, zipContent, guessMimeType(resourceFileName)); } else { sendError(os, 404); } } } private void startDisplayAction() { displayActionUsageCount++; if (displayAction == null) { displayAction = new DisplayAction(); Modules.sceDisplayModule.addDisplayAction(displayAction); } } private void stopDisplayAction() { displayActionUsageCount--; if (displayAction != null && displayActionUsageCount <= 0) { Modules.sceDisplayModule.removeDisplayAction(displayAction); displayAction = null; } } private static String getBaseUrl(HTTPServerDescriptor descriptor, HashMap<String, String> request, int forcedPort) { String hostName = request.get(host); int port = forcedPort > 0 ? forcedPort : descriptor.getPort(); String protocol = request.get("x-forwarded-proto"); if (protocol == null) { if (forcedPort > 0) { protocol = forcedPort == 443 ? "https" : "http"; } else { protocol = descriptor.isSsl() ? "https" : "http"; } } StringBuilder baseUrl = new StringBuilder(); baseUrl.append(protocol); baseUrl.append("://"); baseUrl.append(hostName); // Add the port if this is not the default one if (port != getDefaultPortForProtocol(protocol)) { baseUrl.append(":"); baseUrl.append(port); } baseUrl.append("/"); return baseUrl.toString(); } private static String getUrl(HTTPServerDescriptor descriptor, HashMap<String, String> request, String pathValue, int forcedPort) { if (pathValue.startsWith("https://") || pathValue.startsWith("http://")) { int endOfPath = pathValue.indexOf("/", 8); if (endOfPath >= 0) { pathValue = pathValue.substring(endOfPath); } else { pathValue = ""; } } String baseUrl = getBaseUrl(descriptor, request, forcedPort); String query = ""; if (request.containsKey(parameters)) { query = "?" + request.get(parameters); } if (pathValue == null) { return baseUrl + query; } if (pathValue.startsWith("/")) { pathValue = pathValue.substring(1); } return baseUrl + pathValue + query; } private static String getArchitecture(HashMap<String, String> request) { String architecture = null; String userAgent = request.get("user-agent"); if (userAgent != null && userAgent.indexOf("SmartTV") > 0) { // Samsung Smart TV is using the ARM architecture architecture = Integer.toString(Elf32Header.E_MACHINE_ARM); } return architecture; } /* * Send the widgetlist.xml as expected by a Samsung Smart TV. * * The XML response is build dynamically, based on the packages available * under the Widget directory. */ private void sendWidgetlist(HTTPServerDescriptor descriptor, HashMap<String, String> request, OutputStream os, String pathValue) throws IOException { String template = readResource(rootDirectory + widgetlistFile + ".template"); String repeat = extractTemplateRepeat(template); String architecture = getArchitecture(request); String architectureParam = ""; if (architecture != null) { architectureParam = "?architecture=" + architecture; } StringBuilder list = new StringBuilder(); Pattern pattern = Pattern.compile("<widgetname(\\s+itemtype=\"string\")?>(.*)</widgetname>", Pattern.MULTILINE | Pattern.CASE_INSENSITIVE); for (String widget : getWidgetList()) { String zipFileName = String.format("%s/%s", widgetPath, widget); InputStream configXml = getFileFromZip(zipFileName, "config.xml"); if (configXml != null) { String xml = readInputStream(configXml); Matcher matcher = pattern.matcher(xml); if (matcher.find()) { String widgetName = matcher.group(2); String downloadUrl = String.format("%s%s/%s%s", getBaseUrl(descriptor, request, 0), widgetDirectory, widget, architectureParam); String entry = replaceTemplate(repeat, "$WIDGETNAME", widgetName); entry = replaceTemplate(entry, "$DOWNLOADURL", downloadUrl); list.append(entry); } } } String xml = replaceTemplateRepeat(template, list.toString()); sendResponseFile(os, new ByteArrayInputStream(xml.getBytes()), guessMimeType(pathValue)); } private boolean isMatchingELFArchitecture(byte[] header, int length, int machineArchitecture) throws IOException { ByteBuffer byteBuffer = ByteBuffer.wrap(header, 0, length); Elf32Header elfHeader = new Elf32Header(byteBuffer); return elfHeader.isValid() && elfHeader.getE_machine() == machineArchitecture; } private void sendWidget(OutputStream os, String parameters, String pathValue) throws IOException { String architecture = null; if (parameters != null) { Map<String, String> map = parseParameters(parameters); architecture = map.get("architecture"); } if (architecture == null) { sendResponseFile(os, pathValue); } else { // Filter the Widget zip file to only include the ELF files // matching the given architecture. // The Samsung Smart TV is rejecting the installation of a Widget // containing code for another architecture. int machineArchitecture = Integer.parseInt(architecture); ZipInputStream zin = new ZipInputStream(getClass().getResourceAsStream(pathValue)); ByteArrayOutputStream out = new ByteArrayOutputStream(1000000); ZipOutputStream zout = new ZipOutputStream(out); byte[] buffer = new byte[100000]; byte[] header = new byte[0x40]; while (true) { ZipEntry entry = zin.getNextEntry(); if (entry == null) { break; } int length = 0; boolean doCopy = true; if (entry.getName().endsWith(".nexe")) { length = zin.read(header); if (!isMatchingELFArchitecture(header, length, machineArchitecture)) { if (log.isDebugEnabled()) { log.debug(String.format("Skipping the Widget entry '%s' because it is not matching the architecture 0x%X", entry.getName(), machineArchitecture)); } doCopy = false; } } if (doCopy) { zout.putNextEntry(entry); zout.write(header, 0, length); while (true) { length = zin.read(buffer); if (length <= 0) { break; } zout.write(buffer, 0, length); } } } zin.close(); zout.close(); sendResponseFile(os, new ByteArrayInputStream(out.toByteArray()), guessMimeType(pathValue)); } } public Proxy getProxy() { return proxy; } public int getProxyPort() { return proxyPort; } public int getProxyAddress() { return proxyAddress; } public void sendNpNavAuth(String data, OutputStream os) throws IOException { Map<String, String> parameters = parseParameters(data); SceNpTicket ticket = new SceNpTicket(); ticket.version = 0x00000121; ticket.size = 0xF0; ticket.unknown = 0x3000; ticket.sizeParams = 0xA4; addTicketParam(ticket, "XXXXXXXXXXXXXXXXXXXX", 20); addTicketParam(ticket, 0); long now = System.currentTimeMillis(); addTicketDateParam(ticket, now); addTicketDateParam(ticket, now + 10 * 60 * 1000); // now + 10 minutes addTicketLongParam(ticket, 0L); // Used by DRM addTicketParam(ticket, TicketParam.PARAM_TYPE_STRING, "DummyOnlineID", 32); addTicketParam(ticket, "gb", 4); addTicketParam(ticket, TicketParam.PARAM_TYPE_STRING, "XX", 4); addTicketParam(ticket, parameters.get("serviceid"), 24); int status = 0; if (Modules.sceNpModule.parentalControl == sceNp.PARENTAL_CONTROL_ENABLED) { status |= STATUS_ACCOUNT_PARENTAL_CONTROL_ENABLED; } status |= (Modules.sceNpModule.getUserAge() & 0x7F) << 24; addTicketParam(ticket, status); addTicketParam(ticket); addTicketParam(ticket); ticket.unknownBytes = new byte[72]; if (log.isDebugEnabled()) { log.debug(String.format("sendNpNavAuth returning dummy ticket: %s", ticket)); } byte[] response = ticket.toByteArray(); sendOK(os); sendResponseHeader(os, "X-I-5-Status", "OK"); sendResponseHeader(os, "X-I-5-Version", "2.1"); sendResponseHeader(os, "Content-Length", response.length); sendResponseHeader(os, "Content-Type", "application/x-i-5-ticket"); sendEndOfHeaders(os); os.write(response); } public void sendNpGetSelfProfile(String data, OutputStream os) throws IOException { String xml = "<profile result=\"00\">"; xml += "<jid>DummyOnlineID@a8.gb.np.playstation.net</jid>"; xml += "<onlinename upd=\"0\">DummyOnlineID</onlinename>"; xml += "<country>gb</country>"; xml += "<language1>1</language1>"; xml += "<language2 />"; xml += "<language3 />"; xml += "<aboutme />"; xml += "<avatarurl id=\"0\">http://static-resource.np.community.playstation.net/avatar_s/default/DefaultAvatar_s.png</avatarurl>"; xml += "<ptlp>0</ptlp>"; xml += "</profile>"; byte[] response = xml.getBytes(); sendOK(os); sendResponseHeader(os, "Content-Length", response.length); sendResponseHeader(os, "Content-Type", "text/xml;charset=UTF-8"); sendEndOfHeaders(os); os.write(response); if (log.isDebugEnabled()) { log.debug(String.format("Response: %s", xml)); } } public void sendCapM(String data, OutputStream os) throws IOException { int responseLength = 4240; byte[] response = new byte[responseLength]; if (ticket != null) { TicketParam ticketParam = ticket.parameters.get(4); for (int i = 0; i < 8; i++) { response[i + 80] = ticketParam.getBytesValue()[7 - i]; } } sendOK(os); sendResponseHeader(os, "X-I-5-DRM-Version", "1.0"); sendResponseHeader(os, "X-I-5-DRM-Status", "OK; max_console=1; current_console=0"); sendResponseHeader(os, "Content-Length", responseLength); sendResponseHeader(os, "Content-Type", "application/x-i-5-drm"); sendEndOfHeaders(os); if (log.isDebugEnabled()) { log.debug(String.format("Response:%s", Utilities.getMemoryDump(response, 0, responseLength))); } os.write(response, 0, responseLength); } public void sendKdpM(String data, OutputStream os) throws IOException { Map<String, String> parameters = parseParameters(data); String productId = parameters.get("productid"); int responseLength = 4240; byte[] response = new byte[responseLength]; if (productId != null) { ByteBuffer buffer = ByteBuffer.wrap(response); buffer.position(16); Utilities.writeStringZ(buffer, productId); } if (ticket != null) { TicketParam ticketParam = ticket.parameters.get(4); for (int i = 0; i < 8; i++) { response[i + 80] = ticketParam.getBytesValue()[7 - i]; } } sendOK(os); sendResponseHeader(os, "X-I-5-DRM-Version", "1.0"); sendResponseHeader(os, "X-I-5-DRM-Status", "OK"); sendResponseHeader(os, "Content-Length", responseLength); sendResponseHeader(os, "Content-Type", "application/x-i-5-drm"); sendEndOfHeaders(os); if (log.isDebugEnabled()) { log.debug(String.format("Response:%s", Utilities.getMemoryDump(response, 0, responseLength))); } os.write(response, 0, responseLength); } public void sendVideoStore(OutputStream os) throws IOException { byte[] response = new byte[1]; response[0] = (byte) '3'; int responseLength = response.length; sendOK(os); sendResponseHeader(os, "Content-Length", responseLength); sendEndOfHeaders(os); if (log.isDebugEnabled()) { log.debug(String.format("Response:%s", Utilities.getMemoryDump(response, 0, responseLength))); } os.write(response, 0, responseLength); } }