/******************************************************************************* * Copyright 2013-2016 alladin-IT GmbH * * 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 at.alladin.rmbt.qos.testserver; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.security.GeneralSecurityException; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import org.postgresql.util.Base64; import at.alladin.rmbt.qos.testserver.ServerPreferences.TestServerServiceEnum; import at.alladin.rmbt.qos.testserver.entity.ClientToken; import at.alladin.rmbt.qos.testserver.servers.AbstractUdpServer; import at.alladin.rmbt.qos.testserver.udp.UdpPacketReceivedCallback; import at.alladin.rmbt.qos.testserver.udp.UdpTestCandidate; import at.alladin.rmbt.qos.testserver.udp.UdpTestCompleteCallback; import at.alladin.rmbt.qos.testserver.udp.VoipTestCandidate; import at.alladin.rmbt.qos.testserver.util.TestServerConsole; import at.alladin.rmbt.util.net.rtp.RealtimeTransportProtocol.PayloadType; import at.alladin.rmbt.util.net.rtp.RealtimeTransportProtocol.RtpException; import at.alladin.rmbt.util.net.rtp.RtpPacket; import at.alladin.rmbt.util.net.rtp.RtpUtil; import at.alladin.rmbt.util.net.rtp.RtpUtil.RtpQoSResult; /** * handles all client requests * @author lb * */ public class ClientHandler implements Runnable { public final static boolean ABORT_ON_DUPLICATE_UDP_PACKETS = false; public final static Pattern ID_REGEX_PATTERN = Pattern.compile("\\+ID([\\d]*)"); public final static Pattern TOKEN_REGEX_PATTERN = Pattern.compile("TOKEN ([\\d\\w-]*)_([\\d]*)_(.*)"); /** * enable/disable token check */ public final static boolean CHECK_TOKEN = false; private final ServerSocket serverSocket; private final Socket socket; protected final FilterInputStream in; protected final FilterOutputStream out; protected final BufferedReader reader; protected final String name; protected ConcurrentHashMap<Integer, UdpTestCandidate> clientUdpOutDataMap = new ConcurrentHashMap<>(); protected ConcurrentHashMap<Integer, UdpTestCandidate> clientUdpInDataMap = new ConcurrentHashMap<>(); protected ConcurrentHashMap<Integer, VoipTestCandidate> clientVoipDataMap = new ConcurrentHashMap<>(); /** * protocol version used by the client */ protected String clientProtocolVersion = QoSServiceProtocol.PROTOCOL_VERSION_1; /** * * @param serverSocket * @param socket * @throws IOException */ public ClientHandler(ServerSocket serverSocket, Socket socket) throws IOException { this.serverSocket = serverSocket; this.socket = socket; this.in = new BufferedInputStream(socket.getInputStream()); this.out = new FilterOutputStream(socket.getOutputStream()); this.reader = new BufferedReader(new InputStreamReader(in)); this.name = "[ClientHandler " + socket.getInetAddress().toString() + "]"; } /* * (non-Javadoc) * @see java.lang.Runnable#run() */ @Override public void run() { TestServerConsole.log("New connection from: " + socket.getInetAddress().toString(), 0, TestServerServiceEnum.TEST_SERVER); String message; String command = null; try { socket.setSoTimeout(QoSServiceProtocol.TIMEOUT_CLIENTHANDLER_CONNECTION_MIN_VALUE); out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_GREETING)); out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_ACCEPT_TOKEN)); message = reader.readLine(); TestServerConsole.log("GOT: " + message, 1, TestServerServiceEnum.TEST_SERVER); ClientToken token = checkToken(message); TestServerConsole.log("TOKEN OK", 1, TestServerServiceEnum.TEST_SERVER); out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_OK)); out.write(getBytesWithNewline(QoSServiceProtocol.RESPONSE_ACCEPT_COMMANDS)); boolean quit = false; TestServer.clientHandlerSet.add(this); while(!quit) { try { command = reader.readLine(); TestServerConsole.log("COMMAND: " + command + " from: " + socket.getInetAddress().toString(), 0, TestServerServiceEnum.TEST_SERVER); if (command != null) { if (command.startsWith(QoSServiceProtocol.CMD_NON_TRANSPARENT_PROXY_TEXT)) { runNonTransparentProxyTest(command); } else if (command.startsWith(QoSServiceProtocol.CMD_TCP_TEST_IN)) { runIncomingTcpTest(command, token); } else if (command.startsWith(QoSServiceProtocol.CMD_TCP_TEST_OUT)) { runOutgoingTcpTest(command, token); } else if (command.startsWith(QoSServiceProtocol.CMD_UDP_TEST_OUT)) { runOutgoingUdpTest(command, token); } else if (command.startsWith(QoSServiceProtocol.CMD_UDP_TEST_IN)) { runIncomingUdpTest(command, token); } else if (command.startsWith(QoSServiceProtocol.CMD_VOIP_TEST)) { runVoipTest(command, token); } else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_PORT_RANGE)) { sendCommand(TestServer.serverPreferences.getUdpPortMin() + " " + TestServer.serverPreferences.getUdpPortMax(), command); } else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_PORT)) { sendRandomUdpPort(command); } else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_RESULT_OUT)) { runRcvCommand(command, token, false); } else if (command.startsWith(QoSServiceProtocol.REQUEST_UDP_RESULT_IN)) { runRcvCommand(command, token, true); } else if (command.startsWith(QoSServiceProtocol.REQUEST_VOIP_RESULT)) { runVoipResultCommand(command, token); } else if (command.startsWith(QoSServiceProtocol.REQUEST_QUIT)) { quit = true; } else if (command.startsWith(QoSServiceProtocol.REQUEST_NEW_CONNECTION_TIMEOUT)) { requestNewConnectionTimeout(command); } else if (command.startsWith(QoSServiceProtocol.REQUEST_PROTOCOL_VERSION)) { } else if (command.startsWith(QoSServiceProtocol.REQUEST_PROTOCOL_KEEPALIVE)) { } else { sendCommand(QoSServiceProtocol.RESPONSE_ACCEPT_COMMANDS, command); quit = true; } } else { quit = true; } } catch (Exception e) { TestServerConsole.error("ClientHandler: " + socket.getInetAddress().toString() + (command == null ? " [No command submitted]" : " [Command: " + command + "] - Exception catched and consumed."), e, 0, TestServerServiceEnum.TEST_SERVER); throw e; } } } catch (Exception e) { TestServerConsole.error(name, e, 0, TestServerServiceEnum.TEST_SERVER); } finally { TestServer.clientHandlerSet.remove(this); if (socket != null && !socket.isClosed()) { try { socket.close(); TestServerConsole.log(name + " Connection closed!", 0, TestServerServiceEnum.TEST_SERVER); } catch (Exception e) { TestServerConsole.error(name + " Could not close socket!", e, 0, TestServerServiceEnum.TEST_SERVER); } } } } /** * * @param string * @return */ public synchronized static byte[] getBytesWithNewline(String string) { if (string.endsWith("\n")) { return getBytesWithNewline(string, false); } else { return getBytesWithNewline(string, true); } } /** * * @param string * @param appendNewLine * @return */ public synchronized static byte[] getBytesWithNewline(String string, boolean appendNewLine) { if (appendNewLine) { return (string + "\n").getBytes(); } else { return string.getBytes(); } } /** * * @param token * @return * @throws IOException */ private ClientToken checkToken(String token) throws IOException { ClientToken clientToken; try { TestServerConsole.log("Got token: " + token, 0, TestServerServiceEnum.TEST_SERVER); Matcher m = TOKEN_REGEX_PATTERN.matcher(token); m.find(); if (m.groupCount()!=3) { throw new IOException("BAD TOKEN: Bad Arguments!\n"); } else { String uuid = m.group(1); long timeStamp = Long.parseLong(m.group(2)); String hmac = m.group(3); if (CHECK_TOKEN) { String controlHmac = calculateHMAC(TestServer.serverPreferences.getSecretKey(), uuid + "_" + timeStamp); if (controlHmac.equals(hmac) && (timeStamp + QoSServiceProtocol.TOKEN_LEGAL_TIME >= System.currentTimeMillis())) { clientToken = new ClientToken(uuid, timeStamp, hmac); return clientToken; } else { throw new IOException("BAD TOKEN. Bad Key!\n" + controlHmac + " <-> " + hmac + "\n"); } } else { return new ClientToken(uuid, timeStamp, hmac); } } } catch (IOException e) { throw e; } catch (Exception e) { e.printStackTrace(); throw new IOException("BAD TOKEN: " + token); } } /** * * @param secret * @param data * @return */ private static String calculateHMAC(final String secret, final String data) { try { final SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), "HmacSHA1"); final Mac mac = Mac.getInstance("HmacSHA1"); mac.init(signingKey); final byte[] rawHmac = mac.doFinal(data.getBytes()); final String result = new String(Base64.encodeBytes(rawHmac)); return result; } catch (final GeneralSecurityException e) { TestServerConsole.log("Unexpected error while creating hash: " + e.getMessage(), 2, TestServerServiceEnum.TEST_SERVER); return ""; } } /** * * @param token * @throws IOException */ private void sendRandomUdpPort(final String command) throws IOException { int randomPort = 0; Random rand = new Random(); if ((TestServer.serverPreferences.getUdpPortMax() > 0) && (TestServer.serverPreferences.getUdpPortMin() <= TestServer.serverPreferences.getUdpPortMax())) { randomPort = rand.nextInt(TestServer.serverPreferences.getUdpPortMax() - TestServer.serverPreferences.getUdpPortMin()) + TestServer.serverPreferences.getUdpPortMin(); } TestServerConsole.log("Requested UDP Port. Picked random port number: " + randomPort, 0, TestServerServiceEnum.TEST_SERVER); sendCommand(String.valueOf(randomPort), command); } /** * * @param command * @param token * @throws IOException */ private void runIncomingTcpTest(String command, ClientToken token) throws IOException { final int port; Pattern p = Pattern.compile(QoSServiceProtocol.CMD_TCP_TEST_IN + " ([\\d]*)"); Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("tcp incoming test command syntax error: " + command); } else { port = Integer.parseInt(m.group(1)); } Runnable tcpInRunnable = new Runnable() { @Override public void run() { Socket testSocket = null; try { testSocket = new Socket(socket.getInetAddress(), port); BufferedOutputStream out = new BufferedOutputStream(testSocket.getOutputStream()); out.write(getBytesWithNewline("HELLO TO " + port)); out.flush(); testSocket.close(); } catch (Exception e) { TestServerConsole.error(name, e, 2, TestServerServiceEnum.TCP_SERVICE); } finally { if (testSocket != null && !testSocket.isClosed()) { try { testSocket.close(); } catch (IOException e) { e.printStackTrace(); } } } } }; TestServer.getCommonThreadPool().execute(tcpInRunnable); } /** * * @param command * @param token * @throws IOException * @throws InterruptedException */ private void runOutgoingTcpTest(String command, ClientToken token) throws Exception { int port; Pattern p = Pattern.compile(QoSServiceProtocol.CMD_TCP_TEST_OUT + " ([\\d]*)"); Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("tcp outgoing test command syntax error: " + command); } else { port = Integer.parseInt(m.group(1)); } try { TestServer.registerTcpCandidate(port, socket); sendCommand(QoSServiceProtocol.RESPONSE_OK, command); } catch (Exception e) { TestServerConsole.error(name + (command == null ? " [No command submitted]" : " [Command: " + command + "]"), e, 1, TestServerServiceEnum.TCP_SERVICE); } finally { //is beeing done inside TcpServer now: //tcpServer.removeCandidate(socket.getInetAddress()); } } /** * * @param command * @param token * @throws IOException * @throws InterruptedException */ private void runIncomingUdpTest(final String command, final ClientToken token) throws IOException, InterruptedException { final int port; final int timeout = 5000; final int numPackets; Pattern p = Pattern.compile(QoSServiceProtocol.CMD_UDP_TEST_IN + " ([\\d]*) ([\\d]*)"); Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=2) { throw new IOException("udp incoming test command syntax error: " + command); } else { port = Integer.parseInt(m.group(1)); numPackets = Integer.parseInt(m.group(2)); } //DatagramSocket sock = new DatagramSocket(port); final UdpTestCandidate clientData = new UdpTestCandidate(); clientData.setNumPackets(numPackets); final DatagramSocket sock = new DatagramSocket(); final CountDownLatch latch = new CountDownLatch(1); final Runnable sendUdpPacketsRunnable = new Runnable() { @Override public void run() { sendUdpPackets(socket.getInetAddress(), sock, port, 3000, numPackets, true, 100, token, clientData); latch.countDown(); } }; TestServer.getCommonThreadPool().execute(sendUdpPacketsRunnable); final Matcher idMatcher = ID_REGEX_PATTERN.matcher(command); if (!idMatcher.find()) { latch.await(timeout, TimeUnit.MILLISECONDS); sendRcvResult(clientData, port, command); } } /** * * @param targetHost * @param sock * @param port * @param timeOut * @param numPackets * @param awaitResponse * @param delay * @param token * @return */ private UdpTestCandidate sendUdpPackets(InetAddress targetHost, DatagramSocket sock, int port, int timeOut, int numPackets, boolean awaitResponse, int delay, ClientToken token, final UdpTestCandidate clientData) { clientUdpInDataMap.put(port, clientData); ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); DataOutputStream dataOut = new DataOutputStream(byteOut); TestServerConsole.log("INIT sending UDP packets (amount = " + numPackets + ") to " + targetHost + " on port " + port + " - using DatagramSocket: " + sock.getLocalAddress() + ":" + sock.getLocalPort(), 2, TestServerServiceEnum.UDP_SERVICE); try { sock.setSoTimeout(timeOut); } catch (SocketException e) { e.printStackTrace(); return clientData; } byte[] data; for (int i = 0; i < numPackets; i++) { byteOut.reset(); try { Thread.sleep(delay); dataOut.writeByte(QoSServiceProtocol.UDP_TEST_AWAIT_RESPONSE_IDENTIFIER); dataOut.writeByte(i); dataOut.write(token.getUuid().getBytes()); } catch (IOException | InterruptedException e) { e.printStackTrace(); sock.close(); return clientData; } try { byteOut.flush(); data = byteOut.toByteArray(); DatagramPacket packet = new DatagramPacket(data, data.length, targetHost, port); sock.send(packet); if (awaitResponse) { try { byte buffer[] = new byte[1024]; DatagramPacket dp = new DatagramPacket(buffer, buffer.length); sock.receive(dp); int packetNumber = buffer[1]; TestServerConsole.log(dp.getAddress() + ": UDP Test received packet: #" + packetNumber + " -> " + buffer, 2, TestServerServiceEnum.UDP_SERVICE); //check udp packet: if (buffer[0] != QoSServiceProtocol.UDP_TEST_RESPONSE) { TestServerConsole.log(dp.getAddress() + ": bad UDP IN TEST packet identifier", 0, TestServerServiceEnum.UDP_SERVICE); throw new IOException("bad UDP IN TEST packet identifier"); } //check for duplicate packets: if (clientData.getPacketsReceived().contains(packetNumber)) { TestServerConsole.log(dp.getAddress() + ": duplicate UDP IN TEST packet id", 0, TestServerServiceEnum.UDP_SERVICE); clientData.getPacketDuplicates().add(packetNumber); if (ABORT_ON_DUPLICATE_UDP_PACKETS) { throw new IOException("duplicate UDP IN TEST packet id"); } } else { clientData.getPacketsReceived().add(new Integer(packetNumber)); } } catch (SocketTimeoutException e) { //packet not received e.printStackTrace(); } } } catch (IOException e) { e.printStackTrace(); sock.close(); return clientData; } TestServerConsole.log("Sent packet pnum:" + i + " to " + targetHost + ":" + port +", sent message:" + data, 2, TestServerServiceEnum.TEST_SERVER); } try { byteOut.close(); } catch (Exception e) { e.printStackTrace(); return clientData; } finally { sock.close(); } TestServerConsole.log(socket.getInetAddress() + ": Udp Incoming finished! RCV PACKETS: " + clientData.getPacketsReceived().size() + ", DUP: " + clientData.getPacketDuplicates().size(), 2, TestServerServiceEnum.UDP_SERVICE); return clientData; } /** * * @param command * @throws IOException * @throws InterruptedException */ private void runOutgoingUdpTest(final String command, final ClientToken token) throws IOException, InterruptedException { final long timeout = 5000; final Pattern p = Pattern.compile(QoSServiceProtocol.CMD_UDP_TEST_OUT + " ([\\d]*) ([\\d]*)"); final Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=2) { throw new IOException("udp outgoing test command syntax error: " + command); } final int port = Integer.parseInt(m.group(1)); final int numPackets = Integer.parseInt(m.group(2)); TestServerConsole.log("Starting UDP OUT TEST (requested packets: " + numPackets + ") on port :" + port + " for " + socket.getInetAddress().toString(), 1, TestServerServiceEnum.UDP_SERVICE); final UdpTestCandidate udpData = new UdpTestCandidate(); udpData.setNumPackets(numPackets); clientUdpOutDataMap.put(port, udpData); final UdpPacketReceivedCallback receiveCallback = new UdpPacketReceivedCallback() { @Override public boolean onReceive(final DatagramPacket dp, final String uuid, final AbstractUdpServer<?> udpServer) { final byte[] data = dp.getData(); final int packetNumber = data[1]; final UdpTestCandidate clientUdpData = udpServer.getClientData(uuid); //check udp packet: if (data[0] != QoSServiceProtocol.UDP_TEST_ONE_DIRECTION_IDENTIFIER && data[0] != QoSServiceProtocol.UDP_TEST_AWAIT_RESPONSE_IDENTIFIER) { TestServerConsole.log(dp.getAddress() + ": bad UDP IN TEST packet identifier", 0, TestServerServiceEnum.UDP_SERVICE); clientUdpData.setError(true); clientUdpData.setErrorMsg("bad UDP IN TEST packet identifier"); } //check for duplicate packets: if (clientUdpData.getPacketsReceived().contains(packetNumber)) { //DUP TestServerConsole.log(dp.getAddress() + ": duplicate UDP IN TEST packet id", 0, TestServerServiceEnum.UDP_SERVICE); clientUdpData.getPacketDuplicates().add(packetNumber); if (ABORT_ON_DUPLICATE_UDP_PACKETS) { clientUdpData.setError(true); clientUdpData.setErrorMsg("duplicate UDP IN TEST packet id"); } } else { //regular packet received: clientUdpData.getPacketsReceived().add(new Integer(packetNumber)); if (data[0] == QoSServiceProtocol.UDP_TEST_AWAIT_RESPONSE_IDENTIFIER) { data[0] = QoSServiceProtocol.UDP_TEST_RESPONSE; DatagramPacket response = new DatagramPacket(data, dp.getLength(), dp.getAddress(), dp.getPort()); try { udpServer.send(response); } catch (Exception e) { //ignore exception (can be a blocked outgoing port; in this case the test should continue normally) } } TestServerConsole.log(name + " received regular Packet #"+ packetNumber, 2, TestServerServiceEnum.UDP_SERVICE); //if all packets have been received and an onComplete callback exists run it and remove the client data //from the udp server if (clientUdpData.getPacketsReceived().size() >= clientUdpData.getNumPackets() && clientUdpData.getOnUdpTestCompleteCallback() != null) { if (clientUdpData.getOnUdpTestCompleteCallback().onComplete(udpServer)) { udpServer.pollClientData(token.getUuid()); } } return true; } return false; } }; //packet receive callback udpData.setOnUdpPacketReceivedCallback(receiveCallback); final CountDownLatch latch = new CountDownLatch(1); final UdpTestCompleteCallback finishCallback = new UdpTestCompleteCallback() { @Override public boolean onComplete(final AbstractUdpServer<?> udpServer) { try { TestServerConsole.log("UDP OUT TEST on port :" + port + " for " + socket.getInetAddress().toString() + ":" + socket.getPort() + " finished successfully...", 1, TestServerServiceEnum.UDP_SERVICE); latch.countDown(); return true; } catch (Exception e) { e.printStackTrace(); } return false; } }; //add callback to udp client data in case all udp packets will arrive. in this case we can send back "RCV" before the //final timeout is reached udpData.setOnUdpTestCompleteCallback(finishCallback); //register udp client data TestServer.registerUdpCandidate(socket.getLocalAddress(), port, token.getUuid(), udpData); //tell the client that we are ready try { sendCommand(QoSServiceProtocol.RESPONSE_OK, command); } catch (IOException e) { e.printStackTrace(); } final Matcher idMatcher = ID_REGEX_PATTERN.matcher(command); if (!idMatcher.find()) { latch.await(timeout, TimeUnit.MILLISECONDS); sendRcvResult(clientUdpOutDataMap.get(port), port, command); } } /** * * @param command * @param token * @throws IOException * @throws InterruptedException */ private void runVoipTest(final String command, final ClientToken token) throws IOException, InterruptedException { /* * syntax: VOIPTEST 0 1 2 3 4 5 6 7 * 0 = outgoing port (server port) * 1 = incoming port (client port) * 2 = sample rate (in Hz) * 3 = bits per sample * 4 = packet delay in ms * 5 = call duration (test duration) in ms * 6 = starting sequence number (see rfc3550, rtp header: sequence number) * 7 = payload type */ final Pattern p = Pattern.compile(QoSServiceProtocol.CMD_VOIP_TEST + " ([\\d]*) ([\\w]*) ([\\d]*) ([\\d]*) ([\\d]*) ([\\d]*) ([\\d]*) ([\\d]*)"); final Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=8) { throw new IOException("voip test command syntax error: " + command); } final int portOut = Integer.parseInt(m.group(1)); int portIn; try { portIn = Integer.parseInt(m.group(2)); } catch (final Exception e) { portIn = 0; } final int sampleRate = Integer.parseInt(m.group(3)); final int bps = Integer.parseInt(m.group(4)); final int delay = Integer.parseInt(m.group(5)); final int callDuration = Integer.parseInt(m.group(6)); final long sequenceNumber = Integer.parseInt(m.group(7)); final int payloadTypeValue = Integer.parseInt(m.group(8)); final int ssrc = TestServer.randomizer.next(); TestServerConsole.log("Starting VOIP TEST (sample rate: " + sampleRate + ", bps: " + bps + ", delay: " + delay + ", call duration: " + callDuration + ", ssrc: " + ssrc + ", seq number: " + sequenceNumber + ") on outgoing port :" + portOut + "/incoming port: " + portIn + " for " + socket.getInetAddress().toString(), 1, TestServerServiceEnum.UDP_SERVICE); final VoipTestCandidate voipData = new VoipTestCandidate(sequenceNumber, sampleRate); clientVoipDataMap.put(ssrc, voipData); final UdpPacketReceivedCallback receiveCallback = new UdpPacketReceivedCallback() { @Override public boolean onReceive(final DatagramPacket dp, final String uuid, final AbstractUdpServer<?> udpServer) { final long timestampNs = System.nanoTime(); final byte[] data = dp.getData(); final VoipTestCandidate clientVoipData = (VoipTestCandidate) udpServer.getClientData(uuid); try { if (clientVoipData.getRtpControlDataList().size() == 0) { final InetAddress targetAddr = dp.getAddress(); final int targetPort = dp.getPort(); TestServerConsole.log(getName() + " Voip: Received first packet! Starting response stream for: " + targetAddr.toString() + ":" + targetPort, 1, TestServerServiceEnum.UDP_SERVICE); final Runnable rtpStreamSendRunnable = new Runnable() { @Override public void run() { PayloadType payloadType = PayloadType.getByCodecValue(payloadTypeValue); payloadType = PayloadType.UNKNOWN.equals(payloadType) ? PayloadType.PCMA : payloadType; try { RtpUtil.runVoipStream(udpServer.getSocket(), false, targetAddr, targetPort, sampleRate, bps, payloadType, sequenceNumber, ssrc, callDuration, delay, 10000, true, null); } catch (Exception e) { TestServerConsole.error(getName(), e, 0, TestServerServiceEnum.UDP_SERVICE); } } }; TestServer.getCommonThreadPool().execute(rtpStreamSendRunnable); } RtpPacket rtpPacket = new RtpPacket(data); TestServerConsole.log(getName() + " RTP Packet received. Sequence Number: " + rtpPacket.getSequnceNumber() + ", TS: " + timestampNs + ", SSRC: " + rtpPacket.getSsrc(), 1, TestServerServiceEnum.UDP_SERVICE); clientVoipData.resetTtl(3000); clientVoipData.addRtpControlData(rtpPacket, timestampNs); } catch (RtpException e) { TestServerConsole.error(getName(), e, 1, TestServerServiceEnum.UDP_SERVICE); return true; } //check udp packet: return true; } }; //packet receive callback voipData.setOnUdpPacketReceivedCallback(receiveCallback); //register udp client data TestServer.registerUdpCandidate(socket.getLocalAddress(), portOut, "VOIP_" + String.valueOf(ssrc), voipData); //tell the client that we are ready try { sendCommand(QoSServiceProtocol.RESPONSE_OK + " " + String.valueOf(ssrc), command); } catch (IOException e) { TestServerConsole.error(getName(), e, 0, TestServerServiceEnum.UDP_SERVICE); } } /** * * @param command * @param token * @throws IOException */ private void runRcvCommand(String command, ClientToken token, boolean isIncoming) throws IOException { Pattern p = Pattern.compile((isIncoming ? QoSServiceProtocol.REQUEST_UDP_RESULT_IN : QoSServiceProtocol.REQUEST_UDP_RESULT_OUT) + " ([\\d]*)"); Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("RCV command syntax error: " + command); } else { final int port = Integer.parseInt(m.group(1)); sendRcvResult(isIncoming ? clientUdpInDataMap.get(port) : clientUdpOutDataMap.get(port), port, command); } } private void sendRcvResult(final UdpTestCandidate result, final int port, final String command) throws IOException { if (result != null && result.getPacketsReceived() != null && !result.isError()) { TestServerConsole.log("RESULT OK, RCV PACKETS: " + result.getPacketsReceived().size() + ", DUP: " + result.getPacketDuplicates().size(), 1, TestServerServiceEnum.UDP_SERVICE); sendCommand(QoSServiceProtocol.RESPONSE_UDP_NUM_PACKETS_RECEIVED + " " + result.getPacketsReceived().size() + " " + result.getPacketDuplicates().size(), command); } else { TestServerConsole.log("RESULT ERROR, error: " + (result != null ? result.getErrorMsg() : "sorry, no error message available!"), 1, TestServerServiceEnum.UDP_SERVICE); sendCommand(QoSServiceProtocol.RESPONSE_UDP_NUM_PACKETS_RECEIVED + " 0 0", command); } } /** * * @param command * @param token * @throws IOException */ private void runVoipResultCommand(String command, ClientToken token) throws IOException { Pattern p = Pattern.compile(QoSServiceProtocol.REQUEST_VOIP_RESULT + " ([\\d]*)"); Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("GET VOIPRESULT command syntax error: " + command); } else { final int ssrc = Integer.parseInt(m.group(1)); sendVoipResult(command, token, ssrc); } } private void sendVoipResult(String command, ClientToken token, int ssrc) throws IOException { final VoipTestCandidate voipTc = clientVoipDataMap.get(ssrc); if (voipTc != null) { try { RtpQoSResult result = RtpUtil.calculateQoS(voipTc.getRtpControlDataList(), voipTc.getInitialSequenceNumber(), voipTc.getSampleRate()); final String voipResult = QoSServiceProtocol.RESPONSE_VOIP_RESULT + " " + result.getMaxJitter() + " " + result.getMeanJitter() + " " + result.getMaxDelta() + " " + result.getSkew() + " " + result.getReceivedPackets() + " " + result.getOutOfOrder() + " " + result.getMinSequential() + " " + result.getMaxSequencial(); TestServerConsole.log("Sending VOIP results for SSRC " + ssrc + ": " + voipResult, 2, TestServerServiceEnum.UDP_SERVICE); sendCommand(voipResult, command); } catch (Exception e) { TestServerConsole.error(getName(), e, 1, TestServerServiceEnum.UDP_SERVICE); } } else { sendCommand(QoSServiceProtocol.RESPONSE_ERROR_ILLEGAL_ARGUMENT + " " + ssrc, command); } } /** * runs the non transparent proxy test: * * 1. open socket on requested port and send "OK" to let client continue the test * 2. wait for incoming HTTP protocol request * 3. send request back to client (echo) * * @param command * @throws IOException * @throws InterruptedException */ private void runNonTransparentProxyTest(String command) throws Exception { int echoPort; Pattern p = Pattern.compile(QoSServiceProtocol.CMD_NON_TRANSPARENT_PROXY_TEXT + " ([\\d]*)"); Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("non transparent proxy test command syntax error: " + command); } else { echoPort = Integer.parseInt(m.group(1)); } try { TestServer.registerTcpCandidate(echoPort, socket); sendCommand(QoSServiceProtocol.RESPONSE_OK, command); TestServerConsole.log("NTP: sendind OK. waiting for request...", 1, TestServerServiceEnum.TCP_SERVICE); } catch (Exception e) { TestServerConsole.error(name, e, 1, TestServerServiceEnum.TCP_SERVICE); } finally { //is beeing done inside TcpServer now: //tcpServer.removeCandidate(socket.getInetAddress()); } } /** * * @param command * @throws IOException */ public void requestNewConnectionTimeout(String command) throws IOException { final Pattern p = Pattern.compile(QoSServiceProtocol.REQUEST_NEW_CONNECTION_TIMEOUT + " ([\\d]*)"); final Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("request new connection timeout command syntax error: " + command); } Integer requestedConnTimeout = Integer.parseInt(m.group(1)); if (requestedConnTimeout < QoSServiceProtocol.TIMEOUT_CLIENTHANDLER_CONNECTION_MIN_VALUE) { sendErrorCommand(QoSServiceProtocol.RESPONSE_ERROR_ILLEGAL_ARGUMENT + " " + requestedConnTimeout, command); } else { socket.setSoTimeout(requestedConnTimeout); sendCommand(QoSServiceProtocol.RESPONSE_OK, command); } } /** * * @param command * @throws IOException */ public void requestProtocolVersion(String command) throws IOException { final Pattern p = Pattern.compile(QoSServiceProtocol.REQUEST_PROTOCOL_VERSION + " ([a-zA-Z0-9]*)"); final Matcher m = p.matcher(command); m.find(); if (m.groupCount()!=1) { throw new IOException("request protocol version command syntax error: " + command); } String requestedProtocolVersion = m.group(1); if (!QoSServiceProtocol.SUPPORTED_PROTOCOL_VERSION_SET.contains(requestedProtocolVersion)) { sendErrorCommand(QoSServiceProtocol.RESPONSE_ERROR_UNSUPPORTED + " " + requestedProtocolVersion, command); } else { clientProtocolVersion = requestedProtocolVersion; sendCommand(QoSServiceProtocol.RESPONSE_OK, command); } } /** * * @return */ public ServerSocket getServerSocket() { return this.serverSocket; } /** * * @param outCommand * @throws IOException */ public synchronized void sendCommand(String outCommand) throws IOException { sendCommand(outCommand, null); } /** * * @param outCommand * @param inCommand * @throws IOException */ public synchronized void sendCommand(String outCommand, String inCommand) throws IOException { String id = null; if (inCommand != null) { Matcher m = ID_REGEX_PATTERN.matcher(inCommand); if (m.find()) { id = m.group(1); outCommand = outCommand + (id != null ? " +ID" + id : ""); } } TestServerConsole.log(name + " sending answer: [" + outCommand + "] to: [" + inCommand +"] ", 2, TestServerServiceEnum.TEST_SERVER); out.write(getBytesWithNewline(outCommand)); } /** * * @param error * @throws IOException */ public synchronized void sendErrorCommand(String error) throws IOException { sendCommand(QoSServiceProtocol.RESPONSE_ERROR_RESPONSE + error); } /** * * @param error * @param inCommand * @throws IOException */ public synchronized void sendErrorCommand(String error, String inCommand) throws IOException { sendCommand(QoSServiceProtocol.RESPONSE_ERROR_RESPONSE + error, inCommand); } @Override public String toString() { return "ClientHandler [socket=" + socket + ", name=" + name + ", clientUdpOutDataMap=" + clientUdpOutDataMap + ", clientUdpInDataMap=" + clientUdpInDataMap + "]"; } public String getName() { return name; } public ConcurrentHashMap<Integer, UdpTestCandidate> getClientUdpOutDataMap() { return clientUdpOutDataMap; } public ConcurrentHashMap<Integer, UdpTestCandidate> getClientUdpInDataMap() { return clientUdpInDataMap; } }