lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传

上一篇: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方式。
表单提交方式由

标签的method属性决定。method="get"是GET方式,method="post"是POST方式。
另外,标签的action属性指定表单要提交到哪个页面上。如果action为空字符串"",那么就是提交到当前页面上。
GET方式提交表单后,所有带有name属性的表单控件的内容都会出现在URL(浏览器网址)上,也就是说GET方式其实就是以URL参数的方式提交表单,这个之前已经讲过了。
我们今天要讲的是POST方式提交的表单。POST方式提交后,表单控件的内容不会出现在URL上,这一定程度上提高了安全性。POST方式还有一个好处,就是提交的数据量比GET方式更大,不受URL最大长度的限制。
POST表单又细分为两种类型:普通表单和文件上传表单。当标签不存在enctype属性时,表单为普通表单。当标签的enctype="multipart/form-data"时,表单为文件上传表单。
文件上传表单是专门用来上传文件的表单,其格式与普通表单完全不一样,需要单独解析。在Adobe Dreamweaver CS3这款网页设计软件中,只要插入了文件框控件,Dreamweaver就会自动帮我们在标签上添加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.1
Accept: 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.html
Accept-Language: zh-cn
User-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-urlencoded
Accept-Encoding: gzip, deflate
Host: stm32f103ze
Content-Length: 810
Connection: Keep-Alive
Cache-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

第三个函数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功能要求我们实现的那三个函数。请看代码:

/* 开始处理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)
{
  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)
  {
    pbuf_copy_partial(p, state->content + state->content_pos, p->tot_len, 0);
    state->content_pos += p->tot_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], 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]);
      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程序打包。网页的内容如下:





表单测试





  

表单测试

单选框:

复选框:

我们来看看程序的运行结果:

lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传_第1张图片

点击提交按钮提交表单后,浏览器的网址没变,但是提示404错误(找不到网页),这是httpd_post_finished函数没有对response_uri字符数组赋值造成的。
在串口输出中我们可以看到提交的表单内容。其中包括三个文本框输入的内容,还有单选框选择的项目,还有勾选了的复选框。没有勾选的复选框不会出现在表单内容中。
表单内容还有下拉菜单框选择的项目,以及文件选择框选择的文件名(不含文件路径和文件内容),最后是多行文本框输入的内容(换行也正确显示了)。

lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传_第2张图片

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,把所有的空格都要替换为 。





表单提交成功



表单提交成功


您选择的文件是:

您在多行文本框中输入的内容为:

新定义了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函数中一起释放。

/* 结束处理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], 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-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传_第3张图片

可以在多行文本框里面提交一段很长的文本,只要lwip的内存(MEM_SIZE)够大就能显示成功。换行符、空格和tab字符也能正确显示。

lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传_第4张图片

文件上传类型POST表单的解析

(本节例程名称:post_test3)
普通POST表单只会提交文件框里面所选文件的文件名,不会上传文件的内容。如果要想上传文件内容的话就得使用文件上传类型的POST表单,在

标签上添加enctype="multipart/form-data"属性。这种文件上传类型的POST表单提交后的内容格式和普通表单完全不一样,需要单独处理。

我们先来看一下文件上传表单提交后的http header内容。

HTTP/1.1
Accept: */*
Referer: http://stm32f103ze/form_test.html
Accept-Language: zh-cn
User-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=---------------------------7e729f1bf0a7a
Accept-Encoding: gzip, deflate
Host: stm32f103ze
Content-Length: 39415
Connection: Keep-Alive
Cache-Control: no-cache

其中Content-Type以multipart/form-data开头,后面还有一个boundary字符串,叫做分界符字符串。这个分界符字符串非常重要,分界符字符串是表单内容中各个控件内容的分界符。
Content-Length是整个表单内容(包括文件内容)的总长度。lwip调用httpd_post_begin函数时传入的content_len参数就等于http header中Content-Length属性的数值。

我们再来看一下表单内容的格式。

lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传_第5张图片

lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传_第6张图片

可以看到,每个表单控件都是用分界符字符串隔开的。
对于普通表单控件(如文本框、单选框、复选框、下拉菜单框等等),name字符串是控件名,后面是控件内容的原文。控件内容是没有进行urlencode编码的,这和普通表单内容的格式很不一样。
对于文件框控件,还多了一个filename字符串,里面存的是文件框选择的文件名。Content-Type是文件的类型,比如image/pjpeg是jpg图片文件(这个数据是浏览器提供的)。后面就是文件的原始二进制内容了。
整个表单内容最后是以分界符字符再加两个'-'字符结束的。

由于文件上传表单的内容一般都很大,STM32内存是放不下的,我们可以先把整个表单内容用FatFs写入到SD卡或SPI Flash的一个临时文件上,后面再来慢慢读取并解析,分离出其中的表单控件内容,还有文件内容。临时文件名为test_XXXXXXXX.bin,其中XXXXXXXX是connection指针(连接标识对象)指向的内存地址。
如果没有外接存储设备,也可以在STM32的内部Flash里面划分一块固定的区域,用来存放表单内容。如STM32F407VG单片机有1MB的Flash,前面的扇区0~5可以用来存放程序,把扇区6~11划分为表单内容的临时存放区域,flash地址为0x08040000-0x080fffff,大小为768KB。这样做的好处是不再需要FatFs了,直接用0x08040000地址就可以访问flash存储空间,但是需要使用大容量flash的stm32型号,而且最大能上传的文件的大小也只有几百KB。一般来说,这样的大小用来实现网页在线上传hex文件升级stm32程序,完全足够了。

下一篇:lwip-2.1.3自带的httpd网页服务器使用教程(五)使用COOKIE实现用户登录

你可能感兴趣的:(STM32,服务器,运维,lwip,httpd,stm32)