- authentification in the native administration module (formerly message

    admin). Files are "server/nativeadmin.h" and "server/nativeadmin.cpp".
    Note: it's not tested because nothing uses this module  at the moment
    but it should work;
  - more accurate define name (concerns both telnet and native-admin);
  - native-admin configuration options in vls.cfg;
  - Makefile modification to follow file changes.
parent f1db247a
......@@ -137,7 +137,7 @@ SERVERSRC= server/admin.cpp \
server/manager.cpp \
server/program.cpp \
server/telnet.cpp \
server/messageadmin.cpp \
server/nativeadmin.cpp \
server/vls.cpp \
server/tsstreamer.cpp \
server/file/fileinput.cpp \
......
......@@ -18,7 +18,7 @@
#include "request.h"
#include "admin.h"
#include "telnet.h"
#include "messageadmin.h"
#include "nativeadmin.h"
#include "../core/network.cpp"
......@@ -172,7 +172,7 @@ C_Admin::C_Admin(handle hLogger, C_RequestHub* pRequestHub)
m_pRequestHub = pRequestHub;
m_bRequestsEnabled = true;
m_pMessageAdmin = new C_MessageAdmin(m_hLog, this);
m_pNativeAdmin = new C_NativeAdmin(m_hLog, this);
m_pTelnet = new C_Telnet(m_hLog, this);
}
......@@ -183,7 +183,7 @@ C_Admin::C_Admin(handle hLogger, C_RequestHub* pRequestHub)
//------------------------------------------------------------------------------
C_Admin::~C_Admin()
{
delete m_pMessageAdmin;
delete m_pNativeAdmin;
delete m_pTelnet;
}
......@@ -343,7 +343,7 @@ int C_Admin::Init()
}
if(!iRc)
iRc = m_pMessageAdmin->Init();
iRc = m_pNativeAdmin->Init();
if(!iRc)
iRc = m_pTelnet->Init();
......@@ -357,7 +357,7 @@ int C_Admin::Init()
//------------------------------------------------------------------------------
int C_Admin::Run()
{
int iRc = m_pMessageAdmin->Run();
int iRc = m_pNativeAdmin->Run();
if(!iRc)
iRc = m_pTelnet->Run();
......@@ -371,7 +371,7 @@ int C_Admin::Run()
//------------------------------------------------------------------------------
int C_Admin::Stop()
{
int iRc = m_pMessageAdmin->Stop();
int iRc = m_pNativeAdmin->Stop();
iRc |= m_pTelnet->Stop();
......@@ -385,7 +385,7 @@ int C_Admin::Stop()
int C_Admin::Destroy()
{
int iRc = m_pTelnet->Destroy();
iRc |= m_pMessageAdmin->Destroy();
iRc |= m_pNativeAdmin->Destroy();
return iRc;
}
......@@ -447,7 +447,7 @@ C_Answer C_Admin::ParseCmdLine(C_AdminSession* pSession,
// Empty command line ?
if(vArgs.Size() == 0)
{
cAnswer.SetStatus(ADMIN_EMPTY_CMDLINE);
cAnswer.SetStatus(ADMIN_EMPTY_COMMAND);
return cAnswer;
}
......@@ -478,6 +478,7 @@ C_Answer C_Admin::ParseCmdLine(C_AdminSession* pSession,
int iError = 0;
unsigned int uiMandatory = 0;
unsigned int uiOptional = 0;
for(unsigned int ui = 1; ui < vArgs.Size(); ui++)
{
C_String strArg(vArgs[ui]);
......@@ -547,6 +548,83 @@ C_Answer C_Admin::ParseCmdLine(C_AdminSession* pSession,
return cAnswer;
}
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
C_Answer C_Admin::ValidateRequest(C_AdminSession* pSession,
C_Request& cRequest)
{
ASSERT(pSession);
C_Answer cAnswer("Admin");
C_String strCmd = cRequest.GetCmd();
// Empty command line ?
if(strCmd.Length() == 0)
{
cAnswer.SetStatus(ADMIN_EMPTY_COMMAND);
return cAnswer;
}
// Unknown command ?
C_CommandDesc* pCmdDesc = m_cCmdDescriptions.Get(strCmd);
if(!pCmdDesc)
{
cAnswer.SetStatus(ADMIN_UNKNOWN_COMMAND);
cAnswer.AddMessage(strCmd + ": unknown command.");
return cAnswer;
}
// The user is not allowed to run this command ?
if(pSession->m_vCommands.Find(strCmd) < 0)
{
cAnswer.SetStatus(ADMIN_COMMAND_DENIED);
cAnswer.AddMessage(strCmd + ": permission denied.");
return cAnswer;
}
// Check arguments' validity
// - mandatory arguments shouldn't be empty;
// - booleans should be "0" or "1";
// - we don't check arguments which aren't in the description because we
// don't have access to the hashtable in the C_Request. It doesn't matter
// because they are ignored.
int iError = 0;
for(unsigned int ui = 0; ui < pCmdDesc->m_vMandatoryArgs.Size(); ui++)
{
C_String strArg = cRequest.GetArg(pCmdDesc->m_vMandatoryArgs[ui]);
if(strArg.Length() == 0)
{
cAnswer.AddMessage(strCmd + ": \"" +
pCmdDesc->m_vMandatoryArgs[ui] + "\" is mandatory");
cAnswer.AddMessage(pCmdDesc->m_strUsage);
iError = GEN_ERR;
}
}
for(unsigned int ui = 0; ui < pCmdDesc->m_vBooleans.Size(); ui++)
{
C_String strArg = cRequest.GetArg(pCmdDesc->m_vBooleans[ui]);
if((strArg != "0") && (strArg != "1"))
{
cAnswer.AddMessage(strCmd + ": \"" +
strArg + " isn't a boolean value (\"0\" or \"1\")");
cAnswer.AddMessage(pCmdDesc->m_strUsage);
iError = GEN_ERR;
}
}
if(iError)
cAnswer.SetStatus(ADMIN_COMMAND_NOT_VALID);
else
cAnswer.SetStatus(ADMIN_WELLFORMED_COMMAND);
return cAnswer;
}
//------------------------------------------------------------------------------
// Request treatment
......@@ -629,7 +707,7 @@ C_Answer C_Admin::HandleRequest(const C_Request& cRequest)
//------------------------------------------------------------------------------
void C_Admin::HandleEvent(const C_Event& cEvent)
{
m_pMessageAdmin->PropagateEvent(cEvent);
m_pNativeAdmin->PropagateEvent(cEvent);
// To do: send something to the users loggued by telnet
}
......
......@@ -22,8 +22,8 @@
//------------------------------------------------------------------------------
// Constants
//------------------------------------------------------------------------------
#define ADMIN_WELLFORMED_COMMAND 0
#define ADMIN_EMPTY_CMDLINE 1
#define ADMIN_WELLFORMED_COMMAND 0
#define ADMIN_EMPTY_COMMAND 1
#define ADMIN_UNKNOWN_COMMAND -1
#define ADMIN_COMMAND_NOT_VALID -2
#define ADMIN_COMMAND_DENIED -3
......@@ -31,7 +31,7 @@
//------------------------------------------------------------------------------
// Forward declaration
//------------------------------------------------------------------------------
class C_MessageAdmin;
class C_NativeAdmin;
class C_Telnet;
class C_Admin;
......@@ -163,7 +163,7 @@ class C_Admin : public C_EventHandler, public C_RequestHandler
C_Mutex m_cRequestLock;
C_Mutex m_cSessionLock;
C_MessageAdmin* m_pMessageAdmin;
C_NativeAdmin* m_pNativeAdmin;
C_Telnet* m_pTelnet;
};
......
/*******************************************************************************
* messageadmin.cpp: *
* nativeadmin.cpp: *
* (c)1999-2000 VideoLAN *
*------------------------------------------------------------------------------*
* *
......@@ -18,31 +18,31 @@
#include "../core/network.h"
#include "request.h"
#include "admin.h"
#include "messageadmin.h"
#include "nativeadmin.h"
#include "../core/network.cpp"
/*******************************************************************************
* E_MessageAdmin exception
* E_NativeAdmin exception
********************************************************************************
*
*******************************************************************************/
E_MessageAdmin::E_MessageAdmin(const C_String& strMsg) :
E_NativeAdmin::E_NativeAdmin(const C_String& strMsg) :
E_Exception(GEN_ERR, strMsg)
{
}
E_MessageAdmin::E_MessageAdmin(const C_String& strMsg, const E_Exception& e) :
E_NativeAdmin::E_NativeAdmin(const C_String& strMsg, const E_Exception& e) :
E_Exception(GEN_ERR, strMsg, e)
{
}
/*******************************************************************************
* C_MessageAdminSession class
********************************************************************************
* C_NativeAdminSession class
********************************************************************************
*
*******************************************************************************/
......@@ -51,15 +51,17 @@ E_MessageAdmin::E_MessageAdmin(const C_String& strMsg, const E_Exception& e) :
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
C_MessageAdminSession::C_MessageAdminSession(C_Socket* pConnection,
void* pAdmin) :
C_NativeAdminSession::C_NativeAdminSession(C_Socket* pConnection,
void* pAdmin) :
C_AdminSession((C_Admin*)pAdmin),
m_cStream(pConnection)
{
ASSERT(pAdmin);
ASSERT(pConnection);
m_pConnection = pConnection;
m_pAdmin = (C_Admin*)pAdmin;
// Starting mode
m_iPhase = INIT_PHASE;
}
......@@ -68,8 +70,9 @@ C_MessageAdminSession::C_MessageAdminSession(C_Socket* pConnection,
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdminSession::Init()
void C_NativeAdminSession::OnInit()
{
m_iPhase = LOGIN_PHASE;
}
......@@ -78,7 +81,7 @@ void C_MessageAdminSession::Init()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdminSession::Close()
void C_NativeAdminSession::OnClose()
{
try
{
......@@ -87,7 +90,7 @@ void C_MessageAdminSession::Close()
catch(E_Exception e)
{
C_String strPeer = m_pConnection->GetPeerName();
throw E_MessageAdmin("Unable to close connection with "+strPeer, e);
throw E_NativeAdmin("Unable to close connection with " + strPeer, e);
}
}
......@@ -97,14 +100,15 @@ void C_MessageAdminSession::Close()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdminSession::ProcessData()
void C_NativeAdminSession::ProcessData()
{
try
{
// First get the data from the network
C_Buffer<byte> cData(256);
m_cStream >> cData;
ASSERT(cData.GetSize() > 0);
if(cData.GetSize() <= 0)
throw E_NativeAdmin("Connection reset by peer");
unsigned int iPos = 0;
unsigned int iSize = cData.GetSize();
......@@ -131,7 +135,7 @@ void C_MessageAdminSession::ProcessData()
}
catch(E_Stream<C_Socket> e)
{
throw E_MessageAdmin("Connection error", e);
throw E_NativeAdmin("Connection error", e);
}
}
......@@ -141,7 +145,7 @@ void C_MessageAdminSession::ProcessData()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdminSession::SendEvent(const C_Event& cEvent)
void C_NativeAdminSession::SendEvent(const C_Event& cEvent)
{
try
{
......@@ -154,7 +158,7 @@ void C_MessageAdminSession::SendEvent(const C_Event& cEvent)
catch(E_Exception e)
{
C_String strPeer = m_pConnection->GetPeerName();
throw E_MessageAdmin("Unable to send data to "+strPeer, e);
throw E_NativeAdmin("Unable to send data to "+strPeer, e);
}
}
......@@ -164,7 +168,7 @@ void C_MessageAdminSession::SendEvent(const C_Event& cEvent)
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdminSession::HandleMessage()
void C_NativeAdminSession::HandleMessage()
{
printf("Handling message: \"%s\"\n", m_strMessage.GetString());
......@@ -174,9 +178,79 @@ void C_MessageAdminSession::HandleMessage()
{
if(iType == REQUEST_TYPE)
{
C_Answer cAnswer = m_pAdmin->HandleRequest(cMessage.GetRequest());
C_Message cMessageAnswer(cAnswer);
m_cStream << cMessageAnswer.GetString();
C_Request cRequest = cMessage.GetRequest();
if(m_iPhase == LOGIN_PHASE)
{
C_Answer cAnswer("NativeAdmin");
if(cRequest.GetCmd() == "login")
{
C_String strLogin = cRequest.GetArg("login");
C_String strPasswd = cRequest.GetArg("password");
int iRc = Authentificate(strLogin, strPasswd);
if(!iRc)
{
cAnswer.SetStatus(NO_ERR);
cAnswer.AddMessage("Authentification succeeded");
m_iPhase = COMMAND_PHASE;
}
else
{
cAnswer.AddMessage("Authentification failed");
m_iPhase = LOGIN_PHASE;
}
}
else
{
cAnswer.AddMessage("Not authentificated");
}
C_Message cMessageAnswer(cAnswer);
m_cStream << cMessageAnswer.GetString();
}
else if(m_iPhase == COMMAND_PHASE)
{
C_Answer cAdminAnswer = m_pAdmin->ValidateRequest(this, cRequest);
switch(cAdminAnswer.GetStatus())
{
case ADMIN_WELLFORMED_COMMAND:
if(cRequest.GetCmd() == "logout")
{
throw E_NativeAdmin("logout requested");
}
else
{
C_Answer cAnswer = m_pAdmin->HandleRequest(cRequest);
C_Message cMessageAnswer(cAnswer);
m_cStream << cMessageAnswer.GetString();
}
break;
case ADMIN_COMMAND_NOT_VALID:
case ADMIN_UNKNOWN_COMMAND:
case ADMIN_COMMAND_DENIED:
{
C_Message cMessageAnswer(cAdminAnswer);
m_cStream << cMessageAnswer.GetString();
}
break;
case ADMIN_EMPTY_COMMAND:
{
C_Answer cAnswer("");
C_Message cMessageAnswer(cAnswer);
m_cStream << cMessageAnswer.GetString();
}
break;
default:
ASSERT(false);
break;
}
}
else
{
ASSERT(false);
}
}
else if(iType == ANSWER_TYPE)
printf("shouldn't receive any answer => trash\n");
......@@ -191,7 +265,7 @@ void C_MessageAdminSession::HandleMessage()
/*******************************************************************************
* C_MessageAdmin class
* C_NativeAdmin class
********************************************************************************
*
*******************************************************************************/
......@@ -201,8 +275,8 @@ void C_MessageAdminSession::HandleMessage()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
C_MessageAdmin::C_MessageAdmin(handle hLog, C_Admin *pAdmin) :
C_ConnectionsHandler<C_MessageAdminSession>(hLog, pAdmin)
C_NativeAdmin::C_NativeAdmin(handle hLog, C_Admin *pAdmin) :
C_ConnectionsHandler<C_NativeAdminSession>(hLog, pAdmin)
{
ASSERT(hLog);
ASSERT(pAdmin);
......@@ -217,7 +291,7 @@ C_MessageAdmin::C_MessageAdmin(handle hLog, C_Admin *pAdmin) :
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdmin::PropagateEvent(const C_Event& cEvent)
void C_NativeAdmin::PropagateEvent(const C_Event& cEvent)
{
// m_cEventsLock.Lock();
......@@ -235,13 +309,13 @@ void C_MessageAdmin::PropagateEvent(const C_Event& cEvent)
// send the event to all the sessions
// C_HashTable<handle, Session> m_cSessions;
C_HashTableIterator<handle, C_MessageAdminSession> cIterator =
C_HashTableIterator<handle, C_NativeAdminSession> cIterator =
m_cSessions.CreateIterator();
while(cIterator.HasNext())
{
C_HashTableNode<handle, C_MessageAdminSession>* pNode =
cIterator.GetNext();
C_MessageAdminSession* pSession = pNode->GetValue();
C_HashTableNode<handle, C_NativeAdminSession>* pNode =
cIterator.GetNext();
C_NativeAdminSession* pSession = pNode->GetValue();
pSession->SendEvent(cEvent);
}
......@@ -257,7 +331,7 @@ void C_MessageAdmin::PropagateEvent(const C_Event& cEvent)
//------------------------------------------------------------------------------
// Initialization
//------------------------------------------------------------------------------
int C_MessageAdmin::Init()
int C_NativeAdmin::Init()
{
C_Application* pApp = C_Application::GetApp();
ASSERT(pApp);
......@@ -265,21 +339,22 @@ int C_MessageAdmin::Init()
int iRc = NO_ERR;
// Get config
C_String strAddr = pApp->GetSetting("MessageAdmin.LocalAddress", "0.0.0.0");
C_String strPort = pApp->GetSetting("MessageAdmin.LocalPort", "9998");
C_String strAddr = pApp->GetSetting("NativeAdmin.LocalAddress", "0.0.0.0");
C_String strPort = pApp->GetSetting("NativeAdmin.LocalPort", "9998");
try
{
C_ConnectionsHandler<C_MessageAdminSession>::Init(strAddr, strPort);
C_ConnectionsHandler<C_NativeAdminSession>::Init(strAddr, strPort);
}
catch(E_Exception e)
{
Log(m_hLog, LOG_ERROR, "Message server initialisation failed:\n"+e.Dump());
Log(m_hLog, LOG_ERROR,
"Native administrator initialisation failed:\n" + e.Dump());
iRc = GEN_ERR;
}
if(!iRc)
Log(m_hLog, LOG_NOTE, "Message server initialised");
Log(m_hLog, LOG_NOTE, "Native administrator initialised");
return iRc;
}
......@@ -288,7 +363,7 @@ int C_MessageAdmin::Init()
//------------------------------------------------------------------------------
// Execution
//------------------------------------------------------------------------------
int C_MessageAdmin::Run()
int C_NativeAdmin::Run()
{
int iRc = NO_ERR;
......@@ -308,7 +383,7 @@ int C_MessageAdmin::Run()
//------------------------------------------------------------------------------
// Stop
//------------------------------------------------------------------------------
int C_MessageAdmin::Stop()
int C_NativeAdmin::Stop()
{
int iRc = NO_ERR;
try
......@@ -327,23 +402,23 @@ int C_MessageAdmin::Stop()
//------------------------------------------------------------------------------
// Cleaning
//------------------------------------------------------------------------------
int C_MessageAdmin::Destroy()
int C_NativeAdmin::Destroy()
{
int iRc = NO_ERR;
try
{
C_ConnectionsHandler<C_MessageAdminSession>::Destroy();
C_ConnectionsHandler<C_NativeAdminSession>::Destroy();
}
catch(E_Exception e)
{
Log(m_hLog, LOG_ERROR,
"Error during Message server destruction:\n"+e.Dump());
"Error during native administrator destruction:\n"+e.Dump());
iRc = GEN_ERR;
}
if(!iRc)
Log(m_hLog, LOG_NOTE, "Message server destroyed");
Log(m_hLog, LOG_NOTE, "Native administrator destroyed");
return iRc;
}
......@@ -352,7 +427,7 @@ int C_MessageAdmin::Destroy()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdmin::InitWork()
void C_NativeAdmin::InitWork()
{
}
......@@ -360,16 +435,16 @@ void C_MessageAdmin::InitWork()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdmin::DoWork()
void C_NativeAdmin::DoWork()
{
try
{
C_ConnectionsHandler<C_MessageAdminSession>::Run();
Log(m_hLog, LOG_NOTE, "Message server started");
C_ConnectionsHandler<C_NativeAdminSession>::Run();
Log(m_hLog, LOG_NOTE, "Native administrator started");
}
catch(E_Exception e)
{
throw E_MessageAdmin("Message server startup failed", e);
throw E_NativeAdmin("Native administrator startup failed", e);
}
}
......@@ -377,16 +452,16 @@ void C_MessageAdmin::DoWork()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdmin::StopWork()
void C_NativeAdmin::StopWork()
{
try
{
C_ConnectionsHandler<C_MessageAdminSession>::Stop();
Log(m_hLog, LOG_NOTE, "Message server stopped");
C_ConnectionsHandler<C_NativeAdminSession>::Stop();
Log(m_hLog, LOG_NOTE, "Native administrator stopped");
}
catch(E_Exception e)
{
throw E_MessageAdmin("Could not stop message server", e);
throw E_NativeAdmin("Could not stop native administrator", e);
}
}
......@@ -396,7 +471,7 @@ void C_MessageAdmin::StopWork()
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
void C_MessageAdmin::CleanWork()
void C_NativeAdmin::CleanWork()
{
}
/*******************************************************************************
* messageadmin.h: *
* nativeadmin.h: *
* (c)1999 VideoLAN *
*------------------------------------------------------------------------------*
* *
*******************************************************************************/
#ifndef _MESSAGEADMIN_H_
#define _MESSAGEADMIN_H_
#ifndef _NATIVEADMIN_H_
#define _NATIVEADMIN_H_
//------------------------------------------------------------------------------
// Native admin module definitions
//------------------------------------------------------------------------------
#define INIT_PHASE 1
#define LOGIN_PHASE 2
#define COMMAND_PHASE 4
//------------------------------------------------------------------------------
// E_MessageAdmin class
// E_NativeAdmin class
//------------------------------------------------------------------------------
class E_MessageAdmin : public E_Exception
class E_NativeAdmin : public E_Exception
{
public:
E_MessageAdmin(const C_String& strMsg);
E_MessageAdmin(const C_String& strMsg, const E_Exception& e);
E_NativeAdmin(const C_String& strMsg);
E_NativeAdmin(const C_String& strMsg, const E_Exception& e);
};
//------------------------------------------------------------------------------
// C_MessageAdminSession class
// C_NativeAdminSession class
//------------------------------------------------------------------------------
//
//------------------------------------------------------------------------------
class C_MessageAdminSession
class C_NativeAdminSession : public C_AdminSession
{
public:
C_MessageAdminSession(C_Socket* pConnection, void* pAdmin);
C_NativeAdminSession(C_Socket* pConnection, void* pAdmin);
// Session Initialisation
void Init();
// Data processing
void ProcessData();
// Event management
void SendEvent(const C_Event& cEvent);
// Session termination
void Close();
protected:
protected:
virtual void OnInit();
virtual void OnClose();
void HandleMessage();
private:
// Admin
C_Admin* m_pAdmin;
private:
// Connection to the peer
C_Socket* m_pConnection;
C_Stream<C_Socket> m_cStream;
// Session status
int m_iPhase;
// Internal data
C_String m_strMessage;
};
//------------------------------------------------------------------------------
// C_MessageAdmin class
// C_NativeAdmin class
//------------------------------------------------------------------------------
class C_MessageAdmin : protected C_ConnectionsHandler<C_MessageAdminSession>,
protected C_Thread
class C_NativeAdmin : protected C_ConnectionsHandler<C_NativeAdminSession>,
protected C_Thread
{
public:
C_MessageAdmin(handle hLog, C_Admin* pAdmin);
public:
C_NativeAdmin(handle hLog, C_Admin* pAdmin);
void PropagateEvent(const C_Event& cEvent);
......@@ -72,13 +79,13 @@ class C_MessageAdmin : protected C_ConnectionsHandler<C_MessageAdminSession>,
int Stop();
int Destroy();
protected:
protected:
void InitWork();
void DoWork();
void StopWork();
void CleanWork();
private:
private:
// Helpers
handle m_hLog;
C_Admin* m_pAdmin;
......
......@@ -610,7 +610,7 @@ void C_TelnetSession::ExecCommand()
case ADMIN_COMMAND_DENIED:
SendAnswer(cAdminAnswer);
break;
case ADMIN_EMPTY_CMDLINE:
case ADMIN_EMPTY_COMMAND:
// Nothing to do
break;
default:
......
......@@ -33,7 +33,12 @@ END
# Telnet Administration
BEGIN "Telnet"
LocalPort = "9999" # Port to use for that purpose
LocalPort = "9999" # Port to use for that purpose
END
# Native Administration
BEGIN "NativeAdmin"
LocalPort = "9998" # Port to use for that purpose
END
# Streams sources declaration
......
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