fr.delthas.skype.NotifConnector Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of javaskype Show documentation
Show all versions of javaskype Show documentation
An API for Skype using MSNP24
package fr.delthas.skype;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.net.ssl.SSLSocketFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
class NotifConnector {
private static class Packet {
public final String command;
public final String params;
public final String body;
public Packet(String command, String params, String body) {
this.command = command;
this.params = params;
this.body = body;
}
@Override
public String toString() {
return String.format("Command: %s Params: %s Body: %s", command, params, body);
}
}
private static final String EPID = generateEPID(); // generate EPID at runtime
private static final String DEFAULT_SERVER_HOSTNAME = "s.gateway.messenger.live.com";
private static final int DEFAULT_SERVER_PORT = 443;
private static final Pattern patternFirstLine = Pattern.compile("([A-Z]+) \\d+ ([A-Z]+(?:\\\\[A-Z]+)?) (\\d+)");
private static final Pattern patternHeaders = Pattern.compile("\\A(?:(?:Set-Registration: (.+)|[A-Za-z\\-]+: .+)\\R)*\\R");
private static final Pattern patternXFR = Pattern.compile("([a-zA-Z0-9\\.]+):(\\d+)");
private static final long pingInterval = 5 * 60000000000L; // minutes
private long lastMessageSentTime;
private Thread pingThread;
private final DocumentBuilder documentBuilder;
private final Skype skype;
private final String username, password;
private boolean disconnectRequested = false;
private Socket socket;
private BufferedWriter writer;
private BufferedInputStream inputStream;
private int sequenceNumber;
private String registration;
private CountDownLatch connectLatch = new CountDownLatch(1);
public NotifConnector(Skype skype, String username, String password) {
this.skype = skype;
this.username = username;
this.password = password;
try {
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (ParserConfigurationException e) {
// Should never happen, throw RE if it does
throw new RuntimeException(e);
}
}
private void processPacket(Packet packet) throws IOException {
// System.out.println("<<<" + packet.command + " " + packet.params + "\n" + packet.body);
switch (packet.command) {
case "GET":
if (packet.params.equals("MSGR")) {
String mainNode;
try {
Document doc = getDocument(packet.body);
mainNode = doc.getFirstChild().getNodeName();
} catch (ParseException e) {
break;
}
switch (mainNode) {
case "recentconversations-response":
List conversations = getXMLFields(packet.body, "id");
StringBuilder sb = new StringBuilder("");
for (String conversation : conversations) {
Object group = parseEntity(conversation);
if (!(group instanceof Group)) {
continue;
}
sb.append("19:").append(((Group) group).getId()).append("@thread.skype ");
}
String body = sb.append(" ").toString();
sendPacket("GET", "MSGR\\THREADS", body);
break;
case "threads-response":
NodeList threadNodes;
try {
Document doc = getDocument(packet.body);
threadNodes = doc.getElementsByTagName("thread");
} catch (ParseException e) {
break;
}
for (int i = 0; i < threadNodes.getLength(); i++) {
updateThread(threadNodes.item(i));
}
connectLatch.countDown(); // stop blocking: we're connected
break;
default:
}
}
break;
case "SDG":
if (!packet.body.isEmpty()) {
FormattedMessage formatted;
try {
formatted = FormattedMessage.parseMessage(packet.body);
} catch (IllegalArgumentException e) {
// weird message with no interesting content
break;
}
String messageType = formatted.headers.get("Message-Type");
if (messageType == null) {
break;
}
Object sender = parseEntity(formatted.sender);
Object receiver = parseEntity(formatted.receiver);
switch (messageType) {
case "Text":
case "RichText":
if (!(sender instanceof User)) {
break;
}
if (receiver instanceof Group) {
skype.groupMessageReceived((Group) receiver, (User) sender, formatted.body);
} else {
skype.userMessageReceived((User) sender, formatted.body);
}
break;
case "ThreadActivity/AddMember":
List usernames = getXMLFields(formatted.body, "target");
skype.usersAddedToGroup(usernames.stream().map(username -> (User) parseEntity(username)).collect(Collectors.toList()), (Group) sender);
break;
case "ThreadActivity/DeleteMember":
usernames = getXMLFields(formatted.body, "target");
skype.usersAddedToGroup(usernames.stream().map(username -> (User) parseEntity(username)).collect(Collectors.toList()), (Group) sender);
break;
case "ThreadActivity/TopicUpdate":
skype.groupTopicChanged((Group) sender, getXMLField(formatted.body, "value"));
break;
case "ThreadActivity/RoleUpdate":
Document doc = getDocument(formatted.body);
NodeList targetNodes = doc.getElementsByTagName("target");
List> roles = new ArrayList<>(targetNodes.getLength());
for (int i = 0; i < targetNodes.getLength(); i++) {
Node targetNode = targetNodes.item(i);
User user = null;
Role role = null;
for (int j = 0; j < targetNode.getChildNodes().getLength(); j++) {
Node targetPropertyNode = targetNode.getChildNodes().item(j);
if (targetPropertyNode.getNodeName().equals("id")) {
user = (User) parseEntity(targetPropertyNode.getTextContent());
skype.updateUser(user);
} else if (targetPropertyNode.getNodeName().equals("role")) {
role = Role.getRole(targetPropertyNode.getTextContent());
}
}
if (user != null && role != null) {
roles.add(new Pair<>(user, role));
}
}
skype.usersRolesChanged((Group) sender, roles);
break;
default:
break;
}
}
break;
case "NFY":
switch (packet.params) {
case "MSGR\\DEL":
FormattedMessage formatted = FormattedMessage.parseMessage(packet.body);
((User) parseEntity(formatted.sender)).setPresence(Presence.OFFLINE);
break;
case "MSGR\\PUT":
formatted = FormattedMessage.parseMessage(packet.body);
String presenceString = getXMLField(formatted.body, "Status");
if (presenceString == null) {
// happens when a user switches from offline to "hidden"
presenceString = "";
}
((User) parseEntity(formatted.sender)).setPresence(presenceString);
break;
case "MSGR\\THREAD":
Document document = getDocument(packet.body);
updateThread(document);
break;
default:
break;
}
break;
case "XFR":
String newAddress = getXMLField(packet.body, "target");
if (newAddress == null) {
throw new ParseException();
}
Matcher matcherXFR = patternXFR.matcher(newAddress);
if (!matcherXFR.matches()) {
throw new ParseException();
}
String hostname = matcherXFR.group(1);
String portString = matcherXFR.group(2);
int port;
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
throw new ParseException(e);
}
connectTo(hostname, port);
break;
case "CNT":
String nonce = getXMLField(packet.body, "nonce");
if (nonce == null) {
throw new ParseException();
}
// we *need* to make sure these are not null to avoid going APPCRASH
Objects.requireNonNull(username);
Objects.requireNonNull(password);
Objects.requireNonNull(nonce);
String uic = SkyLoginConnector.getUIC(username, password, nonce);
sendPacket("ATH", "CON\\USER", "" + uic + " " + username + " ");
break;
case "ATH":
sendPacket("BND", "CON\\MSGR",
"2 . . skype " + EPID + " ");
break;
case "BND":
// apparently no challenge is needed anymore, but skype may put it back in the future
String challenge = getXMLField(packet.body, "nonce");
if (challenge != null) {
String query = Challenge.createQuery(challenge);
sendPacket("PUT", "MSGR\\CHALLENGE",
"" + Challenge.PRODUCT_ID + " " + query + " ");
}
String formattedPublicationBody = String.format(
"%s 0:4194560 %s . true ",
skype.getSelf().getPresence().getPresenceString(), EPID, EPID, username, EPID);
String formattedPublicationMessage = FormattedMessage.format("8:" + username + ";epid={" + EPID + "}", "8:" + username, "Publication: 1.0",
formattedPublicationBody, "Uri: /user", "Content-Type: application/user+xml");
sendPacket("PUT", "MSGR\\PRESENCE", formattedPublicationMessage);
sendPacket("PUT", "MSGR\\SUBSCRIPTIONS",
" ");
List contacts = skype.getContacts();
StringBuilder contactsStringBuilder = new StringBuilder("");
for (User contact : contacts) {
contactsStringBuilder.append(" ");
}
contactsStringBuilder.append(" ");
String contactsString = contactsStringBuilder.toString();
sendPacket("PUT", "MSGR\\CONTACTS", contactsString);
sendPacket("GET", "MSGR\\RECENTCONVERSATIONS", "0 100 ");
break;
case "OUT":
// we got disconnected
skype.error(new IOException("Disconnected: " + packet.body));
break;
case "PUT":
break;
case "PNG":
// ignore pong
break;
default:
System.out.println("Received unknown message: " + packet);
}
}
private Packet readPacket() throws IOException, ParseException {
StringBuilder firstLineBuilder = new StringBuilder();
byte[] oneByteBuffer = new byte[1];
boolean CRFlag = false;
while (true) {
if (inputStream.read(oneByteBuffer) == -1) {
return null;
}
char character = (char) (oneByteBuffer[0] & 0xFF);
if (CRFlag) {
if (character == '\n') {
break;
}
throw new ParseException();
}
if (character == '\n') {
break;
}
if (character == '\r') {
CRFlag = true;
} else {
firstLineBuilder.append(character);
}
}
String firstLine = firstLineBuilder.toString();
Matcher matcherFirstLine = patternFirstLine.matcher(firstLine);
if (!matcherFirstLine.matches()) {
throw new ParseException("Error matching message first line: " + firstLine + " ");
}
String command = matcherFirstLine.group(1);
String parameters = matcherFirstLine.group(2);
String payloadSizeString = matcherFirstLine.group(3);
int payloadSize;
try {
payloadSize = Integer.parseInt(payloadSizeString);
} catch (NumberFormatException e) {
throw new ParseException(e);
}
byte[] payloadRaw = new byte[payloadSize];
int bytesRead = 0;
while (true) {
int n = inputStream.read(payloadRaw, bytesRead, payloadSize - bytesRead);
if (n == -1) {
throw new ParseException();
}
bytesRead += n;
if (bytesRead == payloadSize) {
break;
}
}
String payload = new String(payloadRaw, StandardCharsets.UTF_8);
Matcher matcherHeaders = patternHeaders.matcher(payload);
if (!matcherHeaders.find()) {
throw new ParseException();
}
String newRegistration = matcherHeaders.group(1);
if (newRegistration != null) {
registration = newRegistration;
}
String body = payload.substring(matcherHeaders.end());
return new Packet(command, parameters, body);
}
public void connect() throws IOException, InterruptedException {
lastMessageSentTime = System.nanoTime();
connectTo(DEFAULT_SERVER_HOSTNAME, DEFAULT_SERVER_PORT);
new Thread(() -> {
while (!disconnectRequested) {
try {
Packet packet = readPacket();
if (packet == null) {
continue;
}
processPacket(packet);
} catch (IOException e) {
if (disconnectRequested) {
// there may be errors reading from the closed stream when disconnecting
// quit without throwing
return;
}
skype.error(e);
connectLatch.countDown();
break;
}
}
}, "Skype-Receiver-Thread").start();
pingThread = new Thread(() -> {
while (!disconnectRequested) {
if (System.nanoTime() - lastMessageSentTime > pingInterval) {
try {
sendPacket("PNG", "CON", "");
} catch (IOException e) {
skype.error(e);
break;
}
}
try {
Thread.sleep(pingInterval / 1000000);
} catch (InterruptedException e) {
// stop sleeping early
}
}
}, "Skype-Ping-Thread");
connectLatch.await(); // block until connected
pingThread.start();
}
public void sendUserMessage(User user, String message) throws IOException {
sendMessage("8:" + user.getUsername(), message);
}
public void sendGroupMessage(Group group, String message) throws IOException {
sendMessage("19:" + group.getId() + "@thread.skype", message);
}
public void addUserToGroup(User user, Role role, Group group) throws IOException {
String body = String.format("19:%[email protected] 8:%s %s ",
group.getId(), user.getUsername(), role.getRoleString());
sendPacket("PUT", "MSGR\\THREAD", body);
}
public void removeUserFromGroup(User user, Group group) throws IOException {
String body = String.format("19:%[email protected] 8:%s ", group.getId(),
user.getUsername());
sendPacket("DEL", "MSGR\\THREAD", body);
}
public void changeGroupTopic(Group group, String topic) throws IOException {
String body = String.format("19:%[email protected] %s ", group.getId(), topic);
sendPacket("PUT", "MSGR\\THREAD", body);
}
public void changeUserRole(User user, Role role, Group group) throws IOException {
String body = String.format("19:%[email protected] 8:%s %s ",
group.getId(), user.getUsername(), role.getRoleString());
sendPacket("PUT", "MSGR\\THREAD", body);
}
public void changePresence(Presence presence) throws IOException {
String formattedPublicationBody = String.format("" + presence.getPresenceString() + " ");
String formattedPublicationMessage = FormattedMessage.format("8:" + username + ";epid={" + EPID + "}", "8:" + username, "Publication: 1.0",
formattedPublicationBody, "Uri: /user", "Content-Type: application/user+xml");
sendPacket("PUT", "MSGR\\PRESENCE", formattedPublicationMessage);
}
private void sendMessage(String entity, String message) throws IOException {
String body = FormattedMessage.format("8:" + username + ";epid={" + EPID + "}", entity, "Messaging: 2.0", message,
"Content-Type: application/user+xml", "Message-Type: RichText");
sendPacket("SDG", "MSGR", body);
}
public synchronized void disconnect() {
try {
sendPacket("OUT", "CON", "");
} catch (IOException e) {
// we're closing anyway
}
disconnectRequested = true;
pingThread.interrupt();
connectLatch.countDown();
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// ignore any error during close
}
}
}
private synchronized void sendPacket(String command, String parameters, String body) throws IOException {
String headerString = registration != null ? "Registration: " + registration + "\r\n" : "";
String messageString = String.format("%s %d %s %d\r\n%s\r\n%s", command, ++sequenceNumber, parameters,
body.getBytes(StandardCharsets.UTF_8).length + 2 + headerString.length(), headerString, body);
writer.write(messageString);
writer.flush();
lastMessageSentTime = System.nanoTime();
// System.out.println(">>>" + messageString);
}
private void connectTo(String hostname, int port) throws IOException {
if (socket != null) {
socket.close();
}
socket = SSLSocketFactory.getDefault().createSocket(hostname, port);
writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
inputStream = new BufferedInputStream(socket.getInputStream());
sequenceNumber = 0;
sendPacket("CNT", "CON", "2 . . . en-us ");
}
private void updateThread(Node threadNode) {
Group group = null;
String topic = null;
Node members = null;
for (int i = 0; i < threadNode.getChildNodes().getLength(); i++) {
Node node = threadNode.getChildNodes().item(i);
if (node.getNodeName().equals("id")) {
group = (Group) parseEntity(node.getTextContent());
} else if (node.getNodeName().equals("members")) {
members = node;
} else if (node.getNodeName().equals("properties")) {
for (int j = 0; j < node.getChildNodes().getLength(); j++) {
Node topicNode = node.getChildNodes().item(j);
if (topicNode.getNodeName().equals("topic")) {
topic = topicNode.getTextContent();
}
}
}
}
if (group == null || topic == null || members == null) {
return;
}
List> users = new ArrayList<>(members.getChildNodes().getLength());
for (int i = 0; i < members.getChildNodes().getLength(); i++) {
Node memberNode = members.getChildNodes().item(i);
if (!memberNode.getNodeName().equals("member")) {
continue;
}
User user = null;
Role role = null;
for (int j = 0; j < memberNode.getChildNodes().getLength(); j++) {
Node memberPropertyNode = memberNode.getChildNodes().item(j);
if (memberPropertyNode.getNodeName().equals("mri")) {
user = (User) parseEntity(memberPropertyNode.getTextContent());
skype.updateUser(user);
} else if (memberPropertyNode.getNodeName().equals("role")) {
role = Role.getRole(memberPropertyNode.getTextContent());
}
}
if (user != null && role != null) {
users.add(new Pair<>(user, role));
}
}
group.setTopic(topic);
group.setUsers(users);
}
private Object parseEntity(String rawEntity) {
// returns a user or a group
int senderBegin = rawEntity.indexOf(':');
int network = Integer.parseInt(rawEntity.substring(0, senderBegin));
int end0 = rawEntity.indexOf('@');
int end1 = rawEntity.indexOf(';');
if (end0 == -1) {
end0 = Integer.MAX_VALUE;
}
if (end1 == -1) {
end1 = Integer.MAX_VALUE;
}
int senderEnd = Math.min(end0, end1);
String name;
if (senderEnd == Integer.MAX_VALUE) {
name = rawEntity.substring(senderBegin + 1);
} else {
name = rawEntity.substring(senderBegin + 1, senderEnd);
}
if (network == 8) {
return skype.getUser(name);
} else if (network == 19) {
return skype.getGroup(name);
} else {
throw new IllegalArgumentException();
}
}
private Document getDocument(String XML) throws ParseException {
try {
return documentBuilder.parse(new InputSource(new StringReader(XML)));
} catch (IOException | SAXException e) {
// IOException should never happen, but treat as ParseException anyway
throw new ParseException(e);
}
}
private List getXMLFields(String XML, String fieldName) throws ParseException {
NodeList nodes = getDocument(XML).getElementsByTagName(fieldName);
List fields = new ArrayList<>(nodes.getLength());
for (int i = 0; i < nodes.getLength(); i++) {
fields.add(nodes.item(i).getTextContent());
}
return fields;
}
private String getXMLField(String XML, String fieldName) throws ParseException {
List fields = getXMLFields(XML, fieldName);
if (fields.size() > 1) {
throw new ParseException();
}
if (fields.size() == 0) {
return null;
}
return fields.get(0);
}
private static String generateEPID() {
char[] hexCharacters = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
// EPID format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
char[] EPIDchars = new char[36];
EPIDchars[8] = '-';
EPIDchars[13] = '-';
EPIDchars[18] = '-';
EPIDchars[23] = '-';
Random random = new Random();
for (int i = 0; i < 8; i++) {
EPIDchars[i] = hexCharacters[random.nextInt(hexCharacters.length)];
}
for (int i = 9; i < 13; i++) {
EPIDchars[i] = hexCharacters[random.nextInt(hexCharacters.length)];
}
for (int i = 14; i < 18; i++) {
EPIDchars[i] = hexCharacters[random.nextInt(hexCharacters.length)];
}
for (int i = 19; i < 23; i++) {
EPIDchars[i] = hexCharacters[random.nextInt(hexCharacters.length)];
}
for (int i = 24; i < 36; i++) {
EPIDchars[i] = hexCharacters[random.nextInt(hexCharacters.length)];
}
return new String(EPIDchars);
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy