上一篇:lwip-2.1.3自带的httpd网页服务器使用教程(三)使用CGI获取URL参数(GET类型表单)
在阅读本篇内容之前,请修改httpd.c文件,修复lwip自带httpd服务器里面关于post的一个bug:
bug #64458: When tcp_err() is invoked, tcp_pcb is freed but httpd_post_finished() is not called by httpd.c
复现方法:上传一个大文件,在文件还没上传完的时候,按下浏览器的停止按钮。
现象:lwip不会调用httpd_post_finished()函数,导致内存泄露。
修复方法:将下面的代码添加到http_state_eof函数末尾。
/* bug #64458: When tcp_err() is invoked, tcp_pcb is freed but httpd_post_finished() is not called by httpd.c */ /* Workaround: Copy the following code to the end of "static void http_state_eof(struct http_state *hs)" */#if LWIP_HTTPD_SUPPORT_POST if ((hs->post_content_len_left != 0)#if LWIP_HTTPD_POST_MANUAL_WND || ((hs->no_auto_wnd != 0) && (hs->unrecved_bytes != 0))#endif /* LWIP_HTTPD_POST_MANUAL_WND */ ) { /* make sure the post code knows that the connection is closed */ http_uri_buf[0] = 0; httpd_post_finished(hs, http_uri_buf, LWIP_HTTPD_URI_BUF_LEN); }#endif /* LWIP_HTTPD_SUPPORT_POST*/
HTML表单的分类
HTML表单有两种提交方式:GET方式和POST方式。
表单提交方式由<form>标签的method属性决定。method="get"是GET方式,method="post"是POST方式。
另外,<form>标签的action属性指定表单要提交到哪个页面上。如果action为空字符串"",那么就是提交到当前页面上。
GET方式提交表单后,所有带有name属性的表单控件的内容都会出现在URL(浏览器网址)上,也就是说GET方式其实就是以URL参数的方式提交表单,这个之前已经讲过了。
我们今天要讲的是POST方式提交的表单。POST方式提交后,表单控件的内容不会出现在URL上,这一定程度上提高了安全性。POST方式还有一个好处,就是提交的数据量比GET方式更大,不受URL最大长度的限制。
POST表单又细分为两种类型:普通表单和文件上传表单。当<form>标签不存在enctype属性时,表单为普通表单。当<form>标签的enctype="multipart/form-data"时,表单为文件上传表单。
文件上传表单是专门用来上传文件的表单,其格式与普通表单完全不一样,需要单独解析。在Adobe Dreamweaver CS3这款网页设计软件中,只要插入了文件框控件,Dreamweaver就会自动帮我们在<form>标签上添加enctype="multipart/form-data",自动修改为文件上传类型的表单。
关于文件上传表单,我们留到后面再讲。我们先讲普通表单。
普通类型POST表单的解析
(本节例程名称:post_test)
要想接收POST表单数据,首先需要在lwip的lwipopts.h里面开启LWIP_HTTPD_SUPPORT_POST选项。
// 配置HTTPD#define LWIP_HTTPD_SUPPORT_POST 1
开启LWIP_HTTPD_SUPPORT_POST选项后,需要自己实现下面三个函数。
err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd);err_t httpd_post_receive_data(void *connection, struct pbuf *p);void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len);
第一个函数httpd_post_begin是开始处理某个POST表单提交请求时调用的函数。
其中,参数connection是当前HTTP连接的唯一标识,是一个内存地址,但是里面的数据是lwip httpd私有的,不允许私自去操作。
参数uri是访问的网页名称,例如“/form_test.html”。
http_request是http header的全部内容,http_request_len是http header的总长度,例如:
HTTP/1.1Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*Referer: http://stm32f103ze/form_test.htmlAccept-Language: zh-cnUser-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 1.1.4322)Content-Type: application/x-www-form-urlencodedAccept-Encoding: gzip, deflateHost: stm32f103zeContent-Length: 810Connection: Keep-AliveCache-Control: no-cache
其中最重要的是Content-Type属性,如果为application/x-www-form-urlencoded就是普通表单,如果以multipart/form-data开头就是文件上传表单。
content_len是全部表单内容的总长度。
httpd_post_begin函数的返回值如果是ERR_OK,则程序表示接受当前HTTP连接,lwip会继续调用后续的httpd_post_receive_data和httpd_post_finished函数。如果返回其他非ERR_OK值,则程序表示拒绝了当前HTTP连接,后续不再调用httpd_post_receive_data和httpd_post_finished函数。拒绝连接时,可以用strlcpy或strncpy函数给字符串response_uri赋值(字符串缓冲区的大小为response_uri_len),表示拒绝连接后要显示的网页文件(浏览器URL不会发生变化)。如果拒绝连接时不修改response_uri字符串的内容,则显示的是默认的404错误页面。
*post_auto_wnd变量仅当LWIP_HTTPD_POST_MANUAL_WND=1时有效,*post_auto_wnd的默认值是1,表示http服务器自动管理TCP滑动窗口。若在httpd_post_begin函数内将*post_auto_wnd的值设为0,那么我们就可以自己调用httpd_post_data_recved函数管理TCP滑动窗口了,可以动态调节数据的接收速度,很类似于TCP里面的tcp_recved函数。
第二个函数httpd_post_receive_data是接收POST表单内容用的函数,参数p是收到的数据内容。
请注意使用完p之后一定要记得调用pbuf_free函数释放内存。
函数通常返回ERR_OK。如果返回其他值,则表明程序出错,lwip会拒收后面的数据,并调用http_handle_post_finished结束。在文件上传的时候可以用这种方法拒收大文件。
收到的表单数据大致是下面这样,也就是aaa=bbb&ccc=ddd&eee=fff这样的形式,需要我们自行分离控件名称和控件内容,还需要用urldecode函数解码。
textfield=%23include+%3Cstdio.h%3E&textfield2=if+%28strcmp%28uri%2C+%22%2Fform_test.html%22%29+%21%3D+0%29&textfield3=printf%28%22%5Bhttpd_post_begin%5D+connection%3D0x%25p%5Cn%22%2C+connection%29%3B&radio=gpio&checkbox=on&checkbox2=on&checkbox3=on&select=f103c8&fileField=BingWallpaper-20221017.jpg&textarea=err_t+httpd_post_receive_data%28void+*connection%2C+struct+pbuf+*p%29%0D%0A%7B%0D%0A++struct+httpd_post_state+*state%3B%0D%0A++%0D%0A++printf%28%22%5Bhttpd_post_receive_data%5D+connection%3D0x%25p%5Cn%22%2C+connection%29%3B%0D%0A++state+%3D+httpd_post_find_state%28connection%2C+NULL%29%3B%0D%0A++pbuf_copy_partial%28p%2C+state-%3Econtent+%2B+state-%3Econtent_pos%2C+p-%3Etot_len%2C+0%29%3B%0D%0A++state-%3Econtent_pos+%2B%3D+p-%3Etot_len%3B%0D%0A++pbuf_free%28p%29%3B%0D%0A++return+ERR_OK%3B%0D%0A%7D
IE6浏览器会在末尾多发送2字节的/r/n换行符,如果Content-Length=377的话,httpd_post_receive_data会收到379字节数据。由于我们用malloc分配的是Content-Length大小的内存,为了避免缓冲区溢出,一定要把这个换行符丢掉。
第三个函数httpd_post_finished是结束处理POST表单请求的函数。response_uri是结束时要显示的页面,response_uri_len是response_uri缓冲区的大小。如果response_uri没有赋值,则显示的是404错误页面。
由于http连接标识connection是一块存放httpd服务器私有数据的void *型内存块,里面的内容是不能乱动的。那我们要想存放网页的数据该怎么办呢?
我们可以定义一个自定义结构体struct httpd_post_state(名称可以随便起)的链表httpd_post_list,例如:
struct httpd_post_state{ struct httpd_post_state *next; // 链表的下一个节点 void *connection; // http连接标识 char *content; // 表单内容 int content_len; // 表单长度 int content_pos; // 已接收的表单内容的长度 int multipart; // 是否为文件上传表单 char **params; // 表单控件名列表 char **values; // 表单控件值表 int param_count; // 表单控件个数};static struct httpd_post_state *httpd_post_list; // 保存http post请求数据的链表
httpd_post_list链表是一个全局变量,其初始值为NULL空指针。每当有新的post请求到来时,就用mem_malloc(sizeof(struct httpd_post_state))新分配一块内存,把网页数据都存放到里面,然后把这块新分配的结构体内存加入到httpd_post_list链表中。结构体里面的connection成员的值就等于lwip调用httpd_post_begin函数时传入的connection参数值。
后面接收表单数据时,lwip调用httpd_post_receive_data函数传入了connection标识,就在httpd_post_list链表里面去寻找connection成员和connection参数相匹配的那个链表节点,就能取出网页数据了。
post请求处理结束时,httpd_post_finished函数被lwip调用,在该函数内根据connection标识找到struct httpd_post_state结构体,将其从httpd_post_list链表中移除,然后用mem_free释放结构体占用的内存。
请看代码:
/* 为新http post请求创建链表节点 */static struct httpd_post_state *httpd_post_create_state(void *connection, int content_len){ struct httpd_post_state *state, *p; LWIP_ASSERT("connection != NULL", connection != NULL); LWIP_ASSERT("connection is new", httpd_post_find_state(connection) == NULL); LWIP_ASSERT("content_len >= 0", content_len >= 0); state = mem_malloc(sizeof(struct httpd_post_state) + content_len + 1); if (state == NULL) return NULL; memset(state, 0, sizeof(struct httpd_post_state)); state->connection = connection; // http连接标识 state->content = (char *)(state + 1); // 指向结构体后面content_len+1字节的内存空间, 用于保存收到的表单内容 state->content[content_len] = '/0'; // 字符串结束符 state->content_len = content_len; // 表单内容长度 // 将新分配的节点添加到链表末尾 if (httpd_post_list != NULL) { // 找到尾节点 for (p = httpd_post_list; p->next != NULL; p = p->next); // 将state挂到尾节点的后面, 成为新的尾节点 p->next = state; } else httpd_post_list = state; // 链表为空, 直接赋值, 成为第一个节点 return state;}/* 根据http连接标识找到链表节点 */static struct httpd_post_state *httpd_post_find_state(void *connection){ struct httpd_post_state *p; LWIP_ASSERT("connection != NULL", connection != NULL); for (p = httpd_post_list; p != NULL; p = p->next) { if (p->connection == connection) break; } return p;}/* 从链表中删除节点 */static void httpd_post_delete_state(struct httpd_post_state *state){ struct httpd_post_state *p; LWIP_ASSERT("state != NULL", state != NULL); if (httpd_post_list != state) { // 找到当前节点的前一个节点 for (p = httpd_post_list; p != NULL && p->next != state; p = p->next) LWIP_ASSERT("p != NULL", p != NULL); // 从链表中移除 p->next = state->next; } else httpd_post_list = state->next; // 释放节点所占用的内存空间 state->next = NULL; state->connection = NULL; state->content = NULL; if (state->params != NULL) { mem_free(state->params); state->params = NULL; state->values = NULL; } mem_free(state);}
在上面的代码中,httpd_post_create_state就是创建链表节点的函数。链表头是全局变量httpd_post_list,其初始值为NULL,代表这是一个空链表。当第一个post请求到来时,用mem_malloc分配第一个链表节点,用state表示,httpd_post_list=state, state->next=NULL。后面又来了第二个post请求,那么就又分配了个state2。此时httpd_post_list=state, state->next=state2, state2->next=NULL。每次都是把新节点插入到链表末尾。
我们在分配内存块的时候,分配的内存大小是sizeof(struct httpd_post_state) + content_len + 1。不仅为struct httpd_post_state结构体分配了内存,还同时为表单内容分配了内存。content_len是表单内容的总大小,后面再加1是为了存放字符串结束符'/0'。这样做的好处是两个内容在同一块连续的内存上,后面删除链表节点的时候就只用释放一次内存,不用先释放struct httpd_post_state再释放content内存。
state->content = (char *)(state + 1);这句话就是把struct httpd_post_state结构体后面多分配出来的content_len + 1字节的内存的首地址赋给state->content成员变量,方便访问。
(char *)(state + 1)就是(char *)&state[1]的意思。这里+1加的可不是1字节,而是加的sizeof(struct httpd_post_state)字节,请一定要和((char *)state + 1)区分开。((char *)state + 1)加的是sizeof(char)=1字节,(char *)(state + 1)加的是sizeof(state)=sizeof(struct httpd_post_state)字节。(有点绕,如果不能理解的话记住这个结论就行了)
因此,state->content指向的内存块的大小为content_len + 1字节,state->content[content_len] = '/0';这句话就是把那最后一字节赋上字符串结束符'/0'。
httpd_post_find_state函数是根据connection连接标识寻找struct httpd_post_state *链表节点的函数,函数的返回值是找到的链表节点。
httpd_post_delete_state函数就是在post请求处理结束时删除链表节点并释放内存的函数。
接下来我们来实现lwip post功能要求我们实现的那三个函数。请看代码:
#define STRPTR(s) (((s) != NULL) ? (s) : "(null)")/* 开始处理http post请求*/err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd){ struct httpd_post_state *state; printf("[httpd_post_begin] connection=0x%p, uri=%s/n", connection, uri); printf("%.*s/n", http_request_len, http_request); if (strcmp(uri, "/form_test.html") != 0) { //strlcpy(response_uri, "/bad_request.html", response_uri_len); return ERR_ARG; } state = httpd_post_create_state(connection, content_len); if (state == NULL) { //strlcpy(response_uri, "/out_of_memory.html", response_uri_len); return ERR_MEM; } state->multipart = httpd_is_multipart(http_request, http_request_len); if (state->multipart) { //strlcpy(response_uri, "/bad_request.html", response_uri_len); httpd_post_delete_state(state); return ERR_ARG; } return ERR_OK;}/* 接收表单数据 */err_t httpd_post_receive_data(void *connection, struct pbuf *p){ int len; struct httpd_post_state *state; struct pbuf *q; printf("[httpd_post_receive_data] connection=0x%p, payload=0x%p, len=%d/n", connection, p->payload, p->tot_len); for (q = p; q != NULL; q = q->next) printf("%.*s", q->len, (char *)q->payload); printf("/n"); state = httpd_post_find_state(connection); if (state != NULL) { len = p->tot_len; if (state->content_pos + len > state->content_len) { // (兼容IE6) 忽略尾部多余的/r/n, 防止缓冲区溢出 printf("[httpd_post_receive_data] ignored the last %d byte(s)/n", state->content_pos + len - state->content_len); len = state->content_len - state->content_pos; } pbuf_copy_partial(p, state->content + state->content_pos, (u16_t)len, 0); state->content_pos += len; pbuf_free(p); } return ERR_OK;}/* 结束处理http post请求*/void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){ int i; struct httpd_post_state *state; printf("[httpd_post_finished] connection=0x%p/n", connection); state = httpd_post_find_state(connection); if (state != NULL) { httpd_post_parse(state); printf("param_count=%d/n", state->param_count); for (i = 0; i < state->param_count; i++) printf("[Param] name=%s, value=%s/n", state->params[i], STRPTR(state->values[i])); httpd_post_delete_state(state); //strlcpy(response_uri, "/success.html", response_uri_len); }}
在httpd_post_begin函数中,首先判断网页名称(uri变量)是不是正确的,如果不正确就返回ERR_ARG错误码。
在response_uri没有赋值的情况下,返回非ERR_OK值后显示的是404错误页面,如果response_uri赋值了的话就是显示response_uri字符串指定的那个错误页面(比如可以赋值为/bad_request.html),lwip不再调用后续的httpd_post_receive_data和httpd_post_finished函数。
网页名称uri是正确的话就继续往后执行,调用刚才定义的httpd_post_create_state函数创建链表节点,如果链表节点创建失败同样要报错。创建成功的话就用httpd_is_multipart函数判断一下当前表单是普通表单还是文件上传表单,如果是文件上传表单(函数返回1)则报错。
httpd_is_multipart函数的实现如下:
/* 根据http header判断当前表单是否为文件上传表单 */static int httpd_is_multipart(const char *http_request, int http_request_len){ char value[100]; char *s = "multipart/form-data"; httpd_get_header(http_request, http_request_len, "Content-Type", value, sizeof(value)); return (strncasecmp(value, s, strlen(s)) == 0);}/* 在http header中找出指定名称属性的值 */static int httpd_get_header(const char *http_request, int http_request_len, const char *name, char *valuebuf, int bufsize){ const char *endptr; int linelen, namelen, valuelen; namelen = strlen(name); while (http_request_len != 0) { endptr = lwip_strnstr(http_request, "/r/n", http_request_len); if (endptr != NULL) { linelen = endptr - http_request; endptr += 2; } else { linelen = http_request_len; endptr = http_request + http_request_len; } if (strncasecmp(http_request, name, namelen) == 0 && http_request[namelen] == ':') { http_request += namelen + 1; linelen -= namelen + 1; while (*http_request == ' ') { http_request++; linelen--; } valuelen = linelen; if (valuelen > bufsize - 1) valuelen = bufsize - 1; memcpy(valuebuf, http_request, valuelen); valuebuf[valuelen] = '/0'; return linelen; } http_request_len -= endptr - http_request; http_request = endptr; } valuebuf[0] = '/0'; return -1;}
其实就是看http header里面的Content-Type属性的值是否以multipart/form-data开头,如果是的话那就是文件上传表单。
httpd_post_receive_data函数是接收post表单内容的函数,函数把接收到的表单内容p通过pbuf_copy_partial函数复制到state->content缓冲区里面,注意防止缓冲区溢出。pbuf_copy_partial函数就是把struct pbuf *链表里面所有的payload内容复制到一个数组中。
httpd_post_finished函数是在接收完所有表单内容后调用的,在函数里面用httpd_post_parse函数解析表单内容,把state->content里面存的表单控件名和控件值分离出来,存到struct httpd_post_state结构体的params和values里面。
httpd_post_parse函数的代码如下:
/* 从普通表单内容中分离出控件名称和控件内容 */static int httpd_post_parse(struct httpd_post_state *state){ char *p; int i, count; if (state == NULL || state->param_count != 0 || state->params != NULL || state->values != NULL) return -1; else if (state->multipart || state->content_pos != state->content_len) return -1; p = state->content; count = 0; while (p != NULL && *p != '/0') { count++; p = strchr(p, '&'); if (p != NULL) { *p = '/0'; p++; } } if (count > 0) { state->params = (char **)mem_malloc(2 * count * sizeof(char *)); if (state->params == NULL) return -1; state->values = state->params + count; p = state->content; for (i = 0; i < count; i++) { state->params[i] = p; state->values[i] = strchr(p, '='); p += strlen(p) + 1; if (state->values[i] != NULL) { *state->values[i] = '/0'; state->values[i]++; } urldecode(state->params[i]); if (state->values[i] != NULL) urldecode(state->values[i]); } } state->param_count = count; return count;}
post请求到这里就处理结束了,如果在httpd_post_finished函数中没有对response_uri字符数组赋值的话,最终浏览器显示的页面为404错误页面,提示找不到网页。如果对response_uri赋了值,那么就显示response_uri字符串指定的网页。
HTML网页的名称为form_test.html,放到lwip-2.1.3/apps/http/fs文件夹下并用makefsdata.exe程序打包。网页的内容如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>表单测试</title><style type="text/css"><!--body { background-color: #CCCCCC; font-family: Arial, Helvetica, sans-serif; font-size: 12px; line-height: 1.8em;}form { background-color: #FFFFFF; width: 650px; padding: 20px 15px; display: block; border: 1px solid #333333; margin: 40px auto;}form h1 { font-size: 26px; text-align: center;}form .left-block { display: inline-block; width: 100px; text-align: right; padding-right: 20px; vertical-align: top;}form .textfield, form textarea { width: 400px;}--></style></head><body><form action="" method="post" name="form1"> <h1>表单测试</h1> <p> <label class="left-block" for="textfield">文本框1:</label> <input type="text" name="textfield" id="textfield" class="textfield" value=""> </p> <p> <label class="left-block" for="textfield2">文本框2:</label> <input type="text" name="textfield2" id="textfield2" class="textfield" value=""> </p> <p> <label class="left-block" for="textfield3">文本框3:</label> <input type="text" name="textfield3" id="textfield3" class="textfield" value=""> </p> <p> <span class="left-block">单选框:</span> <input name="radio" type="radio" id="radio" value="gpio" checked="checked"><label for="radio">GPIO</label> <input type="radio" name="radio" id="radio2" value="adc"><label for="radio2">ADC</label> <input type="radio" name="radio" id="radio3" value="dma"><label for="radio3">DMA</label> </p> <p> <span class="left-block">复选框:</span> <input type="checkbox" name="checkbox" id="checkbox"><label for="checkbox">FSMC</label> <input type="checkbox" name="checkbox2" id="checkbox2"><label for="checkbox2">SDIO</label> <input type="checkbox" name="checkbox3" id="checkbox3"><label for="checkbox3">USB</label> </p> <p> <label class="left-block" for="select">下拉菜单框:</label> <select name="select" id="select"> <option value="f103c8">STM32F103C8</option> <option value="f107vc">STM32F107VC</option> <option value="h743vi">STM32H743VI</option> <option value="l476rg">STM32L476RG</option> </select> </p> <p> <label class="left-block" for="fileField">文件框:</label> <input type="file" name="fileField" id="fileField" class="textfield"> </p> <p> <label class="left-block" for="textarea">内容框:</label> <textarea name="textarea" id="textarea" rows="10"></textarea> </p> <p> <span class="left-block"></span> <input type="submit" value="提交"> </p></form></body></html>
我们来看看程序的运行结果:
点击提交按钮提交表单后,浏览器的网址没变,但是提示404错误(找不到网页),这是httpd_post_finished函数没有对response_uri字符数组赋值造成的,是正常现象。
在串口输出中我们可以看到提交的表单内容。其中包括三个文本框输入的内容,还有单选框选择的项目,还有勾选了的复选框。没有勾选的复选框不会出现在表单内容中。
表单内容还有下拉菜单框选择的项目,以及文件选择框选择的文件名(不含文件路径和文件内容),最后是多行文本框输入的内容(换行也正确显示了)。
IE6下的运行结果:
注意看,httpd_post_receive_data收到的字节数比Content-Length多两个字节,要注意防止malloc缓冲区溢出。
POST参数传递到SSI动态页面
(本节例程名称:post_test2)
POST请求的处理到httpd_post_finished函数就结束了,之后显示的页面由response_uri字符串的内容决定。如果我们想把struct httpd_post_state里面存的params和values的内容传递过去,并显示到网页上,该怎么传过去呢?
我们可以把state指针的地址用snprintf函数通过0x%p打印到response_uri字符串上。比如当state=0x20001234时,response_uri="/success.ssi?state=0x20001234",不释放state所占用的内存,仍然保留在httpd_post_list链表中,但要把connection成员置为空指针NULL。
在lwipopts.h中,把CGI(新式)、SSI和FILE_STATE功能都打开。
#define MEM_SIZE 25600 // lwip的mem_malloc函数使用的堆内存的大小// 配置HTTPD#define LWIP_HTTPD_CGI_SSI 1#define LWIP_HTTPD_FILE_STATE 1#define LWIP_HTTPD_SSI 1#define LWIP_HTTPD_SSI_INCLUDE_TAG 0#define LWIP_HTTPD_SSI_MULTIPART 1#define LWIP_HTTPD_SSI_RAW 1#define LWIP_HTTPD_SUPPORT_POST 1
httpd_post_finished函数执行结束后,lwip会去打开/success.ssi文件,并执行fs_state_init函数,我们在其中创建一个fs_state结构体。
之后,lwip调用httpd_cgi_handler函数解析URL里面的state参数,解析出来后我们就拿到了原来的struct httpd_post_state(下面简称post_state)结构体指针。在httpd_cgi_handler函数里面我们将fs_state和post_state结构体绑定在一起。
然后,lwip会调用SSI的回调函数test_ssi_handler,传入的connection_state参数就是fs_state,通过fs_state可以拿到post_state(就是最开始httpd_post_finished里面的那个state指针),就可以在SSI标签上显示post表单数据了。
网页内容生成完毕后,lwip会调用fs_state_free函数,我们可以在这里面释放掉fs_state和post_state结构体,并把post_state从httpd_post_list链表中移除。
值得注意的是,我们在httpd_post_finished函数里面把state打印到response_uri上后,由于可能会发生tcp_err错误或者出现mem_malloc失败的情况,lwip有可能不会去打开response_uri指定的文件,这将导致内存泄露。所以我们需要一定的超时机制,如果链表里面的post_state结构体在connection置为空指针后超过5秒钟还没有和fs_state结构体绑定,那就认为超时了,直接强制释放post_state结构体。这项检测我们可以放到httpd_post_begin里面进行,每当有新客户端连接的时候都要检查一下整个链表是否有节点超时。
另外,由于用户可以直接在浏览器里面输入success.ssi的网址访问,比如http://stm32f103ze/success.ssi?state=0x12345678这样的网址,其中state是一个无效的指针。为了防止STM32单片机出现HardFault错误,我们从URL取出state指针值后,一定要去链表上搜索一遍,看看这个指针是不是真的在链表上。如果不在链表上,那就是一个无效指针,不予处理。
整个过程还是比较复杂的,大概是这样的一条路径:POST->FILE_STATE(init)->CGI->SSI->FILE_STATE(free)。
让我们来看看代码吧,先看一下新添加的success.ssi动态网页,里面包含了filename和content这两个SSI标签。注意content这个标签是直接放在网页上的,没有放到多行文本框里面,所以待会儿在用htmlspecialchars的时候一定要记得nbsp参数要设置为1,把所有的空格都要替换为 。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""http://www.w3.org/TR/html4/loose.dtd"><html><head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>表单提交成功</title></head><body><h1>表单提交成功</h1><hr><p><b>您选择的文件是: </b><!--#filename--></p><p> <b>您在多行文本框中输入的内容为: </b><br> <!--#content--></p></body></html>
新定义了struct httpd_fs_state结构体,原来的struct httpd_post_state结构体新增加了fs_state和post_finish_time成员,这两个成员分别记录的是绑定的httpd_fs_state对象和post请求处理完成的时间。
新增了httpd_post_is_valid_state函数,用于判断httpd_post_state指针是否有效,也就是说能不能在httpd_post_list链表中找到指定的httpd_post_state指针。
struct httpd_fs_state{ struct httpd_post_state *post_state; char *filename; char *content;};struct httpd_post_state{ struct httpd_post_state *next; // 链表的下一个节点 struct httpd_fs_state *fs_state; void *connection; // http连接标识 char *content; // 表单内容 int content_len; // 表单长度 int content_pos; // 已接收的表单内容的长度 int multipart; // 是否为文件上传表单 char **params; // 表单控件名列表 char **values; // 表单控件值表 int param_count; // 表单控件个数 uint32_t post_finish_time; // post请求处理完成的时间};/* 判断state是否在链表中 */static int httpd_post_is_valid_state(struct httpd_post_state *state){ struct httpd_post_state *p; for (p = httpd_post_list; p != NULL; p = p->next) { if (p == state) return 1; } return 0;}
当post请求处理结束时,我们在httpd_post_finished函数中把state->connection置为NULL,把state指针的地址用snprintf函数打印到response_uri字符串上,通过URL参数传递给后续要显示的页面。state对象暂不释放,也暂不从链表上移除。后续要显示的页面是success.ssi。
如果内存充足,且网络没有出错(如tcp_err),lwip就会去打开response_uri字符串指定的success.ssi网页,调用fs_state_init函数。我们在fs_state_init函数中建立一个空的fs_state对象。这个时候由于URL参数还没开始解析,我们是不知道刚才state(后面改称为post_state)指针的地址的,所以只能建立一个空的fs_state对象放在那里。fs_state和post_state对象在网页内容生成完毕后在fs_state_free函数中一起释放。
#define STRPTR(s) (((s) != NULL) ? (s) : "(null)")/* 结束处理http post请求*/void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){ int i; struct httpd_post_state *state; printf("[httpd_post_finished] connection=0x%p/n", connection); state = httpd_post_find_state(connection); if (state != NULL) { httpd_post_parse(state); printf("param_count=%d/n", state->param_count); for (i = 0; i < state->param_count; i++) printf("[Param] name=%s, value=%s/n", state->params[i], STRPTR(state->values[i])); state->connection = NULL; // 与connection脱离关系 state->post_finish_time = sys_now(); // 记录当前时间 snprintf(response_uri, response_uri_len, "/success.ssi?state=0x%p", state); // 将state指针通过url参数传递给ssi动态页面 printf("response_uri=%s/n", response_uri); }}void *fs_state_init(struct fs_file *file, const char *name){ struct httpd_fs_state *fs_state = NULL; if (strcmp(name, "/success.ssi") == 0) { fs_state = mem_malloc(sizeof(struct httpd_fs_state)); if (fs_state != NULL) { printf("%s: new fs_state(0x%p)/n", __func__, fs_state); memset(fs_state, 0, sizeof(struct httpd_fs_state)); } else printf("%s: mem_malloc() failed/n", __func__); } return fs_state;}void fs_state_free(struct fs_file *file, void *state){ struct httpd_fs_state *fs_state = state; if (fs_state != NULL) { if (fs_state->post_state != NULL) { printf("%s: delete post_state(0x%p)/n", __func__, fs_state->post_state); httpd_post_delete_state(fs_state->post_state); fs_state->post_state = NULL; } printf("%s: delete fs_state(0x%p)/n", __func__, fs_state); if (fs_state->filename != NULL) { mem_free(fs_state->filename); fs_state->filename = NULL; } if (fs_state->content != NULL) { mem_free(fs_state->content); fs_state->content = NULL; } mem_free(fs_state); }}
接下来lwip开始解析URL参数,并调用httpd_cgi_handler函数。在httpd_cgi_handler函数中接收到success.ssi页面的state值后,就可以得到struct httpd_post_state *指针了,这正是刚才httpd_post_finished函数通过URL参数传递的state对象。
得到指针后先判断一下是否在httpd_post_list链表中,如果在链表中,说明post_state是一个有效的指针。如果不在链表上,那就是一个无效的指针。
post_state判定为有效指针后,就把这个post_state对象和刚才在fs_state_init里面创建的fs_state对象绑定。
后面lwip在替换SSI标签内容的时候,会调用test_ssi_handler函数,从connection_state参数中取出fs_state对象,再找到绑定的post_state对象,就可以得到post表单提交的数据了。注意在显示的时候htmlspecialchars的nbsp参数要设为1,这样才能正确显示空格和tab字符。
fs_state_init里面用mem_malloc创建fs_state对象有可能失败。失败了的话,lwip还是会调用httpd_cgi_handler、test_ssi_handler和fs_state_free函数。在httpd_cgi_handler函数里面解析state参数的时候,如果发现fs_state对象没有创建成功,就应该直接删除post_state对象。在test_ssi_handler函数里面判断到fs_state为空,也不会执行任何操作。
void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue, void *connection_state){ char *p; int i, j; struct httpd_fs_state *fs_state = connection_state; struct httpd_post_state *post_state; uintptr_t ptr; if (strcmp(uri, "/success.ssi") != 0) return; // 这里只用判断uri是否正确, 不用判断fs_state是否为NULL, fs_state=NULL的情况在后面的代码中处理 for (i = 0; i < iNumParams; i++) { if (strcmp(pcParam[i], "state") == 0) { ptr = strtol(pcValue[i], NULL, 16); post_state = (struct httpd_post_state *)ptr; if (httpd_post_is_valid_state(post_state)) { printf("%s: valid post state 0x%p from URL parameter/n", __func__, post_state); if (fs_state != NULL) { fs_state->post_state = post_state; post_state->fs_state = fs_state; for (j = 0; j < post_state->param_count; j++) { // 在网页上直接显示, 必须要把空格转成 , 所以htmlspecialchars的参数nbsp要设为1 // 如果是在多行文本框内显示的话, 一定不能把空格转成 (否则表单提交后会出错), 参数nbsp要设为0 if (strcmp(post_state->params[j], "fileField") == 0) fs_state->filename = htmlspecialchars(post_state->values[j], 1); else if (strcmp(post_state->params[j], "textarea") == 0) { p = htmlspecialchars(post_state->values[j], 1); if (p != NULL) { fs_state->content = nl2br(p); mem_free(p); } } } } else { // 刚才在fs_state_init函数中mem_malloc分配内存失败, fs_state对象没有创建成功 // fs_state为NULL, 删除post_state对象 printf("%s: delete post_state(0x%p)/n", __func__, post_state); httpd_post_delete_state(post_state); } } else { // URL参数传入的是无效的state指针 printf("%s: invalid post state 0x%p from URL parameter/n", __func__, post_state); } break; } }}static u16_t test_ssi_handler(const char *ssi_tag_name, char *pcInsert, int iInsertLen, u16_t current_tag_part, u16_t *next_tag_part, void *connection_state){ struct httpd_fs_state *fs_state = connection_state; u16_t curr, len; if (fs_state != NULL && fs_state->post_state != NULL) { if (strcmp(ssi_tag_name, "filename") == 0) { if (fs_state->filename != NULL) { strlcpy(pcInsert, fs_state->filename, iInsertLen); return strlen(pcInsert); } } else if (strcmp(ssi_tag_name, "content") == 0) { if (fs_state->content != NULL) { len = strlen(fs_state->content); curr = len - current_tag_part; if (curr > iInsertLen - 1) { curr = iInsertLen - 1; *next_tag_part = current_tag_part + curr; } memcpy(pcInsert, fs_state->content + current_tag_part, curr); pcInsert[curr] = '/0'; return curr; } } } return HTTPD_SSI_TAG_UNKNOWN;}
如果含有state指针的response_uri字符串交给lwip后,lwip因为某些原因没有去打开response_uri字符串指定的success.ssi网页,为了避免内存泄露,我们需要再写一个httpd_post_cleanup函数,搜索httpd_post_list链表上所有5秒内没有和fs_state对象完成绑定的节点,将这些节点强制删除。我们选择在httpd_post_begin里面调用httpd_post_cleanup函数,每次有新post请求到来的时候都清理一下链表。
/* 清除已处理完post请求却没有打开SSI网页的post_state结构体 */static void httpd_post_cleanup(void){ struct httpd_post_state *p, *q; uint32_t now; now = sys_now(); p = httpd_post_list; while (p != NULL) { q = p->next; if (p->connection == NULL && p->fs_state == NULL && now - p->post_finish_time >= 5000) { printf("%s: delete post_state(0x%p)/n", __func__, p); httpd_post_delete_state(p); } p = q; }}/* 开始处理http post请求*/err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd){ struct httpd_post_state *state; printf("[httpd_post_begin] connection=0x%p, uri=%s/n", connection, uri); httpd_post_cleanup(); ...}
程序运行结果:
可以在多行文本框里面提交一段很长的文本,只要lwip的内存(MEM_SIZE)够大就能显示成功。换行符、空格和tab字符也能正确显示。
文件上传类型POST表单的解析
(本节例程名称:post_test3)
普通POST表单只会提交文件框里面所选文件的文件名,不会上传文件的内容。如果要想上传文件内容的话就得使用文件上传类型的POST表单,在<form>标签上添加enctype="multipart/form-data"属性。这种文件上传类型的POST表单提交后的内容格式和普通表单完全不一样,需要单独处理。
我们先来看一下文件上传表单提交后的http header内容。
HTTP/1.1Accept: */*Referer: http://stm32f103ze/form_test.htmlAccept-Language: zh-cnUser-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 1.1.4322)Content-Type: multipart/form-data; boundary=---------------------------7e729f1bf0a7aAccept-Encoding: gzip, deflateHost: stm32f103zeContent-Length: 39415Connection: Keep-AliveCache-Control: no-cache
其中Content-Type以multipart/form-data开头,后面还有一个boundary字符串,叫做分界符字符串。这个分界符字符串非常重要,分界符字符串是表单内容中各个控件内容的分界符。
Content-Length是整个表单内容(包括文件内容)的总长度。lwip调用httpd_post_begin函数时传入的content_len参数就等于http header中Content-Length属性的数值。
我们再来看一下表单内容的格式。
可以看到,每个表单控件都是用分界符字符串隔开的。整个表单以分界符字符串开始,结束也是靠的分界符字符串,只不过结束时多了两个横杠。
分界符字符串后面是控件属性区,里面存放的是控件名和文件属性。对于普通表单控件(如文本框、单选框、复选框、下拉菜单框等等),控件属性区只有name字符串,表示控件名。对于文件框控件,控件属性区除了name字符串外,还有filename字符串和Content-Type属性。filename字符串表示文件框选择的文件名,有的浏览器提供了完整的文件路径和文件名,有的浏览器就只提供了文件名。Content-Type属性表示的是文件的类型,比如image/pjpeg是jpg图片文件,这个数据也是由浏览器提供的。
name和filename的值是用双引号括起来的,所以值里面是不允许有双引号出现的。如果用户上传了名叫".txt的文件(Windows系统不允许文件名里面有双引号,但Linux系统允许),那么其中的双引号会被强制转换为%22这三个字符,除了双引号字符外,其他所有的字符都不会转换。所以,上传".txt和%22.txt这两个文件后,得到的文件名都是%22.txt。由于程序在收到%22这三个字符后无法区分原始内容是"还是%22,所以我们选择不对%22进行解码操作。文件名里面是允许有分号出现的,分号也不会被转换,在程序里面我们不能直接用strtok_r或explode函数按分号把字符串拆分成数组,只能直接用for循环遍历字符串,双引号内的分号必须忽略。
控件属性区完了之后再隔一个空行就是控件内容区了。普通表单控件的控件内容区存放的是控件里面填写的内容,文件框控件的控件内容区存放的是文件的内容。控件内容区是以原文形式存储的,是没有进行urlencode编码的,这和普通表单内容的格式很不一样。控件内容区里面任意二进制内容都可以出现,包括/r/n换行符甚至/0符,所以程序里面有一个很重要的任务就是正确区分某个/r/n到底是不是控件或文件里面的内容。一般来说,如果读到的/r/n后面恰好是分界符字符串,那么这个/r/n仅仅是表单的换行符,并不是控件或文件里面的内容。如果读到的/r/n后面不是分界符字符串,那么这个/r/n就属于控件或文件内容。
lwip2.1版本相比2.0版本新增了一个名叫pbuf_free_header的函数,这个函数非常有用,可以只释放pbuf的前面一部分,这给长文本的分行解析带来了很大的方便。比如某一次TCP接收,收到的是第一行、第二行的全部内容和第三行的一部分内容,那么我们在解析完前面两行后,调用pbuf_free_header释放前面两行所占用的空间,未接收完全的第三行暂时无法解析,就仍然留在pbuf里面。这样我们就可以实现一边接收TCP数据,一边一行一行地解析。不用把整个内容接收完了之后才来慢慢解析。pbuf_free_header函数的原型如下:
struct pbuf *pbuf_free_header(struct pbuf *q, u16_t size);
不过要特别注意的是,pbuf_free_header函数有可能会改变q的地址。假设q是由三段内存组成的:0~9、10~14、15~19,共20字节。我们如果释放前面12个字节的话,那么第一块内存就会被完全释放掉,第二块内存只释放了一半,q将变为指向第二块内存,由函数的返回值返回。如果size参数大于或等于q的总大小的话,那么q就会被完全释放,函数返回NULL,这就相当于是调用了pbuf_free函数了。
我们来看看完整的文件上传代码,大约一共有900行:
#include <ff.h>#include <lwip/apps/httpd.h>#include <lwip/def.h>#include <lwip/mem.h>#include <string.h>#include <time.h>#include "strutil.h"#include "test.h"struct httpd_post_multipart_state{ struct pbuf *p; // 未处理的数据 int reading_content; // 当前是否正在处理表单控件内容或文件内容 uint64_t content_len; // 已读取的控件内容或文件内容的长度 char boundary[70]; // 边界字符串 int boundary_len; // 边界字符串的长度 int is_file; // 当前是在读取文件内容还是控件内容 (0:控件内容, 其他值:文件内容) char *filename; // 当前正在读取的文件的文件名 FIL fil; // 打开的文件 int crlf; // 控件内容或文件内容中是否还有未保存的/r/n char *strbuf; // 存放解析出来的所有字符串 int strbuf_used; // 字符串缓冲区的已用空间 int strbuf_capacity; // 字符串缓冲区的容量};struct httpd_post_file{ char *name; // 文件名 char *path; // 保存路径 uint64_t size; // 文件大小 char *mimetype; // 文件类型};struct httpd_post_state{ struct httpd_post_state *next; // 链表的下一个节点 void *connection; // http连接标识 char *content; // 表单内容 int content_len; // 表单长度 int content_pos; // 已接收的表单内容的长度 struct httpd_post_multipart_state *multipart; // 文件上传状态 char **params; // 表单控件名列表 char **values; // 表单控件值表 struct httpd_post_file *files; // 文件列表 int param_count; // 表单控件个数};static struct httpd_post_state *httpd_post_create_state(void *connection, int content_len, int multipart);static struct httpd_post_state *httpd_post_find_state(void *connection);static void httpd_post_delete_state(struct httpd_post_state *state);static int httpd_get_header(const char *http_request, int http_request_len, const char *name, char *valuebuf, int bufsize);static int httpd_is_multipart(const char *http_request, int http_request_len);static int httpd_post_parse(struct httpd_post_state *state);static int httpd_post_parse_multipart(struct httpd_post_state *state);static err_t httpd_post_process_multipart(struct httpd_post_state *state, struct pbuf *p);static err_t httpd_post_process_multipart_line(struct httpd_post_state *state, int linelen);static int httpd_post_generate_file_path(const char *srcname, char *buffer, int bufsize);static err_t httpd_post_store_multipart_string(struct httpd_post_multipart_state *multipart, char flag, const char *str, int len, char **strout);static struct httpd_post_state *httpd_post_list; // 保存http post请求数据的链表/* 为新http post请求创建链表节点 */static struct httpd_post_state *httpd_post_create_state(void *connection, int content_len, int multipart){ struct httpd_post_state *state, *p; LWIP_ASSERT("connection != NULL", connection != NULL); LWIP_ASSERT("connection is new", httpd_post_find_state(connection) == NULL); LWIP_ASSERT("content_len >= 0", content_len >= 0); if (!multipart) state = mem_malloc(sizeof(struct httpd_post_state) + content_len + 1); else state = mem_malloc(sizeof(struct httpd_post_state) + sizeof(struct httpd_post_multipart_state)); if (state == NULL) { printf("%s: mem_malloc() failed/n", __func__); return NULL; } memset(state, 0, sizeof(struct httpd_post_state)); state->connection = connection; // http连接标识 if (!multipart) { state->content = (char *)(state + 1); // 指向结构体后面content_len+1字节的内存空间, 用于保存收到的表单内容 state->content[content_len] = '/0'; // 字符串结束符 } else { state->multipart = (struct httpd_post_multipart_state *)(state + 1); memset(state->multipart, 0, sizeof(struct httpd_post_multipart_state)); } state->content_len = content_len; // 表单内容长度 // 将新分配的节点添加到链表末尾 if (httpd_post_list != NULL) { // 找到尾节点 for (p = httpd_post_list; p->next != NULL; p = p->next); // 将state挂到尾节点的后面, 成为新的尾节点 p->next = state; } else httpd_post_list = state; // 链表为空, 直接赋值, 成为第一个节点 return state;}/* 根据http连接标识找到链表节点 */static struct httpd_post_state *httpd_post_find_state(void *connection){ struct httpd_post_state *p; LWIP_ASSERT("connection != NULL", connection != NULL); for (p = httpd_post_list; p != NULL; p = p->next) { if (p->connection == connection) break; } return p;}/* 从链表中删除节点 */static void httpd_post_delete_state(struct httpd_post_state *state){ struct httpd_post_state *p; LWIP_ASSERT("state != NULL", state != NULL); if (httpd_post_list != state) { // 找到当前节点的前一个节点 for (p = httpd_post_list; p != NULL && p->next != state; p = p->next) LWIP_ASSERT("p != NULL", p != NULL); // 从链表中移除 p->next = state->next; } else httpd_post_list = state->next; // 释放节点所占用的内存空间 state->next = NULL; state->connection = NULL; state->content = NULL; if (state->multipart != NULL) { LWIP_ASSERT("state->multipart->p == NULL", state->multipart->p == NULL); LWIP_ASSERT("file has been closed", state->multipart->is_file == 0); if (state->multipart->strbuf != NULL) { mem_free(state->multipart->strbuf); state->multipart->filename = NULL; state->multipart->strbuf = NULL; } state->multipart = NULL; } if (state->params != NULL) { mem_free(state->params); state->params = NULL; state->values = NULL; state->files = NULL; } mem_free(state);}/* 在http header中找出指定名称属性的值 */static int httpd_get_header(const char *http_request, int http_request_len, const char *name, char *valuebuf, int bufsize){ const char *endptr; int linelen, namelen, valuelen; namelen = strlen(name); while (http_request_len != 0) { endptr = lwip_strnstr(http_request, "/r/n", http_request_len); if (endptr != NULL) { linelen = endptr - http_request; endptr += 2; } else { linelen = http_request_len; endptr = http_request + http_request_len; } if (strncasecmp(http_request, name, namelen) == 0 && http_request[namelen] == ':') { http_request += namelen + 1; linelen -= namelen + 1; while (*http_request == ' ') { http_request++; linelen--; } valuelen = linelen; if (valuelen > bufsize - 1) valuelen = bufsize - 1; memcpy(valuebuf, http_request, valuelen); valuebuf[valuelen] = '/0'; return linelen; } http_request_len -= endptr - http_request; http_request = endptr; } valuebuf[0] = '/0'; return -1;}/* 根据http header判断当前表单是否为文件上传表单 */static int httpd_is_multipart(const char *http_request, int http_request_len){ char value[100]; char *s = "multipart/form-data"; httpd_get_header(http_request, http_request_len, "Content-Type", value, sizeof(value)); return (strncasecmp(value, s, strlen(s)) == 0);}/* 从普通表单内容中分离出控件名称和控件内容 */static int httpd_post_parse(struct httpd_post_state *state){ char *p; int i, count; if (state == NULL || state->param_count != 0 || state->params != NULL || state->values != NULL) return -1; else if (state->multipart != NULL || state->content_pos != state->content_len) return -1; p = state->content; count = 0; while (p != NULL && *p != '/0') { count++; p = strchr(p, '&'); if (p != NULL) { *p = '/0'; p++; } } if (count > 0) { state->params = (char **)mem_malloc(2 * count * sizeof(char *)); if (state->params == NULL) { printf("%s: mem_malloc() failed/n", __func__); return -1; } state->values = state->params + count; p = state->content; for (i = 0; i < count; i++) { state->params[i] = p; state->values[i] = strchr(p, '='); p += strlen(p) + 1; if (state->values[i] != NULL) { *state->values[i] = '/0'; state->values[i]++; } urldecode(state->params[i]); if (state->values[i] != NULL) urldecode(state->values[i]); } } state->param_count = count; return count;}/* 分离出文件上传表单的控件名称和控件内容 */static int httpd_post_parse_multipart(struct httpd_post_state *state){ char *p; int i, count, memsize; void *mem; FRESULT fr; if (state == NULL || state->param_count != 0 || state->params != NULL || state->values != NULL) return -1; else if (state->multipart == NULL) return -1; if (state->multipart->is_file != 0) { // 关闭文件 if (state->multipart->is_file == 2) { printf("%s: close file. size=%llu/n", __func__, state->multipart->content_len); f_close(&state->multipart->fil); } state->multipart->is_file = 0; } if (state->content_pos == state->content_len) printf("%s: fully processed/n", __func__); else { printf("%s: partly processed (%d/%d)/n", __func__, state->content_pos, state->content_len); goto err; } count = 0; for (p = state->multipart->strbuf; p - state->multipart->strbuf < state->multipart->strbuf_used; p += strlen(p) + 1) { if (*p == 'n') count++; } if (count > 0) { memsize = 2 * count * sizeof(char *) + count * sizeof(struct httpd_post_file); mem = mem_malloc(memsize); if (mem == NULL) { printf("%s: mem_malloc() failed/n", __func__); goto err; } memset(mem, 0, memsize); state->params = (char **)mem; state->values = state->params + count; state->files = (struct httpd_post_file *)(state->values + count); i = -1; for (p = state->multipart->strbuf; p - state->multipart->strbuf < state->multipart->strbuf_used; p += strlen(p) + 1) { if (*p == 'n') { i++; state->params[i] = p + 1; } else if (i >= 0) { switch (*p) { case 'f': state->files[i].name = p + 1; // 继续往下执行 case 'c': state->values[i] = p + 1; break; case 'p': state->files[i].path = p + 1; break; case 's': state->files[i].size = strtoull(p + 1, NULL, 10); break; case 't': state->files[i].mimetype = p + 1; break; } } } } state->param_count = count; return count; err: // 出错时删除所有上传的文件 for (p = state->multipart->strbuf; p - state->multipart->strbuf < state->multipart->strbuf_used; p += strlen(p) + 1) { if (*p == 'p') { printf("%s: delete uploaded file %s/n", __func__, p + 1); fr = f_unlink(p + 1); if (fr != FR_OK) printf("%s: f_unlink() failed. fr=%d/n", __func__, fr); } } return -1;}/* 接收并处理文件上传表单的数据 */static err_t httpd_post_process_multipart(struct httpd_post_state *state, struct pbuf *p){ err_t err; int linelen; int no_more_data = (p == NULL); // 当前是否为最后一次处理 uint16_t pos; if (!no_more_data) { // 将新收到的数据包和前面没有处理的数据包拼在一起 if (state->multipart->p == NULL) state->multipart->p = p; else pbuf_cat(state->multipart->p, p); } while ((p = state->multipart->p) != NULL && (pos = pbuf_strstr(p, "/r/n")) != 0xffff) { // 收到完整的一行就立即处理 linelen = pos + 2; err = httpd_post_process_multipart_line(state, linelen); // 每调用一次这个函数, 变量p就必须重新赋值一次 if (err != ERR_OK) return err; } // (1) 读取表单控件内容或文件内容时, 一行的长度有可能很长 // 当pbuf堆积的数据量超过一定大小后就要立即处理, 避免占用太多内存 // (2) 如果已经接收完所有的数据(no_more_data=1), 那么就要立即处理所有未处理的数据 if (p != NULL && ((state->multipart->reading_content && p->tot_len >= 500) || no_more_data)) { if (pbuf_get_at(p, p->tot_len - 1) == '/r' && !no_more_data) { // 数据块以/r结尾且还没接收完所有数据(no_more_data=0)的话, 暂不处理最后一个/r (因为不知道后面会不会是/n) // 前面的数据倒是可以处理, 因为已经确定前面没有/r/n了, 只可能存在单独的/r或/n字符, 单独的/r或/n字符是没有用的 linelen = p->tot_len - 1; } else linelen = p->tot_len; // 处理整个数据块 err = httpd_post_process_multipart_line(state, linelen); p = state->multipart->p; // 一定要记得重新给p赋值 if (err != ERR_OK) return err; } return ERR_OK;}/* 处理文件上传表单数据中的一行内容 */static err_t httpd_post_process_multipart_line(struct httpd_post_state *state, int linelen){ char buffer[100]; // 设置一个固定的缓冲区, 以免连续的/r/n换行导致内存一直不停地分配又释放, 浪费时间 // 但是这个缓冲区又不能太大, 不能超过单片机的栈的大小 char str[70]; char *line, *p, *q, *r; err_t err = ERR_OK; int len, quoted, ret; int line_complete = 0; FRESULT fr; UINT bw; if (linelen + 1 <= sizeof(buffer)) line = buffer; else { // 固定缓冲区放不下了才新分配内存 line = mem_malloc(linelen + 1); if (line == NULL) { printf("%s: mem_malloc() failed/n", __func__); return ERR_MEM; } } pbuf_copy_partial(state->multipart->p, line, linelen, 0); line[linelen] = '/0'; state->multipart->p = pbuf_free_header(state->multipart->p, linelen); state->content_pos += linelen; if (linelen >= 2 && strcmp(line + linelen - 2, "/r/n") == 0) line_complete = 1; if (state->multipart->boundary_len == 0) { if (line[0] == '-' && line[1] == '-') { // 读取边界字符串的内容 printf("%s: first boundary/n", __func__); if (!line_complete) { err = ERR_ARG; printf("%s: incomplete boundary string/n", __func__); goto end; } else if (sizeof(state->multipart->boundary) < linelen + 1) { err = ERR_MEM; printf("%s: boundary string is too long/n", __func__); goto end; } strlcpy(state->multipart->boundary, line, sizeof(state->multipart->boundary)); state->multipart->boundary_len = linelen - 2; } } else if (strncasecmp(line, state->multipart->boundary, state->multipart->boundary_len) == 0) { // 遇到边界字符串就说明上一个控件的内容已经读完了 printf("%s: boundary/n", __func__); if (!line_complete) { err = ERR_ARG; printf("%s: incomplete boundary string/n", __func__); goto end; } state->multipart->reading_content = 0; if (state->multipart->is_file != 0) { // 关闭文件 if (state->multipart->is_file == 2) { printf("%s: close file. size=%llu/n", __func__, state->multipart->content_len); f_close(&state->multipart->fil); } state->multipart->is_file = 0; snprintf(str, sizeof(str), "%llu", state->multipart->content_len); err = httpd_post_store_multipart_string(state->multipart, 's', str, -1, NULL); // 记录文件大小 if (err != ERR_OK) goto end; } } else if (!state->multipart->reading_content) { if (!line_complete) { err = ERR_ARG; printf("%s: incomplete field information/n", __func__); goto end; } line[linelen - 2] = '/0'; if (strncasecmp(line, "Content-", 8) == 0) { quoted = 0; for (p = line; p != NULL; p = r) { for (r = p; *r != '/0'; r++) { if (*r == '"') quoted = !quoted; else if (*r == ';' && !quoted) { *r++ = '/0'; break; } } if (*r == '/0') r = NULL; q = strchr(p, '='); if (q == NULL) q = strchr(p, ':'); if (q != NULL) { *q = '/0'; q++; trim(p); trim(q); if (*q == '"') { len = strlen(q); if (q[len - 1] == '"') { q[len - 1] = '/0'; q++; } } if (strcmp(p, "name") == 0) err = httpd_post_store_multipart_string(state->multipart, 'n', q, -1, NULL); else if (strcmp(p, "filename") == 0) { state->multipart->is_file = 1; err = httpd_post_store_multipart_string(state->multipart, 'f', q, -1, &state->multipart->filename); } else if (strcasecmp(p, "Content-Type") == 0) err = httpd_post_store_multipart_string(state->multipart, 't', q, -1, NULL); else err = ERR_OK; if (err != ERR_OK) goto end; } } } else if (line[0] == '/0') { // 遇到空行说明要开始读取控件的内容了 printf("%s: empty line/n", __func__); state->multipart->reading_content = 1; state->multipart->content_len = 0; state->multipart->crlf = 0; if (state->multipart->is_file == 0) { err = httpd_post_store_multipart_string(state->multipart, 'c', "", 0, NULL); if (err != ERR_OK) goto end; } else if (state->multipart->is_file == 1 && state->multipart->filename[0] != '/0') { // 打开文件 ret = httpd_post_generate_file_path(state->multipart->filename, str, sizeof(str)); if (ret != -1) { err = httpd_post_store_multipart_string(state->multipart, 'p', str, -1, NULL); if (err != ERR_OK) goto end; printf("%s: open file %s/n", __func__, str); fr = f_open(&state->multipart->fil, str, FA_CREATE_ALWAYS | FA_WRITE); if (fr == FR_OK) state->multipart->is_file = 2; // 文件打开成功 else printf("%s: f_open() failed. fr=%d/n", __func__, fr); // 文件打开失败 } } } } else { if (state->multipart->crlf) { printf("%s: read 2 bytes of content/n", __func__); state->multipart->content_len += 2; if (state->multipart->is_file == 0) { err = httpd_post_store_multipart_string(state->multipart, 0, "/r/n", 2, NULL); if (err != ERR_OK) goto end; } else if (state->multipart->is_file == 2) { // 写文件 fr = f_write(&state->multipart->fil, "/r/n", 2, &bw); if (bw != 2) { printf("%s: f_write() failed. fr=%d, len=2, bw=%u/n", __func__, fr, bw); err = ERR_MEM; goto end; } } } if (line_complete) { state->multipart->crlf = 1; len = linelen - 2; } else { state->multipart->crlf = 0; len = linelen; } if (len > 0) { printf("%s: read %d byte(s) of content/n", __func__, len); state->multipart->content_len += len; if (state->multipart->is_file == 0) { err = httpd_post_store_multipart_string(state->multipart, 0, line, len, NULL); if (err != ERR_OK) goto end; } else if (state->multipart->is_file == 2) { // 写文件 fr = f_write(&state->multipart->fil, line, len, &bw); if (bw != len) { printf("%s: f_write() failed. fr=%d, len=%d, bw=%u/n", __func__, fr, len, bw); err = ERR_MEM; goto end; } } } }end: if (line != buffer) mem_free(line); return err;}/* 为上传的文件选择一个保存路径 */static int httpd_post_generate_file_path(const char *srcname, char *buffer, int bufsize){ char datestr[9]; char ext[10] = {0}; char *p; int i; struct tm tm; time_t t; FRESULT fr; time(&t); localtime_r(&t, &tm); strftime(datestr, sizeof(datestr), "%Y%m%d", &tm); if (srcname != NULL) { p = strrchr(srcname, '.'); if (p != NULL) { for (i = 0; i < sizeof(ext) - 1 && p[i] != '/0'; i++) ext[i] = tolower(p[i]); ext[i] = '/0'; } } for (i = 0; i < 10000; i++) { snprintf(buffer, bufsize, "C://fileupload//%s_%04d%s", datestr, i, ext); fr = f_stat(buffer, NULL); if (fr == FR_NO_FILE) break; // 文件名可用 else if (fr == FR_NOT_ENABLED || fr == FR_NO_PATH) { printf("%s: f_stat() failed. fr=%d/n", __func__, fr); return -1; } } if (i == 10000) { // 所有文件名都不可用 printf("%s: file %s already exists/n", __func__, buffer); return -1; } return i;}/* 在字符串缓冲区中保存一个字符串 */// flag!=0: 新增一个字符串, 标志为flag// flag=0: 在之前的字符串后面追加内容// 此函数会将str里面的所有'/0'字符存储成'?'static err_t httpd_post_store_multipart_string(struct httpd_post_multipart_state *multipart, char flag, const char *str, int len, char **strout){ char *mem, *p; int i, memsize, size; // 计算字符串的长度 if (len < 0) len = strlen(str); if (strout != NULL) *strout = NULL; // 计算所需的存储空间 if (flag != 0) size = len + 2; // flag+字符串+结束符 else { LWIP_ASSERT("appending to an existing string", multipart->strbuf_used != 0); size = len; // 需要追加的字符串 } if (multipart->strbuf_used + size > multipart->strbuf_capacity) { // 缓冲区不够的话, 就再开辟一块更大的空间 memsize = multipart->strbuf_capacity + size + 300; mem = mem_malloc(memsize); if (mem == NULL) { printf("%s: mem_malloc() failed/n", __func__); return ERR_MEM; } if (multipart->strbuf != NULL) { // 将旧缓冲区存放的内容复制到新缓冲区, 并删除旧缓冲区 memcpy(mem, multipart->strbuf, multipart->strbuf_used); mem_free(multipart->strbuf); } // 替换成新缓冲区 multipart->strbuf = mem; multipart->strbuf_capacity = memsize; } // 将字符串放入缓冲区 if (flag != 0) { p = multipart->strbuf + multipart->strbuf_used; *p++ = flag; } else p = multipart->strbuf + multipart->strbuf_used - 1; // 取代之前的'/0'字符 if (strout != NULL) *strout = p; for (i = 0; i < len; i++) { if (str[i] != '/0') p[i] = str[i]; else p[i] = '?'; } p[len] = '/0'; multipart->strbuf_used += size; return ERR_OK;}/* 开始处理http post请求*/err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd){ int multipart; struct httpd_post_state *state; printf("[httpd_post_begin] connection=0x%p, uri=%s/n", connection, uri); printf("%.*s/n", http_request_len, http_request); if (strcmp(uri, "/form_test.html") != 0 && strcmp(uri, "/upload_test.html") != 0) { //strlcpy(response_uri, "/bad_request.html", response_uri_len); return ERR_ARG; } multipart = httpd_is_multipart(http_request, http_request_len); state = httpd_post_create_state(connection, content_len, multipart); if (state == NULL) { //strlcpy(response_uri, "/out_of_memory.html", response_uri_len); return ERR_MEM; } return ERR_OK;}/* 接收表单数据 */err_t httpd_post_receive_data(void *connection, struct pbuf *p){ err_t err; struct httpd_post_state *state; printf("[httpd_post_receive_data] connection=0x%p, payload=0x%p, len=%d/n", connection, p->payload, p->tot_len); state = httpd_post_find_state(connection); if (state != NULL) { if (state->multipart == NULL) { pbuf_copy_partial(p, state->content + state->content_pos, p->tot_len, 0); state->content_pos += p->tot_len; pbuf_free(p); } else { err = httpd_post_process_multipart(state, p); if (err != ERR_OK) return err; } } return ERR_OK;}/* 结束处理http post请求*/void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){ int i; struct httpd_post_state *state; printf("[httpd_post_finished] connection=0x%p/n", connection); state = httpd_post_find_state(connection); if (state != NULL) { if (state->multipart == NULL) httpd_post_parse(state); else { httpd_post_process_multipart(state, NULL); httpd_post_parse_multipart(state); } printf("param_count=%d/n", state->param_count); for (i = 0; i < state->param_count; i++) { if (state->files != NULL && state->files[i].name != NULL) printf("[File] name=%s, filename=%s, path=%s, size=%llu, mimetype=%s/n", state->params[i], state->files[i].name, STRPTR(state->files[i].path), state->files[i].size, STRPTR(state->files[i].mimetype)); else printf("[Param] name=%s, value=%s/n", state->params[i], STRPTR(state->values[i])); } httpd_post_delete_state(state); //strlcpy(response_uri, "/success.html", response_uri_len); }}void test_init(void){ FRESULT fr; fr = f_stat("C://fileupload", NULL); if (fr == FR_NO_FILE) { // 文件夹不存在, 创建文件夹 fr = f_mkdir("C://fileupload"); if (fr == FR_OK) printf("%s: created fileupload folder/n", __func__); else printf("%s: failed to create fileupload folder. fr=%d/n", __func__, fr); } else if (fr == FR_NOT_ENABLED) { // 这种情况一般是因为fatfs没有初始化 printf("%s: disk has not been initialized/n", __func__); } else if (fr != FR_OK) printf("%s: f_stat() failed. fr=%d/n", __func__, fr);}
分界字符串取的是表单内容的首行,而不是http header里面Content-Type中的boundary字符串,主要是因为这两者前导横杠的个数不同。http header Content-Type的boundary字符串有27个前导横杠,而表单内容里面的每个分界字符串都是29个前导横杠,多了两个横杠。在和表单里面其他的分界字符串作比较时,如果取的是表单内容首行的分界字符串,就不用特别处理横杠的个数,直接用strcmp函数比较就可以了,更加便捷。
程序运行结果:
文件上传完成后,浏览器显示404错误页面是因为httpd_post_finished函数没有对response_uri字符数组赋值。在串口里面可以看到各个表单控件的名称和内容,文件框控件还可以看到文件名、文件保存路径、文件大小和文件类型。串口显示中文乱码是Tera Term软件本身的bug,不是我们程序的问题。
上传的文件统一保存到C:/fileupload目录中,以当前日期加数字命名。
在fatfs里面,要启用C盘盘符访问,需要在ff13c/ffconf.h里面定义:
#define FF_STR_VOLUME_ID 1 // 允许使用盘符
#define FF_VOLUME_STRS "C"
路径中既可以使用正斜杠/,也可以使用反斜杠/。在C语言字符串里面,反斜杠必须双写://。
ffconf.h里面的FF_FS_LOCK选项也很重要,这个值最好设置为大于0的数,表示启用文件锁并指定可以同时打开的文件个数,同一个文件不能同时多次打开。不然的话,如果多个函数同时打开同一个文件的话,整个文件系统很可能就会被破坏掉!这个选项在HTTP和FTP程序里面是很有用的。
lwip自带的http服务器默认最大只能上传95MB的文件,这是因为httpd.c里面将HTTP_HDR_CONTENT_LEN_DIGIT_MAX_LEN定义成了10。10减去2(/r/n的长度)后是8,Content-Length最大只允许为8位数,99999999字节差不多就是95.37MB左右。
如果HTTP_HDR_CONTENT_LEN_DIGIT_MAX_LEN=11的话,那么最大可以上传999999999字节,也就是953.67MB的文件。
如果HTTP_HDR_CONTENT_LEN_DIGIT_MAX_LEN=12,最大只能上传2147483647字节,即2047.999999MB的文件,这是因为content_len变量和atoi函数返回值的类型为int。如果想要上传更大的文件的话,httpd.c就得大改了,要把所有的跟content_len变量有关的变量类型由int全部改成int64_t。
下一篇:lwip-2.1.3自带的httpd网页服务器使用教程(五)使用COOKIE实现用户登录