初级代码游戏的专栏介绍与文章目录-CSDN博客
我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。
这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。
系列入口:编程实战:自己编写HTTP服务器(系列1:概述和应答)-CSDN博客
本文介绍处理框架。
目录
一、框架概述
二、保持连接
三、基本认证
四、静态文件和文件下载
4.1 主要代码
4.2 规则
4.3 添加内容类型头标的代码(应答类)
4.4 处理类型的代码
五、框架的完整代码
六、处理特定功能
一、框架概述
处理框架针对的是一个连接,里面用了循环,支持HTTP1.1,如果不循环就是1.0了(1.1和1.0就这点区别)。
这个框架具有下列功能:
- 默认页,访问“/”会被重定向
- 基本认证
- 一系列的内置页面,后缀名为“asp”,嘿嘿,不要跟真正的asp混淆了
- 静态网页,因为支持静态网页,所以可以让前端帮忙设计css
因为主要目的是嵌入到已有的C++程序中,所以内置页面是我的重点,但作为普通服务器,有一个支持静态页面的功能就足够了。
续:
流程其实也是相当一目了然的。
二、保持连接
保持连接是HTTP1.1的特征,通过头标“Connection=Keep-Alive”(=后面有没有空格注意一下,可以从前两篇如何构造和分解头标的代码确认)指示。如果不打算保持连接,发送完毕后直接关闭连接即可。
三、基本认证
基本认证的主流程部分:
//处理用户认证,登录用的目录不需要认证,登录目录可以用XMLHttp来实现整合登录
//login目录下的内容无需认证,这要求login下引用的图片也必须在login目录下
if(NULL!=this->pfCheckUser)
{
char const * logondir="/login/";
if(0!=strncmp(logondir,m_request.GetResource().c_str(),strlen(logondir)))
{
string user;
string password;
if(m_request.GetAuthorization(user,password))
{
if(!pfCheckUser(user.c_str(),password.c_str()))
{
m_respond.Send401(m_s,m_pServerDatas->m_realm.c_str());
if(isKeepAlive)continue;
else
{
m_s.Close();
break;
}
}
string cookie="logon_user";
if(m_request.GetCookie(cookie)!=user)m_respond.AddCookie(cookie,user);
}
else
{
m_respond.Send401(m_s,m_pServerDatas->m_realm.c_str());
if(isKeepAlive)continue;
else
{
m_s.Close();
break;
}
}
}
}
pfCheckUser是个函数指针,指向检测用户名密码的函数,如果未设置就表示不需要认证。定义如下:
bool (*pfCheckUser)(char const * _user,char const * _pass);//检查普通用户口令
那么浏览器如何知道需要认证呢?这是通过401应答告知浏览器的,所以看一下应答类的Send401的代码:
//需要认证
bool Send401(CZBSocket & s, string const & realm)
{
m_status_line = "HTTP/1.1 401 Unauthorized";
string str = "Basic realm=\"" + realm + "\"";
AddHeader("WWW-Authenticate", str);
AddHeaderContentLength();
return Flush(s);
}
realm是认证的域,一般就是网站域名。浏览器收到401应答就会要求用户输入用户名和密码,然后放在请求头里面发过来,请求类的GetAuthorization从请求里面解析出用户名和密码:
bool GetAuthorization(string & user, string & password)const
{
string AUTHORIZATION = "Authorization: Basic ";
string::size_type pos_start;
string::size_type pos_end;
string base64;
if (m_fullrequest.npos == (pos_start = m_fullrequest.find(AUTHORIZATION)))return false;
if (m_fullrequest.npos == (pos_end = m_fullrequest.find("\r\n", pos_start)))return false;
base64 = m_fullrequest.substr(pos_start + AUTHORIZATION.size(), pos_end - pos_start - AUTHORIZATION.size());
char buf[2048];
int len;
if (0 > (len = CBase64::Base64Dec(buf, base64.c_str(), (int)base64.size())))
{
LOG << "base64解码错误" << ENDE;
}
buf[len] = '\0';
DEBUG_LOG << buf << ENDI;
CStringSplit st(buf, ":");
if (st.size() != 2)return false;
user = st[0];
password = st[1];
return true;
}
头标格式是:“Authorization: Basic 用户名:密码”,其中用户名和密码部分用Base64编码,所以要先解码然后拆成两个字符串。
由于基本认证是明文传输用户名和密码(强调:Base64是明文编码,不是加密),所以浏览器可能会提出安全警告,如果连接是HTTPS的,那就没问题了。使用HTTPS只需要把socket发送接收替换成SSL的对应接口就可以了。
四、静态文件和文件下载
4.1 主要代码
框架里的所有asp文件其实都是嵌入的特定C++功能,并非真实的文件,算是我故意搞鬼吧。
最后的else里执行doPageFile()才是处理静态文件。代码如下:
bool doPageFile(char const * file=NULL)
{
fstream f;
long bufsize=1024*1024;
char * buf;
long count,len;
buf=new char[bufsize];
if(NULL==buf)
{
LOG<<"内存不足"<<ENDE;
return true;
}
m_respond.AddHeaderExpires(time(NULL),60);//60秒过期
string filename;
if(NULL==file || strlen(file)==0)
{
filename=m_request.GetResource();
LOG<<m_pServerDatas->m_root.c_str()<<ENDI;
if('/'==filename[filename.size()-1])
{//对于目录要打开默认页
filename+="default.htm";
}
if(0==m_pServerDatas->m_root.size())return false;
if('/'==filename[0])filename.erase(0,1);
filename=m_pServerDatas->m_root.c_str()+filename;
//检查是否是目录
#ifndef _MS_VC
struct stat statbuf;
if(0==stat(filename.c_str(),&statbuf))
{
if(S_ISDIR(statbuf.st_mode))
{
OnPageStart("404 NOT FOUND");
m_respond.AppendBody("请求的资源是目录,请在资源最后增加\"/\"");
OnPageEnd();
return true;
}
}
#endif
}
else
{
filename=file;
string filetitle;
filetitle=filename.substr(filename.npos==filename.find_last_of("/")?0:filename.find_last_of("/")+1);
m_respond.AddHeader("Content-disposition","attachment; filename="+filetitle);
}
LOG<<filename.c_str()<<ENDI;
m_respond.AddHeaderContentTypeByFilename(filename.c_str());
f.open(filename.c_str(),ios::in|ios::binary);
if(!f.good())
{
OnPageStart("404 NOT FOUND");
m_respond.AppendBody("<P>404 NOT FOUND<P>请求的资源不存在<P>");
OnPageEnd();
return false;
}
f.seekg(0,ios::end);
len=f.tellg();
m_respond.AddHeaderContentLength(len);
f.seekg(0,ios::beg);
if(!m_respond.Flush(m_s))return false;
while(f.good())
{
if(len-f.tellg()>bufsize-1)count=bufsize-1;
else count=len-f.tellg();
if(0==count)break;
f.read(buf,count);
if(!m_s.Send(buf,count))
{
m_s.Close();
break;
}
}
f.close();
delete[] buf;
return true;
}
读取文件没什么好说的,标准编程。
4.2 规则
- 如果资源以“/”结束,附加“default.htm”,也就是打开默认页,禁止目录浏览(当然你也可以允许目录浏览)
- 如果是目录但不是以“/”结束,返回404并提醒需要加“/”(注意这里不是返回404应答,是200正常应答,很多网站都这么搞的,不然浏览器显示的是浏览器自己的404错误页,无法给用户显示有价值的信息)
- 如果入口参数传递了文件名,作为附件发送,方法是添加一个特定的头标“Content-disposition”,内容为“attachment; filename=文件名”,不添加就是普通内容,浏览器会直接显示。文件下载一般是网站的网页里面通过传递参数给特定入口点来实现的,比如我用/DownFile.asp作为下载入口,文件名是参数,内部调用这个函数来实现具体功能
- 如果文件读取成功,文件内容就是body,添加内容类型头标和内容长度头标
4.3 添加内容类型头标的代码(应答类)
void AddHeaderContentTypeByFilename(char const * filename)//添加内容类型到应答头,如果已经存在则替换,根据文件名后缀判断
{
for (size_t i = 0; i < m_headers.size(); ++i)
{
if (m_headers[i].first != "Content-Type")continue;
m_headers[i].second = CMIMEType::GetMIMEType(filename);
return;
}
m_headers.push_back(pair<string, string >("Content-Type", CMIMEType::GetMIMEType(filename)));
}
4.4 处理类型的代码
char const * GetMIMEType(char const * filename)
{
STATIC_C char const mimetype[][2][64]=
{
{"htm","Text/html; charset=UTF-8"},
{"html","Text/html; charset=UTF-8"},
{"txt","Text/plain; charset=UTF-8"},
{"log","Text/plain; charset=UTF-8"},
{"xml","Text/xml; charset=UTF-8"},
{"sh","Text/plain; charset=UTF-8"},
{"css","Text/css; charset=UTF-8"},
{"url","application/x-www-form-urlencoded"},
{"",""}
};
STATIC_C char const * defaulttype="application/octet-stream";//最后方案,找不到就用这个
long pos=strlen(filename)-1;
while(pos>=0 && filename[pos]!='.' && filename[pos]!='/')--pos;
if(pos<0 || filename[pos]=='/')return defaulttype;
char const * ext=filename+pos+1;
char const (* p)[2][64]=mimetype;
while(strlen((*p)[0])!=0)
{
if(strcmp((*p)[0],ext)==0)return (*p)[1];
++p;
}
return defaulttype;
}
五、框架的完整代码
//处理一个已经建立的连接
virtual bool SocketProcess(CSocket & s, bool * pShutDown, long * pRet, long i_child, SocketServerControlBlock::T_CHILD_DATA * pThisProcessData)
{
m_s=s;
m_i_child = i_child;
m_pThisChildData = pThisProcessData;
//支持HTTP1.1,一个连接处理多个请求
while(m_s.IsConnected() && !*pShutDown)
{
bool isReady = false;
pThisProcessData->SetHttpProcessInfo("wait...");
if (!m_s.IsSocketReadReady(1, isReady))
{
LOG<<"socket error"<<ENDE;
m_s.Close();
return true;
}
if (!isReady)
{
//DEBUG_LOG << "socekt not ready On HTTP Process" << ENDI;
continue;
}
bool isKeepAlive=false;
m_request.Clear();
m_respond.Init();
pThisProcessData->SetHttpProcessInfo("RecvRequest...");
if(!m_request.RecvRequest(m_s))
{
if(m_s.IsConnected())
{
pThisProcessData->SetHttpProcessInfo("错误的请求");
LOG << getpid() << "错误的请求:" << m_request.GetFullRequest() << ENDE;
doPageBedRequest();
m_s.Close();
}
else
{
pThisProcessData->SetHttpProcessInfo("连接已关闭");
LOG<<getpid()<<"连接已关闭"<<ENDE;
}
break;
}
++pThisProcessData->request_count;
LOG<<getpid()<<"接收到请求,接连信息:\n"<<m_s.debuginfo()<<ENDI;
LOG<<getpid()<<"接收到请求,请求信息:\n"<<m_request.GetFullRequest()<<ENDI;
pThisProcessData->SetHttpProcessInfo(m_request.GetResource().c_str());
//检查是否需要保持连接
if(m_request.GetHeader("Connection")=="Keep-Alive")
{
LOG<<getpid()<<"保持连接"<<ENDI;
isKeepAlive=true;
}
else
{
LOG<<"不保持连接"<<ENDI;
isKeepAlive=false;
}
if("/"==m_request.GetResource())
{
m_respond.Send302(m_s,"/login/login.htm");
if(isKeepAlive)continue;
else
{
m_s.Close();
break;
}
}
//处理用户认证,登录用的目录不需要认证,登录目录可以用XMLHttp来实现整合登录
//login目录下的内容无需认证,这要求login下引用的图片也必须在login目录下
if(NULL!=this->pfCheckUser)
{
char const * logondir="/login/";
if(0!=strncmp(logondir,m_request.GetResource().c_str(),strlen(logondir)))
{
string user;
string password;
if(m_request.GetAuthorization(user,password))
{
if(!pfCheckUser(user.c_str(),password.c_str()))
{
m_respond.Send401(m_s,m_pServerDatas->m_realm.c_str());
if(isKeepAlive)continue;
else
{
m_s.Close();
break;
}
}
string cookie="logon_user";
if(m_request.GetCookie(cookie)!=user)m_respond.AddCookie(cookie,user);
}
else
{
m_respond.Send401(m_s,m_pServerDatas->m_realm.c_str());
if(isKeepAlive)continue;
else
{
m_s.Close();
break;
}
}
}
}
if("/default.asp"==m_request.GetResource() || "/default.htm"==m_request.GetResource())
{
OnPageStart("default");
doPageDefault();
OnPageEnd(false);
}
else if("/functionlist.asp"==m_request.GetResource())
{
OnPageStart("Function List");
doPageFunctionList();
OnPageEnd(false);
}
else if("/admin.asp"==m_request.GetResource())
{
doPageAdmin(pShutDown);
}
else if("/RegistSlave.asp"==m_request.GetResource())
{
doPageRegistSlive(pShutDown);
}
else if("/shell.asp"==m_request.GetResource())
{
OnPageStart("shell");
doPageShell();
OnPageEnd();
m_s.Close();//所有此类页面都可能无法预先确定输出长度
isKeepAlive=false;
}
else if("/ViewFile.asp"==m_request.GetResource())
{
char buf[2048];
sprintf(buf,"查看文件 %s ",m_request.GetParam("file").c_str());
OnPageStart(buf);
doPageViewFile();
OnPageEnd();
m_s.Close();//所有此类页面都可能无法预先确定输出长度
isKeepAlive=false;
}
else if("/stopserver.asp"==m_request.GetResource())
{
OnPageStart("stop server",true);
if(NULL!=pfCheckAdmin && !pfCheckAdmin(m_request.GetParam("password").c_str()))
{
m_respond.AppendBody("口令错误");
OnPageEnd();
}
else
{
(*pShutDown) = true;
m_respond.AppendBody("收到停止信号,服务正在停止......");
m_respond.Flush(m_s);
OnPageEnd();
}
isKeepAlive=false;
m_s.Close();
}
else if("/DownFile.asp"==m_request.GetResource())
{
if(doPageFile(m_request.GetParam("file").c_str()))
{
m_respond.Flush(s);
}
else
{
m_respond.Flush(s);
m_s.Close();//所有此类页面都可能无法预先确定输出长度
isKeepAlive=false;
}
}
else if(m_request.GetResource().substr(0,5)=="/bin/" || m_request.GetResource().substr(0,7)=="/admin/")
{
//执行用户功能
string resourcetype = m_request.GetResourceType();
if (resourcetype == "asp" || resourcetype == "aspx" || resourcetype == "asmx")
{
doPageFunction();//内置页面
}
else
{
doPageCGI();//动态链接库实现的用户功能
}
}
else
{
doPageFile();
m_respond.Flush(s);
}
//客户指定不保持连接或应答不支持保持连接则关闭连接
if(!isKeepAlive || !m_respond.isCanKeepAlive())
{
m_s.Close();
break;
}
}
return true;
}
中间用不上的部分都可以删掉。
六、处理特定功能
待续
(这里是结束,但不是整个系列的结束)