提交 412a2721 authored 作者: kapil's avatar kapil

Merge branch 'nsg-4.3' of ssh://git.sangoma.com/smg_freeswitch into nsg-4.3

......@@ -7,6 +7,7 @@ applications/mod_hash
applications/mod_spandsp
dialplans/mod_dialplan_xml
endpoints/mod_sofia
endpoints/mod_megaco
../../libs/freetdm/mod_freetdm
xml_int/mod_xml_cdr
event_handlers/mod_event_socket
......
ifndef ARCH
ARCH=$(shell uname -m)
endif
ifeq ($(ARCH),x86_64)
LOCAL_CFLAGS+=-DBIT_64 -DALIGN_64BIT
endif
BASE=../../../..
LOCAL_OBJS=megaco.o megaco_stack.o megaco_xml.o
LOCAL_LDFLAGS=-lsng_megaco
include $(BASE)/build/modmake.rules
<configuration name="megaco.conf" description="Megaco Controllee">
<!--Each instances of MG will have each mg profile -->
<sng_mg_interfaces>
<sng_mg_interface name="default">
<param name="id" value="1"/> <!-- /* equivalent to SSAP ID of MEGACO layer */-->
<param name="protocol" value="MEGACO"/> <!-- /* Protocol Type , Supported values are MEGACO/MGCP */ -->
<param name="transportProfileId" value="1"/> <!-- /* Link to transport layer configuration -->
<param name="localIp" value="xxx-xxx-xx-xx"/> <!-- /* Local node IP */ -->
<param name="port" value="2944" /> <!-- /* Port */ -->
<param name="myDomainName" value="mg.sangoma.com" /> <!--/* Local domain name */ -->
<param name="mid" value="<lab.sangoma.com>" /> <!-- /* Message Identifier (MID) of MEGACO message */ -->
<param name="peerId" value="1" /> <!--/* MGC Peer Configuration profile ID */-->
<!--/*We can define multiple peer_ids depends on number of MGC per MG.
MGC Priority - peer order can defines the priority or we can priority attribute in peer_id element..Not needed now..we can think later
Primart/Secondary MGC - we can think later in future when we need this functionality..as of now not requied. */
-->
</sng_mg_interface>
</sng_mg_interfaces>
<!--/*transport profiles which can be TCP, UDP or SCTP */-->
<sng_transport_interfaces>
<!--/* for TUCL we dont need any layer specific config parameters */ -->
<sng_transport_interface name="TPT-1">
<param name="id" value="1" /> <!-- /* transport profile id */ -->
<param name="transportType" value="UDP"/> <!-- /* transport profile type values could be UDP/TCP/SCTP */ -->
</sng_transport_interface>
</sng_transport_interfaces>
<sng_mg_peer_interfaces> <!--/* Supported number of peers */ -->
<sng_mg_peer_interface name="MG_PEER1">
<param name="id" value="1"/> <!-- /* Peer profile ID */-->
<param name="ip" value="xxx-xxx-xx-xx"/> <!-- /* Peer node IP */ -->
<param name="port" value="2944"/> <!--/* peer port */ -->
<param name="encodingScheme" value="TEXT"/> <!--/* H.248 Encoding scheme TEXT/BINARY */ -->
<param name="mid" value="remote.mgc.com" /> <!-- /* Message Identifier (MID) of remote MGC MEGACO message */-->
</sng_mg_peer_interface>
</sng_mg_peer_interfaces>
</configuration>
/*
* Copyright (c) 2012, Sangoma Technologies
* Mathieu Rene <mrene@avgs.ca>
* All rights reserved.
*
* <Insert license here>
*/
#include "mod_megaco.h"
megaco_profile_t *megaco_profile_locate(const char *name)
{
megaco_profile_t *profile = switch_core_hash_find_rdlock(megaco_globals.profile_hash, name, megaco_globals.profile_rwlock);
if (profile) {
if (switch_thread_rwlock_tryrdlock(profile->rwlock) != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Profile %s is locked\n", name);
profile = NULL;
}
}
return profile;
}
void megaco_profile_release(megaco_profile_t *profile)
{
switch_thread_rwlock_unlock(profile->rwlock);
}
static switch_status_t config_profile(megaco_profile_t *profile, switch_bool_t reload)
{
switch_xml_t cfg, xml, mg_interfaces, mg_interface, tpt_interfaces, tpt_interface, peer_interfaces, peer_interface;
switch_status_t status = SWITCH_STATUS_FALSE;
switch_event_t *event = NULL;
const char *file = "megaco.conf";
const char* mg_profile_tpt_id = NULL;
const char* mg_profile_peer_id = NULL;
if (!(xml = switch_xml_open_cfg(file, &cfg, NULL))) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Could not open %s\n", file);
goto done;
}
if (!(mg_interfaces = switch_xml_child(cfg, "sng_mg_interfaces"))) {
goto done;
}
/* iterate through MG Interface list to build all MG profiles */
for (mg_interface = switch_xml_child(mg_interfaces, "sng_mg_interface"); mg_interface; mg_interface = mg_interface->next) {
const char *name = switch_xml_attr_soft(mg_interface, "name");
if (strcmp(name, profile->name)) {
continue;
}
/* parse MG profile */
if(SWITCH_STATUS_FALSE == sng_parse_mg_profile(mg_interface)) {
goto done;
}
mg_profile_tpt_id = switch_xml_attr_soft(mg_interface, "id");
/* Now get required transport profile against mg_profile_tpt_id*/
if (!(tpt_interfaces = switch_xml_child(cfg, "sng_transport_interfaces"))) {
goto done;
}
for (tpt_interface = switch_xml_child(tpt_interfaces, "sng_transport_interface"); tpt_interface; tpt_interface = tpt_interface->next) {
const char *id = switch_xml_attr_soft(tpt_interface, "id");
if (strcmp(id, mg_profile_tpt_id)) {
continue;
}
/* parse MG transport profile */
if(SWITCH_STATUS_FALSE == sng_parse_mg_tpt_profile(tpt_interface)) {
goto done;
}
}
/* as of now supporting only one peer */
mg_profile_peer_id = switch_xml_attr_soft(mg_interface, "peerId");
/* Now get required peer profile against mg_profile_peer_id*/
if (!(peer_interfaces = switch_xml_child(cfg, "sng_mg_peer_interfaces"))) {
goto done;
}
for (peer_interface = switch_xml_child(peer_interfaces, "sng_mg_peer_interface"); peer_interface; peer_interface = peer_interface->next) {
const char *id = switch_xml_attr_soft(peer_interface, "id");
if (strcmp(id, mg_profile_peer_id)) {
continue;
}
/* parse MG Peer profile */
if(SWITCH_STATUS_FALSE == sng_parse_mg_peer_profile(peer_interface)) {
goto done;
}
}
/* configure the MEGACO stack */
status = sng_mgco_cfg(profile->name);
/* we should break from here , profile name should be unique */
break;
}
done:
if (xml) {
switch_xml_free(xml);
}
if (event) {
switch_event_destroy(&event);
}
return status;
}
switch_status_t megaco_profile_start(const char *profilename)
{
switch_memory_pool_t *pool;
megaco_profile_t *profile;
switch_assert(profilename);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Starting profile: %s\n", profilename);
switch_core_new_memory_pool(&pool);
profile = switch_core_alloc(pool, sizeof(*profile));
profile->pool = pool;
profile->name = switch_core_strdup(pool, profilename);
switch_thread_rwlock_create(&profile->rwlock, pool);
if (config_profile(profile, SWITCH_FALSE) != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error configuring profile %s\n", profile->name);
goto fail;
}
/* start MEGACP stack */
if(SWITCH_STATUS_FALSE == sng_mgco_start(profilename)) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error starting MEGACO Stack for profile %s\n", profile->name);
goto fail;
}
switch_core_hash_insert_wrlock(megaco_globals.profile_hash, profile->name, profile, megaco_globals.profile_rwlock);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Started profile: %s\n", profile->name);
return SWITCH_STATUS_SUCCESS;
fail:
switch_core_destroy_memory_pool(&pool);
return SWITCH_STATUS_FALSE;
}
switch_status_t megaco_profile_destroy(megaco_profile_t **profile)
{
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Stopping profile: %s\n", (*profile)->name);
switch_thread_rwlock_wrlock((*profile)->rwlock);
/* TODO: Kapil: Insert stack per-interface shutdown code here */
switch_thread_rwlock_unlock((*profile)->rwlock);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Stopped profile: %s\n", (*profile)->name);
switch_core_hash_delete_wrlock(megaco_globals.profile_hash, (*profile)->name, megaco_globals.profile_rwlock);
switch_core_destroy_memory_pool(&(*profile)->pool);
return SWITCH_STATUS_SUCCESS;
}
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:t
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4:
*/
差异被折叠。
/*
* Copyright (c) 2012, Sangoma Technologies
* Kapil Gupta <kgupta@sangoma.com>
* All rights reserved.
*
* <Insert license here>
*/
#include "sng_megaco/sng_ss7.h"
#ifndef _MEGACO_CFG_H_
#define _MEGACO_CFG_H_
#define MAX_MID_LEN 30
#define MAX_DOMAIN_LEN 30
#define MAX_NAME_LEN 25
#define MAX_MG_PROFILES 5
typedef struct sng_mg_peer{
char name[MAX_NAME_LEN]; /* Peer Name as defined in config file */
uint16_t id; /* Peer ID as defined in config file */
uint8_t ipaddr[MAX_DOMAIN_LEN]; /* Peer IP */
uint16_t port; /*Peer Port */
uint8_t mid[MAX_MID_LEN]; /* Peer H.248 MID */
uint16_t encoding_type; /* Encoding TEXT/Binary */
}sng_mg_peer_t;
typedef struct sng_mg_peers{
uint16_t total_peer; /* Total number of MGC Peer */
sng_mg_peer_t peers[MG_MAX_PEERS+1];
}sng_mg_peers_t;
typedef struct sng_mg_transport_profile{
char name[MAX_NAME_LEN]; /* Peer Name as defined in config file */
uint32_t id; /* map to tsap id */
uint16_t transport_type; /* transport type */
}sng_mg_transport_profile_t;
typedef enum{
SNG_MG_TPT_NONE,
SNG_MG_TPT_UDP,
SNG_MG_TPT_TCP,
SNG_MG_TPT_SCTP,
SNG_MG_TPT_MTP3
}sng_mg_transport_types_e;
typedef enum{
SNG_MG_NONE,
SNG_MG_MGCP,
SNG_MG_MEGACO,
}sng_mg_protocol_types_e;
#define PRNT_PROTOCOL_TYPE(_val)\
((_val == SNG_MG_MGCP)?"SNG_MG_MGCP":\
(_val == SNG_MG_MEGACO)?"SNG_MG_MEGACO":\
"SNG_MG_NONE")
typedef enum{
SNG_MG_ENCODING_NONE,
SNG_MG_ENCODING_BINARY,
SNG_MG_ENCODING_TEXT,
}sng_mg_encoding_types_e;
#define PRNT_ENCODING_TYPE(_val)\
((_val == SNG_MG_ENCODING_TEXT)?"SNG_MG_ENCODING_TEXT":\
(_val == SNG_MG_ENCODING_BINARY)?"SNG_MG_ENCODING_BINARY":\
"SNG_MG_ENCODING_NONE")
/* each profile is corresponds to each MG Instance */
typedef struct sng_mg_cfg{
char name[MAX_NAME_LEN]; /* MG(Virtual MG) Name as defined in config file */
uint32_t id; /* Id - map to MG SAP ID */
uint8_t mid[MAX_MID_LEN]; /* MG H.248 MID */
uint8_t my_domain[MAX_DOMAIN_LEN]; /* local domain name */
uint8_t my_ipaddr[MAX_DOMAIN_LEN]; /* local domain name */
uint32_t port; /* port */
uint16_t peer_id; /* MGC Peer ID */
uint16_t transport_prof_id; /* Transport profile id ..this also will be the spId for MG SAP*/
uint16_t protocol_type; /* MEGACO/MGCP */
}sng_mg_cfg_t;
typedef struct sng_mg_gbl_cfg{
sng_mg_cfg_t mgCfg[MAX_MG_PROFILES + 1];
sng_mg_transport_profile_t mgTptProf[MG_MAX_PEERS+1]; /* transport profile */
sng_mg_peers_t mgPeer;
}sng_mg_gbl_cfg_t;
extern switch_status_t sng_parse_mg_peer_profile(switch_xml_t mg_peer_profile);
extern switch_status_t sng_parse_mg_tpt_profile(switch_xml_t mg_tpt_profile);
extern switch_status_t sng_parse_mg_profile(switch_xml_t mg_interface);
void handle_sng_log(uint8_t level, char *fmt, ...);
void handle_mgco_sta_ind(Pst *pst, SuId suId, MgMgtSta* msg);
void handle_mgco_txn_sta_ind(Pst *pst, SuId suId, MgMgcoInd* msg);
void handle_mgco_cmd_ind(Pst *pst, SuId suId, MgMgcoCommand* msg);
void handle_mgco_cntrl_cfm(Pst *pst, SuId suId, MgMgtCntrl* cntrl, Reason reason);
void handle_mgco_txn_ind(Pst *pst, SuId suId, MgMgcoMsg* msg);
void handle_mgco_audit_cfm(Pst *pst, SuId suId, MgMgtAudit* audit, Reason reason);
void handle_mg_alarm(Pst *pst, MgMngmt *sta);
void handle_tucl_alarm(Pst *pst, HiMngmt *sta);
switch_status_t sng_mgco_init(sng_isup_event_interface_t* event);
switch_status_t sng_mgco_cfg(const char* profilename);
switch_status_t sng_mgco_start(const char* profilename);
switch_status_t sng_mgco_stack_shutdown(void);
/*****************************************************************************************************/
#define GET_MG_CFG_IDX(_profilename, _idx){\
for(idx=0; idx < MAX_MG_PROFILES; idx++){\
/* id zero is not acceptable */\
if(megaco_globals.g_mg_cfg.mgCfg[idx].id){\
if (strcmp(megaco_globals.g_mg_cfg.mgCfg[idx].name, profilename)) {\
continue;\
} else{\
break;\
}\
}\
}\
}
#define GET_TPT_ID(_id) megaco_globals.g_mg_cfg.mgTptProf[megaco_globals.g_mg_cfg.mgCfg[_id].transport_prof_id].id
#define GET_MU_SAP_ID(_id) megaco_globals.g_mg_cfg.mgCfg[_id].id
#define GET_TPT_TYPE(_id) megaco_globals.g_mg_cfg.mgTptProf[megaco_globals.g_mg_cfg.mgCfg[_id].transport_prof_id].transport_type
#define GET_ENCODING_TYPE(_id) megaco_globals.g_mg_cfg.mgPeer.peers[megaco_globals.g_mg_cfg.mgCfg[_id].peer_id].encoding_type
#endif /* _MEGACO_CFG_H_ */
差异被折叠。
/*
* Copyright (c) 2012, Sangoma Technologies
* Mathieu Rene <mrene@avgs.ca>
* All rights reserved.
*
* <Insert license here>
*/
#include "mod_megaco.h"
struct megaco_globals megaco_globals;
static sng_isup_event_interface_t sng_event;
SWITCH_MODULE_LOAD_FUNCTION(mod_megaco_load);
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_megaco_shutdown);
SWITCH_MODULE_DEFINITION(mod_megaco, mod_megaco_load, mod_megaco_shutdown, NULL);
#define MEGACO_FUNCTION_SYNTAX "profile [name] [start | stop]"
SWITCH_STANDARD_API(megaco_function)
{
int argc;
char *argv[10];
char *dup = NULL;
if (zstr(cmd)) {
goto usage;
}
dup = strdup(cmd);
argc = switch_split(dup, ' ', argv);
if (argc < 1 || zstr(argv[0])) {
goto usage;
}
if (!strcmp(argv[0], "profile")) {
if (zstr(argv[1]) || zstr(argv[2])) {
goto usage;
}
if (!strcmp(argv[2], "start")) {
megaco_profile_t *profile = megaco_profile_locate(argv[1]);
if (profile) {
megaco_profile_release(profile);
stream->write_function(stream, "-ERR Profile %s is already started\n", argv[2]);
} else {
megaco_profile_start(argv[1]);
stream->write_function(stream, "+OK\n");
}
} else if (!strcmp(argv[2], "stop")) {
megaco_profile_t *profile = megaco_profile_locate(argv[1]);
if (profile) {
megaco_profile_release(profile);
megaco_profile_destroy(&profile);
stream->write_function(stream, "+OK\n");
} else {
stream->write_function(stream, "-ERR No such profile\n");
}
}
}
goto done;
usage:
stream->write_function(stream, "-ERR Usage: "MEGACO_FUNCTION_SYNTAX"\n");
done:
switch_safe_free(dup);
return SWITCH_STATUS_SUCCESS;
}
static switch_status_t console_complete_hashtable(switch_hash_t *hash, const char *line, const char *cursor, switch_console_callback_match_t **matches)
{
switch_hash_index_t *hi;
void *val;
const void *vvar;
switch_console_callback_match_t *my_matches = NULL;
switch_status_t status = SWITCH_STATUS_FALSE;
for (hi = switch_hash_first(NULL, hash); hi; hi = switch_hash_next(hi)) {
switch_hash_this(hi, &vvar, NULL, &val);
switch_console_push_match(&my_matches, (const char *) vvar);
}
if (my_matches) {
*matches = my_matches;
status = SWITCH_STATUS_SUCCESS;
}
return status;
}
static switch_status_t list_profiles(const char *line, const char *cursor, switch_console_callback_match_t **matches)
{
switch_status_t status;
switch_thread_rwlock_rdlock(megaco_globals.profile_rwlock);
status = console_complete_hashtable(megaco_globals.profile_hash, line, cursor, matches);
switch_thread_rwlock_unlock(megaco_globals.profile_rwlock);
return status;
}
SWITCH_MODULE_LOAD_FUNCTION(mod_megaco_load)
{
switch_api_interface_t *api_interface;
memset(&megaco_globals, 0, sizeof(megaco_globals));
megaco_globals.pool = pool;
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
switch_core_hash_init(&megaco_globals.profile_hash, pool);
switch_thread_rwlock_create(&megaco_globals.profile_rwlock, pool);
SWITCH_ADD_API(api_interface, "megaco", "megaco", megaco_function, MEGACO_FUNCTION_SYNTAX);
switch_console_set_complete("add megaco profile ::megaco::list_profiles start");
switch_console_set_complete("add megaco profile ::megaco::list_profiles stop");
switch_console_add_complete_func("::megaco::list_profiles", list_profiles);
/* Initialize MEGACO Stack */
sng_event.mg.sng_mgco_txn_ind = handle_mgco_txn_ind;
sng_event.mg.sng_mgco_cmd_ind = handle_mgco_cmd_ind;
sng_event.mg.sng_mgco_txn_sta_ind = handle_mgco_txn_sta_ind;
sng_event.mg.sng_mgco_sta_ind = handle_mgco_sta_ind;
sng_event.mg.sng_mgco_cntrl_cfm = handle_mgco_cntrl_cfm;
sng_event.mg.sng_mgco_audit_cfm = handle_mgco_audit_cfm;
/* Alarm CB */
sng_event.sm.sng_mg_alarm = handle_mg_alarm;
sng_event.sm.sng_tucl_alarm = handle_tucl_alarm;
/* Log */
sng_event.sm.sng_log = handle_sng_log;
/* initualize MEGACO stack */
return sng_mgco_init(&sng_event);
}
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_megaco_shutdown)
{
sng_mgco_stack_shutdown();
return SWITCH_STATUS_SUCCESS;
}
/*****************************************************************************************************************************/
void handle_sng_log(uint8_t level, char *fmt, ...)
{
int log_level;
char print_buf[1024];
va_list ptr;
memset(&print_buf[0],0,sizeof(1024));
va_start(ptr, fmt);
switch(level)
{
case SNG_LOGLEVEL_DEBUG: log_level = SWITCH_LOG_DEBUG; break;
case SNG_LOGLEVEL_INFO: log_level = SWITCH_LOG_INFO; break;
case SNG_LOGLEVEL_WARN: log_level = SWITCH_LOG_WARNING; break;
case SNG_LOGLEVEL_ERROR: log_level = SWITCH_LOG_ERROR; break;
case SNG_LOGLEVEL_CRIT: log_level = SWITCH_LOG_CRIT; break;
default: log_level = SWITCH_LOG_DEBUG; break;
};
vsprintf(&print_buf[0], fmt, ptr);
switch_log_printf(SWITCH_CHANNEL_LOG, log_level, " MOD_MEGACO: %s \n", &print_buf[0]);
va_end(ptr);
}
/*****************************************************************************************************************************/
void handle_mgco_txn_ind(Pst *pst, SuId suId, MgMgcoMsg* msg)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_mgco_cmd_ind(Pst *pst, SuId suId, MgMgcoCommand* cmd)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_mgco_sta_ind(Pst *pst, SuId suId, MgMgtSta* sta)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_mgco_txn_sta_ind(Pst *pst, SuId suId, MgMgcoInd* txn_sta_ind)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_mgco_cntrl_cfm(Pst *pst, SuId suId, MgMgtCntrl* cntrl, Reason reason)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_mgco_audit_cfm(Pst *pst, SuId suId, MgMgtAudit* audit, Reason reason)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_mg_alarm(Pst *pst, MgMngmt *sta)
{
/*TODO*/
}
/*****************************************************************************************************************************/
void handle_tucl_alarm(Pst *pst, HiMngmt *sta)
{
/*TODO*/
}
/*****************************************************************************************************************************/
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:t
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4:
*/
/*
* Copyright (c) 2012, Sangoma Technologies
* Mathieu Rene <mrene@avgs.ca>
* All rights reserved.
*
* <Insert license here>
*/
#ifndef MOD_MEGACO_H
#define MOD_MEGACO_H
#include <switch.h>
#include "megaco_stack.h"
struct megaco_globals {
switch_memory_pool_t *pool;
switch_hash_t *profile_hash;
switch_thread_rwlock_t *profile_rwlock;
sng_mg_gbl_cfg_t g_mg_cfg;
};
extern struct megaco_globals megaco_globals; /* < defined in mod_megaco.c */
typedef enum {
PF_RUNNING = (1 << 0)
} megaco_profile_flags_t;
typedef struct megaco_profile_s {
char *name;
switch_memory_pool_t *pool;
switch_thread_rwlock_t *rwlock; /* < Reference counting rwlock */
megaco_profile_flags_t flags;
} megaco_profile_t;
megaco_profile_t *megaco_profile_locate(const char *name);
void megaco_profile_release(megaco_profile_t *profile);
switch_status_t megaco_profile_start(const char *profilename);
switch_status_t megaco_profile_destroy(megaco_profile_t **profile);
#endif /* MOD_MEGACO_H */
/* For Emacs:
* Local Variables:
* mode:c
* indent-tabs-mode:t
* tab-width:4
* c-basic-offset:4
* End:
* For VIM:
* vim:set softtabstop=4 shiftwidth=4 tabstop=4:
*/
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论