我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。
这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。
系列入口:编程实战:自己编写HTTP服务器(系列1:概述和应答)-CSDN博客
本文介绍执行后台命令的shell.asp的实现。
目录
一、概述
这个功能就相当于一个终端,不过只能执行一个命令。有什么好处看自己,可以加入自己喜欢的特性。
入口:
别的不说了,主体代码是doPageShell()。
二、主体代码
主体代码如下:
bool doPageShell()
{
#ifdef _MS_VC
return true;
#else
FILE * fp;
string changedir=m_request.GetParam("changedir");
string curdir=m_request.GetParam("curdir");
string cmd=m_request.GetParam("command");
bool noform=(m_request.GetParam("noform")=="true");
bool term=(m_request.GetParam("term")=="true");
long bufsize=1024*1024;
char * buf=new char[bufsize];
if(NULL==buf)
{
m_respond.AppendBody("<P><FONT color=RED>内存不足</FONT><P>");
return true;
}
//切换路径
if(0!=curdir.size())
{
if(0!=chdir(curdir.c_str()))
{
m_respond.AppendBody("<P><FONT color=RED>设置初始路径出错</FONT><P>"+curdir+"<P>");
return true;
}
}
if(0!=changedir.size())
{
if(0!=chdir(changedir.c_str()))
{
m_respond.AppendBody("<P><FONT color=RED>切换工作路径出错</FONT><P>"+changedir+"<P>");
return true;
}
}
//执行命令
if(0==cmd.size())
{
m_respond.AppendBody("<P>空命令<P>");
}
else
{
if(0!=setpgid(getpid(),getpid()))
{
m_respond.AppendBody("设置进程组ID出错<P>");
}
if(NULL==(fp=popen((cmd+" 2>&1").c_str(),"r")))
{
m_respond.AppendBody("<P><FONT color=RED>无法执行,原因:popen error</FONT><P>");
if(!m_respond.Flush(m_s))return true;
}
else
{
int fd=fileno(fp);
int flags;
fd_set fdset;
struct timeval tv;
tv.tv_sec=300;
tv.tv_usec=0;
flags = fcntl(fd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);
char * tmpp;
m_respond.AppendBody("开始执行 ");
m_respond.AppendBody(CHtmlDoc::HTMLEncode(cmd));
m_respond.Flush(m_s);
m_respond.AppendBody("<HR></HR><CODE>");
while(true)
{
FD_ZERO(&fdset);
FD_SET(fd,&fdset);
#ifdef _HPOS
int selectret=select(fd+1,(int *)&fdset,NULL,NULL,&tv);
#else
int selectret=select(fd+1,&fdset,NULL,NULL,&tv);
#endif
if(selectret<0)
{
LOG<<"select error"<<ENDE;
}
else if(0==selectret)
{//超时没有数据
m_respond.AppendBody("注意,长时间没有收到输出.");
if(!m_respond.Flush(m_s))
{
LOG<<"发送失败,客户端已断开,直接退出"<<ENDI;
if(term)
{
if(0!=kill(0,SIGTERM))
{
LOG<<"发送停止信号出错,shell会持续执行到命令结束"<<ENDE;
}
}
return true;
}
continue;
}
else
{
}
bool fileend=false;
while(true)
{
tmpp=fgets(buf,int(bufsize-1),fp);
if(NULL==tmpp)
{
//正常结束
if(0!=feof(fp))
{
fileend=true;
break;
}
if(EWOULDBLOCK==errno || EAGAIN==errno)
{//无数据
break;
}
else
{//出错结束
m_respond.AppendBody("<P><FONT color=RED>执行出错,原因:read error</FONT><P>");
m_respond.AppendBody(strerror(errno));
m_respond.Flush(m_s);
fileend=true;
break;
}
}
else
{
m_respond.AppendBody(LogToHtml(buf,false,false));
m_respond.AppendBodyHtmlScroll();
if(!m_respond.Flush(m_s))
{
LOG<<"发送失败,客户端已断开,直接退出"<<ENDI;
if(term)
{
if(0!=kill(0,SIGTERM))
{
LOG<<"发送停止信号出错,shell会持续执行到命令结束"<<ENDE;
}
}
return true;
}
}
}
if(fileend)break;
}
m_respond.AppendBody("</CODE><HR></HR>");
int ret=pclose(fp);
if(0!=ret)
{
if(WIFEXITED(ret))
{
sprintf(buf,"<FONT color=RED>执行完毕,返回代码 %d 。</FONT><BR>",WEXITSTATUS(ret));//(0xFF00&ret)/256);
}
else if(WIFSIGNALED(ret))
{
sprintf(buf,"<FONT color=RED>被信号终止,信号 %d 。</FONT><BR>",WTERMSIG(ret));
}
else if(WCOREDUMP(ret))
{
sprintf(buf,"<FONT color=RED>执行失败,COREDUMP。</FONT><BR>");
}
else
{
sprintf(buf,"<FONT color=RED>未知的返回值:%d。</FONT><BR>",ret);
}
}
else sprintf(buf,"执行完毕,返回代码 %d 。<BR>",ret);
m_respond.AppendBody(buf);
}
}
if(!noform)
{
char cwd[1024];
if(NULL!=getcwd(cwd,1024))
{
sprintf(buf,
"<FORM ACTION=\"/shell.asp\" METHOD=\"GET\" >\n"
"当前路径:%s<BR>"
"<INPUT TYPE=\"hidden\" NAME=\"curdir\" VALUE=\"%s\" >\n"
"切换路径到:<INPUT TYPE=\"text\" SIZE=\"30\" NAME=\"changedir\" ><BR>\n"
"Shell命令: <INPUT TYPE=\"text\" SIZE=\"30\" NAME=\"command\" VALUE=\"%s\">\n"
"<INPUT TYPE=SUBMIT VALUE=\"执行\" >\n"
"</FORM>\n"
,cwd,cwd,cmd.c_str());
}
else
{
sprintf(buf,"获取当前工作路径出错");
}
m_respond.AppendBody(buf);
}
delete[] buf;
return true;
#endif
}
三、详解
3.1 参数
最关键命令参数:command,包含要执行的命令,可以是一串命令的组合,也就是你能输到控制台运行的东西都行。
运行命令一定需要工作目录,显然不能在服务进程的工作目录下执行,谁知道会发生什么呢。目录用curdir和changedir参数来控制,如果curdir是当前工作目录,具体就是这个页面执行命令的工作目录,changedir用于切换目录,可以是相对目录。执行时首先将目录切换到curdir(因为服务进程有自己的工作目录,和期待的执行命令的工作目录不同),然后再切换到changedir,其实就是执行两次chdir()。
代码如下:
//切换路径
if(0!=curdir.size())
{
if(0!=chdir(curdir.c_str()))
{
m_respond.AppendBody("<P><FONT color=RED>设置初始路径出错</FONT><P>"+curdir+"<P>");
return true;
}
}
if(0!=changedir.size())
{
if(0!=chdir(changedir.c_str()))
{
m_respond.AppendBody("<P><FONT color=RED>切换工作路径出错</FONT><P>"+changedir+"<P>");
return true;
}
}
3.2 设置进程组和打开管道执行命令
启动新进程一般都要设置点东西,解除新进程和服务进程的关系,主要是进程间使用进程组广播信号的问题,新进程压根不应该知道服务进程存在。
popen()很实用的运行命令并获取输出的方法,其内部会打开单向管道,运行命令,返回输出(文件句柄)。
为了能看到命令的所有输出,我们需要在命令后面追加“ 2>&1”将标准出错重定向到标准输出。
相关代码如下:
if(0!=setpgid(getpid(),getpid()))
{
m_respond.AppendBody("设置进程组ID出错<P>");
}
if(NULL==(fp=popen((cmd+" 2>&1").c_str(),"r")))
{
m_respond.AppendBody("<P><FONT color=RED>无法执行,原因:popen error</FONT><P>");
if(!m_respond.Flush(m_s))return true;
}
else
{
后面就是常规的从文件描述符读取数据直到结束。当然里面混合了格式化、发送,以及命令返回码的处理。
3.3 读取数据和返回码处理
select可以检查是否有数据,这个函数一般用于socket,但是其实是针对文件描述符的。
用fgets()逐行读取数据,如果出错errno会有各种结果,需要分别判断。
pclose获得返回码,具体分析和进程返回码是一样的。
(这里是结束,但不是整个系列的结束)