找回密码
 加入
搜索
查看: 4004|回复: 3

[系统综合] 一套ROS API VB 源代码· 在AU3上具体如何实现呢···[已解决]

[复制链接]
发表于 2012-7-17 17:17:48 | 显示全部楼层 |阅读模式
本帖最后由 sdc7 于 2012-7-19 08:28 编辑

已经实现· 不在麻烦大家·~


这个是VB的例子~
下面是API中文文档· 求AU3实现·                       ROS  API 中文说明

前言:  
    ROS 是 一个比较流行的软路由系统,它的强大在于它的高度定制性,它提供了应用程序编程使用的API接口,是它应用更加灵活,我们可以自己开发软件或WEB程序来操控ROS,比较实用的例子就是当用ROS管理小区网络时,我们用自己写的软件来管理上网账号,安全又方便,总之好处多多,
  
   本文基于官方API文档:http://wiki.mikrotik.com/wiki/API 翻译而成,结合了作者的理解,水平有限,难免有错漏的地方,望大家批评指正,谢谢!
  
正文:

目录
1 简介
2 通讯协议
2.1 API词
2.1.1 命令词
2.1.2 属性词
2.1.3 API 属性词
2.1.4 查询语句
2.1.5 回复语句
2.2 API 特定命令说明
3 初始登录
4 标签(.Tags )
5 API命令说明
5.1 查询词说明
5.2 OID
6 API 命令例子
6.1 /system/package/getall
6.2 /user/active/listen
6.3 /cancel, simultaneous commands
7 客户端程序例子
8 参考
8.1 API examples in the Wiki
8.2 API examples on the MikroTik Forum
8.3 API exmaples elsewhere




简介:


    应用程序编程接口(API),允许用户创建定制的软件解决方案与RouterOS的

沟通,收集信息,调整配置和管理路由器。 API紧随命令行界面(CLI)的语法。

它可以用来创建转换或自定义的配置工具,以帮助管理使用RouterOS的路由器。

使用API需要RouterOS版本3.x或更高的版本。

默认情况下,API使用端口8728,默认服务是禁用的。通讯服务的名称是API,请

在IP-SERVER里开启,服务管理的详细信息,请参阅相应的手册部分。



通讯协议:

     应用程序与路由器的通信是通过发送和接收路由器的一个或多个编码的句子来

完成的。一个句子是以零字符结尾的单词序列。词是句子以某种方式编码 -(编码

长度是数据的一部分),路由器发送和接收回复并发送这些句子。每个句子发送到路由器

使用API没有特定的顺序,每个命令字是以零字符标记结束的。当路由器

接收到完整的句子(命令字,或多个属性的话,零字符结束),它就开始执行命令,

并将结果返回给应用程序。


API语句:
   词是句子的一部分。每个词长都用某种方式编码 - 词长编码跟随词的内容就是一个句子。词的长度应为将要发送的字节计数(不包括词长编码)。
词长编码如下:


Value of length        # of bytes        Encoding
0 <= Len <= 0x7F        1        Len, lowest byte  词长度
0x80 <= Len <= 0x3FFF        2        Len | 0x8000, two lower bytes  
0x4000 <= Len <= 0x1FFFFF        3        Len | 0xC00000, three lower bytes
0x200000<=Len <= 0xFFFFFFF        4        Len | 0xE0000000
Len >= 0x10000000        5        0xF0 and Len as four bytes

对应的10进制:

长度        字节数        词长编码
0 <= 长度<=127        1        长度低位
128 <= 长度<= 16383        2        位或(长度 ,32768) 取低2位
16384<= 长度<= 2097151        3        位或(长度 ,12582912)取低3位
2097152 <= 长度 <= 268435455        4        位或(长度 ,3758096384)
长度 >= 268435456        5        {240} + 到字节集(长度)


&#61558;        每个词的编码长度,然后紧接着许多字节的词内容(词长编码 + 词内容);
&#61558;        字组合成句子,以零字符结束;
&#61558;        最高长度可以达到0x7FFFFFFFFF,最高占用4字节;
&#61558;        词长编码字节(Len)总是在最前面(网络顺序);
&#61558;        如果单词的第一个字节是> =0xF8,那么它是一个保留的控制字节。未知的控制字节API客户端接收后无法继续,因为它不知道如何解释以下字节;
&#61558;        目前,控制字节不使用;
&#61558;        句子一般情况是这样的:<词长编码><词的内容>,主要有5种类型:命令语句,属性语句,API属性语句,查询语句,回复语句;

        
命令语句(Command word)

   在句子的第一个字是由名字(属性)和零长度的词终止字的命令。命令字的名称应以'/'开始。命令中的名字,与命令行界面输入的一样,要注意的API中的命令不能有空格,需要用以'/'替换,比如查看网卡信息 "/int pri" 在API里就必须这样"/int/pri",不然无法识别;

注意:发送的命令必须严格按照这样的顺序:
  
&#61656;        编码长度
&#61656;        内容前缀"/"
&#61656;        命令行的转换命令(空格用"/"替换)


API特定的命令:

   getall
   login
   cancel


命令连接例子:
/login
/ip/address/getall
/user/active/listen
/interface/vlan/remove
/system/reboot

2.1.2 属性语句(Attribute word)
     每个命令都有其自己的属性列表,命令内容决定属性。
     属性结构由5部分组成,顺序如下:
编码长度
内容前缀( ! - = )
属性名称
    分离符号( ! - = )
    属性值(可以被忽略,说明这个属性没有值)
注意:为了编码方便,一个命令里的多个属性赋值可以在一句里完成
       属性值可以为空
没有编码的长度前缀的例子:

=address=10.0.0.1        
=name=iu=c3Eeg        
=disable-running-check=yes


注意:属性词和API参数的顺序并不重要,不应依赖;


2.1.3 API属性语句


API属性语句的结构必须严格按照下面的顺序:

编码长度
内容与名称前缀"=."
属性名称
名称后缀符"="
属性值

system/resource/print  
=.proplist=uptime,cpu-load,uptime.oid,cpu-load.oid  


目前只有这样的API属性的标签。

注意:如果句子包含了属性语句标签,返回的每一个句子和从路由器标记句子将标记相同的标签,关于标签后面的章节有单独的介绍;



2.1.4查询语句
  
     查询语句支持对参数进行一定范围内的模糊查询,在下面的章节中有单独介绍;

例如句子使用查询词的属性:

/interface/print
?type=ether
?type=vlan
?#|!

查询语句以符号"?"开始,目前查询语句只支持"print"命令;
警告:查询语句始终是在最前面;

2.1.5 回复语句

     回复语句只能由路由器发送,它仅发送完整的句子,由客户端发送响应。

&#61550;        回复语句的第一个字是以"!"开始的;
&#61550;        发送的每一句话产生至少一个答复(如果连接没有得到终止);
&#61550;        每一句的最后答复是答复的第一个字"!done" ;
&#61550;        错误和异常情况以"!trap"开始;
&#61550;        开始数据回复以"!re"开始;
&#61550;        如果连接被关闭,RouterOS发送"!fatal"作为致命的原因进行答复并且关闭连接;


2.2  API 语句

     API语句是使用API通信的主要对象

&#61550;        空的句子被忽略;
&#61550;        句子是以字符"0"作为结束标志的;
&#61550;        客户端登陆后发送句子有数量和大小的限制;
&#61550;        属性语句没有顺序区别,比如.proplist属性语句的顺序和计数就是多变的;

句子结构如下:

&#61550;        第一句话应该包含命令字;
&#61550;        应包含结束标志字符{0};
&#61550;        可以包含0个或多个属性词,没有特定的顺序,不管什么属性词必须在句子里发送,属性词的顺序并不重要;
&#61550;        可以包含没有一个或几个查询词,查询词在句子的顺序是很重要的;


注:零长度的词(字节'0')终止了一句,如果没有提供,路由器将无法测试句字的有效性,只能把收到的句子当做句子的一部分;



初始登录
/login

!done
=ret=ebddd18303a54111e2dea05a92ab46b4

/login
=name=admin
=response=001ea726ed53ae38520c8334f82d44c9f2

!done

注意:每个命令和响应结束都有一句空的语句;
&#61550;        首先,客户端发送"/ login"命令
&#61550;        路由器的回复包含"=ret=需要的参数"
&#61550;        客户端发送第二个"/login"命令,接着是用户名("=name=username")和密码("=response=response")验证命令;
&#61550;        在错误的情况下,答复包含= RET =错误消息。
&#61550;        在成功登录客户端的情况下,就可以开始发出命令。

标签(.tag)
&#61550;        它是可以同时运行多个命令,而不必等待前一个完成。如果API的客户端是这样做的,需要区分命令的反应,它可以使用在命令句子'.tag'API的参数。
&#61550;        如果你有“.tag”命令句与非空值的参数,然后'.tag'参数完全相同的值将包含在该命令生成的所有答复。
&#61550;        如果不包括'.tag'参数,或它的值是空的,那么这个命令所有的反应将不会有“.tag”参数。

命令描述

&#61550;        /cancle (取消)
&#61550;        可选参数:=tag=tag ,取消所有正在运行的命令;
&#61550;        不能取消本身
&#61550;        所有取消的命令都是中断操作,并且在通常情况下会产生'!trap' 和'!done' 的回复;
&#61550;        请注意,"/cancel "是单独的命令,可以有它自己独特的'.tag' 参数,它是不相关'=.tag' 这个命令的参数;


&#61550;        listen (监听)
&#61550;        listen是在控制台print 命令可用的情况下使用,它没有预期中的效果(即可能无法正常工作);
&#61550;        !re 数据回复句子会产生特定的项目列表中的一些变化;
&#61550;        当项目被删除或以其他任何方式清除,数据回复句子( '!re')的属性值会包含'=.dead=yes' ;
&#61550;        此命令不会终止。终止使用取消命令"/cancle"

&#61550;        getall (获取全部信息)
&#61557;        getall命令是在控制台print命令可用的情况下使用,自3.21版本以后的getall命令是print命令的别名。
&#61557;        回复包含= .id =项目内部编号属性
&#61550;        print  (显示)

&#61550;        API的print命令和控制台的print命令的不同主要有以下几个方面:
&#61557;        虽然参数不支持,但可以使用查询词(见下文)筛选项目。
&#61557;        传回的项目可能有额外的属性。
&#61557;        返回的属性的顺序是没有定义的。
&#61557;        如果列表中包含重复的条目,这些条目的处理没有被定义。
&#61557;        如果属性格式目前是在.proplist里,但项目里没有这个属性,该项目没有这个属性的值,(?名称将评估该项目为假)
&#61557;        如果没有设置.proplist参数,将打印所有的属性,甚至那些比较耗时的项目(如文件内容性能记数),因此推荐使用.proplist参数,设置=detail= argument,虽然可能会有遗漏,但是换回的却是高性能。
查询
print命令接受限制返回的句子设置的查询词。此功能是自RouterOS的3.21开始的。

&#61550;        查询词以符号"?'开始。
&#61550;        查询词的顺序是在最前面的,从第一个字开始模糊查询。
&#61550;        对查询列表中每个项目进行评估,如果查询成功,项目被处理,如果查询失败,项目将被忽略。
&#61550;        查询评估在堆栈使用布尔逻辑值。最初,堆栈包含无限量的“真”值。在评估结束时,如果堆栈包含至少一个“假”值,查询失败。
&#61550;        查询词按照下列规定操作:

查询词                               描叙

?name                   如果项目属性名称的值不为空,堆栈压入真,否则压入假
?-name                  如果项目属性名称的值为空,堆栈压入真,否则压入假
?name=x 或 ?=name=x    如果项目属性名称的值=x,堆栈压入真,否则压入假
?<name=x               如果项目属性名称的值<x ,堆栈压入真,否则压入假
?>name=x               如果项目属性名称的值>x ,堆栈压入真,否则压入假
?# 操作符    适用于操作在堆栈的值。
&#61550;        操作字符串是从左向右计算的。
&#61550;        任何其他的字符或单词的末尾的十进制数字序列被解释为一个堆栈指数。最高值指数0。
&#61550;        后跟一个字符的索引,压入该指数值的副本
&#61550;        指数是由单词的末尾替换所有值与该指数的值。
&#61550;        !操作符是反义字符,替换堆顶值为反义值;
&#61550;        & 操作符是逻辑"与"操作符,从堆栈弹出2个值,进行逻辑与操作并将结果压于入堆栈
&#61550;        | 操作符是逻辑"或"操作符, 从堆栈弹出2个值,进行逻辑“或”操作并将结果压于入堆栈
&#61550;        . 索引后什么都不做
&#61550;        . 另一个字符后压入副本值到堆顶。

例子:
取得所有的以太网和VLAN接口:
/interface/print
?type=ether
?type=vlan
?#|

获取所有有备注的路由:
/ip/route/print
?>comment=

OID
print命令可以返回属性是在SNMP OID值。此功能出现在3.23版本以后。
在控制台,OID值可以运行'print oid'命令看出。在API这些属性有".OID"结束的名称,并可以加入他们的名字的值用".proplist"检索。一个例子:


/system/resource/print  
=.proplist=uptime,cpu-load,uptime.oid,cpu-load.oid   

!re  
=uptime=01:22:53  
=cpu-load=0  
=uptime.oid=.1.3.6.1.2.1.1.3.0  
=cpu-load.oid=.1.3.6.1.2.1.25.3.3.1.2.1  
!done  

命令试例 :
/system/package/getall
/system/package/getall

!re
=.id=*5802
=disabled=no
=name=routeros-x86
=version=3.0beta2
=build-time=oct/18/2006 16:24:41
=scheduled=

!re
=.id=*5805
=disabled=no
=name=system
=version=3.0beta2
=build-time=oct/18/2006 17:20:46
=scheduled=

... 更多 !re 回复句子 ...
!re
=.id=*5902
=disabled=no
=name=advanced-tools
=version=3.0beta2
=build-time=oct/18/2006 17:20:49
=scheduled=

!done


/user/active/listen
/user/active/listen

!re
=.id=*68
=radius=no
=when=oct/24/2006 08:40:42
=name=admin
=address=0.0.0.0
=via=console

!re
=.id=*68
=.dead=yes

加上 /cancel 的例子:
/login

!done
=ret=856780b7411eefd3abadee2058c149a3

/login
=name=admin
=response=005062f7a5ef124d34675bf3e81f56c556

!done

-- first start listening for interface changes (tag is 2)
/interface/listen
.tag=2

-- disable interface (tag is 3)
/interface/set
=disabled=yes
=.id=ether1
.tag=3

-- this is done for disable command (tag 3)
!done
.tag=3

-- enable interface (tag is 4)
/interface/set
=disabled=no
=.id=ether1
.tag=4

-- this update is generated by change made by first set command (tag 3)
!re
=.id=*1
=disabled=yes
=dynamic=no
=running=no
=name=ether1
=mtu=1500
=type=ether
.tag=2

-- this is done for enable command (tag 4)
!done
.tag=4

-- get interface list (tag is 5)
/interface/getall
.tag=5

-- this update is generated by change made by second set command (tag 4)
!re
=.id=*1
=disabled=no
=dynamic=no
=running=yes
=name=ether1
=mtu=1500
=type=ether
.tag=2

-- these are replies to getall command (tag 5)
!re
=.id=*1
=disabled=no
=dynamic=no
=running=yes
=name=ether1
=mtu=1500
=type=ether
.tag=5

!re
=.id=*2
=disabled=no
=dynamic=no
=running=yes
=name=ether2
=mtu=1500
=type=ether
.tag=5

-- here interface getall ends (tag 5)
!done
.tag=5

-- stop listening - request to cancel command with tag 2, cancel itself uses tag 7
/cancel
=tag=2
.tag=7

-- listen command is interrupted (tag 2)
!trap
=category=2
=message=interrupted
.tag=2

-- cancel command is finished (tag 7)
!done
.tag=7

-- listen command is finished (tag 2)
!done
.tag=2
客户端例子:
&#61550;        this is simple API client in Python2
&#61550;        example for Python3
&#61550;        usage: api.py ip-address username password
&#61550;        after that type words from keyboard, terminating them with newline
&#61550;        Since empty word terminates sentence, you should press enter twice after last word before sentence will be sent to router.

