]>
git.cameronkatri.com Git - mandoc.git/blob - cgi.c
1 /* $Id: cgi.c,v 1.51 2014/04/23 21:40:47 schwarze Exp $ */
3 * Copyright (c) 2011, 2012 Kristaps Dzonsons <kristaps@bsd.lv>
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
40 #include <sys/types.h>
44 #include "apropos_db.h"
46 #include "mandoc_aux.h"
53 #if defined(__linux__) || defined(__sun)
72 * A query as passed to the search function.
75 const char *arch
; /* architecture */
76 const char *sec
; /* manual section */
77 const char *expr
; /* unparsed expression string */
78 int manroot
; /* manroot index (or -1)*/
79 int legacy
; /* whether legacy mode */
89 static int atou(const char *, unsigned *);
90 static void catman(const struct req
*, const char *);
91 static int cmp(const void *, const void *);
92 static void format(const struct req
*, const char *);
93 static void html_print(const char *);
94 static void html_printquery(const struct req
*);
95 static void html_putchar(char);
96 static int http_decode(char *);
97 static void http_parse(struct req
*, char *);
98 static void http_print(const char *);
99 static void http_putchar(char);
100 static void http_printquery(const struct req
*);
101 static int pathstop(DIR *);
102 static void pathgen(DIR *, char *, struct req
*);
103 static void pg_index(const struct req
*, char *);
104 static void pg_search(const struct req
*, char *);
105 static void pg_show(const struct req
*, char *);
106 static void resp_bad(void);
107 static void resp_baddb(void);
108 static void resp_error400(void);
109 static void resp_error404(const char *);
110 static void resp_begin_html(int, const char *);
111 static void resp_begin_http(int, const char *);
112 static void resp_end_html(void);
113 static void resp_index(const struct req
*);
114 static void resp_search(struct res
*, size_t, void *);
115 static void resp_searchform(const struct req
*);
117 static const char *progname
; /* cgi script name */
118 static const char *cache
; /* cache directory */
119 static const char *css
; /* css directory */
120 static const char *host
; /* hostname */
122 static const char * const pages
[PAGE__MAX
] = {
123 "index", /* PAGE_INDEX */
124 "search", /* PAGE_SEARCH */
125 "show", /* PAGE_SHOW */
129 * This is just OpenBSD's strtol(3) suggestion.
130 * I use it instead of strtonum(3) for portability's sake.
133 atou(const char *buf
, unsigned *v
)
139 lval
= strtol(buf
, &ep
, 10);
140 if (buf
[0] == '\0' || *ep
!= '\0')
142 if ((errno
== ERANGE
&& (lval
== LONG_MAX
||
143 lval
== LONG_MIN
)) ||
144 (lval
> INT_MAX
|| lval
< 0))
147 *v
= (unsigned int)lval
;
152 * Print a character, escaping HTML along the way.
153 * This will pass non-ASCII straight to output: be warned!
173 putchar((unsigned char)c
);
178 http_printquery(const struct req
*req
)
182 http_print(req
->q
.expr
? req
->q
.expr
: "");
184 http_print(req
->q
.sec
? req
->q
.sec
: "");
186 http_print(req
->q
.arch
? req
->q
.arch
: "");
191 html_printquery(const struct req
*req
)
194 printf("&expr=");
195 html_print(req
->q
.expr
? req
->q
.expr
: "");
197 html_print(req
->q
.sec
? req
->q
.sec
: "");
198 printf("&arch=");
199 html_print(req
->q
.arch
? req
->q
.arch
: "");
203 http_print(const char *p
)
213 * Call through to html_putchar().
214 * Accepts NULL strings.
217 html_print(const char *p
)
227 * Parse out key-value pairs from an HTTP request variable.
228 * This can be either a cookie or a POST/GET string, although man.cgi
229 * uses only GET for simplicity.
232 http_parse(struct req
*req
, char *p
)
234 char *key
, *val
, *manroot
;
237 memset(&req
->q
, 0, sizeof(struct query
));
246 p
+= (int)strcspn(p
, ";&");
249 if (NULL
!= (val
= strchr(key
, '=')))
252 if ('\0' == *key
|| NULL
== val
|| '\0' == *val
)
255 /* Just abort handling. */
257 if ( ! http_decode(key
))
259 if (NULL
!= val
&& ! http_decode(val
))
262 if (0 == strcmp(key
, "expr"))
264 else if (0 == strcmp(key
, "query"))
266 else if (0 == strcmp(key
, "sec"))
268 else if (0 == strcmp(key
, "sektion"))
270 else if (0 == strcmp(key
, "arch"))
272 else if (0 == strcmp(key
, "manpath"))
274 else if (0 == strcmp(key
, "apropos"))
275 legacy
= 0 == strcmp(val
, "0");
278 /* Test for old man.cgi compatibility mode. */
280 req
->q
.legacy
= legacy
> 0;
283 * Section "0" means no section when in legacy mode.
284 * For some man.cgi scripts, "default" arch is none.
287 if (req
->q
.legacy
&& NULL
!= req
->q
.sec
)
288 if (0 == strcmp(req
->q
.sec
, "0"))
290 if (req
->q
.legacy
&& NULL
!= req
->q
.arch
)
291 if (0 == strcmp(req
->q
.arch
, "default"))
294 /* Default to first manroot. */
296 if (NULL
!= manroot
) {
297 for (i
= 0; i
< (int)req
->psz
; i
++)
298 if (0 == strcmp(req
->p
[i
].name
, manroot
))
300 req
->q
.manroot
= i
< (int)req
->psz
? i
: -1;
308 if (isalnum((unsigned char)c
)) {
309 putchar((unsigned char)c
);
311 } else if (' ' == c
) {
319 * HTTP-decode a string. The standard explanation is that this turns
320 * "%4e+foo" into "n foo" in the regular way. This is done in-place
321 * over the allocated string.
331 for ( ; '\0' != *p
; p
++) {
333 if ('\0' == (hex
[0] = *(p
+ 1)))
335 if ('\0' == (hex
[1] = *(p
+ 2)))
337 if (1 != sscanf(hex
, "%x", &c
))
343 memmove(p
+ 1, p
+ 3, strlen(p
+ 3) + 1);
345 *p
= '+' == *p
? ' ' : *p
;
353 resp_begin_http(int code
, const char *msg
)
357 printf("Status: %d %s\n", code
, msg
);
359 puts("Content-Type: text/html; charset=utf-8\n"
360 "Cache-Control: no-cache\n"
368 resp_begin_html(int code
, const char *msg
)
371 resp_begin_http(code
, msg
);
373 printf("<!DOCTYPE HTML PUBLIC "
374 " \"-//W3C//DTD HTML 4.01//EN\""
375 " \"http://www.w3.org/TR/html4/strict.dtd\">\n"
378 "<META HTTP-EQUIV=\"Content-Type\""
379 " CONTENT=\"text/html; charset=utf-8\">\n"
380 "<LINK REL=\"stylesheet\" HREF=\"%s/man-cgi.css\""
381 " TYPE=\"text/css\" media=\"all\">\n"
382 "<LINK REL=\"stylesheet\" HREF=\"%s/man.css\""
383 " TYPE=\"text/css\" media=\"all\">\n"
384 "<TITLE>System Manpage Reference</TITLE>\n"
387 "<!-- Begin page content. //-->\n", css
, css
);
399 resp_searchform(const struct req
*req
)
403 puts("<!-- Begin search form. //-->");
404 printf("<DIV ID=\"mancgi\">\n"
405 "<FORM ACTION=\"%s/search.html\" METHOD=\"get\">\n"
407 "<LEGEND>Search Parameters</LEGEND>\n"
408 "<INPUT TYPE=\"submit\" "
409 " VALUE=\"Search\"> for manuals satisfying \n"
410 "<INPUT TYPE=\"text\" NAME=\"expr\" VALUE=\"",
412 html_print(req
->q
.expr
? req
->q
.expr
: "");
413 printf("\">, section "
414 "<INPUT TYPE=\"text\""
415 " SIZE=\"4\" NAME=\"sec\" VALUE=\"");
416 html_print(req
->q
.sec
? req
->q
.sec
: "");
418 "<INPUT TYPE=\"text\""
419 " SIZE=\"8\" NAME=\"arch\" VALUE=\"");
420 html_print(req
->q
.arch
? req
->q
.arch
: "");
423 puts(", <SELECT NAME=\"manpath\">");
424 for (i
= 0; i
< (int)req
->psz
; i
++) {
425 printf("<OPTION %s VALUE=\"",
426 (i
== req
->q
.manroot
) ||
427 (0 == i
&& -1 == req
->q
.manroot
) ?
428 "SELECTED=\"selected\"" : "");
429 html_print(req
->p
[i
].name
);
431 html_print(req
->p
[i
].name
);
437 "<INPUT TYPE=\"reset\" VALUE=\"Reset\">\n"
441 puts("<!-- End search form. //-->");
445 resp_index(const struct req
*req
)
448 resp_begin_html(200, NULL
);
449 resp_searchform(req
);
457 resp_begin_html(400, "Query Malformed");
458 printf("<H1>Malformed Query</H1>\n"
460 "The query your entered was malformed.\n"
461 "Try again from the\n"
462 "<A HREF=\"%s/index.html\">main page</A>.\n"
468 resp_error404(const char *page
)
471 resp_begin_html(404, "Not Found");
472 puts("<H1>Page Not Found</H1>\n"
474 "The page you're looking for, ");
478 "could not be found.\n"
479 "Try searching from the\n"
480 "<A HREF=\"%s/index.html\">main page</A>.\n"
488 resp_begin_html(500, "Internal Server Error");
489 puts("<P>Generic badness happened.</P>");
497 resp_begin_html(500, "Internal Server Error");
498 puts("<P>Your database is broken.</P>");
503 resp_search(struct res
*r
, size_t sz
, void *arg
)
506 const struct req
*req
;
508 req
= (const struct req
*)arg
;
511 assert(req
->q
.manroot
>= 0);
513 for (matched
= i
= 0; i
< sz
; i
++)
518 for (i
= 0; i
< sz
; i
++)
522 * If we have just one result, then jump there now
525 puts("Status: 303 See Other");
526 printf("Location: http://%s%s/show/%d/%u/%u.html?",
527 host
, progname
, req
->q
.manroot
,
528 r
[i
].volume
, r
[i
].rec
);
529 http_printquery(req
);
531 "Content-Type: text/html; charset=utf-8\n");
535 resp_begin_html(200, NULL
);
536 resp_searchform(req
);
538 puts("<DIV CLASS=\"results\">");
542 "No results found.\n"
549 qsort(r
, sz
, sizeof(struct res
), cmp
);
553 for (i
= 0; i
< sz
; i
++) {
557 "<TD CLASS=\"title\">\n"
558 "<A HREF=\"%s/show/%d/%u/%u.html?",
559 progname
, req
->q
.manroot
,
560 r
[i
].volume
, r
[i
].rec
);
561 html_printquery(req
);
563 html_print(r
[i
].title
);
565 html_print(r
[i
].cat
);
566 if (r
[i
].arch
&& '\0' != *r
[i
].arch
) {
568 html_print(r
[i
].arch
);
572 "<TD CLASS=\"desc\">");
573 html_print(r
[i
].desc
);
585 pg_index(const struct req
*req
, char *path
)
592 catman(const struct req
*req
, const char *file
)
600 if (NULL
== (f
= fopen(file
, "r"))) {
605 resp_begin_html(200, NULL
);
606 resp_searchform(req
);
607 puts("<DIV CLASS=\"catman\">\n"
610 while (NULL
!= (p
= fgetln(f
, &len
))) {
612 for (i
= 0; i
< (int)len
- 1; i
++) {
614 * This means that the catpage is out of state.
615 * Ignore it and keep going (although the
619 if ('\b' == p
[i
] || '\n' == p
[i
])
623 * Print a regular character.
624 * Close out any bold/italic scopes.
625 * If we're in back-space mode, make sure we'll
626 * have something to enter when we backspace.
629 if ('\b' != p
[i
+ 1]) {
637 } else if (i
+ 2 >= (int)len
)
655 * Handle funny behaviour troff-isms.
656 * These grok'd from the original man2html.c.
659 if (('+' == p
[i
] && 'o' == p
[i
+ 2]) ||
660 ('o' == p
[i
] && '+' == p
[i
+ 2]) ||
661 ('|' == p
[i
] && '=' == p
[i
+ 2]) ||
662 ('=' == p
[i
] && '|' == p
[i
+ 2]) ||
663 ('*' == p
[i
] && '=' == p
[i
+ 2]) ||
664 ('=' == p
[i
] && '*' == p
[i
+ 2]) ||
665 ('*' == p
[i
] && '|' == p
[i
+ 2]) ||
666 ('|' == p
[i
] && '*' == p
[i
+ 2])) {
675 } else if (('|' == p
[i
] && '-' == p
[i
+ 2]) ||
676 ('-' == p
[i
] && '|' == p
[i
+ 1]) ||
677 ('+' == p
[i
] && '-' == p
[i
+ 1]) ||
678 ('-' == p
[i
] && '+' == p
[i
+ 1]) ||
679 ('+' == p
[i
] && '|' == p
[i
+ 1]) ||
680 ('|' == p
[i
] && '+' == p
[i
+ 1])) {
704 * Clean up the last character.
705 * We can get to a newline; don't print that.
713 if (i
== (int)len
- 1 && '\n' != p
[i
])
728 format(const struct req
*req
, const char *file
)
736 char opts
[PATH_MAX
+ 128];
738 if (-1 == (fd
= open(file
, O_RDONLY
, 0))) {
743 mp
= mparse_alloc(MPARSE_SO
, MANDOCLEVEL_FATAL
, NULL
, NULL
);
744 rc
= mparse_readfd(mp
, fd
, file
);
747 if (rc
>= MANDOCLEVEL_FATAL
) {
752 snprintf(opts
, sizeof(opts
), "fragment,"
753 "man=%s/search.html?sec=%%S&expr=Nm~^%%N$,"
754 /*"includes=/cgi-bin/man.cgi/usr/include/%%I"*/,
757 mparse_result(mp
, &mdoc
, &man
, NULL
);
758 if (NULL
== man
&& NULL
== mdoc
) {
764 resp_begin_html(200, NULL
);
765 resp_searchform(req
);
767 vp
= html_alloc(opts
);
782 pg_show(const struct req
*req
, char *path
)
790 unsigned int vol
, rec
, mr
;
796 /* Parse out mroot, volume, and record from the path. */
798 if (NULL
== path
|| NULL
== (sub
= strchr(path
, '/'))) {
803 if ( ! atou(path
, &mr
)) {
808 if (NULL
== (sub
= strchr(path
, '/'))) {
813 if ( ! atou(path
, &vol
) || ! atou(sub
, &rec
)) {
816 } else if (mr
>= (unsigned int)req
->psz
) {
822 * Begin by chdir()ing into the manroot.
823 * This way we can pick up the database files, which are
824 * relative to the manpath root.
827 if (-1 == chdir(req
->p
[(int)mr
].path
)) {
828 perror(req
->p
[(int)mr
].path
);
833 memset(&ps
, 0, sizeof(struct manpaths
));
834 manpath_manconf(&ps
, "etc/catman.conf");
836 if (vol
>= (unsigned int)ps
.sz
) {
841 sz
= strlcpy(file
, ps
.paths
[vol
], PATH_MAX
);
842 assert(sz
< PATH_MAX
);
843 strlcat(file
, "/", PATH_MAX
);
844 strlcat(file
, MANDOC_IDX
, PATH_MAX
);
846 /* Open the index recno(3) database. */
848 idx
= dbopen(file
, O_RDONLY
, 0, DB_RECNO
, NULL
);
858 if (0 != (rc
= (*idx
->get
)(idx
, &key
, &val
, 0))) {
859 rc
< 0 ? resp_baddb() : resp_error400();
861 } else if (0 == val
.size
) {
866 cp
= (char *)val
.data
;
869 if (NULL
== memchr(cp
, '\0', val
.size
- 1))
872 file
[(int)sz
] = '\0';
873 strlcat(file
, "/", PATH_MAX
);
874 strlcat(file
, cp
, PATH_MAX
);
887 pg_search(const struct req
*req
, char *path
)
892 const char *ep
, *start
;
898 if (req
->q
.manroot
< 0 || 0 == req
->psz
) {
899 resp_search(NULL
, 0, (void *)req
);
903 memset(&opt
, 0, sizeof(struct opts
));
906 opt
.arch
= req
->q
.arch
;
907 opt
.cat
= req
->q
.sec
;
915 * Begin by chdir()ing into the root of the manpath.
916 * This way we can pick up the database files, which are
917 * relative to the manpath root.
920 assert(req
->q
.manroot
< (int)req
->psz
);
921 if (-1 == (chdir(req
->p
[req
->q
.manroot
].path
))) {
922 perror(req
->p
[req
->q
.manroot
].path
);
923 resp_search(NULL
, 0, (void *)req
);
927 memset(&ps
, 0, sizeof(struct manpaths
));
928 manpath_manconf(&ps
, "etc/catman.conf");
931 * Poor man's tokenisation: just break apart by spaces.
932 * Yes, this is half-ass. But it works for now.
935 while (ep
&& isspace((unsigned char)*ep
))
938 while (ep
&& '\0' != *ep
) {
939 cp
= mandoc_reallocarray(cp
, sz
+ 1, sizeof(char *));
941 while ('\0' != *ep
&& ! isspace((unsigned char)*ep
))
943 cp
[sz
] = mandoc_malloc((ep
- start
) + 1);
944 memcpy(cp
[sz
], start
, ep
- start
);
945 cp
[sz
++][ep
- start
] = '\0';
946 while (isspace((unsigned char)*ep
))
951 * Pump down into apropos backend.
952 * The resp_search() function is called with the results.
955 expr
= req
->q
.legacy
?
956 termcomp(sz
, cp
, &tt
) : exprcomp(sz
, cp
, &tt
);
960 (ps
.sz
, ps
.paths
, &opt
, expr
, tt
,
961 (void *)req
, &ressz
, &res
, resp_search
);
963 /* ...unless errors occured. */
968 resp_search(NULL
, 0, NULL
);
970 for (i
= 0; i
< sz
; i
++)
986 char *p
, *path
, *subpath
;
988 /* Scan our run-time environment. */
990 if (NULL
== (cache
= getenv("CACHE_DIR")))
991 cache
= "/cache/man.cgi";
993 if (NULL
== (progname
= getenv("SCRIPT_NAME")))
996 if (NULL
== (css
= getenv("CSS_DIR")))
999 if (NULL
== (host
= getenv("HTTP_HOST")))
1003 * First we change directory into the cache directory so that
1004 * subsequent scanning for manpath directories is rooted
1005 * relative to the same position.
1008 if (-1 == chdir(cache
)) {
1011 return(EXIT_FAILURE
);
1012 } else if (NULL
== (cwd
= opendir(cache
))) {
1015 return(EXIT_FAILURE
);
1018 memset(&req
, 0, sizeof(struct req
));
1020 strlcpy(buf
, ".", PATH_MAX
);
1021 pathgen(cwd
, buf
, &req
);
1024 /* Next parse out the query string. */
1026 if (NULL
!= (p
= getenv("QUERY_STRING")))
1027 http_parse(&req
, p
);
1030 * Now juggle paths to extract information.
1031 * We want to extract our filetype (the file suffix), the
1032 * initial path component, then the trailing component(s).
1033 * Start with leading subpath component.
1036 subpath
= path
= NULL
;
1037 req
.page
= PAGE__MAX
;
1039 if (NULL
== (path
= getenv("PATH_INFO")) || '\0' == *path
)
1040 req
.page
= PAGE_INDEX
;
1042 if (NULL
!= path
&& '/' == *path
&& '\0' == *++path
)
1043 req
.page
= PAGE_INDEX
;
1045 /* Strip file suffix. */
1047 if (NULL
!= path
&& NULL
!= (p
= strrchr(path
, '.')))
1048 if (NULL
!= p
&& NULL
== strchr(p
, '/'))
1051 /* Resolve subpath component. */
1053 if (NULL
!= path
&& NULL
!= (subpath
= strchr(path
, '/')))
1056 /* Map path into one we recognise. */
1058 if (NULL
!= path
&& '\0' != *path
)
1059 for (i
= 0; i
< (int)PAGE__MAX
; i
++)
1060 if (0 == strcmp(pages
[i
], path
)) {
1061 req
.page
= (enum page
)i
;
1069 pg_index(&req
, subpath
);
1072 pg_search(&req
, subpath
);
1075 pg_show(&req
, subpath
);
1078 resp_error404(path
);
1082 for (i
= 0; i
< (int)req
.psz
; i
++) {
1083 free(req
.p
[i
].path
);
1084 free(req
.p
[i
].name
);
1088 return(EXIT_SUCCESS
);
1092 cmp(const void *p1
, const void *p2
)
1095 return(strcasecmp(((const struct res
*)p1
)->title
,
1096 ((const struct res
*)p2
)->title
));
1100 * Check to see if an "etc" path consists of a catman.conf file. If it
1101 * does, that means that the path contains a tree created by catman(8)
1102 * and should be used for indexing.
1112 while (NULL
!= (d
= readdir(dir
))) {
1114 stat(d
->d_name
, &sb
);
1115 if (S_IFREG
& sb
.st_mode
)
1117 if (DT_REG
== d
->d_type
)
1119 if (0 == strcmp(d
->d_name
, "catman.conf"))
1127 * Scan for indexable paths.
1128 * This adds all paths with "etc/catman.conf" to the buffer.
1131 pathgen(DIR *dir
, char *path
, struct req
*req
)
1142 sz
= strlcat(path
, "/", PATH_MAX
);
1143 if (sz
>= PATH_MAX
) {
1144 fprintf(stderr
, "%s: Path too long", path
);
1149 * First, scan for the "etc" directory.
1150 * If it's found, then see if it should cause us to stop. This
1151 * happens when a catman.conf is found in the directory.
1155 while (0 == rc
&& NULL
!= (d
= readdir(dir
))) {
1157 stat(d
->d_name
, &sb
);
1158 if (!(S_IFDIR
& sb
.st_mode
)
1160 if (DT_DIR
!= d
->d_type
1162 || strcmp(d
->d_name
, "etc"))
1165 path
[(int)sz
] = '\0';
1166 ssz
= strlcat(path
, d
->d_name
, PATH_MAX
);
1168 if (ssz
>= PATH_MAX
) {
1169 fprintf(stderr
, "%s: Path too long", path
);
1171 } else if (NULL
== (cd
= opendir(path
))) {
1181 /* This also strips the trailing slash. */
1182 path
[(int)--sz
] = '\0';
1183 req
->p
= mandoc_reallocarray(req
->p
,
1184 req
->psz
+ 1, sizeof(struct paths
));
1186 * Strip out the leading "./" unless we're just a ".",
1187 * in which case use an empty string as our name.
1189 req
->p
[(int)req
->psz
].path
= mandoc_strdup(path
);
1190 req
->p
[(int)req
->psz
].name
=
1191 cp
= mandoc_strdup(path
+ (1 == sz
? 1 : 2));
1194 * The name is just the path with all the slashes taken
1195 * out of it. Simple but effective.
1197 for ( ; '\0' != *cp
; cp
++)
1204 * If no etc/catman.conf was found, recursively enter child
1205 * directory and continue scanning.
1209 while (NULL
!= (d
= readdir(dir
))) {
1211 stat(d
->d_name
, &sb
);
1212 if (!(S_IFDIR
& sb
.st_mode
)
1214 if (DT_DIR
!= d
->d_type
1216 || '.' == d
->d_name
[0])
1219 path
[(int)sz
] = '\0';
1220 ssz
= strlcat(path
, d
->d_name
, PATH_MAX
);
1222 if (ssz
>= PATH_MAX
) {
1223 fprintf(stderr
, "%s: Path too long", path
);
1225 } else if (NULL
== (cd
= opendir(path
))) {
1230 pathgen(cd
, path
, req
);