/*
* $Id$
*
* (C) 2003-2006 Gabest
* (C) 2006-2010 see AUTHORS
*
* This file is part of mplayerc.
*
* Mplayerc is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* Mplayerc is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*/
#include "stdafx.h"
#include "mplayerc.h"
#include "resource.h"
#include "MainFrm.h"
#include
#include
#include "WebServerSocket.h"
#include "WebClientSocket.h"
#include "WebServer.h"
CAtlStringMap CWebServer::m_internalpages;
CAtlStringMap CWebServer::m_downloads;
CAtlStringMap CWebServer::m_mimes;
CWebServer::CWebServer(CMainFrame* pMainFrame, int nPort)
: m_pMainFrame(pMainFrame)
, m_nPort(nPort)
{
if(m_internalpages.IsEmpty())
{
m_internalpages[_T("/")] = &CWebClientSocket::OnIndex;
m_internalpages[_T("/index.html")] = &CWebClientSocket::OnIndex;
m_internalpages[_T("/browser.html")] = &CWebClientSocket::OnBrowser;
m_internalpages[_T("/controls.html")] = &CWebClientSocket::OnControls;
m_internalpages[_T("/command.html")] = &CWebClientSocket::OnCommand;
m_internalpages[_T("/status.html")] = &CWebClientSocket::OnStatus;
m_internalpages[_T("/player.html")] = &CWebClientSocket::OnPlayer;
m_internalpages[_T("/snapshot.jpg")] = &CWebClientSocket::OnSnapShotJpeg;
m_internalpages[_T("/404.html")] = &CWebClientSocket::OnError404;
m_internalpages[_T("/convres.html")] = &CWebClientSocket::OnConvRes;
}
if(m_downloads.IsEmpty())
{
m_downloads[_T("/default.css")] = IDF_DEFAULT_CSS;
m_downloads[_T("/vbg.gif")] = IDF_VBR_GIF;
m_downloads[_T("/vbs.gif")] = IDF_VBS_GIF;
m_downloads[_T("/sliderbar.gif")] = IDF_SLIDERBAR_GIF;
m_downloads[_T("/slidergrip.gif")] = IDF_SLIDERGRIP_GIF;
m_downloads[_T("/sliderback.gif")] = IDF_SLIDERBACK_GIF;
m_downloads[_T("/1pix.gif")] = IDF_1PIX_GIF;
m_downloads[_T("/headericon.png")] = IDF_HEADERICON_PNG;
m_downloads[_T("/headerback.png")] = IDF_HEADERBACK_PNG;
m_downloads[_T("/headerclose.png")] = IDF_HEADERCLOSE_PNG;
m_downloads[_T("/leftside.png")] = IDF_LEFTSIDE_PNG;
m_downloads[_T("/rightside.png")] = IDF_RIGHTSIDE_PNG;
m_downloads[_T("/bottomside.png")] = IDF_BOTTOMSIDE_PNG;
m_downloads[_T("/leftbottomside.png")] = IDF_LEFTBOTTOMSIDE_PNG;
m_downloads[_T("/rightbottomside.png")] = IDF_RIGHTBOTTOMSIDE_PNG;
m_downloads[_T("/seekbarleft.png")] = IDF_SEEKBARLEFT_PNG;
m_downloads[_T("/seekbarmid.png")] = IDF_SEEKBARMID_PNG;
m_downloads[_T("/seekbarright.png")] = IDF_SEEKBARRIGHT_PNG;
m_downloads[_T("/seekbargrip.png")] = IDF_SEEKBARGRIP_PNG;
m_downloads[_T("/logo.png")] = IDF_LOGO1;
m_downloads[_T("/controlback.png")] = IDF_CONTROLBACK_PNG;
m_downloads[_T("/controlbuttonplay.png")] = IDF_CONTROLBUTTONPLAY_PNG;
m_downloads[_T("/controlbuttonpause.png")] = IDF_CONTROLBUTTONPAUSE_PNG;
m_downloads[_T("/controlbuttonstop.png")] = IDF_CONTROLBUTTONSTOP_PNG;
m_downloads[_T("/controlbuttonskipback.png")] = IDF_CONTROLBUTTONSKIPBACK_PNG;
m_downloads[_T("/controlbuttondecrate.png")] = IDF_CONTROLBUTTONDECRATE_PNG;
m_downloads[_T("/controlbuttonincrate.png")] = IDF_CONTROLBUTTONINCRATE_PNG;
m_downloads[_T("/controlbuttonskipforward.png")] = IDF_CONTROLBUTTONSKIPFORWARD_PNG;
m_downloads[_T("/controlbuttonstep.png")] = IDF_CONTROLBUTTONSTEP_PNG;
m_downloads[_T("/controlvolumeon.png")] = IDF_CONTROLVOLUMEON_PNG;
m_downloads[_T("/controlvolumeoff.png")] = IDF_CONTROLVOLUMEOFF_PNG;
m_downloads[_T("/controlvolumebar.png")] = IDF_CONTROLVOLUMEBAR_PNG;
m_downloads[_T("/controlvolumegrip.png")] = IDF_CONTROLVOLUMEGRIP_PNG;
}
CRegKey key;
CString str(_T("MIME\\Database\\Content Type"));
if(ERROR_SUCCESS == key.Open(HKEY_CLASSES_ROOT, str, KEY_READ))
{
TCHAR buff[256];
DWORD len = countof(buff);
for(int i = 0; ERROR_SUCCESS == key.EnumKey(i, buff, &len); i++, len = countof(buff))
{
CRegKey mime;
TCHAR ext[64];
ULONG len = countof(ext);
if(ERROR_SUCCESS == mime.Open(HKEY_CLASSES_ROOT, str + _T("\\") + buff, KEY_READ)
&& ERROR_SUCCESS == mime.QueryStringValue(_T("Extension"), ext, &len))
m_mimes[CStringA(ext).MakeLower()] = CStringA(buff).MakeLower();
}
}
m_mimes[".html"] = "text/html";
m_mimes[".txt"] = "text/plain";
m_mimes[".css"] = "text/css";
m_mimes[".gif"] = "image/gif";
m_mimes[".jpeg"] = "image/jpeg";
m_mimes[".jpg"] = "image/jpeg";
m_mimes[".png"] = "image/png";
GetModuleFileName(AfxGetInstanceHandle(), str.GetBuffer(_MAX_PATH), _MAX_PATH);
str.ReleaseBuffer();
m_webroot = CPath(str);
m_webroot.RemoveFileSpec();
CString WebRoot = AfxGetAppSettings().strWebRoot;
WebRoot.Replace('/', '\\');
WebRoot.Trim();
CPath p(WebRoot);
if(WebRoot.Find(_T(":\\")) < 0 && WebRoot.Find(_T("\\\\")) < 0) m_webroot.Append(WebRoot);
else m_webroot = p;
m_webroot.Canonicalize();
m_webroot.MakePretty();
if(!m_webroot.IsDirectory()) m_webroot = CPath();
CAtlList sl;
Explode(AfxGetAppSettings().strWebServerCGI, sl, ';');
POSITION pos = sl.GetHeadPosition();
while(pos)
{
CAtlList sl2;
CString ext = Explode(sl.GetNext(pos), sl2, '=', 2);
if(sl2.GetCount() < 2) continue;
m_cgi[ext] = sl2.GetTail();
}
m_ThreadId = 0;
m_hThread = ::CreateThread(NULL, 0, StaticThreadProc, (LPVOID)this, 0, &m_ThreadId);
}
CWebServer::~CWebServer()
{
if(m_hThread != NULL)
{
PostThreadMessage(m_ThreadId, WM_QUIT, 0, 0);
if (WaitForSingleObject(m_hThread, 10000) == WAIT_TIMEOUT)
TerminateThread (m_hThread, 0xDEAD);
EXECUTE_ASSERT(CloseHandle(m_hThread));
}
}
DWORD WINAPI CWebServer::StaticThreadProc(LPVOID lpParam)
{
return ((CWebServer*)lpParam)->ThreadProc();
}
DWORD CWebServer::ThreadProc()
{
if(!AfxSocketInit(NULL))
return (DWORD)-1;
CWebServerSocket s(this, m_nPort);
MSG msg;
while((int)GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
static void PutFileContents(LPCTSTR fn, const CStringA& data)
{
if(FILE* f = _tfopen(fn, _T("wb")))
{
fwrite((LPCSTR)data, 1, data.GetLength(), f);
fclose(f);
}
}
void CWebServer::Deploy(CString dir)
{
CStringA data;
if(LoadResource(IDR_HTML_INDEX, data, RT_HTML)) PutFileContents(dir + _T("index.html"), data);
if(LoadResource(IDR_HTML_BROWSER, data, RT_HTML)) PutFileContents(dir + _T("browser.html"), data);
if(LoadResource(IDR_HTML_CONTROLS, data, RT_HTML)) PutFileContents(dir + _T("controls.html"), data);
if(LoadResource(IDR_HTML_404, data, RT_HTML)) PutFileContents(dir + _T("404.html"), data);
if(LoadResource(IDR_HTML_PLAYER, data, RT_HTML)) PutFileContents(dir + _T("player.html"), data);
POSITION pos = m_downloads.GetStartPosition();
while(pos)
{
CString fn;
UINT id;
m_downloads.GetNextAssoc(pos, fn, id);
if(LoadResource(id, data, _T("FILE")))
PutFileContents(dir + fn, data);
}
}
bool CWebServer::ToLocalPath(CString& path, CString& redir)
{
if(!path.IsEmpty() && m_webroot.IsDirectory())
{
CString str = path;
str.Replace('/', '\\');
str.TrimLeft('\\');
CPath p;
p.Combine(m_webroot, str);
p.Canonicalize();
if(p.IsDirectory())
{
CAtlList sl;
Explode(AfxGetAppSettings().strWebDefIndex, sl, ';');
POSITION pos = sl.GetHeadPosition();
while(pos)
{
str = sl.GetNext(pos);
CPath p2 = p;
p2.Append(str);
if(p2.FileExists())
{
p = p2;
redir = path;
if(redir.GetAt(redir.GetLength()-1) != '/') redir += '/';
redir += str;
break;
}
}
}
if(_tcslen(p) > _tcslen(m_webroot) && p.FileExists())
{
path = (LPCTSTR)p;
return true;
}
}
return false;
}
bool CWebServer::LoadPage(UINT resid, CStringA& str, CString path)
{
CString redir;
if(ToLocalPath(path, redir))
{
if(FILE* f = _tfopen(path, _T("rb")))
{
fseek(f, 0, 2);
char* buff = str.GetBufferSetLength(ftell(f));
fseek(f, 0, 0);
int len = fread(buff, 1, str.GetLength(), f);
fclose(f);
return len == str.GetLength();
}
}
return LoadResource(resid, str, RT_HTML);
}
void CWebServer::OnAccept(CWebServerSocket* pServer)
{
CAutoPtr p(DNew CWebClientSocket(this, m_pMainFrame));
if(pServer->Accept(*p))
{
CString name;
UINT port;
if(AfxGetAppSettings().fWebServerLocalhostOnly && p->GetPeerName(name, port) && name != _T("127.0.0.1"))
{
p->Close();
return;
}
m_clients.AddTail(p);
}
}
void CWebServer::OnClose(CWebClientSocket* pClient)
{
POSITION pos = m_clients.GetHeadPosition();
while(pos)
{
POSITION cur = pos;
if(m_clients.GetNext(pos) == pClient)
{
m_clients.RemoveAt(cur);
break;
}
}
}
void CWebServer::OnRequest(CWebClientSocket* pClient, CStringA& hdr, CStringA& body)
{
CPath p(pClient->m_path);
CStringA ext = p.GetExtension().MakeLower();
CStringA mime;
if(ext.IsEmpty()) mime = "text/html";
else m_mimes.Lookup(ext, mime);
hdr = "HTTP/1.0 200 OK\r\n";
bool fHandled = false, fCGI = false;
if(!fHandled && m_webroot.IsDirectory())
{
CStringA tmphdr;
fHandled = fCGI = CallCGI(pClient, tmphdr, body, mime);
if(fHandled)
{
tmphdr.Replace("\r\n", "\n");
CAtlList hdrlines;
ExplodeMin(tmphdr, hdrlines, '\n');
POSITION pos = hdrlines.GetHeadPosition();
while(pos)
{
POSITION cur = pos;
CAtlList sl;
CStringA key = Explode(hdrlines.GetNext(pos), sl, ':', 2);
if(sl.GetCount() < 2) continue;
key.Trim().MakeLower();
if(key == "content-type")
{
mime = sl.GetTail().Trim();
hdrlines.RemoveAt(cur);
}
else if(key == "content-length")
{
hdrlines.RemoveAt(cur);
}
}
tmphdr = Implode(hdrlines, '\n');
tmphdr.Replace("\n", "\r\n");
hdr += tmphdr + "\r\n";
}
}
RequestHandler rh = NULL;
if(!fHandled && m_internalpages.Lookup(pClient->m_path, rh) && (pClient->*rh)(hdr, body, mime))
{
if(mime.IsEmpty()) mime = "text/html";
CString redir;
if(pClient->m_get.Lookup(_T("redir"), redir)
|| pClient->m_post.Lookup(_T("redir"), redir))
{
if(redir.IsEmpty()) redir = '/';
hdr =
"HTTP/1.0 302 Found\r\n"
"Location: " + CStringA(redir) + "\r\n";
return;
}
fHandled = true;
}
if(!fHandled && m_webroot.IsDirectory())
{
fHandled = LoadPage(0, body, pClient->m_path);
}
UINT resid;
CStringA res;
if(!fHandled && m_downloads.Lookup(pClient->m_path, resid) && LoadResource(resid, res, _T("FILE")))
{
if(mime.IsEmpty()) mime = "application/octet-stream";
memcpy(body.GetBufferSetLength(res.GetLength()), res.GetBuffer(), res.GetLength());
fHandled = true;
}
if(!fHandled)
{
hdr = mime == "text/html"
? "HTTP/1.0 301 Moved Permanently\r\n" "Location: /404.html\r\n"
: "HTTP/1.0 404 Not Found\r\n";
return;
}
if(mime == "text/html" && !fCGI)
{
hdr +=
"Expires: Thu, 19 Nov 1981 08:52:00 GMT\r\n"
"Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0\r\n"
"Pragma: no-cache\r\n";
CStringA debug;
if(AfxGetAppSettings().fWebServerPrintDebugInfo)
{
debug += "
\r\n";
CString key, value;
POSITION pos;
pos = pClient->m_hdrlines.GetStartPosition();
while(pos)
{
pClient->m_hdrlines.GetNextAssoc(pos, key, value);
debug += "HEADER[" + key + "] = " + value + "
\r\n";
}
debug += "cmd: " + pClient->m_cmd + "
\r\n";
debug += "path: " + pClient->m_path + "
\r\n";
debug += "ver: " + pClient->m_ver + "
\r\n";
pos = pClient->m_get.GetStartPosition();
while(pos)
{
pClient->m_get.GetNextAssoc(pos, key, value);
debug += "GET[" + key + "] = " + value + "
\r\n";
}
pos = pClient->m_post.GetStartPosition();
while(pos)
{
pClient->m_post.GetNextAssoc(pos, key, value);
debug += "POST[" + key + "] = " + value + "
\r\n";
}
pos = pClient->m_cookie.GetStartPosition();
while(pos)
{
pClient->m_cookie.GetNextAssoc(pos, key, value);
debug += "COOKIE[" + key + "] = " + value + "
\r\n";
}
pos = pClient->m_request.GetStartPosition();
while(pos)
{
pClient->m_request.GetNextAssoc(pos, key, value);
debug += "REQUEST[" + key + "] = " + value + "
\r\n";
}
}
body.Replace("[path]", CStringA(pClient->m_path));
body.Replace("[indexpath]", "/index.html");
body.Replace("[commandpath]", "/command.html");
body.Replace("[browserpath]", "/browser.html");
body.Replace("[controlspath]", "/controls.html");
body.Replace("[wmcname]", "wm_command");
body.Replace("[setposcommand]", CMD_SETPOS);
body.Replace("[setvolumecommand]", CMD_SETVOLUME);
body.Replace("[debug]", debug);
// TODO: add more general tags to replace
}
// gzip
if(AfxGetAppSettings().fWebServerUseCompression && hdr.Find("Content-Encoding:") < 0)
do
{
CString accept_encoding;
pClient->m_hdrlines.Lookup(_T("accept-encoding"), accept_encoding);
accept_encoding.MakeLower();
CAtlList sl;
ExplodeMin(accept_encoding, sl, ',');
if(!sl.Find(_T("gzip"))) break;
CHAR path[_MAX_PATH], fn[_MAX_PATH];
if(!GetTempPathA(_MAX_PATH, path) || !GetTempFileNameA(path, "mpc_gz", 0, fn))
break;
gzFile gf = gzopen(fn, "wb9");
if(!gf || gzwrite(gf, (LPVOID)(LPCSTR)body, body.GetLength()) != body.GetLength())
{
if(gf) gzclose(gf);
DeleteFileA(fn);
break;
}
gzclose(gf);
FILE* f = fopen(fn, "rb");
if(!f)
{
DeleteFileA(fn);
break;
}
fseek(f, 0, 2);
CHAR* s = body.GetBufferSetLength(ftell(f));
fseek(f, 0, 0);
int len = fread(s, 1, body.GetLength(), f);
ASSERT(len == body.GetLength());
fclose(f);
DeleteFileA(fn);
hdr += "Content-Encoding: gzip\r\n";
}
while(0);
CStringA content;
content.Format(
"Content-Type: %s\r\n"
"Content-Length: %d\r\n",
mime, body.GetLength());
hdr += content;
}
static DWORD WINAPI KillCGI(LPVOID lParam)
{
HANDLE hProcess = (HANDLE)lParam;
if(WaitForSingleObject(hProcess, 30000) == WAIT_TIMEOUT)
TerminateProcess(hProcess, 0);
return 0;
}
bool CWebServer::CallCGI(CWebClientSocket* pClient, CStringA& hdr, CStringA& body, CStringA& mime)
{
CString path = pClient->m_path, redir = path;
if(!ToLocalPath(path, redir)) return false;
CString ext = CPath(path).GetExtension().MakeLower();
CPath dir(path);
dir.RemoveFileSpec();
CString cgi;
if(!m_cgi.Lookup(ext, cgi) || !CPath(cgi).FileExists())
return false;
HANDLE hProcess = GetCurrentProcess();
HANDLE hChildStdinRd, hChildStdinWr, hChildStdinWrDup = NULL;
HANDLE hChildStdoutRd, hChildStdoutWr, hChildStdoutRdDup = NULL;
SECURITY_ATTRIBUTES saAttr;
ZeroMemory(&saAttr, sizeof(saAttr));
saAttr.nLength = sizeof(saAttr);
saAttr.bInheritHandle = TRUE;
if(CreatePipe(&hChildStdoutRd, &hChildStdoutWr, &saAttr, 0))
{
BOOL fSuccess = DuplicateHandle(hProcess, hChildStdoutRd, hProcess, &hChildStdoutRdDup, 0, FALSE, DUPLICATE_SAME_ACCESS);
UNUSED_ALWAYS(fSuccess);
CloseHandle(hChildStdoutRd);
}
if(CreatePipe(&hChildStdinRd, &hChildStdinWr, &saAttr, 0))
{
BOOL fSuccess = DuplicateHandle(hProcess, hChildStdinWr, hProcess, &hChildStdinWrDup, 0, FALSE, DUPLICATE_SAME_ACCESS);
UNUSED_ALWAYS(fSuccess);
CloseHandle(hChildStdinWr);
}
STARTUPINFO siStartInfo;
ZeroMemory(&siStartInfo, sizeof(siStartInfo));
siStartInfo.cb = sizeof(siStartInfo);
siStartInfo.hStdError = hChildStdoutWr;
siStartInfo.hStdOutput = hChildStdoutWr;
siStartInfo.hStdInput = hChildStdinRd;
siStartInfo.dwFlags |= STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW;
siStartInfo.wShowWindow = SW_HIDE;
PROCESS_INFORMATION piProcInfo;
ZeroMemory(&piProcInfo, sizeof(piProcInfo));
CStringA envstr;
LPVOID lpvEnv = GetEnvironmentStrings();
if(lpvEnv)
{
CString str;
CAtlList env;
for(LPTSTR lpszVariable = (LPTSTR)lpvEnv; *lpszVariable; lpszVariable += _tcslen(lpszVariable)+1)
if(lpszVariable != (LPTSTR)lpvEnv)
env.AddTail(lpszVariable);
env.AddTail(_T("GATEWAY_INTERFACE=CGI/1.1"));
env.AddTail(_T("SERVER_SOFTWARE=Media Player Classic/6.4.x.y"));
env.AddTail(_T("SERVER_PROTOCOL=") + pClient->m_ver);
env.AddTail(_T("REQUEST_METHOD=") + pClient->m_cmd);
env.AddTail(_T("PATH_INFO=") + redir);
env.AddTail(_T("PATH_TRANSLATED=") + path);
env.AddTail(_T("SCRIPT_NAME=") + redir);
env.AddTail(_T("QUERY_STRING=") + pClient->m_query);
if(pClient->m_hdrlines.Lookup(_T("content-type"), str))
env.AddTail(_T("CONTENT_TYPE=") + str);
if(pClient->m_hdrlines.Lookup(_T("content-length"), str))
env.AddTail(_T("CONTENT_LENGTH=") + str);
POSITION pos = pClient->m_hdrlines.GetStartPosition();
while(pos)
{
CString key = pClient->m_hdrlines.GetKeyAt(pos);
CString value = pClient->m_hdrlines.GetNextValue(pos);
key.Replace(_T("-"), _T("_"));
key.MakeUpper();
env.AddTail(_T("HTTP_") + key + _T("=") + value);
}
CString name;
UINT port;
if(pClient->GetPeerName(name, port))
{
str.Format(_T("%d"), port);
env.AddTail(_T("REMOTE_ADDR=")+name);
env.AddTail(_T("REMOTE_HOST=")+name);
env.AddTail(_T("REMOTE_PORT=")+str);
}
if(pClient->GetSockName(name, port))
{
str.Format(_T("%d"), port);
env.AddTail(_T("SERVER_NAME=")+name);
env.AddTail(_T("SERVER_PORT=")+str);
}
env.AddTail(_T("\0"));
str = Implode(env, '\0');
envstr = CStringA(str, str.GetLength());
FreeEnvironmentStrings((LPTSTR)lpvEnv);
}
TCHAR* cmdln = DNew TCHAR[32768];
_sntprintf(cmdln, 32768, _T("\"%s\" \"%s\""), cgi, path);
if(hChildStdinRd && hChildStdoutWr)
if(CreateProcess(
NULL, cmdln, NULL, NULL, TRUE, 0,
envstr.GetLength() ? (LPVOID)(LPCSTR)envstr : NULL,
dir, &siStartInfo, &piProcInfo))
{
DWORD ThreadId;
CreateThread(NULL, 0, KillCGI, (LPVOID)piProcInfo.hProcess, 0, &ThreadId);
static const int BUFFSIZE = 1024;
DWORD dwRead, dwWritten = 0;
int i = 0, len = pClient->m_data.GetLength();
for(; i < len; i += dwWritten)
if(!WriteFile(hChildStdinWrDup, (LPCSTR)pClient->m_data + i, min(len - i, BUFFSIZE), &dwWritten, NULL))
break;
CloseHandle(hChildStdinWrDup);
CloseHandle(hChildStdoutWr);
body.Empty();
CStringA buff;
while(i == len && ReadFile(hChildStdoutRdDup, buff.GetBuffer(BUFFSIZE), BUFFSIZE, &dwRead, NULL) && dwRead)
{
buff.ReleaseBufferSetLength(dwRead);
body += buff;
}
int hdrend = body.Find("\r\n\r\n");
if(hdrend >= 0)
{
hdr = body.Left(hdrend+2);
body = body.Mid(hdrend+4);
}
CloseHandle(hChildStdinRd);
CloseHandle(hChildStdoutRdDup);
CloseHandle(piProcInfo.hProcess);
CloseHandle(piProcInfo.hThread);
}
else
{
body = _T("CGI Error");
}
delete [] cmdln;
return true;
}