#!/usr/bin/python
import sys, posix, time, md5, binascii, socket, select
class ApiRos:
    "Routeros api"
    def __init__(self, sk):
        self.sk = sk
        self.currenttag = 0
      
    def login(self, username, pwd):
        for repl, attrs in self.talk(["/login"]):
            chal = binascii.unhexlify(attrs['=ret'])
        md = md5.new()
        md.update('\x00')
        md.update(pwd)
        md.update(chal)
        self.talk(["/login", "=name=" + username,
                   "=response=00" + binascii.hexlify(md.digest())])
    def talk(self, words):
        if self.writeSentence(words) == 0: return
        r = []
        while 1:
            i = self.readSentence();
            if len(i) == 0: continue
            reply = i[0]
            attrs = {}
            for w in i[1:]:
                j = w.find('=', 1)
                if (j == -1):
                    attrs[w] = ''
               else:
                    attrs[w[:j]] = w[j+1:]
            r.append((reply, attrs))
            if reply == '!done': return r

    def writeSentence(self, words):
        ret = 0
        for w in words:
            self.writeWord(w)
            ret += 1
        self.writeWord('')
        return ret

    def readSentence(self):
        r = []
        while 1:
            w = self.readWord()
            if w == '': return r
            r.append(w)
            
    def writeWord(self, w):
        print "<<< " + w
        self.writeLen(len(w))
        self.writeStr(w)

    def readWord(self):
        ret = self.readStr(self.readLen())
        print ">>> " + ret
        return ret

    def writeLen(self, l):
        if l < 0x80:
            self.writeStr(chr(l))
        elif l < 0x4000:
            l |= 0x8000
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))
        elif l < 0x200000:
            l |= 0xC00000
            self.writeStr(chr((l >> 16) & 0xFF))
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))
        elif l < 0x10000000:        
            l |= 0xE0000000         
            self.writeStr(chr((l >> 24) & 0xFF))
            self.writeStr(chr((l >> 16) & 0xFF))
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))
        else:                       
            self.writeStr(chr(0xF0))
            self.writeStr(chr((l >> 24) & 0xFF))
            self.writeStr(chr((l >> 16) & 0xFF))
            self.writeStr(chr((l >> 8) & 0xFF))
            self.writeStr(chr(l & 0xFF))

    def readLen(self):              
        c = ord(self.readStr(1))   
        if (c & 0x80) == 0x00:      
            pass                    
        elif (c & 0xC0) == 0x80:   
            c &= ~0xC0              
            c <<= 8                 
            c += ord(self.readStr(1))   
        elif (c & 0xE0) == 0xC0:   
            c &= ~0xE0              
            c <<= 8                 
            c += ord(self.readStr(1))   
            c <<= 8                 
            c += ord(self.readStr(1))   
        elif (c & 0xF0) == 0xE0:   
            c &= ~0xF0              
            c <<= 8                 
            c += ord(self.readStr(1))   
            c <<= 8                 
            c += ord(self.readStr(1))   
            c <<= 8                 
            c += ord(self.readStr(1))   
        elif (c & 0xF8) == 0xF0:   
            c = ord(self.readStr(1))     
            c <<= 8                 
            c += ord(self.readStr(1))   
            c <<= 8                 
            c += ord(self.readStr(1))   
            c <<= 8                 
            c += ord(self.readStr(1))   
        return c                    

    def writeStr(self, str):        
        n = 0;                     
        while n < len(str):         
            r = self.sk.send(str[n:])
            if r == 0: raise RuntimeError, "connection closed by remote end"
            n += r                  

    def readStr(self, length):      
        ret = ''                    
        while len(ret) < length:   
            s = self.sk.recv(length - len(ret))
            if s == '': raise RuntimeError, "connection closed by remote end"
            ret += s
        return ret

def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((sys.argv[1], 8728))  
    apiros = ApiRos(s);            
    apiros.login(sys.argv[2], sys.argv[3]);

    inputsentence = []

    while 1:
        r = select.select([s, sys.stdin], [], [], None)
        if s in r[0]:
            # something to read in socket, read sentence
            x = apiros.readSentence()

        if sys.stdin in r[0]:
            # read line from input and strip off newline
            l = sys.stdin.readline()
            l = l[:-1]

            # if empty line, send sentence and start with new
            # otherwise append to input sentence
            if l == '':
                apiros.writeSentence(inputsentence)
                inputsentence = []
            else:
                inputsentence.append(l)

if __name__ == '__main__':
    main()

例子运行实例:
debian@localhost:~/api-test$ ./api.py 10.0.0.1 admin ''
<<< /login
<<<
>>> !done
>>> =ret=93b438ec9b80057c06dd9fe67d56aa9a
>>>
<<< /login
<<< =name=admin
<<< =response=00e134102a9d330dd7b1849fedfea3cb57
<<<
>>> !done
>>>

/user/getall
<<< /user/getall
<<<
>>> !re
>>> =.id=*1
>>> =disabled=no
>>> =name=admin
>>> =group=full
>>> =address=0.0.0.0/0
>>> =netmask=0.0.0.0
>>>
>>> !done
>>>

参考资料
&#61607;        API command notes
API examples in the Wiki
&#61607;        in PHP#1
&#61607;        in PHP using PEAR2#2
&#61607;        in Delphi#1
&#61607;        in Delphi#2
&#61607;        in C
&#61607;        in C using winsock
&#61607;        API in C++
&#61607;        in C#
&#61607;        in Flash Actionscript 3
&#61607;        in Ruby on rails
&#61607;        in VB .NET
&#61607;        in java
&#61607;        in NodeJS
&#61607;        Python3
API examples on the MikroTik Forum
&#61607;        in Perlby Hugh
&#61607;        in Delphiby Rodolfo
&#61607;        in Delphi #2by Chupaka
&#61607;        in NodeJSby Trakkasure
&#61607;        in VBby lucho512
&#61607;        on PHP for sparks frameworkby vthinkteam
API exmaples elsewhere
&#61607;        in .NET (C#) high-level api solutionby danikf
&#61607;        in PHPby boen_robot
Retrieved from "http://wiki.mikrotik.com/wiki/Manual:API"

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?加入

×
 楼主| 发表于 2012-7-17 17:28:43 | 显示全部楼层
部分AU3代码 可以通信 ,但是不知道那个密码的处理是不是特殊的MD5
Local $szIPADDRESS = "192.168.0.240" ;注意这个服务器的IP
Dim $sRepy
TCPStartup()
$Connected = -1
$Connected = TCPConnect($szIPADDRESS, 8728)
If @error Then
MsgBox(4112, "Error", "TCPConnect failed with WSA error: " & @error)
Else
        TCPSend($Connected,Binary("0x06")&StringToBinary("/login",4)&Binary("0x00"))
        If @error Then
        Exit
        EndIf
        While 1
                $sBuff = TCPRecv($Connected, 1024 * 1000)
                If @error Then ExitLoop
                If StringLen($sBuff) > 0 Then
                $sRepy &= $sBuff
                MsgBox(0, "", BinaryToString($sRepy,4))
                TCPSend($Connected,Binary("0x06")&StringToBinary("/login",4)&Binary("0x00"))
                EndIf
        WEnd
EndIf
 楼主| 发表于 2012-7-17 17:28:47 | 显示全部楼层
部分AU3代码 可以通信 ,但是不知道那个密码的处理是不是特殊的MD5
Local $szIPADDRESS = "192.168.0.240" ;注意这个服务器的IP
Dim $sRepy
TCPStartup()
$Connected = -1
$Connected = TCPConnect($szIPADDRESS, 8728)
If @error Then
MsgBox(4112, "Error", "TCPConnect failed with WSA error: " & @error)
Else
        TCPSend($Connected,Binary("0x06")&StringToBinary("/login",4)&Binary("0x00"))
        If @error Then
        Exit
        EndIf
        While 1
                $sBuff = TCPRecv($Connected, 1024 * 1000)
                If @error Then ExitLoop
                If StringLen($sBuff) > 0 Then
                $sRepy &= $sBuff
                MsgBox(0, "", BinaryToString($sRepy,4))
                TCPSend($Connected,Binary("0x06")&StringToBinary("/login",4)&Binary("0x00"))
                EndIf
        WEnd
EndIf
发表于 2012-8-27 00:20:04 | 显示全部楼层
回复一下支持楼主~
您需要登录后才可以回帖 登录 | 加入

本版积分规则

QQ|手机版|小黑屋|AUTOIT CN ( 鲁ICP备19019924号-1 )谷歌 百度

GMT+8, 2024-11-18 17:30 , Processed in 0.146279 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表