Commit 7adf708d authored by guus's avatar guus

Update Client Capabilities implementation to match the XEP (OF-339).

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@11580 b35dd754-fafc-0310-a699-88a17e54d16e
parent 75926ddb
......@@ -38,6 +38,7 @@ import java.util.Set;
* @author Armando Jagucki
*
*/
// TODO: Instances of this class should not be cached in distributed caches. The overhead of distributing data is a lot higher than recalculating the hash on every cluster node. We should remove the Externalizable interface, and turn this class into an immutable class.
public class EntityCapabilities implements Cacheable, Externalizable {
/**
......@@ -57,6 +58,11 @@ public class EntityCapabilities implements Cacheable, Externalizable {
*/
private String verAttribute;
/**
* The hash algorithm that was used to create the hash string.
*/
private String hashAttribute;
/**
* Adds an identity to the entity capabilities.
*
......@@ -102,13 +108,22 @@ public class EntityCapabilities implements Cacheable, Externalizable {
return features.contains(feature);
}
/**
* @param verAttribute the verAttribute to set
*/
void setVerAttribute(String verAttribute) {
this.verAttribute = verAttribute;
}
String getVerAttribute() {
return this.verAttribute;
}
void setHashAttribute(String hashAttribute) {
this.hashAttribute = hashAttribute;
}
String getHashAttribute() {
return this.hashAttribute;
}
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
ExternalizableUtil.getInstance().readStrings(in, identities);
ExternalizableUtil.getInstance().readStrings(in, features);
......
......@@ -22,11 +22,13 @@ package org.jivesoftware.openfire.entitycaps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.IQRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.event.UserEventListener;
......@@ -101,19 +103,15 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
* correct 'ver' hash in the map, that was previously encountered in the
* caps packet.
*
* We use a cache for this map so it is cluster safe for remote users
* whose disco#info replies are handled by new nodes in the cluster (after
* an s2s disconnection for example).
*
* Key: Packet ID of our disco#info request.
* Value: The 'ver' hash string from the original caps packet.
*/
private Cache<String, String> verAttributes;
private Map<String, EntityCapabilities> verAttributes;
private EntityCapabilitiesManager() {
entityCapabilitiesMap = CacheFactory.createCache("Entity Capabilities");
entityCapabilitiesUserMap = CacheFactory.createCache("Entity Capabilities Users");
verAttributes = CacheFactory.createCache("Entity Capabilities Pending Hashes");
verAttributes = new HashMap<String, EntityCapabilities>();
}
/**
......@@ -143,15 +141,15 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
// TODO: if this packet is in legacy format, we SHOULD check the 'node',
// 'ver', and 'ext' combinations as specified in the archived version
// 1.3 of the specification, and cache the results. See JM-1447
String hashAttribute = capsElement.attributeValue("hash");
if (hashAttribute == null) {
final String hashAttribute = capsElement.attributeValue("hash");
if (hashAttribute == null || hashAttribute.trim().length() == 0) {
return;
}
// Examine the packet and check if it has and a 'ver' hash
// if not -- do nothing by returning.
String newVerAttribute = capsElement.attributeValue("ver");
if (newVerAttribute == null) {
final String newVerAttribute = capsElement.attributeValue("ver");
if (newVerAttribute == null || newVerAttribute.trim().length() == 0) {
return;
}
......@@ -174,9 +172,13 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
iq.setChildElement("query", "http://jabber.org/protocol/disco#info");
String packetId = iq.getID();
verAttributes.put(packetId, newVerAttribute);
IQRouter iqRouter = XMPPServer.getInstance().getIQRouter();
final EntityCapabilities caps = new EntityCapabilities();
caps.setHashAttribute(hashAttribute);
caps.setVerAttribute(newVerAttribute);
verAttributes.put(packetId, caps);
final IQRouter iqRouter = XMPPServer.getInstance().getIQRouter();
iqRouter.addIQResultListener(packetId, this);
iqRouter.route(iq);
}
......@@ -204,10 +206,13 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
* hash of the original caps packet.
*/
private boolean isValid(IQ packet) {
String newVerHash = generateVerHash(packet);
final EntityCapabilities original = verAttributes.get(packet.getID());
if (original == null) {
return false;
}
final String newVerHash = generateVerHash(packet, original.getHashAttribute());
String originalVerAttribute = verAttributes.get(packet.getID());
return originalVerAttribute.equals(newVerHash);
return newVerHash.equals(original.getVerAttribute());
}
/**
......@@ -220,43 +225,53 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
* outlined in XEP-0115.
*
* @param packet IQ reply to the entity cap request.
* @param algorithm The hashing algorithm to use (e.g. SHA-1)
* @return the generated 'ver' hash
*/
private String generateVerHash(IQ packet) {
public static String generateVerHash(IQ packet, String algorithm) {
// Initialize an empty string S.
String S = "";
final StringBuilder s = new StringBuilder();
// Sort the service discovery identities by category and then by type
// (if it exists), formatted as 'category' '/' 'type'.
List<String> discoIdentities = getIdentitiesFrom(packet);
// (if it exists), formatted as 'category' '/' 'type' / 'lang' / 'name'
final List<String> discoIdentities = getIdentitiesFrom(packet);
Collections.sort(discoIdentities);
// For each identity, append the 'category/type' to S, followed by the
// '<' character.
// For each identity, append the 'category/type/lang/name' to S,
// followed by the '<' character.
for (String discoIdentity : discoIdentities) {
S += discoIdentity;
S += '<';
s.append(discoIdentity);
s.append('<');
}
// Sort the supported features.
List<String> discoFeatures = getFeaturesFrom(packet);
// Sort the supported service discovery features.
final List<String> discoFeatures = getFeaturesFrom(packet);
Collections.sort(discoFeatures);
// For each feature, append the feature to S, followed by the '<'
// character.
for (String discoFeature : discoFeatures) {
S += discoFeature;
S += '<';
s.append(discoFeature);
s.append('<');
}
// If the service discovery information response includes XEP-0128
// data forms, sort the forms by the FORM_TYPE (i.e., by the XML
// character data of the <value/> element).
final List<String> extendedDataForms = getExtendedDataForms(packet);
Collections.sort(extendedDataForms);
for (String extendedDataForm : extendedDataForms) {
s.append(extendedDataForm);
// no need to add '<', this is done in #getExtendedDataForms()
}
// Compute ver by hashing S using the SHA-1 algorithm as specified in
// RFC 3174 (with binary output) and encoding the hash using Base64 as
// specified in Section 4 of RFC 4648 (note: the Base64 output
// MUST NOT include whitespace and MUST set padding bits to zero).
S = StringUtils.hash(S, "SHA-1");
S = StringUtils.encodeBase64(StringUtils.decodeHex(S));
return S;
final String hashed = StringUtils.hash(s.toString(), "SHA-1");
return StringUtils.encodeBase64(StringUtils.decodeHex(hashed));
}
public void answerTimeout(String packetId) {
......@@ -272,26 +287,25 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
// The packet was validated, so it can be added to the Entity
// Capabilities cache map.
// Create the entity capabilities object and add it to the cache map...
EntityCapabilities entityCapabilities = new EntityCapabilities();
// Add the resolved identities and features to the entity
// EntityCapabilitiesManager.capabilities object and add it
// to the cache map...
EntityCapabilities caps = verAttributes.get(packetId);
// Store identities.
List<String> identities = getIdentitiesFrom(packet);
for (String identity : identities) {
entityCapabilities.addIdentity(identity);
caps.addIdentity(identity);
}
// Store features.
List<String> features = getFeaturesFrom(packet);
for (String feature : features) {
entityCapabilities.addFeature(feature);
caps.addFeature(feature);
}
String originalVerAttribute = verAttributes.get(packetId);
entityCapabilities.setVerAttribute(originalVerAttribute);
entityCapabilitiesMap.put(originalVerAttribute, entityCapabilities);
entityCapabilitiesUserMap.put(packet.getFrom(), originalVerAttribute);
entityCapabilitiesMap.put(caps.getVerAttribute(), caps);
entityCapabilitiesUserMap.put(packet.getFrom(), caps.getVerAttribute());
}
// Remove cached 'ver' attribute.
......@@ -316,19 +330,41 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
* @param packet the packet
* @return a list of identities
*/
private List<String> getIdentitiesFrom(IQ packet) {
private static List<String> getIdentitiesFrom(IQ packet) {
List<String> discoIdentities = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator identitiesIterator = query.elementIterator("identity");
Iterator<Element> identitiesIterator = query.elementIterator("identity");
if (identitiesIterator != null) {
while (identitiesIterator.hasNext()) {
Element identityElement = (Element) identitiesIterator.next();
Element identityElement = identitiesIterator.next();
String discoIdentity = identityElement.attributeValue("category");
discoIdentity += '/';
discoIdentity += identityElement.attributeValue("type");
StringBuilder discoIdentity = new StringBuilder();
discoIdentities.add(discoIdentity);
String cat = identityElement.attributeValue("category");
String type = identityElement.attributeValue("type");
String lang = identityElement.attributeValue("xml:lang");
String name = identityElement.attributeValue("name");
if (cat != null) {
discoIdentity.append(cat);
}
discoIdentity.append('/');
if (type != null) {
discoIdentity.append(type);
}
discoIdentity.append('/');
if (lang != null) {
discoIdentity.append(lang);
}
discoIdentity.append('/');
if (name != null) {
discoIdentity.append(name);
}
discoIdentities.add(discoIdentity.toString());
}
}
return discoIdentities;
......@@ -340,13 +376,13 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
* @param packet the packet
* @return a list of features
*/
private List<String> getFeaturesFrom(IQ packet) {
private static List<String> getFeaturesFrom(IQ packet) {
List<String> discoFeatures = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator featuresIterator = query.elementIterator("feature");
Iterator<Element> featuresIterator = query.elementIterator("feature");
if (featuresIterator != null) {
while (featuresIterator.hasNext()) {
Element featureElement = (Element) featuresIterator.next();
Element featureElement = featuresIterator.next();
String discoFeature = featureElement.attributeValue("var");
discoFeatures.add(discoFeature);
......@@ -355,6 +391,63 @@ public class EntityCapabilitiesManager implements IQResultListener, UserEventLis
return discoFeatures;
}
/**
* Extracts a list of extended service discovery information from an IQ
* packet.
*
* @param packet
* the packet
* @return a list of extended service discoverin information features.
*/
private static List<String> getExtendedDataForms(IQ packet) {
List<String> results = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator<Element> extensionIterator = query.elementIterator(QName.get(
"x", "jabber:x:data"));
if (extensionIterator != null) {
while (extensionIterator.hasNext()) {
Element extensionElement = extensionIterator.next();
final StringBuilder formType = new StringBuilder();
Iterator<Element> fieldIterator = extensionElement
.elementIterator("field");
List<String> vars = new ArrayList<String>();
while (fieldIterator != null && fieldIterator.hasNext()) {
final Element fieldElement = fieldIterator.next();
if (fieldElement.attributeValue("var").equals("FORM_TYPE")) {
formType
.append(fieldElement.element("value").getText());
formType.append('<');
} else {
final StringBuilder var = new StringBuilder();
var.append(fieldElement.attributeValue("var"));
var.append('<');
Iterator<Element> valIter = fieldElement
.elementIterator("value");
List<String> values = new ArrayList<String>();
while (valIter != null && valIter.hasNext()) {
Element value = valIter.next();
values.add(value.getText());
}
Collections.sort(values);
for (String v : values) {
var.append(v);
var.append('<');
}
vars.add(var.toString());
}
}
Collections.sort(vars);
for (String v : vars) {
formType.append(v);
}
results.add(formType.toString());
}
}
return results;
}
public void userDeleting(User user, Map<String, Object> params) {
// Delete this user's association in entityCapabilitiesUserMap.
JID jid = XMPPServer.getInstance().createJID(user.getUsername(), null, true);
......
......@@ -126,7 +126,6 @@ public class CacheFactory {
cacheNames.put("Remote Server Configurations", "serversConfigurations");
cacheNames.put("Entity Capabilities", "entityCapabilities");
cacheNames.put("Entity Capabilities Users", "entityCapabilitiesUsers");
cacheNames.put("Entity Capabilities Pending Hashes", "entityCapabilitiesPendingHashes");
cacheNames.put("Clearspace SSO Nonce", "clearspaceSSONonce");
cacheNames.put("PEPServiceManager", "pepServiceManager");
......@@ -194,8 +193,6 @@ public class CacheFactory {
cacheProps.put("cache.entityCapabilities.maxLifetime", JiveConstants.DAY * 2);
cacheProps.put("cache.entityCapabilitiesUsers.size", -1l);
cacheProps.put("cache.entityCapabilitiesUsers.maxLifetime", JiveConstants.DAY * 2);
cacheProps.put("cache.entityCapabilitiesPendingHashes.size", -1l);
cacheProps.put("cache.entityCapabilitiesPendingHashes.maxLifetime", JiveConstants.DAY * 2);
cacheProps.put("cache.pluginCacheInfo.size", -1l);
cacheProps.put("cache.pluginCacheInfo.maxLifetime", -1l);
cacheProps.put("cache.clearspaceSSONonce.size", -1l);
......
......@@ -864,25 +864,6 @@ http://www.tangosol.com/UserGuide-Reference-CacheConfig.jsp
</init-params>
</cache-mapping>
<cache-mapping>
<cache-name>Entity Capabilities Pending Hashes</cache-name>
<scheme-name>near-distributed</scheme-name>
<init-params>
<init-param>
<param-name>back-size-high</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>back-expiry</param-name>
<param-value>48h</param-value>
</init-param>
<init-param>
<param-name>back-size-low</param-name>
<param-value>0</param-value>
</init-param>
</init-params>
</cache-mapping>
<cache-mapping>
<cache-name>PEPServiceManager</cache-name>
<scheme-name>near-distributed</scheme-name>
......
......@@ -16,143 +16,156 @@
package org.jivesoftware.util;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import static org.junit.Assert.assertEquals;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.entitycaps.EntityCapabilitiesManager;
import org.jivesoftware.util.cache.CacheFactory;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xmpp.packet.IQ;
/**
* Test cases for the EntityCapabilitiesManager class.
* Test cases for the {@link EntityCapabilitiesManager} class.
*
* @author Armando Jagucki
* @see <a
* href="http://xmpp.org/extensions/xep-0115.html">XEP-0115:&nbsp;Entity&nbsp;Capabilities</a>
* @author Guus der Kinderen, guus.der.kinderen@gmail.com
*/
public class EntityCapabilitiesManagerTest {
@Test
public void testGenerateVerHash() {
@BeforeClass
public static void setUp() throws Exception {
CacheFactory.initialize();
}
IQ iq = new IQ(IQ.Type.result);
/**
* Tests the CAPS verification string generation based on the
* "Simple Generation Example" provided in section 5.2 of XEP-0115 (version
* 1.4 and later).
*/
@Test
public void testSimpleGenerationExample() throws Exception {
// formulate the result stanza
final IQ iq = new IQ(IQ.Type.result);
iq.setFrom("nurse@capulet.lit/chamber");
iq.setTo("juliet@capulet.lit");
iq.setID("disco123");
iq.setID("simpleexample1");
Element query = iq.setChildElement("query", "http://jabber.org/protocol/disco#info");
final Element query = iq.setChildElement("query",
"http://jabber.org/protocol/disco#info");
Element identity = query.addElement("identity");
// Consider an entity whose category is "client", whose service
// discovery type is "pc", service discovery name is "Exodus 0.9.1"
// (...)
final Element identity = query.addElement("identity");
identity.addAttribute("category", "client");
identity.addAttribute("type", "pc");
Element feature = query.addElement("feature");
feature.addAttribute("var", "http://jabber.org/protocol/disco#info");
feature = query.addElement("feature");
feature.addAttribute("var", "http://jabber.org/protocol/disco#items");
feature = query.addElement("feature");
feature.addAttribute("var", "http://jabber.org/protocol/muc");
assertEquals("Generating ver Hash #1", "8RovUdtOmiAjzj+xI7SK5BCw3A8=", generateVerHash(iq));
}
@Test
public void testGenerateVerHash2() {
String S = "client/pc<http://jabber.org/protocol/disco#info<http://jabber.org/protocol/disco#items<http://jabber.org/protocol/muc<";
assertEquals("Generating ver Hash #2", "8RovUdtOmiAjzj+xI7SK5BCw3A8=", StringUtils.encodeBase64(StringUtils.decodeHex(StringUtils.hash(S, "SHA-1"))));
}
@Test
public void testGenerateVerHash3() {
String S = "client/pda<http://jabber.org/protocol/geoloc<http://jabber.org/protocol/geoloc+notify<http://jabber.org/protocol/tune<http://jabber.org/protocol/tune+notify<";
assertEquals("Generating ver Hash #3", "DqGwXvV/QC6X9QrPOFAwJoDwHkk=", StringUtils.encodeBase64(StringUtils.decodeHex(StringUtils.hash(S, "SHA-1"))));
}
@Test
public void testGenerateVerHash4() {
String S = "client/pc<http://jabber.org/protocol/activity<http://jabber.org/protocol/activity+notify<http://jabber.org/protocol/geoloc<http://jabber.org/protocol/geoloc+notify<http://jabber.org/protocol/muc<http://jabber.org/protocol/tune<http://jabber.org/protocol/tune+notify<";
assertEquals("Generating ver Hash #4", "Hm1UHUVZowSehEBlWo8lO8mPy/M=", StringUtils.encodeBase64(StringUtils.decodeHex(StringUtils.hash(S, "SHA-1"))));
identity.addAttribute("name", "Exodus 0.9.1");
// (...) and whose supported features are
// "http://jabber.org/protocol/disco#info",
// "http://jabber.org/protocol/disco#items",
// "http://jabber.org/protocol/muc" and
// "http://jabber.org/protocol/caps"
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/disco#info");
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/disco#items");
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/muc");
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/caps");
// Using the SHA-1 algorithm (...)
final String verification = EntityCapabilitiesManager.generateVerHash(
iq, "sha-1");
// the verification string result must be QgayPKawpkPSDYmwT/WM94uAlu0=
assertEquals("QgayPKawpkPSDYmwT/WM94uAlu0=", verification);
}
/**
* Generates a 'ver' hash attribute.
*
* In order to help prevent poisoning of entity capabilities information,
* the value of the 'ver' attribute is generated according to the method
* outlined in XEP-0115.
*
* @param packet
* @return the generated 'ver' hash
*/
public String generateVerHash(IQ packet) {
// Initialize an empty string S.
String S = "";
/*
* Sort the service discovery identities by category and then by type
* (if it exists), formatted as 'category' '/' 'type'.
* Tests the CAPS verification string generation based on the
* "Complex Generation Example" provided in section 5.3 of XEP-0115 (version
* 1.4 and later).
*/
List<String> discoIdentities = new ArrayList<String>();
Element query = packet.getChildElement();
Iterator identitiesIterator = query.elementIterator("identity");
if (identitiesIterator != null) {
while (identitiesIterator.hasNext()) {
Element identityElement = (Element) identitiesIterator.next();
String discoIdentity = identityElement.attributeValue("category");
discoIdentity += '/';
discoIdentity += identityElement.attributeValue("type");
discoIdentities.add(discoIdentity);
}
Collections.sort(discoIdentities);
}
/*
* For each identity, append the 'category/type' to S, followed by the
* '<' character.
*/
for (String discoIdentity : discoIdentities) {
S += discoIdentity;
S += '<';
}
// Sort the supported features.
List<String> discoFeatures = new ArrayList<String>();
Iterator featuresIterator = query.elementIterator("feature");
if (featuresIterator != null) {
while (featuresIterator.hasNext()) {
Element featureElement = (Element) featuresIterator.next();
String discoFeature = featureElement.attributeValue("var");
discoFeatures.add(discoFeature);
}
Collections.sort(discoFeatures);
}
/*
* For each feature, append the feature to S, followed by the '<'
* character.
*/
for (String discoFeature : discoFeatures) {
S += discoFeature;
S += '<';
}
/*
* Compute ver by hashing S using the SHA-1 algorithm as specified in
* RFC 3174 (with binary output) and encoding the hash using Base64 as
* specified in Section 4 of RFC 4648 (note: the Base64 output
* MUST NOT include whitespace and MUST set padding bits to zero).
*/
S = StringUtils.hash(S, "SHA-1");
S = StringUtils.encodeBase64(StringUtils.decodeHex(S));
return S;
@Test
public void testComplexGenerationExample() throws Exception {
// formulate the result stanza
final IQ iq = new IQ(IQ.Type.result);
iq.setFrom("nurse@capulet.lit/chamber");
iq.setTo("juliet@capulet.lit");
iq.setID("simpleexample1");
final Element query = iq.setChildElement("query",
"http://jabber.org/protocol/disco#info");
query.addAttribute("node",
"http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=");
// Two identities: "client/pc/Psi" and "client/pc/"
final Element identityA = query.addElement("identity");
identityA.addAttribute("category", "client");
identityA.addAttribute("type", "pc");
identityA.addAttribute("name", "Psi 0.11");
identityA.addAttribute("xml:lang", "en");
final Element identityB = query.addElement("identity");
identityB.addAttribute("category", "client");
identityB.addAttribute("type", "pc");
identityB.addAttribute("name", "\u03a8 0.11");
identityB.addAttribute("xml:lang", "el");
// the features: "http://jabber.org/protocol/caps",
// http://jabber.org/protocol/disco#info",
// "http://jabber.org/protocol/disco#items",
// "http://jabber.org/protocol/muc".
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/disco#info");
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/disco#items");
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/muc");
query.addElement("feature").addAttribute("var",
"http://jabber.org/protocol/caps");
// extended service discovery forms
final Element ext = query.addElement(QName.get("x", "jabber:x:data"));
ext.addAttribute("type", "result");
final Element formField = ext.addElement("field");
formField.addAttribute("var", "FORM_TYPE");
formField.addAttribute("type", "hidden");
formField.addElement("value")
.setText("urn:xmpp:dataforms:softwareinfo");
final Element ipField = ext.addElement("field");
ipField.addAttribute("var", "ip_version");
ipField.addElement("value").setText("ipv4");
ipField.addElement("value").setText("ipv6");
final Element osField = ext.addElement("field");
osField.addAttribute("var", "os");
osField.addElement("value").setText("Mac");
final Element osvField = ext.addElement("field");
osvField.addAttribute("var", "os_version");
osvField.addElement("value").setText("10.5.1");
final Element softwareField = ext.addElement("field");
softwareField.addAttribute("var", "software");
softwareField.addElement("value").setText("Psi");
final Element softwarevField = ext.addElement("field");
softwarevField.addAttribute("var", "software_version");
softwarevField.addElement("value").setText("0.11");
// Using the SHA-1 algorithm (...)
final String verification = EntityCapabilitiesManager.generateVerHash(
iq, "SHA-1");
// the verification string result must be q07IKJEyjvHSyhy//CH0CxmKi8w=
assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", verification);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment