KeePass源代码分析2

来源:互联网 发布:java 停车场计费系统 编辑:程序博客网 时间:2024/05/18 11:47

 

 

 

 

KeePass源代码分析2---基于插件的架构

 

 

 

KeePass是一个基于插件机制的绿色开源软件,也就是所有的KeePass插件不用像操作系统注册就直接可以使用,这就避免了污染了系统注册表。我们这节来分析KeePass的机制实现机制。

首先确定已经下载了KeePass源代码,并且已经编译成功了。打开KeePass项目工程,可以看到一下的目录结构:

 

我们不用关注KeePassLibC项目工程,因为它是PwSafe项目工程下KeePassLibCpp的一个dll实现版。首先我们来认识这个目录结构:

(1)HeaderFiles文件夹,不用多说,这个KeePass的头文件。

(2)KeePassLibCpp文件夹,这个是KeePass的核心库,包括了KeePass大部分实现和插件机制的设计。其中Crypto子目录实现了各种加密、解密算法,例如BASE64SHA2系列、AES算法以及twofish算法。DataExchange子目录主要实现了KeePass文件格式与其他格式(txthtmlcvsxml等)之间的相互转化。也就是说,KeePass文件格式和其他文件格式可以相互导入和导出。Details子目录主要实现了KeePass向下兼容的功能,也就是说新版的KeePass可以打开旧版的KeePass文件,例如,office 2007可以向下兼容office 2003一样。具体实现在接下来的系列再分析。PasswordGenerator子目录主要实现了强大的密码生成功能。具体实现以后再分析。SDK子目录主要实现了KeePass的插件机制,也是本节重点讨论的东西。SysSpec_windowsUtil子目录主要是一些通用的功能类。PwManager类主要实现了KeePass文件格式,有关KeePass的读取和保存等。是KeePass的一个核心类。

(3)NewGUI文件夹主要是一些扩展的windows控件库。

(4)Plugin文件是KeePass实现的一些重要接口,这些接口完成了KeePass的核心功能,并且暴露给外部模块使用,例如编写KeePass插件时就可以使用这些接口。

(5)Util文件也是提供一些通用功能类。

了解了KeePass代码组织结构, 现在来分析KeePass的插件机制。这需要COM方面的知识,如果还不了解COM方面的知识,可以参考潘爱民的com原理与应用。

KeePass机制如下:每个插件都实现一个规范IKpPlugin接口。IKpPlugin接口包括以下接口。

(6)// *** IKpUnknown methods ***

(7) STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) = 0;

(8) STDMETHOD_(ULONG, AddRef)() = 0;

(9) STDMETHOD_(ULONG, Release)() = 0;

(10)

// *** IKpPlugin methods ***

/// Plugins are notified of events using this method.

/// @return In general you should always return TRUE. Only return FALSE if

/// you want to block further code execution in KeePass in some cases.

(11) STDMETHOD_(BOOL, OnMessage)(DWORD dwCode, LPARAM lParamW, LPARAM lParamL) = 0;

(12)

(13) /// Get a plugin property.

(14) /// @return Property value.

(15) /// @return If the plugin doesn't know the property with the specified name,

(16) /// it should return NULL.

(17) /// @return The returned string must be stored statically, KeePass will not

(18) /// delete the returned pointer.

(19) STDMETHOD_(LPCTSTR, GetProperty)(LPCTSTR lpName) = 0;

(20)

(21) /// Set a plugin property.

(22) /// This method is allowed to ignore properties it doesn't know

(23) /// (i.e. it doesn't need to implement a generic string dictionary).

        STDMETHOD(SetProperty)(LPCTSTR lpName, LPCTSTR lpValue) = 0;

        /// Get the number of main menu items provided by the plugin.

(24) STDMETHOD_(DWORD, GetMenuItemCount)() = 0;

(25)

(26) /// Get a pointer to all main menu items provided by the plugin.

(27) /// @return Pointer to the first item in an array of menu items.

(28) /// @return The menu items must be stored statically, KeePass will not

(29) /// delete the returned pointer.

(30) STDMETHOD_(KP_MENU_ITEM*, GetMenuItems)() = 0;

 

可以看到,IKpPlugin接口主要定义了OnMessageGetMenuItemGetMenuItemCount三个接口函数,我们从字面上很容易就可以看出,GetMenuItemGetMenuItemCount不是获取有关菜单的信息吗,是的。KeePass主程序利用这两个函数获取插件有关菜单的信息,并在主菜单的Tools子菜单的添加相应的用户接口。OnMessage接口函数主要是建立KeePass主程序和插件之间的通信。也就是说,用户点击主程序插件提供的菜单项时,KeePass主程序通过OnMessage接口函数通知相应的插件,插件再进行相应的数据处理。 

上面讲述了插件和主程序是如何交互的,现在就来讲讲KeePass是如何对插件进行管理的。既然KeePass是绿色软件,也就是说KeePass插件不需要向操作系统注册,那KeePass是如何是如何对插件进行管理的呢?答案是KeePass利用CPluginManager类对插件进行统一管理。我们先来描述一下KeePass加载插件的流程。

首先应用程序启动时,主窗口类进行初始化,也就是CPwSafeDlgOnInitDialog进行调用。进行用户界面创建以及配置文件的读取以及设置好语言初始化之后,定位到语句:

VERIFY(CPluginManager::Instance().LoadAllPlugins());

显然这是进行插件加载,我们跟踪进去LoadAllPlugins,我们发现,

std::vector<std_string> vCandidates = FindPluginCandidates();

显然这个函数是查找所有的插件,我们跟踪到FindPluginCandidates函数,看它是如何查找插件的:

(31)std::vector<std_string> CPluginManager::FindPluginCandidates()

(32){

(33) std::vector<std_string> v;

(34)

(35) const std_string strBaseDir = Executable::instance().getPathOnly();

(36) const std_string strSearchPattern = (strBaseDir + _T("*.dll"));

(37)

(38) WIN32_FIND_DATA wfd;

(39) ZeroMemory(&wfd, sizeof(WIN32_FIND_DATA));

(40) HANDLE hFind = FindFirstFile(strSearchPattern.c_str(), &wfd);

(41) if(hFind == INVALID_HANDLE_VALUE) return v; // Valid, but no files

(42)

(43) while(1)

(44) {

(45) v.push_back(strBaseDir + wfd.cFileName);

(46)

(47) if(FindNextFile(hFind, &wfd) == FALSE) break;

(48) }

(49)

(50) FindClose(hFind);

(51) return v;

(52)}

我们发现该函数只是在程序的当前目录查找所有dll文件,并且返回一个关于dll路径的列表。回到CPluginManager::Instance().LoadAllPlugins()函数。

(53)for(size_t i = 0; i < vCandidates.size(); ++i){

              KP_PLUGIN_INSTANCE kppi;

              CPluginManager::ClearStructure(&kppi);

 

std_string strPluginPath = vCandidates[i];

if(_IsValidPlugin(strPluginPath.c_str(), &kppi) == false) continue;

 

kppi.hinstDLL = LoadLibrary(strPluginPath.c_str());

if(kppi.hinstDLL == NULL) continue;

 

// Call optional library initialization function

LPKPLIBFUNC lpInit = (LPKPLIBFUNC)GetProcAddress(kppi.hinstDLL, KP_I_INITIALIZELIB);

if(lpInit != NULL)

{

if(lpInit(pAPI) != S_OK) { FreePluginDllEx(kppi.hinstDLL); continue; }

}

 

LPKPCREATEINSTANCE lpCreate = (LPKPCREATEINSTANCE)GetProcAddress(

kppi.hinstDLL, KP_I_CREATEINSTANCE);

if(lpCreate == NULL) { FreePluginDllEx(kppi.hinstDLL); continue; }

 

const HRESULT hr = lpCreate(IID_IKpPlugin, (void**)&kppi.pInterface, pAPI);

if((hr != S_OK) || (kppi.pInterface == NULL))

{ FreePluginDllEx(kppi.hinstDLL); continue; }

 

kppi.dwPluginID = m_dwFreePluginID;

++m_dwFreePluginID;

 

kppi.strPath = strPluginPath;

 

m_plugins.push_back(kppi);

}

查找所有的dll文件之后,接下来当然是对dll文件进行验证,总不能只要是dll文件,都加进来吧,这就乱套了。所以在代码中首先调用_IsValidPlugin(strPluginPath.c_str(), &kppi)函数对dll进行验证,如果不是KeePass的插件,则无需进行后续处理。KeePass是如何对插件进行验证的呢?大家可以查看_IsValidPlugin(strPluginPath.c_str(), &kppi)函数的源代码,这里就不多提了。既然已经找到了所有插件,那如何创建插件、以及怎么保存插件的信息呢?

我们首先看个结构,KP_PLUGIN_INSTANCE

typedef struct

{

DWORD dwPluginID; // Assigned by KeePass, used internally

std_string strPath;

HMODULE hinstDLL;

IKpPlugin* pInterface;

std_string strName;

std_string strVersion;

std_string strAuthor;

UINT64 qwVersion;

} KP_PLUGIN_INSTANCE, *LPKP_PLUGIN_INSTANCE;

这个结构记录有关插件的信息,最重要的是hInstDll句柄。我们知道要创建插件,dll必然要一个导出函数,KeePass就可以调用这个导出函数来创建插件。我们看看源代码发现以下宏:KP_I_INITIALIZELIBKP_I_CREATEINSTANCE这两个宏其实是两个函数,一个是初始化插件,另一个是创建插件。Dll插件导出这个两个函数,KeePass获取这个两个函数,从而完成插件的初始化和创建。并且用一个列表把有关插件的信息(例如dll句柄)保存起来,以备后用。

     插件创建好了,接着就是为插件创建用户界面了。返回主对话框的初始化函数。定位到源代码:BuildPluginMenu();跟踪进去,函数依次遍历所有的插件,然后调用插件接口函数创建插件菜单。

const DWORD dwNumCommands = pPlugin->pInterface->GetMenuItemCount();

       KP_MENU_ITEM* pMenuItems = pPlugin->pInterface->GetMenuItems();

 

用户接口也创建好了,现在就是如何响应插件消息了。定位到_CallPlugins(KPM_DELAYED_INIT, 0, 0)

const DWORD dwNumCommands = p->pInterface->GetMenuItemCount();

KP_MENU_ITEM* pMenuItems = p->pInterface->GetMenuItems();

 

if((dwNumCommands == 0) || (pMenuItems == NULL)) continue;

 

for(size_t j = 0; j < dwNumCommands; ++j)

{

  Return p->pInterface->OnMessage(KPM_DIRECT_EXEC, lParamW, lParamL);

}

 

    KeePass根据commandid遍历所有插件,并和插件的所有菜单项进行比对,然后再调用OnMessage接口函数,从而把消息转交给插件处理。从而实现了用户接口和插件的交互。

好了,KeePass插件的整个流程已经分析结束。现在我们来梳理一下。

(1)KeePass定义一组规范,例如IKpPlugin,以及创建插件和初始化插件的函数,例如KpInitializeLibraryKpCreateInstance

(2)插件实现IKpPlugin接口,并实现插件自己的相关功能。而且插件要导出KpInitializeLibraryKpCreateInstance函数,实现插件的创建和初始化。

(3)当程序初始化时KeePass搜索程序当前目录下所有的dll,并且进行验证是不是KeePass的插件。并调用插件的导出函数KpInitializeLibraryKpCreateInstance完成插件的初始化和创建工作。

(4)创建插件之后,KeePass通过插件接口函数GetMenuItemCountGetMenuItems获取插件的菜单信息。并且为每个菜单分配一个唯一、值在WM_PLUGINS_FIRST和      WM_PLUGINS_LAST之间COMMANDId通过宏

ON_COMMAND_RANGE(WM_PLUGINS_FIRST,WM_PLUGINS_LAST, OnPluginMessage)把插件菜单的命令消息映射到函数OnPluginMessage里进行处理。

(5)在函数OnPluginMessage里,KeePass搜索所有的插件,并且对每个子菜单的Id和当前单击的菜单Id进行比较,如果配对就通过插件接口函数OnMessage把消息分发到插件里进行处理。从而完成插件和KeePass之间的交互。

好了,今天就分析到这里。下篇分析KeePass的文件存储结构。

 

原创粉丝点击