środa, 8 listopada 2023

STM32H7 - HTTP, CGI, SSI, POST

W tym poście chciałbym opisać sposób wykonania podstawowych operacji na stronie HTTP, podczas wykorzystywania STM32 jako serwer HTTP.


Poniżej w skrócie opiszę działanie SSI oraz CGI, oraz przedstawię prostą integrację obu zdarzeń w jednej kontrolce. 

Konfiguracja sieci jest identyczna jak dla wcześniejszych projektów. 

CGI:


CGI jest to tzw. Common Gateway Interface. Stosowany do odczytywania danych z serwera, 

W bibliotece LWIP należy zaznaczyć LWIP_HTTPD_CGI na 1:

  1. #define LWIP_HTTPD_CGI 1

Implementacja CGI jest zastosowana w funkcji http_find_file():

  1. #if LWIP_HTTPD_CGI
  2.     http_cgi_paramcount = -1;
  3.     /* Does the base URI we have isolated correspond to a CGI handler? */
  4.     if (httpd_num_cgis && httpd_cgis) {
  5.       for (i = 0; i < httpd_num_cgis; i++) {
  6.         if (strcmp(uri, httpd_cgis[i].pcCGIName) == 0) {
  7.           /*
  8.            * We found a CGI that handles this URI so extract the
  9.            * parameters and call the handler.
  10.            */
  11.           http_cgi_paramcount = extract_uri_parameters(hs, params);
  12.           uri = httpd_cgis[i].pfnCGIHandler(i, http_cgi_paramcount, hs->params,
  13.                                          hs->param_vals);
  14.           break;
  15.         }
  16.       }
  17.     }
  18. #endif /* LWIP_HTTPD_CGI */

Maksymalna liczba CGI jaka może zostać wysłana jednorazowo jest definiowana przez makro LWIP_HTTPD_MAX_CGI_PARAMETERS. Domyślnie wartość ta jest ustawiona na 16. 

  1. /* The maximum number of parameters that the CGI handler can be sent. */
  2. #if !defined LWIP_HTTPD_MAX_CGI_PARAMETERS || defined __DOXYGEN__
  3. #define LWIP_HTTPD_MAX_CGI_PARAMETERS 16
  4. #endif

Oznacza to ilość parametrów przesyłanych jednorazowo, czyli jeśli stworzymy kilka stron do przesyłania parametrów, to należy pilnować aby na żadnej z nich nie została przekroczona wartość zdefiniowana w tym makrze. Tą wartość można też zwiększyć w razie potrzeby. 

Obsługę tagów wykonujemy w funkcji CGIDATA_Handler, która zwraca nazwę strony jaka ma się załadować do kontrolera. 

  1. const char *CGIDATA_Handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[])
  2. {
  3.     if (iIndex == 0)
  4.     {
  5.         for (int i=0; i<iNumParams; i++)
  6.         {
  7.             if (strcmp(pcParam[i], "ipnam") == 0)  // if the fname string is found
  8.             {
  9.                 memset(ipAddr, '\0', 15);
  10.                 strcpy(ipAddr, pcValue[i]);
  11.             }
  12.             else if (strcmp(pcParam[i], "maskn") == 0)  // if the fname string is found
  13.             {
  14.                 memset(maskAddr, '\0', 15);
  15.                 strcpy(maskAddr, pcValue[i]);
  16.             }
  17.             else if (strcmp(pcParam[i], "gaten") == 0)  // if the fname string is found
  18.             {
  19.                 memset(gateAddr, '\0', 15);
  20.                 strcpy(gateAddr, pcValue[i]);
  21.             }
  22.         }
  23.     }
  24.     return "/cgidata.shtml";
  25. }

Inicjalizacja następuje po wywołaniu biblioteki i rozpoczęciu pracy serwera www:

  1. const char *CGIDATA_Handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]);
  2. const tCGI DATA_CGI = {"/data.cgi", CGIDATA_Handler};
  3.  
  4. void http_server_init (void)
  5. {
  6.     httpd_init();
  7.  
  8.     http_set_ssi_handler(ssi_handler, (char const**) TAGS, 3);
  9.  
  10.     CGI_TAB[0] = DATA_CGI;
  11.  
  12.     http_set_cgi_handlers (CGI_TAB, 4);
  13. }

Przekazanie zdefiniowanych parametrów CGI, odbywa się przez funckję:

  1. void
  2. http_set_cgi_handlers(const tCGI *cgis, int num_handlers)
  3. {
  4.   LWIP_ASSERT("no cgis given", cgis != NULL);
  5.   LWIP_ASSERT("invalid number of handlers", num_handlers > 0);
  6.  
  7.   httpd_cgis = cgis;
  8.   httpd_num_cgis = num_handlers;
  9. }

Na przygotowanej stronie http należy umieścić w formularzu oraz przekazać przez parametr action:

  1. <form action="/data.cgi">

Kontrolka obsługująca CGI musi posiadać nazwę, którą wcześniej umieściliśmy w obsłudze zdarzenia:

  1. <tr>
  2.     <td>IP</td>
  3.     <td><input type="text" id="ipnam" name="ipnam" size="15" maxlength="15" value=""></td>
  4. </tr>

W przypadku wykorzystywania kontrolki checkbox należy pamiętać, że wartość jest przesyłana gdy kontrolka jest zaznaczona. Gdy jest odznaczona, parametr nie zostanie przesłany. Z tego powodu w obsłudze CGI należy sprawdzić czy taki parametr pojawił się na liście. Gdy go brakuje oznacza to, że kontrolka jest odznaczona i należy odpowiednio ją obsłużyć. 

SSI:

Czyli Server Side Includes, pozwala na wprowadzanie danych na stronę, przy jej ładowaniu oraz w późniejszym czasie, w określonym interwale czasowym.

W celu umieszczenia danych na stronie, należy wprowadzić odpowiedni tag. Poniżej przykład dla komórki text:

  1. <td><input type="text" id="spstx" name="spstx" size="15" maxlength="15" value=<!--#sps-->></td>

Następnie trzy literowy tak, w tym przypadku sps, musimy umieścić w tablicy.

  1. char const* TAGCHAR[]={"ipn", "sps", "man", "gan"};

Taką tablicę następnie przekazujemy do obsługi zdarzenia:

  1. http_set_ssi_handler(ssi_handler, (char const**) TAGS, 4);

Do funkcji przekazujemy, funkcję, która będzie się zajmowała obsługą wyjątków, tablicę z nazwami tagów oraz wartość odpowiadającą ilości tagów jakie są umieszczone w tablicy:

  1. void
  2. http_set_ssi_handler(tSSIHandler ssi_handler, const char **tags, int num_tags)
  3. {
  4.   LWIP_DEBUGF(HTTPD_DEBUG, ("http_set_ssi_handler\n"));
  5.  
  6.   LWIP_ASSERT("no ssi_handler given", ssi_handler != NULL);
  7.   httpd_ssi_handler = ssi_handler;
  8.  
  9. #if LWIP_HTTPD_SSI_RAW
  10.   LWIP_UNUSED_ARG(tags);
  11.   LWIP_UNUSED_ARG(num_tags);
  12. #else /* LWIP_HTTPD_SSI_RAW */
  13.   LWIP_ASSERT("no tags given", tags != NULL);
  14.   LWIP_ASSERT("invalid number of tags", num_tags > 0);
  15.  
  16.   httpd_tags = tags;
  17.   httpd_num_tags = num_tags;
  18. #endif /* !LWIP_HTTPD_SSI_RAW */
  19. }
  20. #endif /* LWIP_HTTPD_SSI */

Handler obsługujący wysyłanie danych do kontrolera wygląda następująco:

  1. uint16_t ssi_handler (int iIndex, char *pcInsert, int iInsertLen)
  2. {
  3.     char tmpArray[25] = {0x00};
  4.  
  5.     switch (iIndex) {
  6.     case TAG_SSI_IPN_IP_URZADZENIA:
  7.         Get_Board_Ip_String_Data(&tmpArray[0]);
  8.         indx+=1;
  9.         sprintf(pcInsert, tmpArray);
  10.         return strlen(pcInsert);
  11.         break;
  12.     case TAG_SSI_MAN_MASKA_URZADZENIA:
  13.         Get_Board_Mask_String_Data(&tmpArray[0]);
  14.         indx+=1;
  15.         sprintf(pcInsert, tmpArray);
  16.         return strlen(pcInsert);
  17.         break;
  18.     case TAG_SSI_GAN_BRAMA_URZADZENIA:
  19.         Get_Board_Gate_String_Data(&tmpArray[0]);
  20.         indx+=1;
  21.         sprintf(pcInsert, tmpArray);
  22.         return strlen(pcInsert);
  23.         break;
  24. default:
  25. break;
  26.     }
  27. return 0;
  28. }

Instrukcja break umieszczona na końcu każdego case'a jest właściwie nie potrzebna, ponieważ wychodzimy z funkcji przed jej wywołaniem. Jedynym jej zastosowaniem jest utrzymanie poprawnej składni, aby kod był w miarę czytelny.

W każdej instukcji warunkowej przygotowujemy dane do wysłania, które zostaną umieszczone na stronie zamiast taga SSI. Dodatkowo zwiększamy indeks, którym porównujemy wartości z danymi z tabeli. 

Należy pamiętać aby, ustawić wartość LWIP_HTTPD_SSI_INCLUDE_TAG na 0. 

  1. /** Set this to 0 to not send the SSI tag (default is on, so the tag will
  2.  * be sent in the HTML page */
  3. #if !defined LWIP_HTTPD_SSI_INCLUDE_TAG || defined __DOXYGEN__
  4. #define LWIP_HTTPD_SSI_INCLUDE_TAG           0
  5. #endif

Ustawienie wartości na 1, spowoduje umieszczanie tagów razem z przesłanymi wartościami:

  1. <tr>
  2. <td>IP</td>
  3. <td><input type="text" id="ipnam" name="ipnam" size="15" maxlength="15" value=<!--#ipn-->192.156.234.43></td>
  4. </tr>

Spowoduje to błędne wyświetlanie danych na stronie:


Dzięki SSI można przesłać dane na stronę jednorazowo, lub co określony interwał czasowy. Do tego celu należy zastosować atrybuty http. W przypadku odświeżania strony co 30 sekund, należy w kodzie html wpisać:

  1. <meta http-equiv="refresh" content ="1">

SSI, CGI w jednej kontrolce:


W celu obsługi CGI oraz SSI w jednej kontrolce, należy jedynie połączyć wywołania. Dla powyższego przypadku dotyczącego kontrolki text:

  1. <tr>
  2. <td>IP</td>
  3. <td><input type="text" id="ipnam" name="ipnam" size="15" maxlength="15" value=<!--#ipn-->></td>
  4. </tr>

Wartość dla CGI będzie brana na podstawie parametru name, natomiast SSI wykorzysta tag ipn. Całość będzie obsługiwana przez dwa różne handlery. 

POST:

W celu obsługi metody POST należy ustawić flagę:

  1. #define LWIP_HTTPD_SUPPORT_POST 1

Dodatkowo należy wprowadzić obsługę trzech funkcji, 

  1. err_t httpd_post_begin(void *connection, const char *uri, const char *http_request,
  2.                  u16_t http_request_len, int content_len, char *response_uri,
  3.                  u16_t response_uri_len, u8_t *post_auto_wnd)
  4. err_t httpd_post_receive_data(void *connection, struct pbuf *p)
  5. void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len)

Wykorzystałem przykład umieszczony w serwisie github pod tym linkiem:

W pliku HTML musimy wprowadzić następujące dane:

  1. <form id="login-form" action="login.cgi" method="post">

Atrybut action wprowadzi wykorzystywany adres URL. Method określa w jaki sposób dane zostaną przesłane do serwera

Na samym początku jest funckja httpd_post_begin

  1. err_t
  2. httpd_post_begin(void *connection, const char *uri, const char *http_request,
  3.                  u16_t http_request_len, int content_len, char *response_uri,
  4.                  u16_t response_uri_len, u8_t *post_auto_wnd)
  5. {
  6.   LWIP_UNUSED_ARG(connection);
  7.   LWIP_UNUSED_ARG(http_request);
  8.   LWIP_UNUSED_ARG(http_request_len);
  9.   LWIP_UNUSED_ARG(content_len);
  10.   LWIP_UNUSED_ARG(post_auto_wnd);
  11.   if (!memcmp(uri, "/login.cgi", 11))
  12.   {
  13.     if (current_connection != connection) {
  14.       current_connection = connection;
  15.       valid_connection = NULL;
  16.       snprintf(response_uri, response_uri_len, "/loginfail.html");
  17.       *post_auto_wnd = 1;
  18.       return ERR_OK;
  19.     }
  20.   }
  21.   return ERR_VAL;
  22. }

W niej następuje sprawdzenie wejściowych parametrów, ustawienie strony do załadowania, oraz domyślnej strony z odpowiedzią. 

Następna jest funkcja httpd_post_receive_data, która zajmuje się przetwarzaniem odebranych danych z odpowiedzią:

  1. err_t
  2. httpd_post_receive_data(void *connection, struct pbuf *p)
  3. {
  4.   if (current_connection == connection) {
  5.     u16_t token_user = pbuf_memfind(p, "pname=", 6, 0);
  6.     u16_t token_pass = pbuf_memfind(p, "ppass=", 6, 0);
  7.     if ((token_user != 0xFFFF) && (token_pass != 0xFFFF)) {
  8.       u16_t value_user = token_user + 6;
  9.       u16_t value_pass = token_pass + 6;
  10.       u16_t len_user = 0;
  11.       u16_t len_pass = 0;
  12.       u16_t tmp;
  13.       /* find user len */
  14.       tmp = pbuf_memfind(p, "&", 1, value_user);
  15.       if (tmp != 0xFFFF) {
  16.         len_user = tmp - value_user;
  17.       } else {
  18.         len_user = p->tot_len - value_user;
  19.       }
  20.       /* find pass len */
  21.       tmp = pbuf_memfind(p, "&", 1, value_pass);
  22.       if (tmp != 0xFFFF) {
  23.         len_pass = tmp - value_pass;
  24.       } else {
  25.         len_pass = p->tot_len - value_pass;
  26.       }
  27.       if ((len_user > 0) && (len_user < USER_PASS_BUFSIZE) &&
  28.           (len_pass > 0) && (len_pass < USER_PASS_BUFSIZE)) {
  29.         /* provide contiguous storage if p is a chained pbuf */
  30.         char buf_user[USER_PASS_BUFSIZE];
  31.         char buf_pass[USER_PASS_BUFSIZE];
  32.         char *user = (char *)pbuf_get_contiguous(p, buf_user, sizeof(buf_user), len_user, value_user);
  33.         char *pass = (char *)pbuf_get_contiguous(p, buf_pass, sizeof(buf_pass), len_pass, value_pass);
  34.         if (user && pass) {
  35.           user[len_user] = 0;
  36.           pass[len_pass] = 0;
  37.           if (!strcmp(user, "admin") && !strcmp(pass, "123456")) {
  38.             /* user and password are correct, create a "session" */
  39.             valid_connection = connection;
  40.             memcpy(last_user, user, sizeof(last_user));
  41.           }
  42.         }
  43.       }
  44.     }
  45.     /* not returning ERR_OK aborts the connection, so return ERR_OK unless the
  46.        conenction is unknown */
  47.     return ERR_OK;
  48.   }
  49.   return ERR_VAL;
  50. }

Ostatnia funkcja czyli https_post_finished, ustawia stronę do załadowania:

  1. void
  2. httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len)
  3. {
  4.   /* default page is "login failed" */
  5.   snprintf(response_uri, response_uri_len, "/loginfail.html");
  6.   if (current_connection == connection) {
  7.     if (valid_connection == connection) {
  8.       /* login succeeded */
  9.       snprintf(response_uri, response_uri_len, "/cgidata.shtml");
  10.     }
  11.     current_connection = NULL;
  12.     valid_connection = NULL;
  13.   }
  14. }

Sprawdzenie przesyłanych ramek można wykonać w programie wireshark:


Należy pamiętać, że jest to protocol HTTP, nie HTTPS, wobec tego przesłane informacje będą łatwe do znalezienia.

Źródła: