diff --git a/src/tinyweb/tinyweb.c b/src/tinyweb/tinyweb.c new file mode 100644 index 0000000..16ccd47 --- /dev/null +++ b/src/tinyweb/tinyweb.c @@ -0,0 +1,968 @@ +#include "tinyweb.h" +#include "tools.h" + +#ifdef __GNUC__ +#include +#define _strncmpi strncasecmp +#define strcmpi strcasecmp +#endif // __GNUC__ + +#include +#include +#include +#include +#include + +//TinyWeb 增加与完善功能,by lzpong 2016/11/24 + +//值大,发送文件时磁盘和CPU性能更好,占用内存增加 +#define TW_SEND_SIZE 1024*1024*16 + +typedef struct tw_file_t { + //uchar flag; //连接的标志 + FILE* fp; //文件指针 + uchar* buff;//文件发送缓存 + unsigned long long fsize;//文件大小 + unsigned long long lsize;//文件中要发送的块剩余大小 +}tw_file_t; + +typedef struct tw_client { + tw_peerAddr pa;//客户端连接的地址 + tw_file_t ft;//发往客户端的文件,断点续传记录 + WebSocketHandle hd; + tw_reqHeads heads;//Http 头部(如果是http) + membuf_t buf;//http post 分包接收的缓存 +}tw_client; +//================================================= + + +//关闭客户端连接后,释放客户端连接的数据 +static void after_uv_close_client(uv_handle_t* client) { + tw_config* tw_conf = (tw_config*)(client->loop->data); + tw_client* clidata = (tw_client*)client->data; + //如果有发送文件 + if (clidata->ft.fp) + fclose(clidata->ft.fp); + if (clidata->ft.buff) + free(clidata->ft.buff); + //如果是WebSocket + //if (clidata->pa.flag & 0x2) + membuf_uninit(&clidata->hd.buf); + //http post 分包接收的缓存 + membuf_uninit(&clidata->buf); + //关闭连接回调 + if (tw_conf->on_close) + tw_conf->on_close(tw_conf->data, client, &clidata->pa); + free(client->data); + free(client); +} + +//关闭客户端连接 +void tw_close_client(uv_stream_t* client) { + tw_client* clidata = (tw_client*)client->data; + uv_close((uv_handle_t*)client, after_uv_close_client); +} + +//发送数据后,free数据,关闭客户端连接 +static void after_uv_write(uv_write_t* w, int err) { + tw_client* clidata = (tw_client*)w->handle->data; + if (w->data) + free(w->data); //sended data need free + //长连接就不关闭了 + if (!err && clidata->ft.fp) + tw_http_send_file(w->handle, NULL, NULL, NULL, NULL); + else if (!(clidata->pa.flag & 0x1)){ + if (w->handle->flags & 0x01) + printf("after_uv_write sk:%zd error: handle Has been closed\n", clidata->pa.sk); + else + uv_close((uv_handle_t*)w->handle, after_uv_close_client); + } + free(w); +} + +//发送数据到客户端; 如果是短连接,则发送完后会关闭连接 +//data:待发送数据 +//len: 数据长度, -1 将自动计算数据长度 +//need_copy_data:是否需要复制数据 +//need_free_data:是否需要free数据, 如果need_copy_data非零则忽略此参数 +void tw_send_data(uv_stream_t* client, const void* data, size_t len, char need_copy_data, char need_free_data) { + uv_buf_t buf; + uv_write_t* w; + void* newdata = (void*)data; + + if (len == (size_t)-1) + len = strlen((char*)data); + + if (need_copy_data) { + newdata = malloc(len); + memcpy(newdata, data, len); + } + + buf = uv_buf_init((char*)newdata, len); + w = (uv_write_t*)malloc(sizeof(uv_write_t)); + w->data = (need_copy_data || need_free_data) ? newdata : NULL; + uv_write(w, client, &buf, 1, after_uv_write); //free w and w->data in after_uv_write() +} + +//制造头部 SetCookie 字段和值 +//cookie: 缓存区(至少 42+strlen(key)+strlen(val)+strlen(domain)+strlen(path) ) +//ckLen: cookie的长度 +//expires: 多少秒后过期 +//domain: Domain, 域名或IP地址,或NULL +//path: Path, 可以是 heads->path,或NULL +void tw_make_setcookie(char* set_cookie,int ckLen,const char* key,const char* val,int expires,char* domain,char* path) { + int rlen,len=0; + rlen=snprintf(set_cookie, ckLen, "Set-Cookie: %s=%s", key, val); + if (rlen > 0) len = rlen,rlen=0; + if (expires > 0) + rlen = snprintf(set_cookie + len, ckLen - len, "; Max-Age=%d", expires); + if (rlen > 0) len += rlen,rlen=0; + if (domain) + rlen = snprintf(set_cookie + len, ckLen - len, "; Domain=%s", domain); + if (rlen > 0) len += rlen, rlen = 0; + if (path) + rlen = snprintf(set_cookie + len, ckLen - len, "; Path=%s", path); + if (rlen > 0) len += rlen, rlen = 0; + set_cookie[len]='\r'; + set_cookie[len+1]='\n'; + set_cookie[len+2]=0; +} +//制造头部 delete cookie +void tw_make_delcookie(char* del_cookie, int ckLen, char* key) +{ + snprintf(del_cookie, ckLen, "Delete-Cookie: %s\r\n", key); +} + +//发送'200 OK' 响应; 不会释放(free)传入的数据(u8data) +//content_type:Content Type 文档类型 +//u8data:utf-8编码的数据 +//content_length:数据长度,为0或-1时自动计算(strlen)(c_str, end with NULL) +//respone_size:获取响应最终发送的数据长度,为0表示放不需要取此长度 +void tw_send_200_OK(uv_stream_t* client, const char* ext_heads, const char* content_type, const void* u8data, size_t content_length, size_t* respone_size) { + size_t repSize; + const char *type = strchr(content_type, '/'); + //有'.' 没有'/' 至少有两个'/' '/'是在开头 '/'是在末尾 + //都要重新取文件类型 + if (type) { + if (strchr(content_type, '.'))// 有'.' + type = tw_get_content_type(content_type); + else { + type = strchr(type + 1, '/'); + if (type)//至少有两个'/' + type = tw_get_content_type(content_type); + else { + type = strchr(content_type, '/'); + if (type == content_type || type == (content_type + strlen(content_type) - 1)) + type = tw_get_content_type(content_type); + else + type = content_type; + } + } + }//没有'/' + else + type = tw_get_content_type(content_type); + char *data = tw_format_http_respone(client, "200 OK", ext_heads, type, u8data, content_length, &repSize); + tw_send_data(client, data, repSize, 0, 1);//发送后free data + if (respone_size) + *respone_size = repSize; +} + +//返回格式华的HTTP响应内容(需要free返回数据) +//status: "200 OK" +//content_type: 文件类型,如:"text/html" ;可以调用tw_get_content_type()得到 +//content: any utf-8 data, need html-encode if content_type is "text/html" +//content_length: 0或-1自动计算 content 长度(c_str, end with NULL) +//respone_size: if not NULL,可以获取发送的数据长度 the size of respone will be writen to request +//returns malloc()ed c_str, need free() by caller +char* tw_format_http_respone(uv_stream_t* client, const char* status, const char* ext_heads, const char* content_type, const char* content, size_t content_length, size_t* respone_size) { + size_t totalsize, header_size; + char* respone; + char szDate[30]; + getGmtTime(szDate,30,0); + ext_heads == NULL ? ext_heads = "" : 0; + tw_config* tw_conf = (tw_config*)(client->loop->data); + if (content_length == 0 || content_length == (size_t)-1) + content_length = content ? strlen(content) : 0; + totalsize = strlen(status) + strlen(ext_heads) + strlen(content_type) + content_length + 158; + respone = (char*)malloc(totalsize + 1); + header_size = snprintf(respone, totalsize, "HTTP/1.1 %s\r\nDate: %s\r\nServer: TinyWeb\r\nConnection: close\r\nContent-Type:%s; charset=%s\r\nContent-Length:%zd\r\n%s\r\n" + , status, szDate, content_type, tw_conf->charset, content_length, ext_heads); + assert(header_size > 0); + if (content_length) + memcpy(respone + header_size, content, content_length+1); + + if (respone_size) + *respone_size = header_size + content_length; + return respone; +} + +//发送404响应 +static void tw_404_not_found(uv_stream_t* client, const char* pathinfo, const char* ext_heads) { + char* respone; + char buffer[128]; + snprintf(buffer, sizeof(buffer), "

404 Not Found

%s

", pathinfo); + respone = tw_format_http_respone(client, "404 Not Found", ext_heads, "text/html", buffer, -1, NULL); + tw_send_data(client, respone, -1, 0, 1); +} + +//发送301响应,路径永久重定位 +void tw_301_Moved(uv_stream_t* client, tw_reqHeads* heads, const char* ext_heads) { + size_t len = 76 + strlen(heads->path); + char buffer[1245]; + char szDate[30]; + ext_heads == NULL ? ext_heads = "" : 0; + getGmtTime(szDate,30,0); + tw_config* tw_conf = (tw_config*)(client->loop->data); + snprintf(buffer, sizeof(buffer), "HTTP/1.1 301 Moved Permanently\r\nDate: %s\r\n" + "Server: TinyWeb\r\nLocation: http://%s%s%s%s\r\nConnection: close\r\n" + "Content-Type:text/html;charset=%s\r\nContent-Length:%zd\r\n%s\r\n" + "

Moved Permanently

The document has moved here.

" + , szDate + , heads->host, heads->path, (heads->query[0]?"?":""), (heads->query[0]?heads->query:"") + , tw_conf->charset, len, ext_heads + , heads->path, (heads->query[0]?"?":""), (heads->query[0]?heads->query:"")); + tw_send_data(client, buffer, -1, 1, 1); +} +//发送302响应,路径临时重定位 +void tw_302_Moved(uv_stream_t* client, tw_reqHeads* heads, const char* ext_heads) { + char buffer[1245]; + char szDate[30]; + ext_heads == NULL ? ext_heads = "" : 0; + getGmtTime(szDate,30,0); + tw_config* tw_conf = (tw_config*)(client->loop->data); + snprintf(buffer, sizeof(buffer), "HTTP/1.1 302 Moved Temporarily\r\nDate: %s\r\n" + "Server: TinyWeb\r\nLocation: http://%s%s%s%s\r\nConnection: close\r\n" + "Content-Type:text/html;charset=%s\r\nContent-Length:0\r\n%s\r\n" + , szDate + , heads->host, heads->path, (heads->query[0]?"?":""), (heads->query[0]?heads->query:"") + , tw_conf->charset, ext_heads); + tw_send_data(client, buffer, -1, 1, 1); +} + +//http协议发送文件,异步 +//file_path: 文件路径 +void tw_http_send_file(uv_stream_t* client, tw_reqHeads* heads, const char* ext_heads, const char* content_type, const char* file_path) { + char *respone; + char szDate[30]; + tw_config* tw_conf = (tw_config*)(client->loop->data); + tw_client* clidata = (tw_client*)client->data; + tw_file_t* filet = &clidata->ft; + + //发送头部 + if (!filet->fp && file_path) { + filet->fp = fopen(file_path, "rb"); + if (filet->fp) { +#ifdef _WIN64 + _fseeki64(filet->fp, 0, SEEK_END); + filet->fsize = _ftelli64(filet->fp); +#else + fseek(filet->fp, 0, SEEK_END); + filet->fsize = ftell(filet->fp); +#endif + if (heads->Range_frm < 0)//(负数:从文件末尾反过来的位置,即fileSize-sizeFrom) + heads->Range_frm = filet->fsize + heads->Range_frm; + if (heads->Range_to <= 0)//(负数:从文件末尾反过来的位置,即fileSize-sizeTo) + heads->Range_to = filet->fsize + heads->Range_to; + if (filet->fsize < (unsigned long long)heads->Range_frm)//开始位置大于文件 + heads->Range_frm = filet->fsize; + if (heads->Range_to < heads->Range_frm || (unsigned long long)heads->Range_to>filet->fsize)//Range_to 可能没有,或不正确,表示整个文件大小 + heads->Range_to = filet->fsize; + //要下载区段的size + filet->lsize = heads->Range_to - heads->Range_frm; +#ifdef _WIN64 + _fseeki64(filet->fp, heads->Range_frm, SEEK_SET); +#else + fseek(filet->fp, heads->Range_frm, SEEK_SET); +#endif + ext_heads == NULL ? ext_heads = "" : 0; + getGmtTime(szDate,30,0); + respone = (char*)malloc(300 + 1); + int respone_size; + if (heads->Range_frm == 0) //200 OK + respone_size = snprintf(respone, 300, "HTTP/1.1 200 OK\r\nDate: %s\r\nServer: TinyWeb\r\nConnection: close\r\nContent-Type:%s;charset=%s\r\nAccept-Range: bytes\r\nContent-Length:%llu\r\n%s\r\n" + , szDate, content_type, tw_conf->charset, filet->fsize,ext_heads); + else //206 Partial Content + respone_size = snprintf(respone, 300, "HTTP/1.1 206 Partial Content\r\nDate: %s\r\nServer: TinyWeb\r\nConnection: close\r\nContent-Type:%s;charset=%s\r\nAccept-Range: bytes\r\nContent-Range: %lld-%lld/%llu\r\nContent-Length:%llu\r\n%s\r\n" + , szDate, content_type, tw_conf->charset, heads->Range_frm, heads->Range_to, filet->fsize, filet->lsize,ext_heads); + tw_send_data(client, respone, respone_size, 0, 1); + } + else + tw_404_not_found(client, heads->path, ext_heads); + } + else { //发送文件 + size_t read_size=0;// read_bytes; + if (filet->fp) { + if (filet->lsize > 0) { + if(!filet->buff) + filet->buff = (char*)malloc(TW_SEND_SIZE + 1); + //fread 返回实际读取的单元个数。如果小于count,则可能文件结束或读取出错; + //可以用ferror()检测是否读取出错,用feof()函数检测是否到达文件结尾。如果size或count为0,则返回0。 + if(filet->lsize>TW_SEND_SIZE) + read_size = fread(filet->buff, sizeof(char), TW_SEND_SIZE, filet->fp); + else + read_size = fread(filet->buff, sizeof(char), filet->lsize, filet->fp); + filet->lsize -= read_size; + tw_send_data(client, filet->buff, read_size, 0, 0); + } + else + { + fclose(filet->fp); + filet->fp = 0; + filet->fsize = 0; + } + } + } +} + +//根据扩展名(不区分大小写),返回文件类型 content_type +const char* tw_get_content_type(const char* fileExt) { + const static char* octet = "application/octet-stream"; + if (fileExt) + { + //不管什么路径名或者文件名, 只要最后面有点(.),就认为是有扩展名的 + const char *p = strrchr(fileExt, '.'); + if (p) { // /aaa.txt + fileExt = p + 1; + } + } + else //否则没有扩展名 + return octet; + if (strcmpi(fileExt, "htm") == 0 || strcmpi(fileExt, "html") == 0) + return "text/html"; + else if (strcmpi(fileExt, "js") == 0) + return "application/javascript"; + else if (strcmpi(fileExt, "css") == 0) + return "text/css"; + else if (strcmpi(fileExt, "json") == 0) + return "application/json"; + else if (strcmpi(fileExt, "log") == 0 || strcmpi(fileExt, "txt") == 0 || strcmpi(fileExt, "ini") == 0 + || strcmpi(fileExt, "config") == 0 || strcmpi(fileExt, "conf") == 0 || strcmpi(fileExt, "cfg") == 0 + || strcmpi(fileExt, "sh") == 0 || strcmpi(fileExt, "bat") == 0) + return "text/plain"; + else if (strcmpi(fileExt, "jpg") == 0 || strcmpi(fileExt, "jpeg") == 0) + return "image/jpeg"; + else if (strcmpi(fileExt, "png") == 0) + return "image/png"; + else if (strcmpi(fileExt, "gif") == 0) + return "image/gif"; + else if (strcmpi(fileExt, "ico") == 0) + return "image/x-icon"; + else if (strcmpi(fileExt, "xml") == 0) + return "application/xml"; + else if (strcmpi(fileExt, "xhtml") == 0) + return "application/xhtml+xml"; + else if (strcmpi(fileExt, "swf") == 0) + return "application/x-shockwave-flash"; + else if (strcmpi(fileExt, "svg") == 0) + return "image/svg-xml"; + else if (strcmpi(fileExt, "wav") == 0) + return "audio/wav"; + else if (strcmpi(fileExt, "mid") == 0 || strcmpi(fileExt, "midi") == 0) + return "audio/midi"; + else if (strcmpi(fileExt, "wma") == 0) + return "audio/x-ms-wma"; + else if (strcmpi(fileExt, "mp3") == 0) + return "audio/mp3"; + else if (strcmpi(fileExt, "3gp") == 0) + return "video/3gpp"; + else if (strcmpi(fileExt, "avi") == 0) + return "video/x-msvideo"; + else if (strcmpi(fileExt, "mkv") == 0) + return "video/x-matroska"; + else if (strcmpi(fileExt, "mp4") == 0) + return "video/mp4"; + else if (strcmpi(fileExt, "rmvb") == 0) + return "video/vnd.rn-realvideo"; + else if (strcmpi(fileExt, "flv") == 0) + return "flv-application/octet-stream";// "video/x-flv"; + else if (strcmpi(fileExt, "apk") == 0) + return "application/vnd.android.package-archive"; + else + return octet; +} + +//处理客户端请求 +//invoked by tinyweb when GET request comes in +//please invoke write_uv_data() once and only once on every request, to send respone to client and close the connection. +//if not handle this request (by invoking write_uv_data()), you can close connection using tw_close_client(client). +//pathinfo: "/" or "/book/view/1" +//query_stirng: the string after '?' in url, such as "id=0&value=123", maybe NULL or "" +static void tw_request(uv_stream_t* client, tw_reqHeads* heads) { + tw_config* tw_conf = (tw_config*)(client->loop->data); + char fullpath[260];//绝对路径(末尾不带斜杠) + snprintf(fullpath, 259, "%s/%s\0", tw_conf->doc_dir, (heads->path[0] == '/' ? heads->path + 1 : heads->path)); + //去掉末尾的斜杠 + char *p = &fullpath[strlen(fullpath) - 1]; + while (*p == '/' || *p == '\\') + *p = 0, p--; + + char file_dir = isExist(fullpath); + //判断 文件或文件夹,或不存在 + switch (file_dir) + { + case 1://存在:文件 + { + char* postfix = strrchr(heads->path, '.');//从后面开始找文件扩展名 + if (postfix) + { + postfix++; + p = postfix + strlen(postfix) - 1; + while (*p == '/' || *p == '\\') + *p = 0, p--; + } + tw_http_send_file(client, heads, NULL, tw_get_content_type(postfix), fullpath); + } + break; + case 2://存在:文件夹 + { + if (heads->path[strlen(heads->path)-1] != '/') //文件夹要检测末尾'/' + { + int len = strlen(heads->path); + if (len >= sizeof(heads->path)-1) + len=sizeof(heads->path)-2; + heads->path[len] = '/'; + heads->path[len+1] = 0; + tw_301_Moved(client, heads, NULL); + break; + } + char tmp[260]; tmp[0] = 0; + char *s = strdup(tw_conf->doc_index); + p = strtok(s, ";"); + //是否有默认主页 + while (p) + { + snprintf(tmp, 259, "%s/%s", fullpath, p); + if (isFile(tmp)) { + tw_http_send_file(client, heads, NULL, "text/html", tmp); + break; + } + tmp[0] = 0; + p = strtok(NULL, ";"); + } + free(s); + //没用默认主页 + if (!tmp[0]) + { + char* p2=NULL; + uint len; + membuf_t buf; + membuf_init(&buf, 1024 * 2); + char *body = "Welcome to TinyWeb.
Directory access forbidden."; + if (tw_conf->dirlist) { + membuf_append(&buf, "Index of ");//+path +#ifdef _MSC_VER + if (strnicmp(tw_conf->charset, "utf",3) == 0) {//utf-8 + len = strlen(heads->path); + p2 = GB2U8(heads->path, &len); + membuf_append(&buf, p2); + } else +#endif // _MSC_VER + membuf_append(&buf, heads->path); + membuf_append_format(&buf, "\r\n", tw_conf->charset); + membuf_append(&buf, "

Index of ");//+path + if (p2) { + membuf_append(&buf, p2); + free(p2); + } else { + membuf_append(&buf, heads->path); + } + membuf_append(&buf, "

\r\n" + "\r\n" + "" + "\r\n" + "" + "" + "
@NameSizeLast modified


" + "
TinyWeb Server
" + "\r\n"); + } + else + membuf_append_data(&buf, body, strlen(body)); + char *respone = tw_format_http_respone(client, "200 OK", NULL, "text/html", (char*)buf.data, buf.size, NULL); + tw_send_data(client, respone, -1, 0, 1); + membuf_uninit(&buf); + } + } + break; + default://不存在 + tw_404_not_found(client, heads->path, NULL); + break; + } +} + +//获取http头信息,返回指向 Sec-WebSocket-Key 的指针 +static char* tw_get_http_heads(const uv_buf_t* buf, int len, tw_reqHeads* heads) { + char *key=NULL, *start, *head, *p; + char delims[] = "\r\n"; + char* data = strstr(buf->base, "\r\n\r\n"); + if (data) { + *data = 0; + heads->data = data += 4; + heads->len = len - (data-buf->base); + //是http get/post协议 + if (buf->base[0] == 'G' && buf->base[1] == 'E' && buf->base[2] == 'T' && buf->base[3] == ' ') { + heads->method = 1;//GET + } + else if (buf->base[0] == 'P' && buf->base[1] == 'O' && buf->base[2] == 'S' && buf->base[3] == 'T' && buf->base[4] == ' ') { + heads->method = 2;//POST + } + //是http get/post协议 + if (heads->method) + { + char *path = "", *query = ""; + head = strtok(buf->base, delims); + //search path + path = strchr(head + 3, ' ') + 1; + while (isspace(*path)) path++; + start = strchr(path, ' '); + if (start) *start = 0; + //url含有转义编码字符 + if (strstr(path, "%") != 0) + { + url_decode(heads->path); +#ifdef _MSC_VER //Windows下需要转换编码,因为windows系统的编码是GB2312 + size_t len = strlen(path); + char *gb = U82GB(path, &(unsigned int)len); + strncpy(path, gb, len); + path[len] = 0; + free(gb); + //linux 下,系统和源代码文件编码都是是utf8的,就不需要转换 +#endif // _MSC_VER + } + //query param + p = strchr(path, '?'); + if (p) { + query = p + 1; + *p = 0; + } + //确保开头为'/' + if (*path != '/') { + path--; + *path = '/'; + *(path - 1) = 0; + } + //确保结尾不是"/.." + p = strrchr(path, '.'); + if (p && *(p + 1) == 0 && *(p - 1) == '.' && *(p - 2) == '/') { + *(p + 1) = '/'; + *(p + 2) = 0; + } + ////------------尽可能的合并 "../" "/./" + if (strstr(path, "./") != 0) + { + //去掉"/./" + while ((p = strstr(path, "/./"))) + memmove(p, p + 2, strlen(p + 2) + 1); + //尽可能的合并"../" + while ((p = strstr(path, "/.."))) {//存在 .. + if ((p - path) <= 1) { + if ((start = strchr(path + 2, '/'))) + path = start; + else + *p = 0; + continue; + } + *(p - 1) = 0; + start = strrchr(path, '/'); + if (start == NULL) + start = path; + key = strchr(p + 2, '/'); + if (key) + p = key; + else + break; + memmove(start, p, strlen(p) + 1); + } + } + snprintf(heads->path,512, "%s", path); + snprintf(heads->query,1500, "%s", query); + + key = NULL; + //从第二行开始循环处理 头部 + head = strtok(NULL, delims); + while (head) + { + //是否有 Sec-WebSocket-Key + //http upgrade to WebSocket + if (start = strstr(head, "Sec-WebSocket-Key: ")) + { + key = start + 19; + } + //search host + else if (start = strstr(head, "Host: ")) + { + snprintf(heads->host,260,"%s", start + 6); + } + //Range: bytes=sizeFrom-[sizeTo] (sizeTo 可能没有,或不正确,表示整个文件大小) + // (sizeFrom 为负数,表示从文件末尾反过来的位置,即fileSize-sizeFrom) + //Range: bytes=sizeFrom-[sizeTo],sizeFrom-[sizeTo][,sizeFrom-[sizeTo]] 这种多段不支持,只支持一段 + else if (start = strstr(head, "Range: ")) + { + start += 7; + start = strstr(start, "bytes="); + if (start) + start += 6; + p= strstr(start + 1, "-");//防止 sizeFrom 为负数 + heads->Range_to = 0; + if (p)//可能有 sizeTo + { + heads->Range_frm = strtoll(start, &p, 10); + p++;//跳过 '-' + if(*p) + heads->Range_to = strtoll(p, NULL, 10); + } + else //没有 sizeTo + heads->Range_frm = strtol(start, NULL, 10); + } + //Content-Length: 3543 + else if (start = strstr(head, "Content-Length: ")) + { + heads->contentLen = atoi(start + 16); + } + //Cookie: xxxxx + else if (start = strstr(head, "Cookie: ")) + { + snprintf(heads->cookie,260,"%s", start + 8); + } + //下一行 头部 + head = strtok(NULL, delims); + } + if (heads->contentLen < 1) + heads->data = NULL,heads->len=0; + } + } + return key; +} + +//on_read_WebSocket +static void on_read_websocket(uv_stream_t* client, char* data, size_t Len) { + tw_config* tw_conf = (tw_config*)(client->loop->data); + tw_client* clidata = (tw_client*)client->data; + WebSocketHandle* hd = &clidata->hd; + if (NULL == hd->buf.data) + membuf_init(&hd->buf, 128); + + WebSocketGetData(hd, data, Len); + clidata->pa.flag = bitRemove(clidata->pa.flag, 0x4); + + if (hd->isEof) + { + switch (hd->type) { + case 0: //0x0表示附加数据帧 + break; + case 1: //0x1表示文本数据帧 + clidata->pa.flag |= 0x4; + case 2: //0x2表示二进制数据帧 + //接收数据回调 + if (tw_conf->on_data) + tw_conf->on_data(tw_conf->data, client, &clidata->pa, &hd->buf); + break; + case 3: case 4: case 5: case 6: case 7: //0x3 - 7暂时无定义,为以后的非控制帧保留 + //membuf_uninit(hd->buf); + //memset(hd, 0, sizeof(WebSocketHandle)); + break; + case 8: //0x8表示连接关闭 + *(data + 1) = 0;//无数据 + tw_send_data(client, data, 2, 1, 0); + if (hd->buf.size > 2) { //错误信息 + if (tw_conf->on_error) { + char errstr[60] = { 0 }; + snprintf(errstr, 59, "-0:wserr,%s", hd->buf.data + 2); + //出错信息回调 + tw_conf->on_error(tw_conf->data, client, &clidata->pa, 0, errstr); + } + else + fprintf(stderr, "-0:wserr,%s\n", hd->buf.data + 2); + } + break; + case 9: //0x9表示ping + case 10://0xA表示pong + default://0xB - F暂时无定义,为以后的控制帧保留 + *data += 1;//发送pong + *(data + 1) = 0;//无数据 + tw_send_data(client, data, 2, 1, 0); + //memset(hd, 0, sizeof(WebSocketHandle)); + break; + } + //释放 membuf , 收到消息时再分配 + clidata->hd.dfExt = clidata->hd.isEof = clidata->hd.type = 0; + membuf_uninit(&hd->buf); + } +} + +//(循环)读取客户端发送的数据,接收客户的数据 +static void on_uv_read(uv_stream_t* client, ssize_t nread, const uv_buf_t* buf) { + tw_config* tw_conf = (tw_config*)(client->loop->data); + tw_client* clidata = (tw_client*)client->data; + membuf_t mbuf; + if (nread > 0) { + assert(clidata); + //WebSocket + if (clidata->pa.flag & 0x2) //WebSocket + on_read_websocket(client, buf->base, nread); + //long-link + else if (clidata->pa.flag & 0x1) { //SOCKET //long-link + //接收数据回调 + if (tw_conf->on_data) { + mbuf.data = buf->base; + mbuf.size = nread; + mbuf.buffer_size = buf->len; + tw_conf->on_data(tw_conf->data, client, &clidata->pa, &mbuf); + } + } + //http post继续接收 未收完的数据 + else if (clidata->heads.method==2) { + if (nread) { + membuf_append_data(&clidata->buf, buf->base, nread); + clidata->heads.len += nread; + } + if (clidata->heads.len >= clidata->heads.contentLen) { + //所有请求全部回调,返回非0表示已处理 + clidata->heads.data = clidata->buf.data; + if (tw_conf->on_request == 0 || 0 == tw_conf->on_request(tw_conf->data, client, &clidata->pa, &clidata->heads)) + tw_request(client, &clidata->heads); + } + } + //未知 + else { //http 或 未知 + char* p, *p2; + //tw_reqHeads heads; + //memset(&heads, 0, sizeof(tw_reqHeads)); + p = tw_get_http_heads(buf,nread, &clidata->heads);//get Sec-WebSocket-Key ? + if (p) { //WebSocket 握手 + clidata->pa.flag |= 3;//long-link & WebSocket + p2 = WebSocketHandShak(p); + tw_send_data(client, p2, -1, 1, 0); + free(p2); + } + else if (clidata->heads.method) { //HTTP + if (!clidata->heads.path && clidata->heads.path[0] != '/'){//路径没有 '/' 开头 + tw_301_Moved(client, &clidata->heads, NULL); + } + else {//http post 数据 + if (clidata->heads.len >= clidata->heads.contentLen) { + //所有请求全部回调,返回非0表示已处理 + if (tw_conf->on_request == 0 || 0 == tw_conf->on_request(tw_conf->data, client, &clidata->pa, &clidata->heads)) + tw_request(client, &clidata->heads); + } + else {//跟随头部的 post 数据未发送完 + membuf_init(&clidata->buf, 128); + if (clidata->heads.len) + membuf_append_data(&clidata->buf, clidata->heads.data, clidata->heads.len); + } + } + } + else { //SOCKET + clidata->pa.flag |= 1;//long-link + //接收数据回调 + if (tw_conf->on_data) { + mbuf.data = buf->base; + mbuf.size = nread; + mbuf.buffer_size = buf->len; + tw_conf->on_data(tw_conf->data, client, &clidata->pa, &mbuf); + } + } + } + } + else if (nread <= 0) {//在任何情况下出错, read 回调函数 nread 参数都<0,如:出错原因可能是 EOF(遇到文件尾) + if (nread != UV_EOF) { + if (tw_conf->on_error) { + char errstr[60] = { 0 }; + snprintf(errstr, 59, "%d:%s,%s", (int)nread, uv_err_name((int)nread), uv_strerror((int)nread)); + //出错信息回调 + tw_conf->on_error(tw_conf->data, client, &clidata->pa, nread, errstr); + } + else + fprintf(stderr, "%d:%s,%s\n", (int)nread, uv_err_name((int)nread), uv_strerror((int)nread)); + } + //关闭连接. 读取长度为0,或是错误值,都应该关闭连接 + tw_close_client(client); + } + //每次使用完要释放 + if (buf->base) + free(buf->base); +} + +//为每次读取数据分配内存缓存 +static void on_uv_alloc(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { + buf->base = (char*)calloc(1, suggested_size); + buf->len = suggested_size; +} + +//获取客户端的 socket,ip,port +static char tw_getPeerAddr(uv_stream_t* client, tw_peerAddr* pa) +{ + struct sockaddr_in peeraddr[2], hostaddr[2]; //Windows 下不声明成数组(且数组长度大于1),getpeername会失败(errno=10014) + int addrlen = sizeof(peeraddr); + //memset(pa, 0, sizeof(PeerAddr)); + //客户端的地址 + if (client->type == UV_TCP) { +#ifdef WIN32 + pa->sk = ((uv_tcp_t*)client)->socket; +#else + addrlen /= 2; +#if defined(__APPLE__) + if (client->select) + pa->sk = client->select; + else +#endif + pa->sk = client->io_watcher.fd; +#endif + int er = uv_tcp_getpeername((uv_tcp_t*)client, (struct sockaddr*)peeraddr, &addrlen); + if (er < 0) + memset(peeraddr, 0, addrlen); + er = uv_tcp_getsockname((uv_tcp_t*)client, (struct sockaddr*)hostaddr, &addrlen); + if (er < 0) + memset(hostaddr, 0, addrlen); + } + else if (client->type == UV_UDP) { +#ifdef WIN32 + pa->sk = ((uv_udp_t*)client)->socket; +#else + pa->sk = client->io_watcher.fd; +#endif + int er = uv_udp_getsockname((uv_udp_t*)client, (struct sockaddr*)peeraddr, &addrlen); + if (er < 0) + memset(peeraddr, 0, addrlen); + er = uv_udp_getsockname((uv_udp_t*)client, (struct sockaddr*)hostaddr, &addrlen); + if (er < 0) + memset(hostaddr, 0, addrlen); + } + else + return 1; + //网络字节序转换成主机字符序 + uv_ip4_name(peeraddr, pa->ip, sizeof(pa->ip)); + pa->port = ntohs(peeraddr[0].sin_port); + uv_ip4_name(hostaddr, pa->fip, sizeof(pa->ip)); + pa->fport = ntohs(hostaddr[0].sin_port); + + return 0; +} + +//客户端接入 +static void tw_on_connection(uv_stream_t* server, int status) { + //assert(server == (uv_stream_t*)&_server); + tw_client* cli; + if (status == 0) { + //建立客户端信息,在关闭连接时释放 see after_uv_close_client + uv_tcp_t* client = (uv_tcp_t*)calloc(1, sizeof(uv_tcp_t)); + //创建客户端的数据缓存块,在关闭连接时释放 see after_uv_close_client + cli = client->data = calloc(1, sizeof(tw_client)); + uv_tcp_init(server->loop, client);//将客户端放入loop + //接受客户,保存客户端信息 + uv_accept(server, (uv_stream_t*)client); + client->close_cb = after_uv_close_client; + //取客户端 socket,ip,port; + tw_getPeerAddr((uv_stream_t*)client, &cli->pa); + //开始读取客户端数据 + uv_read_start((uv_stream_t*)client, on_uv_alloc, on_uv_read); + //客户端接入回调 + tw_config* tw_conf = (tw_config*)(server->loop->data); + if (tw_conf->on_connect) + tw_conf->on_connect(tw_conf->data, (uv_stream_t*)client, &cli->pa); + } +} + +//================================================================================================== + +//TinyWeb 线程开始运行 +static void tw_run(uv_loop_t* loop) { + tw_config* tw_conf = (tw_config*)loop->data; + printf("TinyWeb v1.2.2 is started, listening on %s:%d\n", tw_conf->ip, tw_conf->port); + uv_run(loop, UV_RUN_DEFAULT); + uv_stop(loop); + if (!uv_loop_close(loop) && loop != uv_default_loop()) { + uv_loop_delete(loop); + } + printf("TinyWeb v1.2.2 is stopped, listening on %s:%d\n", tw_conf->ip, tw_conf->port); + free(tw_conf->doc_dir); + free(tw_conf->doc_index); + free(tw_conf->charset); + free(tw_conf); +} + +//start web server, start with the config +//loop: if is NULL , it will be uv_default_loop() +//conf: the server config +int tinyweb_start(uv_loop_t* loop, tw_config* conf) { + int ret; + assert(conf != NULL); + if (conf->ip == NULL || (conf->ip != NULL && conf->ip[0] == '*')) + conf->ip = "0.0.0.0"; + struct sockaddr_in addr; + uv_ip4_addr(conf->ip, conf->port, &addr); + + tw_config* tw_conf = calloc(1, sizeof(tw_config)); + memcpy(tw_conf, conf, sizeof(tw_config)); + + //设置主目录(末尾不带斜杠) + if (conf->doc_dir) + tw_conf->doc_dir = strdup(conf->doc_dir); + else + tw_conf->doc_dir = strdup("./"); + + printf("WebRoot port:%d Dir:%s\n",tw_conf->port, tw_conf->doc_dir); + //设置默认主页(分号间隔) + if (conf->doc_index && strcmpi(conf->doc_index, "") != 0) + tw_conf->doc_index = strdup(conf->doc_index); + else + tw_conf->doc_index = strdup("index.htm;index.html"); + //设置more编码 + if (conf->charset) + tw_conf->charset = strdup(conf->charset); + else + tw_conf->charset = strdup("utf-8"); + if (loop == NULL) + loop = uv_default_loop(); + ret = uv_tcp_init(loop, &tw_conf->_server); + if (ret < 0) + return ret; + ret = uv_tcp_bind(&tw_conf->_server, (const struct sockaddr*) &addr, 0); + if (ret < 0) + return ret; + ret = uv_listen((uv_stream_t*)&tw_conf->_server, 8, tw_on_connection); + if (ret < 0) + return ret; + loop->data = tw_conf; + //开始线程 + uv_thread_t hare_id; + uv_thread_create(&hare_id, (uv_thread_cb)tw_run, loop); + return 0; +} + +static void on_close_cb(uv_handle_t* handle) { +} + +//stop TinyWeb +//当执行uv_stop之后,uv_run并不能马上退出,而是要等待其内部循环的下一个iteration到来时才会退出; +//如果提前free掉loop就会导致loop失效。当然也可以sleep几十毫秒然后再close,但这么搞不太雅观。 +//uv_stop以后不能马上执行uv_loop_close() +//貌似关闭及释放loop等资源不是很完善的样子 +void tinyweb_stop(uv_loop_t* loop) +{ + if (loop == NULL) + loop = uv_default_loop(); + uv_stop(loop); + if (loop->data) + uv_close((uv_handle_t*)&((tw_config*)loop->data)->_server, on_close_cb); + uv_loop_close(loop); +} + diff --git a/src/tinyweb/tinyweb.h b/src/tinyweb/tinyweb.h new file mode 100644 index 0000000..5b43d3b --- /dev/null +++ b/src/tinyweb/tinyweb.h @@ -0,0 +1,197 @@ +#pragma once +#ifndef __TINYWEB_H__ +#define __TINYWEB_H__ + +#ifdef _MSC_VER +//去掉 warning C4996: '***': The POSIX name for this item is deprecated.Instead, use the ISO C++ conformant name... +#ifndef _CRT_NONSTDC_NO_DEPRECATE +#define _CRT_NONSTDC_NO_DEPRECATE +#endif +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include +#pragma comment(lib, "libuv.lib") +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "IPHLPAPI.lib") +#pragma comment(lib, "Psapi.lib") +#pragma comment(lib, "Userenv.lib") +# if defined(WIN32) && !defined(snprintf) +# define snprintf _snprintf +# endif + +#else //__GNUGC__ + +#include + +#endif + +#include "tools.h" + +#if TinyWeb_Function_Description //TinyWeb功能说明 + +auth lzpong 2016/11/24 +功能基于 libuv 跨平台库 + +0.支持设置文档编码,默认 utf-8 +1.支持使用HTTP: GET/POST方式访问 +2.支持Socket, WebSocket 连接 +3.支持返回404错误页面 +4.支持指定根目录(默认程序所在目录) +5.支持任意格式文件访问(带/不带扩展名, 文件下载) + a.支持静态网页访问:html/htm + b.支持其他静态文件:js, css, png, jpeg/jpg, gif, ico, txt, xml, json, log, wam, wav, mp3, mp4, apk 等 + c.支持其他文件格式, 默认文件类型为:"application/octet-stream" + d.支持不带扩展名文件访问 + e.支持 Range 请求参数下载大文件(Range: bytes=sizeFrom-[sizeTo],支持负反向计算) +6.支持默认index页面(index.html/index.htm),可以自定义设置 +7.支持目录列表 +8.不允许访问根目录上级文件或文件夹 +9.支持回调 + a.接收到HTTP请求后先回调(此功能便于程序返回自定义功能),回调失败或返回0时执行普通http响应 + b.WebSocket 数据回调 + c.socket 数据回调 +10.支持x64,支持超过2G大文件 +11.支持cookie/setcookie +12.支持添加自定义头部信息 +13.支持POST较大的数据(支持分包发送的http Post内容) +==============stable,future +14.支持分包发送的http头部(http get) + + +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct tw_peerAddr { + uchar flag;//标志字节 ([0~7]: [0]是否需要保持连接 [1]是否WebSocket [2]是否WebSocket文本帧) + ushort port; + ushort fport; + size_t sk; + char ip[17]; + char fip[17]; +}tw_peerAddr; + +typedef struct tw_reqHeads { + uchar method;//0:Socket 1:GET 2:POST + char host[260]; //IP:port, domain + char path[512]; //路径 + char query[1500];//参数 + char* data; //数据 + char cookie[260];//cookie + size_t contentLen;//Content Lenth + size_t len; //接收的数据长度 + long long Range_frm, Range_to; +}tw_reqHeads; + +//服务配置 +typedef struct tw_config { +//private data: + uv_tcp_t _server; + +//public data: + uchar dirlist:1; //是否允许列出目录 + char* doc_dir; //Web根目录,绝对路径,末尾带斜杠'\'(uninx为'/'); 默认程序文件所在目录 + char* doc_index;//默认主页文件名,逗号分隔; 默认"index.html,index.htm" + char* ip; //服务的IP地址 is only ipV4, can be NULL or "" or "*", which means "0.0.0.0" + ushort port; //服务监听端口 + char* charset; //文档编码(默认utf-8) + //数据 + void* data;//用户数据,如对象指针 + + //客户端接入 + char (*on_connect)(void* data, uv_stream_t* client, tw_peerAddr* pa); + + //返回非0表示已经处理处理请求 + //返回0表示没有适合的处理请求,将自动查找文件/文件夹,若未找到则发送404响应 + //此功能便于程序返回自定义功能 + //heads成员不需要free + //pa->flag:标志字节 ([0~7]: [0]是否需要保持连接 [1]是否WebSocket [2]是否WebSocket文本帧 + char (*on_request)(void* data, uv_stream_t* client, tw_peerAddr* pa, tw_reqHeads* heads); + + //Socket 或 WebSocket 数据, 可以通过buf->flag 或 pa->flag判断 + //buf成员不需要free + //pa->flag:标志字节 ([0~7]: [0]是否需要保持连接 [1]是否WebSocket [2]是否WebSocket文本帧 + char (*on_data)(void* data, uv_stream_t* client, tw_peerAddr* pa, membuf_t* buf); + + //Socket 检测到错误(此时链接可能已经断开) + //错误消息格式:"%d:%s,%s" + //pa->flag:标志字节 ([0~7]: [0]是否需要保持连接 [1]是否WebSocket [2]是否WebSocket文本帧 + char (*on_error)(void* data, uv_stream_t* client, tw_peerAddr* pa,int errcode, char* errstr); + + //Socket 已关闭(此时链接已经断开) + //flag:标志字节 ([0~7]: [0]是否需要保持连接<非长连接为http> [1]是否WebSocket + char (*on_close)(void* data, uv_stream_t* client, tw_peerAddr* pa); +} tw_config; + + +//start web server, start with the config +//loop: if is NULL , it will be uv_default_loop() +//conf: the server config +//返回值不为0表示错误代码,用uv_err_name(n),和uv_strerror(n)查看原因字符串 +int tinyweb_start(uv_loop_t* loop, tw_config* conf); + +//stop TinyWeb +//loop: if is NULL , it will be &uv_default_loop() +void tinyweb_stop(uv_loop_t* loop); + +//================================================= + +//制造头部 SetCookie 字段和值 +//set_cookie: 缓存区(至少 42+strlen(domain)=strlen(path) ) +//ckLen: set_cookie的长度 +//expires: 多少秒后过期 +//domain: Domain, 域名或IP地址 +//path: Path, 可以是 heads->path +void tw_make_setcookie(char* set_cookie, int ckLen, const char* key, const char* val, int expires, char* domain, char* path); + +//制造头部 delete cookie +void tw_make_delcookie(char* del_cookie, int ckLen, char* key); + +//返回格式华的HTTP响应内容 (需要free返回数据) +//status:http状态,如:"200 OK" +//ext_heads:额外的头部字符串,如:"head1: this-is-head1\r\nSetCookie: TINY_SSID=Tiny1531896250879; Expires=...\r\n" +//content_type:文件类型,如:"text/html" ;可以调用tw_get_content_type()得到 +//content:使用utf-8编码格式的数据,特别是html文件类型的响应 +//content_length:0或-1自动计算 content 长度(c_str, end with NULL) +//respone_size:if not NULL,可以获取发送的数据长度 the size of respone will be writen to request +//returns malloc()ed c_str, need free() by caller +char* tw_format_http_respone(uv_stream_t* client, const char* status, const char* ext_heads, const char* content_type, const char* content, size_t content_length, size_t* respone_size); + +//根据扩展名返回文件类型 content_type +//可以传入路径/文件名/扩展名 +const char* tw_get_content_type(const char* fileExt); + +//发送数据到客户端; 如果是短连接,则发送完后会关闭连接 +//data:待发送数据 +//len: 数据长度, -1 将自动计算数据长度 +//need_copy_data:是否需要复制数据 +//need_free_data:是否需要free数据, 如果need_copy_data非零则忽略此参数 +void tw_send_data(uv_stream_t* client, const void* data, size_t len, char need_copy_data, char need_free_data); + +//发送'200 OK' 响应; 不会释放(free)传入的数据(u8data) +//content_type:Content Type 文档类型 +//u8data:utf-8编码的数据 +//content_length:数据长度,为0或-1时自动计算(strlen)(c_str, end with NULL) +//respone_size:获取响应最终发送的数据长度,为0表示放不需要取此长度 +void tw_send_200_OK(uv_stream_t* client, const char* ext_heads, const char* content_type, const void* u8data, size_t content_length, size_t* respone_size); + +//http协议发送文件,异步 +//file_path: 文件路径 +void tw_http_send_file(uv_stream_t* client, tw_reqHeads* heads, const char* ext_heads, const char* content_type, const char* file_path); + +//发送301响应,路径永久重定位 +void tw_301_Moved(uv_stream_t* client, tw_reqHeads* heads, const char* ext_heads); +//发送302响应,路径临时重定位 +void tw_302_Moved(uv_stream_t* client, tw_reqHeads* heads, const char* ext_heads); + +//关闭客户端连接 +void tw_close_client(uv_stream_t* client); + +#ifdef __cplusplus +} // extern "C" +#endif +#endif //__TINYWEB_H__ \ No newline at end of file