小猿“思前享后”为大家分享优质内容!————Share猿
5.插件开发
首先我们来解释一下什么是插件,以及他们在Tiagse服务器中如何工作。最后,会一步一步讲述怎么去创建插件。
- 写插件代码
- 插件配置
- 如何通过SM和插件来处理数据包
- SASL定制机制和配置
5.1.编写插件代码
Stanza 处理在4个步骤中进行。不同类型的插件负责每一步处理:
1.XMPPPreprocessorIfc
(https://projects.tigase.org/projects/tigase-
server/repository/changes/src/main/java/tigase/xmpp/XMPPPreprocessorIfc.java)
是数据包预处理插件的接口
2. XMPPProcessorIfc
(https://projects.tigase.org/projects/tigase-
server/repository/changes/src/main/java/tigase/xmpp/XMPPProcessor.java)
是包处理插件的接口。
3. XMPPPostprocessorIfc
(https://projects.tigase.org/projects/tigase-
server/repository/changes/src/main/java/tigase/xmpp/XMPPPostprocessorIfc.java)
是包后处理插件的接口。
4. XMPPPacketFilterIfc
(https://projects.tigase.org/projects/tigase-
server/repository/changes/src/main/java/tigase/xmpp/XMPPPacketFilterIfc.java)
是处理结果筛选的接口。
你在查看这些接口的过程中你只会找到一个单一的方法。所有的packet processing都在这里执行。所有这些接口的方法,参数基本类似,具体的参数如下:
- Packet packet-packet是需要被提前进行处理的。这个参数永远不能为空。它是一个不允许被改变的对象,但是在处理过程中,它的相关属性将会发生变化。
- XMPPResourceConnection session-保存所有用户会话数据的用户会话,也可以访问用户的数据存储库。它允许在生命周期中永久存储相关信息。如果在packet prcessing过程中用户不在线,这个参数可以为空。
- NonAuthUserRepository repo-这个是一个在用户session为空的时候能正常使用的用户存储库。这个存储库允许限制访问。它用户用户存储个人数据(但是不允许用户覆盖已有数据),像离线用户数据,同时它还允许读取用户的公共数据,比如
:VCards。 - Queue<Packet> results-这个是一个常用于输入packet processing结果的packets集合。在大多数情况下它是不可用的,但是如果插件需要访问外部数据库,可以通过它把相关信息传递给插件。
在仔细查看一些接口之后,您可以看到它们扩展了另一个接口:XMPPImplIfc(https://projects.tigase.org/projects/tigase-server/repository/changes/src/main/java/tigase/xmpp/XMPPImplIfc.java)它提供了关于插件实现的基本元信息。详情参考JavaDoc (http://docs.tigase.org/tigase-server/snapshot/javadoc/tigase/xmpp/impl/package-summary.html)。
出于本指南的目的,我们正在实施一个简单的插件来处理所有的 <message/>。将数据包转发到目标地址的信息包。传入的数据包转发对于用户连接和传出的信息包被转发到外部目的地地址。这个message plugin(https://projects.tigase.org/projects/tigase-
server/repository/changes/src/main/java/tigase/xmpp/impl/Message.java)在我们的git仓库中已经实现了。代码中已经有了一些相关的注释,但是通过学习这个指南让我更加深入的去了解和学习。
首先,你必须选择你想要实现的插件类型。如果它是一个packet processor你可以选择XMPPProcessorIfc接口。如果是pre-processor,你可以实现XMPPPreprocessorIfc接口。当然你的实现可以同时实现多个接口。还有两个抽象类,其中一个应该用作所有插件的基础XMPPProcessor或者使用AnnotatedXMPPProcessor支持注解。
5.1.1.使用支持的方式
类的声明应该类似下面这样(假设您正在实现这个包处理器):
public class Message extends AnnotatedXMPPProcessor
implements XMPPProcessorIfc
首先我们去创建插件ID。这是配置文件告诉服务器去加载插件的唯一标识。如果插件想要带有特定名称空间的元素包,在大多数情况下你可以用XMLNS。当然我们不能保证其他的packet也会使用这个XML元素。当我们想去处理所有的信息,同时不想去花太多的时间思考这个ID,让我暂时定义它的名字为:messgae。
一个插件使用一个静态ID字段和@ID标注在类上显示它的存在:
@Id(ID)
public class Message extends AnnotatedXMPPProcessor
implements XMPPProcessorIfc {
protected static final String ID = "message";
}
综上所述,这个插件只接收这种类型的packets。在这个例子中,插件在packets中只识别<messgae/>元素and only if they are in the "jabber:client" namespace。显示所有受支持的元素名称空间我们必须添加两个注释:
@Id(ID)
@Handles({
@Handle(path={ "message" },xmlns="jabber:client")
})
public class Message extends AnnotatedXMPPProcessor
implements XMPPProcessorIfc {
private static final String ID = "message";
}
5.1.2.使用较旧的非基于注释的实现
类声明应该是这样的(假设您正在实现包的实现处理器):
public class Message extends XMPPProcessor
implements XMPPProcessorIfc
首先要创建的是上面的插件ID。
一个插件使用以下代码通知它的ID:
private static final String ID = "message";
public String id() { return ID; }
正如前面提到的,这个插件只接收这种类型的packets processing。在这个例子中,插件在packets中只识别<messgae/>元素and only if they are in the "jabber:client" namespace。显示所有受支持的元素名称空间我们必须添加另外两种方法:
public String [] supElements() {
return new String [] {"message"};
}
public String [] supNamespaces() {
return new String [] {"jabber:client"};
}
5.1.3.处理方法的实现
现在我们已经准备好了加载Tigase的插件。下一步是实际的packets processing的方法。对于完成的代码,请参考在Git中的插件代码。我会在这里注释一些可能会让人困惑的元素或者添加更多的代码行,这样我们会更容易理解这个案例。
@Override
public void process(Packet packet, XMPPResourceConnection session,
NonAuthUserRepository repo, Queue<Packet> results, Map<String, Object> settings)
throws XMPPException {
// For performance reasons it is better to do the check
// before calling logging method.
if (log.isLoggable(Level.FINEST)) {
log.log(Level.FINEST, "Processing packet: {0}", packet);
}
// You may want to skip processing completely if the user is offline.
if (session == null) {
return;
} // end of if (session == null)
try {
// Remember to cut the resource part off before comparing JIDs
BareJID id = (packet.getStanzaTo() != null) ?
packet.getStanzaTo().getBareJID() : null;
// Checking if this is a packet TO the owner of the session
if (session.isUserId(id)) {
// Yes this is message to 'this' client
Packet result = packet.copyElementOnly();
// This is where and how we set the address of the component
// which should receive the result packet for the final delivery
// to the end-user. In most cases this is a c2s or Bosh
component
// which keep the user connection.
result.setPacketTo(session.getConnectionId(packet.getStanzaTo()));
// In most cases this might be skipped, however if there is a
// problem during packet delivery an error might be sent back
result.setPacketFrom(packet.getTo());
// Don't forget to add the packet to the results queue or it
// will be lost.
results.offer(result);
return;
} // end of else
// Remember to cut the resource part off before comparing JIDs
id = (packet.getStanzaFrom() != null) ?
packet.getStanzaFrom().getBareJID() : null;
// Checking if this is maybe packet FROM the client
if (session.isUserId(id)) {
// This is a packet FROM this client, the simplest action is
// to forward it to its destination:
// Simple clone the XML element and....
// ... putting it to results queue is enough
results.offer(packet.copyElementOnly());
return;
}
// Can we really reach this place here?
// Yes, some packets don't even have from or to address.
// The best example is IQ packet which is usually a request to
// the server for some data. Such packets may not have any addresses
// And they usually require more complex processing
// This is how you check whether this is a packet FROM the user
// who is owner of the session:
JID jid = packet.getFrom();
// This test is in most cases equal to checking getElemFrom()
if (session.getConnectionId().equals(jid)) {
// Do some packet specific processing here, but we are dealing
// with messages here which normally need just forwarding
Element el_result = packet.getElement().clone();
// If we are here it means FROM address was missing from the
// packet, it is a place to set it here:
el_result.setAttribute("from", session.getJID().toString());
Packet result = Packet.packetInstance(el_result,
session.getJID(),
packet.getStanzaTo());
// ... putting it to results queue is enough
results.offer(result);
}
} catch (NotAuthorizedException e) {
log.warning("NotAuthorizedException for packet: " + packet);
results.offer(Authorization.NOT_AUTHORIZED.getResponseMessage(packet,
"You must authorize session first.", true));
} // end of try-catch
}
5.2.插件配置
插件配置现在不是很简单但是我们要改变它很快。
现在,告诉tigase服务器加载插件配置文件的最好的方式是通过配置init.properties配置文件。在服务器运行时,--sm-plugins属性会激活一系列的插件id。请参考文档获取更多的信息。
显然,你必须要知道怎么把你的插件id添加到插件id列表中。有两种方式可以找到我们的插件id列表。第一种方法是通过日志文件: logs/tigase-console.log。如果你打开日志文件,你会找到下面的相关信息:
Loading plugin: jabber:iq:register ...
Loading plugin: jabber:iq:auth ...
Loading plugin: urn:ietf:params:xml:ns:xmpp-sasl ...
Loading plugin: urn:ietf:params:xml:ns:xmpp-bind ...
Loading plugin: urn:ietf:params:xml:ns:xmpp-session ...
Loading plugin: roster-presence ...
Loading plugin: jabber:iq:privacy ...
Loading plugin: jabber:iq:version ...
Loading plugin: http://jabber.org/protocol/stats ...
Loading plugin: starttls ...
Loading plugin: vcard-temp ...
Loading plugin: http://jabber.org/protocol/commands ...
Loading plugin: jabber:iq:private ...
Loading plugin: urn:xmpp:ping ...
这是在你启动tigase的时候加载的一系列插件。
另一种方式是看session manager源码中默认的插件列表:
private static final String [] PLUGINS_FULL_PROP_VAL =
{"jabber:iq:register", "jabber:iq:auth", "urn:ietf:params:xml:ns:xmpp-sasl",
"urn:ietf:params:xml:ns:xmpp-bind", "urn:ietf:params:xml:ns:xmpp-session",
"roster-presence", "jabber:iq:privacy", "jabber:iq:version",
"http://jabber.org/protocol/stats", "starttls", "msgoffline",
"vcard-temp", "http://jabber.org/protocol/commands", "jabber:iq:private",
"urn:xmpp:ping", "basic-filter", "domain-filter"};
如果你想让你的插件配置到上面的list列表中,你必须添加插件的id值和插件的相关属性到里面。下面我们通过这个例子简单的说一下插件的相关信息:
--sm-plugins=jabber:iq:register,jabber:iq:auth,......,message
假设你的插件在该路径中,它将会在启动的时候被加载进来。
这个也有另一种插件的配置。如果你看过the Writing Plugin Code guide你应该会记得Map settings的处理参数。这是你可以设置配置文件和设置插件在什么时候运行的一些属性。
init.properties会对其进行进一步的初始化。这一系列的属性以下面一个字符串开始:sess-man/plugins-conf/,然后你通过你的key和pair值添加你的插件ID。
sess-man/plugins-conf/pluginID/key1=val1
sess-man/plugins-conf/pluginID/key2=val2
sess-man/plugins-conf/pluginID/key3=val3
可以在一个配置字符串中为几个插件提供设置通过分别指定multiple pluginIDs通过下下面一样的方式:
sess-man/plugins-conf/plugin1,plugin2,plugin3/key1=val1
这样可以同时设置插件1、插件2、插件3。
最后我们千万不要忘了遗漏调了插件ID。
sess-man/plugins-conf/key1=val1
然后配置的键-值对将是一个全局/通用的插件设置加载插件。
5.4.如何通过SM和插件来处理数据包
对于Tigase服务器插件开发来说,了解它是如何工作的很重要。有
不同类型的插件负责在数据流的不同阶段处理数据包。在继续进行实际的编码部分之前,请先阅读下面的介绍。
5.3.1.介绍
在tigase服务器中,插件是负责处理XMPP stanzas的代码。一个单独的插件可能负责处理消息,另一种用于处理目前,一个独立的插件负责iq列表,另一个用于iq的版本上。
一个插件提供关于确切XML的信息元素名通xmlns标签。所以你可以创建一个可以包含所以子插件的插件。
对于特定的stanza元素没有插件,在这种情况下,使用默认动作
这是一个简单的转发节到一个目标地址。也可能有不止一个特定XML元素的插件,然后它们都同时处理同一个stanza在单独的线程中,所以不能保证插件的处理顺序。
每个stanza通过处理少量 processes packets的Session Manager插件。看下面的图片:
该图显示,每一节都由会话管理器处理4个步骤:
- 1.Pre-processing -所有加载的pre-processors都接收数据包进行处理。他们的工作在session manager进程中,同时他们没有内部队列的处理。当他们在会话管理器线程中工作,重要的是他们将处理时间限制在绝对最小值,因为它们可能影响会话管理器的性能。pre-processors的目的是为了防止包阻塞。如果预处理结果是正确的包被阻塞,不执行进一步的处理。
- Processing -这是下一步,如果在任何时候pre-processors都不被阻塞的包处理。它会被插入到所有处理器队列中,并请求对其感兴趣特定的XML元素。每个处理器在一个单独的线程中工作,并且有自己的内部固定大小的处理队列中。
- 3.Post-processing - 如果没有一个处理器,那么这个包就会经过所有的post-
processors。最后被构建到会话管理器中post-processor试图对其应用默认操作在第2步中没有被处理packet的post-processor 。通常情况下,默认操作只是将数据包转发到目的地。最常见的是<message/>包。 -
4.最后,如果以上三个步骤中的任何一个输出/结果数据包,所有这些步骤都将通过过滤器,它可以或可能阻止它们。
需要注意的一件重要的事情是我们有两种或两种可能是包的地方
封锁或者过滤掉。
一个值得我们注意的重要事情是有两种类型或者两个地方的packet可能会阻塞或者被过滤。一个地方是packet被插件process前或者,另一个地方是
是在处理了过滤应用于处理器插件生成的所有结果之后。
还需要注意的是,会话管理器和处理器插件充当包消费者。包被用于处理,一旦处理完成,包就会被销毁。因此,要将数据包转发到目的地,其中一个处理器必须创建一个packet的副本,设置所有属性和属性并将其作为处理结果返回。当然处理器可以产生任意数量的数据包。结果包可以在上面的任何一个中生成
处理的4个步骤。看一看下面的图片:
如果数据包P1是从服务器外部发送的,例如在另一台服务器上的用户或一些组件(MUC、PubSub、传输),然后一个处理器必须创建一个拷贝(P2)包并正确设置所有属性和目标地址。包P1一直在处理过程中被会话管理器所消耗,并且由一个新的信息包生成的一个插件。
当然,在从组件返回给用户的过程中也会发生同样的事情:
来自组件的数据包被处理,其中一个插件必须生成一个副本
包将其传递给用户。当然,当特定数据包没有插件时包转发是应用的默认操作。
它是这样实现的,因为输入包P1可以由许多插件在同样的时间,包应该是不可变的,并且必须在它到达之后才改变用于处理的会话管理器。
最明显的处理工作流程是当用户向服务器发送请求并从服务器得到一个响应:
这个设计有一个令人惊讶的结果。如果你看下面的图片两个用户之间的通信你可以看到数据包在它之前被复制了两次交付到最终目的地:
数据包必须由会话管理器处理两次。第一次被处理的时候代表用户A作为一个输出的数据包,第二次是代表用户B作为输入的数据包。
这是为了确保用户有权限发送一个数据包并且所有的处理都被应用
于数据包,也要确保用户B获得了接收数据包的权限处理应用。例如,如果用户B是离线的,那么就有一个离线消息处理器应该将数据包发送到数据库,而不是用户b。
5.4.SASL自定义机制和配置
这个api同样适用于XMPP Server 5.2.0的版本或者更后更新的版本。
请注意,API正在积极开发中。这个描述可以在任何时候更新。
5.4.1.SASL的基本配置
Tigase XMPP服务器中的SASL实现与Java API兼容,完全相同的
使用接口。
SASL的实现包括以下部分:
- mechanism
- CallbackHandler
SASL插件的属性列表 (sess-man/plugins-conf/urn:ietf:params:xml:ns:xmpp-sasl):
属性 | 描述 |
---|---|
factory | 一个用于SASL机制的工厂类。在详细描述机制配置 |
callbackhandler | 一个默认的回调处理器类。详细描述在CallbackHandler配置 |
callbackhandler-${MECHANISM} | 针对特定的一个回调处理器类机制。在详细描述CallbackHandler配置 |
mechanism-selector | 用于过滤SASL机制的类在流。详细的描述在选择机制 |
机制配置
为了增加新的机制,必须对该机制的新工厂进行注册。这是可以做到的在init中有一条新行。像这样的属性文件:
sess-man/plugins-conf/urn\:ietf\:params\:xml\:ns\:xmpp-
sasl/factory=com.example.OwnFactory
该类必须实现SaslServerFactory接口。所有返回的机制getMechanismNames()方法将被自动注册。
默认工厂是tigase.auth.TigaseSaslServerFactory,它提供了普通的和匿名的机制。
回调的配置
CallbackHandler是一个用于来自存储库并将它们一个机制提供的数据装载/检索身份验证。
要注册一个新的回调处理程序,在init中有一条新行。像这样的属性文件必须添加:
sess-man/plugins-conf/urn\:ietf\:params\:xml\:ns\:xmpp-
sasl/callbackhandler=com.example.DefaultCallbackHandler
也可以为不同的机制注册不同的回调处理程序:
sess-man/plugins-conf/urn\:ietf\:params\:xml\:ns\:xmpp-sasl/callbackhandler-
PLAIN=com.example.PlainCallbackHandler
sess-man/plugins-conf/urn\:ietf\:params\:xml\:ns\:xmpp-sasl/callbackhandler-
OAUTH=com.example.OAuthCallbackHandler
在身份验证过程中,Tigase服务器总是检查特定于选定的处理程序机制,如果没有特定的处理程序,则使用默认的处理程序。
在流中选择可用的机制
tigase.auth.MechanismSelector接口用于选择可用的一个机制流。方法 filterMechanisms()应该返回一个具有可用机制的集合:
- 1.所有注册SASL工厂
- 2.XMPP会话数据(来自XMPPResourceConnection类)
默认选择器仅从Tigase的默认工厂返回机制(TigaseSaslServerFactory)。
尽可能的通过init.properties配置文件指定的类来使用定制选择器:
sess-man/plugins-conf/urn\:ietf\:params\:xml\:ns\:xmpp-sasl/mechanism-
selector=com.example.OwnSelector
5.4.2.日志/认证
在XMPP流被客户端打开之后,服务端检查SASL机制对XMPP session是否可用。取决于流是否被加密,根据域的不同,服务器可以呈现不同的可用身份验证机制。机械选择器负责选择机制。允许列表机制存储在XMPP会话对象中。
当客户端/用户开始身份验证过程时,它使用一种特定的机制。它
必须使用服务器提供的一种机制,以供本次会话使用。服务器检查客户端使用的机制是否在允许的机制列表中。检查成功后,服务端创建SaslServer类实例并进行了交换身份验证信息。根据所使用的机制,身份验证数据是不同的。
当SASL认证被正确的完成,Tigase服务器就有了认证的用户名和认证的BareJID。首先,服务器自动构建用户的JID基于流打开元素中用于属性的域。
如果在成功的认证之后,方法调用:getNegotiatedProperty("IS_ANONYMOUS") 方法返回Boolean.TRUE然后用户的会话被标记为匿名。为有效和注册当我们不想加载任何用户数据,比如roster,vcard,隐私列表等等。这是一个性能和资源使用的含义,可以是对诸如支持聊天这样的用例有用。授权是基于客户端执行的,我们不需要为用户的会话加载任何XMPP特定的数据。
关于实现的更多细节可以在定制机制开发中找到部分。
5.4.3.内置的机制
PLAIN
ANONYMOUS
5.4.4.自定义机制发展
Mechanism
getAuthorizationID()方法来自SaslServer类,该方法返回用户的认证JID。如果该方法只返回像romeo这样的用户名,例如,服务器自动附加域名以生成有效的BareJID:romeo@example.com。以防这个方法返回一个完整的、有效的BareJID,服务器不会改变任何东西。
来自SessionManagerHandler的handleLogin()方法将被用户的Bare JID调用由getAuthorizationID()提供(或稍后使用stream域名创建)。
CallbackHandler
对于每一个会话授权,服务器都会创建一个新的、单独的空handler。工厂创建处理程序实例允许向处理程序注入不同的对象,这取决于
由处理程序类实现的接口
- AuthRepositoryAware -注入AuthRepository;
- DomainAware -注入域名,用户试图对其进行身份验证
- NonAuthUserRepositoryAware - 注入NonAuthUserRepository,尽管我不知道
什么……
一般注解
用于非sasl认证机制的JabberIqAuth使用与之相同的回调SASL机制。
在存储库接口中的方法认证将被弃用。这些接口将被视为用户详细信息提供者。将会有新的方法,允许额外的在数据库上的登录操作,比如最后一次成功的登录记录。
已知问题
因为JabberIqAuth初始化比较特殊,我们强烈建议在init.properties中配置前缀进行使用:
sess-man/plugins-conf/${KEY}=${VALUE}
也可以用下面的方式进行替代:
sess-man/plugins-conf/urn\:ietf\:params\:xml\:ns\:xmpp-sasl/${KEY}=${VALUE}
如果JabberIqAuth是禁用的,那么这是不必要的。
参考文章:
【1】Tigase Development Guide
扫描以下公众号关注小猿↓↓↓↓↓↓↓↓
更多资讯请在简书、微博、今日头条、掘金、CSDN都可以通过搜索“Share猿”找到小猿哦!!!
网友评论