GNU libmicrohttpd  0.9.29
 All Data Structures Files Functions Variables Typedefs Enumerations Enumerator Macros Groups
digestauth.c
Go to the documentation of this file.
1 /*
2  This file is part of libmicrohttpd
3  (C) 2010, 2011, 2012 Daniel Pittman and Christian Grothoff
4 
5  This library is free software; you can redistribute it and/or
6  modify it under the terms of the GNU Lesser General Public
7  License as published by the Free Software Foundation; either
8  version 2.1 of the License, or (at your option) any later version.
9 
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13  Lesser General Public License for more details.
14 
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, write to the Free Software
17  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18 */
25 #include "platform.h"
26 #include <limits.h>
27 #include "internal.h"
28 #include "md5.h"
29 
30 #define HASH_MD5_HEX_LEN (2 * MD5_DIGEST_SIZE)
31 
35 #define _BASE "Digest "
36 
40 #define MAX_USERNAME_LENGTH 128
41 
45 #define MAX_REALM_LENGTH 256
46 
50 #define MAX_AUTH_RESPONSE_LENGTH 128
51 
52 
60 static void
61 cvthex (const unsigned char *bin,
62  size_t len,
63  char *hex)
64 {
65  size_t i;
66  unsigned int j;
67 
68  for (i = 0; i < len; ++i)
69  {
70  j = (bin[i] >> 4) & 0x0f;
71  hex[i * 2] = j <= 9 ? (j + '0') : (j + 'a' - 10);
72  j = bin[i] & 0x0f;
73  hex[i * 2 + 1] = j <= 9 ? (j + '0') : (j + 'a' - 10);
74  }
75  hex[len * 2] = '\0';
76 }
77 
78 
91 static void
92 digest_calc_ha1 (const char *alg,
93  const char *username,
94  const char *realm,
95  const char *password,
96  const char *nonce,
97  const char *cnonce,
98  char *sessionkey)
99 {
100  struct MD5Context md5;
101  unsigned char ha1[MD5_DIGEST_SIZE];
102 
103  MD5Init (&md5);
104  MD5Update (&md5, username, strlen (username));
105  MD5Update (&md5, ":", 1);
106  MD5Update (&md5, realm, strlen (realm));
107  MD5Update (&md5, ":", 1);
108  MD5Update (&md5, password, strlen (password));
109  MD5Final (ha1, &md5);
110  if (0 == strcasecmp (alg, "md5-sess"))
111  {
112  MD5Init (&md5);
113  MD5Update (&md5, ha1, sizeof (ha1));
114  MD5Update (&md5, ":", 1);
115  MD5Update (&md5, nonce, strlen (nonce));
116  MD5Update (&md5, ":", 1);
117  MD5Update (&md5, cnonce, strlen (cnonce));
118  MD5Final (ha1, &md5);
119  }
120  cvthex (ha1, sizeof (ha1), sessionkey);
121 }
122 
123 
137 static void
138 digest_calc_response (const char *ha1,
139  const char *nonce,
140  const char *noncecount,
141  const char *cnonce,
142  const char *qop,
143  const char *method,
144  const char *uri,
145  const char *hentity,
146  char *response)
147 {
148  struct MD5Context md5;
149  unsigned char ha2[MD5_DIGEST_SIZE];
150  unsigned char resphash[MD5_DIGEST_SIZE];
151  char ha2hex[HASH_MD5_HEX_LEN + 1];
152 
153  MD5Init (&md5);
154  MD5Update (&md5, method, strlen(method));
155  MD5Update (&md5, ":", 1);
156  MD5Update (&md5, uri, strlen(uri));
157 #if 0
158  if (0 == strcasecmp(qop, "auth-int"))
159  {
160  /* This is dead code since the rest of this module does
161  not support auth-int. */
162  MD5Update (&md5, ":", 1);
163  if (NULL != hentity)
164  MD5Update (&md5, hentity, strlen(hentity));
165  }
166 #endif
167  MD5Final (ha2, &md5);
168  cvthex (ha2, MD5_DIGEST_SIZE, ha2hex);
169  MD5Init (&md5);
170  /* calculate response */
171  MD5Update (&md5, ha1, HASH_MD5_HEX_LEN);
172  MD5Update (&md5, ":", 1);
173  MD5Update (&md5, nonce, strlen(nonce));
174  MD5Update (&md5, ":", 1);
175  if ('\0' != *qop)
176  {
177  MD5Update (&md5, noncecount, strlen(noncecount));
178  MD5Update (&md5, ":", 1);
179  MD5Update (&md5, cnonce, strlen(cnonce));
180  MD5Update (&md5, ":", 1);
181  MD5Update (&md5, qop, strlen(qop));
182  MD5Update (&md5, ":", 1);
183  }
184  MD5Update (&md5, ha2hex, HASH_MD5_HEX_LEN);
185  MD5Final (resphash, &md5);
186  cvthex (resphash, sizeof (resphash), response);
187 }
188 
189 
204 static int
205 lookup_sub_value (char *dest,
206  size_t size,
207  const char *data,
208  const char *key)
209 {
210  size_t keylen;
211  size_t len;
212  const char *ptr;
213  const char *eq;
214  const char *q1;
215  const char *q2;
216  const char *qn;
217 
218  if (0 == size)
219  return 0;
220  keylen = strlen (key);
221  ptr = data;
222  while ('\0' != *ptr)
223  {
224  if (NULL == (eq = strchr (ptr, '=')))
225  return 0;
226  q1 = eq + 1;
227  while (' ' == *q1)
228  q1++;
229  if ('\"' != *q1)
230  {
231  q2 = strchr (q1, ',');
232  qn = q2;
233  }
234  else
235  {
236  q1++;
237  q2 = strchr (q1, '\"');
238  if (NULL == q2)
239  return 0; /* end quote not found */
240  qn = q2 + 1;
241  }
242  if ( (0 == strncasecmp (ptr,
243  key,
244  keylen)) &&
245  (eq == &ptr[keylen]) )
246  {
247  if (NULL == q2)
248  {
249  len = strlen (q1) + 1;
250  if (size > len)
251  size = len;
252  size--;
253  strncpy (dest,
254  q1,
255  size);
256  dest[size] = '\0';
257  return size;
258  }
259  else
260  {
261  if (size > (q2 - q1) + 1)
262  size = (q2 - q1) + 1;
263  size--;
264  memcpy (dest,
265  q1,
266  size);
267  dest[size] = '\0';
268  return size;
269  }
270  }
271  if (NULL == qn)
272  return 0;
273  ptr = strchr (qn, ',');
274  if (NULL == ptr)
275  return 0;
276  ptr++;
277  while (' ' == *ptr)
278  ptr++;
279  }
280  return 0;
281 }
282 
283 
293 static int
294 check_nonce_nc (struct MHD_Connection *connection,
295  const char *nonce,
296  unsigned long int nc)
297 {
298  uint32_t off;
299  uint32_t mod;
300  const char *np;
301 
302  mod = connection->daemon->nonce_nc_size;
303  if (0 == mod)
304  return MHD_NO; /* no array! */
305  /* super-fast xor-based "hash" function for HT lookup in nonce array */
306  off = 0;
307  np = nonce;
308  while ('\0' != *np)
309  {
310  off = (off << 8) | (*np ^ (off >> 24));
311  np++;
312  }
313  off = off % mod;
314  /*
315  * Look for the nonce, if it does exist and its corresponding
316  * nonce counter is less than the current nonce counter by 1,
317  * then only increase the nonce counter by one.
318  */
319 
320  (void) pthread_mutex_lock (&connection->daemon->nnc_lock);
321  if (0 == nc)
322  {
323  strcpy(connection->daemon->nnc[off].nonce,
324  nonce);
325  connection->daemon->nnc[off].nc = 0;
326  (void) pthread_mutex_unlock (&connection->daemon->nnc_lock);
327  return MHD_YES;
328  }
329  if ( (nc <= connection->daemon->nnc[off].nc) ||
330  (0 != strcmp(connection->daemon->nnc[off].nonce, nonce)) )
331  {
332  (void) pthread_mutex_unlock (&connection->daemon->nnc_lock);
333 #if HAVE_MESSAGES
334  MHD_DLOG (connection->daemon,
335  "Stale nonce received. If this happens a lot, you should probably increase the size of the nonce array.\n");
336 #endif
337  return MHD_NO;
338  }
339  connection->daemon->nnc[off].nc = nc;
340  (void) pthread_mutex_unlock (&connection->daemon->nnc_lock);
341  return MHD_YES;
342 }
343 
344 
353 char *
355 {
356  size_t len;
357  char user[MAX_USERNAME_LENGTH];
358  const char *header;
359 
360  if (NULL == (header = MHD_lookup_connection_value (connection,
363  return NULL;
364  if (0 != strncmp (header, _BASE, strlen (_BASE)))
365  return NULL;
366  header += strlen (_BASE);
367  if (0 == (len = lookup_sub_value (user,
368  sizeof (user),
369  header,
370  "username")))
371  return NULL;
372  return strdup (user);
373 }
374 
375 
389 static void
390 calculate_nonce (uint32_t nonce_time,
391  const char *method,
392  const char *rnd,
393  unsigned int rnd_size,
394  const char *uri,
395  const char *realm,
396  char *nonce)
397 {
398  struct MD5Context md5;
399  unsigned char timestamp[4];
400  unsigned char tmpnonce[MD5_DIGEST_SIZE];
401  char timestamphex[sizeof(timestamp) * 2 + 1];
402 
403  MD5Init (&md5);
404  timestamp[0] = (nonce_time & 0xff000000) >> 0x18;
405  timestamp[1] = (nonce_time & 0x00ff0000) >> 0x10;
406  timestamp[2] = (nonce_time & 0x0000ff00) >> 0x08;
407  timestamp[3] = (nonce_time & 0x000000ff);
408  MD5Update (&md5, timestamp, 4);
409  MD5Update (&md5, ":", 1);
410  MD5Update (&md5, method, strlen(method));
411  MD5Update (&md5, ":", 1);
412  if (rnd_size > 0)
413  MD5Update (&md5, rnd, rnd_size);
414  MD5Update (&md5, ":", 1);
415  MD5Update (&md5, uri, strlen(uri));
416  MD5Update (&md5, ":", 1);
417  MD5Update (&md5, realm, strlen(realm));
418  MD5Final (tmpnonce, &md5);
419  cvthex (tmpnonce, sizeof (tmpnonce), nonce);
420  cvthex (timestamp, 4, timestamphex);
421  strncat (nonce, timestamphex, 8);
422 }
423 
424 
435 static int
436 test_header (struct MHD_Connection *connection,
437  const char *key,
438  const char *value)
439 {
440  struct MHD_HTTP_Header *pos;
441 
442  for (pos = connection->headers_received; NULL != pos; pos = pos->next)
443  {
444  if (MHD_GET_ARGUMENT_KIND != pos->kind)
445  continue;
446  if (0 != strcmp (key, pos->header))
447  continue;
448  if ( (NULL == value) &&
449  (NULL == pos->value) )
450  return MHD_YES;
451  if ( (NULL == value) ||
452  (NULL == pos->value) ||
453  (0 != strcmp (value, pos->value)) )
454  continue;
455  return MHD_YES;
456  }
457  return MHD_NO;
458 }
459 
460 
471 static int
473  const char *args)
474 {
475  struct MHD_HTTP_Header *pos;
476  size_t slen = strlen (args) + 1;
477  char argb[slen];
478  char *argp;
479  char *equals;
480  char *amper;
481  unsigned int num_headers;
482 
483  num_headers = 0;
484  memcpy (argb, args, slen);
485  argp = argb;
486  while ( (NULL != argp) &&
487  ('\0' != argp[0]) )
488  {
489  equals = strchr (argp, '=');
490  if (NULL == equals)
491  {
492  /* add with 'value' NULL */
493  connection->daemon->unescape_callback (connection->daemon->unescape_callback_cls,
494  connection,
495  argp);
496  if (MHD_YES != test_header (connection, argp, NULL))
497  return MHD_NO;
498  num_headers++;
499  break;
500  }
501  equals[0] = '\0';
502  equals++;
503  amper = strchr (equals, '&');
504  if (NULL != amper)
505  {
506  amper[0] = '\0';
507  amper++;
508  }
509  connection->daemon->unescape_callback (connection->daemon->unescape_callback_cls,
510  connection,
511  argp);
512  connection->daemon->unescape_callback (connection->daemon->unescape_callback_cls,
513  connection,
514  equals);
515  if (! test_header (connection, argp, equals))
516  return MHD_NO;
517  num_headers++;
518  argp = amper;
519  }
520 
521  /* also check that the number of headers matches */
522  for (pos = connection->headers_received; NULL != pos; pos = pos->next)
523  {
524  if (MHD_GET_ARGUMENT_KIND != pos->kind)
525  continue;
526  num_headers--;
527  }
528  if (0 != num_headers)
529  return MHD_NO;
530  return MHD_YES;
531 }
532 
533 
547 int
549  const char *realm,
550  const char *username,
551  const char *password,
552  unsigned int nonce_timeout)
553 {
554  size_t len;
555  const char *header;
556  char *end;
557  char nonce[MAX_NONCE_LENGTH];
558  char cnonce[MAX_NONCE_LENGTH];
559  char qop[15]; /* auth,auth-int */
560  char nc[20];
561  char response[MAX_AUTH_RESPONSE_LENGTH];
562  const char *hentity = NULL; /* "auth-int" is not supported */
563  char ha1[HASH_MD5_HEX_LEN + 1];
564  char respexp[HASH_MD5_HEX_LEN + 1];
565  char noncehashexp[HASH_MD5_HEX_LEN + 9];
566  uint32_t nonce_time;
567  uint32_t t;
568  size_t left; /* number of characters left in 'header' for 'uri' */
569  unsigned long int nci;
570 
571  header = MHD_lookup_connection_value (connection,
574  if (NULL == header)
575  return MHD_NO;
576  if (0 != strncmp(header, _BASE, strlen(_BASE)))
577  return MHD_NO;
578  header += strlen (_BASE);
579  left = strlen (header);
580 
581  {
582  char un[MAX_USERNAME_LENGTH];
583 
584  len = lookup_sub_value (un,
585  sizeof (un),
586  header, "username");
587  if ( (0 == len) ||
588  (0 != strcmp(username, un)) )
589  return MHD_NO;
590  left -= strlen ("username") + len;
591  }
592 
593  {
594  char r[MAX_REALM_LENGTH];
595 
596  len = lookup_sub_value(r,
597  sizeof (r),
598  header, "realm");
599  if ( (0 == len) ||
600  (0 != strcmp(realm, r)) )
601  return MHD_NO;
602  left -= strlen ("realm") + len;
603  }
604 
605  if (0 == (len = lookup_sub_value (nonce,
606  sizeof (nonce),
607  header, "nonce")))
608  return MHD_NO;
609  left -= strlen ("nonce") + len;
610  if (left > 32 * 1024)
611  {
612  /* we do not permit URIs longer than 32k, as we want to
613  make sure to not blow our stack (or per-connection
614  heap memory limit). Besides, 32k is already insanely
615  large, but of course in theory the
616  #MHD_OPTION_CONNECTION_MEMORY_LIMIT might be very large
617  and would thus permit sending a >32k authorization
618  header value. */
619  return MHD_NO;
620  }
621  {
622  char uri[left];
623 
624  if (0 == lookup_sub_value (uri,
625  sizeof (uri),
626  header, "uri"))
627  return MHD_NO;
628 
629  /* 8 = 4 hexadecimal numbers for the timestamp */
630  nonce_time = strtoul (nonce + len - 8, (char **)NULL, 16);
631  t = (uint32_t) MHD_monotonic_time();
632  /*
633  * First level vetting for the nonce validity if the timestamp
634  * attached to the nonce exceeds `nonce_timeout' then the nonce is
635  * invalid.
636  */
637  if ( (t > nonce_time + nonce_timeout) ||
638  (nonce_time + nonce_timeout < nonce_time) )
639  return MHD_INVALID_NONCE;
640  if (0 != strncmp (uri,
641  connection->url,
642  strlen (connection->url)))
643  {
644 #if HAVE_MESSAGES
645  MHD_DLOG (connection->daemon,
646  "Authentication failed, URI does not match.\n");
647 #endif
648  return MHD_NO;
649  }
650  {
651  const char *args = strchr (uri, '?');
652 
653  if (NULL == args)
654  args = "";
655  else
656  args++;
657  if (MHD_YES !=
658  check_argument_match (connection,
659  args) )
660  {
661 #if HAVE_MESSAGES
662  MHD_DLOG (connection->daemon,
663  "Authentication failed, arguments do not match.\n");
664 #endif
665  return MHD_NO;
666  }
667  }
668  calculate_nonce (nonce_time,
669  connection->method,
670  connection->daemon->digest_auth_random,
671  connection->daemon->digest_auth_rand_size,
672  connection->url,
673  realm,
674  noncehashexp);
675  /*
676  * Second level vetting for the nonce validity
677  * if the timestamp attached to the nonce is valid
678  * and possibly fabricated (in case of an attack)
679  * the attacker must also know the random seed to be
680  * able to generate a "sane" nonce, which if he does
681  * not, the nonce fabrication process going to be
682  * very hard to achieve.
683  */
684 
685  if (0 != strcmp (nonce, noncehashexp))
686  return MHD_INVALID_NONCE;
687  if ( (0 == lookup_sub_value (cnonce,
688  sizeof (cnonce),
689  header, "cnonce")) ||
690  (0 == lookup_sub_value (qop, sizeof (qop), header, "qop")) ||
691  ( (0 != strcmp (qop, "auth")) &&
692  (0 != strcmp (qop, "")) ) ||
693  (0 == lookup_sub_value (nc, sizeof (nc), header, "nc")) ||
694  (0 == lookup_sub_value (response, sizeof (response), header, "response")) )
695  {
696 #if HAVE_MESSAGES
697  MHD_DLOG (connection->daemon,
698  "Authentication failed, invalid format.\n");
699 #endif
700  return MHD_NO;
701  }
702  nci = strtoul (nc, &end, 16);
703  if ( ('\0' != *end) ||
704  ( (LONG_MAX == nci) &&
705  (ERANGE == errno) ) )
706  {
707 #if HAVE_MESSAGES
708  MHD_DLOG (connection->daemon,
709  "Authentication failed, invalid format.\n");
710 #endif
711  return MHD_NO; /* invalid nonce format */
712  }
713  /*
714  * Checking if that combination of nonce and nc is sound
715  * and not a replay attack attempt. Also adds the nonce
716  * to the nonce-nc map if it does not exist there.
717  */
718 
719  if (MHD_YES != check_nonce_nc (connection, nonce, nci))
720  return MHD_NO;
721 
722  digest_calc_ha1("md5",
723  username,
724  realm,
725  password,
726  nonce,
727  cnonce,
728  ha1);
730  nonce,
731  nc,
732  cnonce,
733  qop,
734  connection->method,
735  uri,
736  hentity,
737  respexp);
738  return (0 == strcmp(response, respexp))
739  ? MHD_YES
740  : MHD_NO;
741  }
742 }
743 
744 
759 int
761  const char *realm,
762  const char *opaque,
763  struct MHD_Response *response,
764  int signal_stale)
765 {
766  int ret;
767  size_t hlen;
768  char nonce[HASH_MD5_HEX_LEN + 9];
769 
770  /* Generating the server nonce */
771  calculate_nonce ((uint32_t) MHD_monotonic_time(),
772  connection->method,
773  connection->daemon->digest_auth_random,
774  connection->daemon->digest_auth_rand_size,
775  connection->url,
776  realm,
777  nonce);
778  if (MHD_YES != check_nonce_nc (connection, nonce, 0))
779  {
780 #if HAVE_MESSAGES
781  MHD_DLOG (connection->daemon,
782  "Could not register nonce (is the nonce array size zero?).\n");
783 #endif
784  return MHD_NO;
785  }
786  /* Building the authentication header */
787  hlen = snprintf (NULL,
788  0,
789  "Digest realm=\"%s\",qop=\"auth\",nonce=\"%s\",opaque=\"%s\"%s",
790  realm,
791  nonce,
792  opaque,
793  signal_stale
794  ? ",stale=\"true\""
795  : "");
796  {
797  char header[hlen + 1];
798 
799  snprintf (header,
800  sizeof(header),
801  "Digest realm=\"%s\",qop=\"auth\",nonce=\"%s\",opaque=\"%s\"%s",
802  realm,
803  nonce,
804  opaque,
805  signal_stale
806  ? ",stale=\"true\""
807  : "");
808  ret = MHD_add_response_header(response,
810  header);
811  }
812  if (MHD_YES == ret)
813  ret = MHD_queue_response(connection,
815  response);
816  return ret;
817 }
818 
819 
820 /* end of digestauth.c */
void * unescape_callback_cls
Definition: internal.h:1017
#define MAX_AUTH_RESPONSE_LENGTH
Definition: digestauth.c:50
const char * MHD_lookup_connection_value(struct MHD_Connection *connection, enum MHD_ValueKind kind, const char *key)
Definition: connection.c:210
int MHD_add_response_header(struct MHD_Response *response, const char *header, const char *content)
Definition: response.c:90
static void cvthex(const unsigned char *bin, size_t len, char *hex)
Definition: digestauth.c:61
static int check_nonce_nc(struct MHD_Connection *connection, const char *nonce, unsigned long int nc)
Definition: digestauth.c:294
#define MHD_YES
Definition: microhttpd.h:129
static int lookup_sub_value(char *dest, size_t size, const char *data, const char *key)
Definition: digestauth.c:205
#define MHD_INVALID_NONCE
Definition: microhttpd.h:2060
enum MHD_ValueKind kind
Definition: internal.h:239
int MHD_digest_auth_check(struct MHD_Connection *connection, const char *realm, const char *username, const char *password, unsigned int nonce_timeout)
Definition: digestauth.c:548
platform-specific includes for libmicrohttpd
static int test_header(struct MHD_Connection *connection, const char *key, const char *value)
Definition: digestauth.c:436
char * value
Definition: internal.h:233
struct MHD_Daemon * daemon
Definition: internal.h:544
#define HASH_MD5_HEX_LEN
Definition: digestauth.c:30
return NULL
Definition: tsearch.c:30
static void digest_calc_ha1(const char *alg, const char *username, const char *realm, const char *password, const char *nonce, const char *cnonce, char *sessionkey)
Definition: digestauth.c:92
#define MHD_HTTP_HEADER_AUTHORIZATION
Definition: microhttpd.h:273
int MHD_queue_auth_fail_response(struct MHD_Connection *connection, const char *realm, const char *opaque, struct MHD_Response *response, int signal_stale)
Definition: digestauth.c:760
internal shared structures
char * method
Definition: internal.h:585
time_t MHD_monotonic_time(void)
Definition: internal.c:169
#define MHD_HTTP_HEADER_WWW_AUTHENTICATE
Definition: microhttpd.h:316
char * MHD_digest_auth_get_username(struct MHD_Connection *connection)
Definition: digestauth.c:354
#define MAX_NONCE_LENGTH
Definition: internal.h:164
static int check_argument_match(struct MHD_Connection *connection, const char *args)
Definition: digestauth.c:472
char * url
Definition: internal.h:591
UnescapeCallback unescape_callback
Definition: internal.h:1012
int MHD_queue_response(struct MHD_Connection *connection, unsigned int status_code, struct MHD_Response *response)
Definition: connection.c:2716
#define MHD_HTTP_UNAUTHORIZED
Definition: microhttpd.h:212
#define _BASE
Definition: digestauth.c:35
char * header
Definition: internal.h:228
void MD5Update(struct MD5Context *ctx, const void *data, unsigned len)
Definition: md5.c:170
void MD5Final(unsigned char digest[16], struct MD5Context *ctx)
Definition: md5.c:222
static void calculate_nonce(uint32_t nonce_time, const char *method, const char *rnd, unsigned int rnd_size, const char *uri, const char *realm, char *nonce)
Definition: digestauth.c:390
void MD5Init(struct MD5Context *ctx)
Definition: md5.c:154
static void digest_calc_response(const char *ha1, const char *nonce, const char *noncecount, const char *cnonce, const char *qop, const char *method, const char *uri, const char *hentity, char *response)
Definition: digestauth.c:138
Definition: md5.h:32
struct MHD_HTTP_Header * next
Definition: internal.h:222
#define MD5_DIGEST_SIZE
Definition: md5.h:30
#define MAX_USERNAME_LENGTH
Definition: digestauth.c:40
#define MAX_REALM_LENGTH
Definition: digestauth.c:45
#define MHD_NO
Definition: microhttpd.h:134
struct MHD_HTTP_Header * headers_received
Definition: internal.h:549