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:
- #define LWIP_HTTPD_CGI 1
Implementacja CGI jest zastosowana w funkcji http_find_file():
- #if LWIP_HTTPD_CGI
- http_cgi_paramcount = -1;
- /* Does the base URI we have isolated correspond to a CGI handler? */
- if (httpd_num_cgis && httpd_cgis) {
- for (i = 0; i < httpd_num_cgis; i++) {
- if (strcmp(uri, httpd_cgis[i].pcCGIName) == 0) {
- /*
- * We found a CGI that handles this URI so extract the
- * parameters and call the handler.
- */
- http_cgi_paramcount = extract_uri_parameters(hs, params);
- uri = httpd_cgis[i].pfnCGIHandler(i, http_cgi_paramcount, hs->params,
- hs->param_vals);
- break;
- }
- }
- }
- #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.
- /* The maximum number of parameters that the CGI handler can be sent. */
- #if !defined LWIP_HTTPD_MAX_CGI_PARAMETERS || defined __DOXYGEN__
- #define LWIP_HTTPD_MAX_CGI_PARAMETERS 16
- #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.
- const char *CGIDATA_Handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[])
- {
- if (iIndex == 0)
- {
- for (int i=0; i<iNumParams; i++)
- {
- if (strcmp(pcParam[i], "ipnam") == 0) // if the fname string is found
- {
- memset(ipAddr, '\0', 15);
- strcpy(ipAddr, pcValue[i]);
- }
- else if (strcmp(pcParam[i], "maskn") == 0) // if the fname string is found
- {
- memset(maskAddr, '\0', 15);
- strcpy(maskAddr, pcValue[i]);
- }
- else if (strcmp(pcParam[i], "gaten") == 0) // if the fname string is found
- {
- memset(gateAddr, '\0', 15);
- strcpy(gateAddr, pcValue[i]);
- }
- }
- }
- return "/cgidata.shtml";
- }
Inicjalizacja następuje po wywołaniu biblioteki i rozpoczęciu pracy serwera www:
- const char *CGIDATA_Handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]);
- const tCGI DATA_CGI = {"/data.cgi", CGIDATA_Handler};
- void http_server_init (void)
- {
- httpd_init();
- http_set_ssi_handler(ssi_handler, (char const**) TAGS, 3);
- CGI_TAB[0] = DATA_CGI;
- http_set_cgi_handlers (CGI_TAB, 4);
- }
Przekazanie zdefiniowanych parametrów CGI, odbywa się przez funckję:
- void
- http_set_cgi_handlers(const tCGI *cgis, int num_handlers)
- {
- LWIP_ASSERT("no cgis given", cgis != NULL);
- LWIP_ASSERT("invalid number of handlers", num_handlers > 0);
- httpd_cgis = cgis;
- httpd_num_cgis = num_handlers;
- }
Na przygotowanej stronie http należy umieścić w formularzu oraz przekazać przez parametr action:
- <form action="/data.cgi">
Kontrolka obsługująca CGI musi posiadać nazwę, którą wcześniej umieściliśmy w obsłudze zdarzenia:
- <tr>
- <td>IP</td>
- <td><input type="text" id="ipnam" name="ipnam" size="15" maxlength="15" value=""></td>
- </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:
- <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.
- char const* TAGCHAR[]={"ipn", "sps", "man", "gan"};
Taką tablicę następnie przekazujemy do obsługi zdarzenia:
- 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:
- void
- http_set_ssi_handler(tSSIHandler ssi_handler, const char **tags, int num_tags)
- {
- LWIP_DEBUGF(HTTPD_DEBUG, ("http_set_ssi_handler\n"));
- LWIP_ASSERT("no ssi_handler given", ssi_handler != NULL);
- httpd_ssi_handler = ssi_handler;
- #if LWIP_HTTPD_SSI_RAW
- LWIP_UNUSED_ARG(tags);
- LWIP_UNUSED_ARG(num_tags);
- #else /* LWIP_HTTPD_SSI_RAW */
- LWIP_ASSERT("no tags given", tags != NULL);
- LWIP_ASSERT("invalid number of tags", num_tags > 0);
- httpd_tags = tags;
- httpd_num_tags = num_tags;
- #endif /* !LWIP_HTTPD_SSI_RAW */
- }
- #endif /* LWIP_HTTPD_SSI */
Handler obsługujący wysyłanie danych do kontrolera wygląda następująco:
- uint16_t ssi_handler (int iIndex, char *pcInsert, int iInsertLen)
- {
- char tmpArray[25] = {0x00};
- switch (iIndex) {
- case TAG_SSI_IPN_IP_URZADZENIA:
- Get_Board_Ip_String_Data(&tmpArray[0]);
- indx+=1;
- sprintf(pcInsert, tmpArray);
- return strlen(pcInsert);
- break;
- case TAG_SSI_MAN_MASKA_URZADZENIA:
- Get_Board_Mask_String_Data(&tmpArray[0]);
- indx+=1;
- sprintf(pcInsert, tmpArray);
- return strlen(pcInsert);
- break;
- case TAG_SSI_GAN_BRAMA_URZADZENIA:
- Get_Board_Gate_String_Data(&tmpArray[0]);
- indx+=1;
- sprintf(pcInsert, tmpArray);
- return strlen(pcInsert);
- break;
- default:
break;
- }
- return 0;
- }
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.
- /** Set this to 0 to not send the SSI tag (default is on, so the tag will
- * be sent in the HTML page */
- #if !defined LWIP_HTTPD_SSI_INCLUDE_TAG || defined __DOXYGEN__
- #define LWIP_HTTPD_SSI_INCLUDE_TAG 0
- #endif
Ustawienie wartości na 1, spowoduje umieszczanie tagów razem z przesłanymi wartościami:
- <tr>
- <td>IP</td>
- <td><input type="text" id="ipnam" name="ipnam" size="15" maxlength="15" value=<!--#ipn-->192.156.234.43></td>
- </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ć:
- <meta http-equiv="refresh" content ="30">
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:
- <tr>
- <td>IP</td>
- <td><input type="text" id="ipnam" name="ipnam" size="15" maxlength="15" value=<!--#ipn-->></td>
- </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ę:
- #define LWIP_HTTPD_SUPPORT_POST 1
Dodatkowo należy wprowadzić obsługę trzech funkcji,
- 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)
Wykorzystałem przykład umieszczony w serwisie github pod tym linkiem:
W pliku HTML musimy wprowadzić następujące dane:
- <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
- 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)
- {
- LWIP_UNUSED_ARG(connection);
- LWIP_UNUSED_ARG(http_request);
- LWIP_UNUSED_ARG(http_request_len);
- LWIP_UNUSED_ARG(content_len);
- LWIP_UNUSED_ARG(post_auto_wnd);
- if (!memcmp(uri, "/login.cgi", 11))
- {
- if (current_connection != connection) {
- current_connection = connection;
- valid_connection = NULL;
- snprintf(response_uri, response_uri_len, "/loginfail.html");
- *post_auto_wnd = 1;
- return ERR_OK;
- }
- }
- return ERR_VAL;
- }
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ą:
- err_t
- httpd_post_receive_data(void *connection, struct pbuf *p)
- {
- if (current_connection == connection) {
- u16_t token_user = pbuf_memfind(p, "pname=", 6, 0);
- u16_t token_pass = pbuf_memfind(p, "ppass=", 6, 0);
- if ((token_user != 0xFFFF) && (token_pass != 0xFFFF)) {
- u16_t value_user = token_user + 6;
- u16_t value_pass = token_pass + 6;
- u16_t len_user = 0;
- u16_t len_pass = 0;
- u16_t tmp;
- /* find user len */
- tmp = pbuf_memfind(p, "&", 1, value_user);
- if (tmp != 0xFFFF) {
- len_user = tmp - value_user;
- } else {
- len_user = p->tot_len - value_user;
- }
- /* find pass len */
- tmp = pbuf_memfind(p, "&", 1, value_pass);
- if (tmp != 0xFFFF) {
- len_pass = tmp - value_pass;
- } else {
- len_pass = p->tot_len - value_pass;
- }
- if ((len_user > 0) && (len_user < USER_PASS_BUFSIZE) &&
- (len_pass > 0) && (len_pass < USER_PASS_BUFSIZE)) {
- /* provide contiguous storage if p is a chained pbuf */
- char buf_user[USER_PASS_BUFSIZE];
- char buf_pass[USER_PASS_BUFSIZE];
- char *user = (char *)pbuf_get_contiguous(p, buf_user, sizeof(buf_user), len_user, value_user);
- char *pass = (char *)pbuf_get_contiguous(p, buf_pass, sizeof(buf_pass), len_pass, value_pass);
- if (user && pass) {
- user[len_user] = 0;
- pass[len_pass] = 0;
- if (!strcmp(user, "admin") && !strcmp(pass, "123456")) {
- /* user and password are correct, create a "session" */
- valid_connection = connection;
- memcpy(last_user, user, sizeof(last_user));
- }
- }
- }
- }
- /* not returning ERR_OK aborts the connection, so return ERR_OK unless the
- conenction is unknown */
- return ERR_OK;
- }
- return ERR_VAL;
- }
Ostatnia funkcja czyli https_post_finished, ustawia stronę do załadowania:
- void
- httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len)
- {
- /* default page is "login failed" */
- snprintf(response_uri, response_uri_len, "/loginfail.html");
- if (current_connection == connection) {
- if (valid_connection == connection) {
- /* login succeeded */
- snprintf(response_uri, response_uri_len, "/cgidata.shtml");
- }
- current_connection = NULL;
- valid_connection = NULL;
- }
- }
Sprawdzenie przesyłanych ramek można wykonać w programie wireshark: