]>
Commit | Line | Data |
---|---|---|
4a9a952e | 1 | |
30a4f2a8 | 2 | /* |
5da06f20 | 3 | * $Id: errorpage.cc,v 1.127 1998/04/23 19:27:22 wessels Exp $ |
30a4f2a8 | 4 | * |
5 | * DEBUG: section 4 Error Generation | |
6 | * AUTHOR: Duane Wessels | |
7 | * | |
42c04c16 | 8 | * SQUID Internet Object Cache http://squid.nlanr.net/Squid/ |
30a4f2a8 | 9 | * -------------------------------------------------------- |
10 | * | |
11 | * Squid is the result of efforts by numerous individuals from the | |
12 | * Internet community. Development is led by Duane Wessels of the | |
13 | * National Laboratory for Applied Network Research and funded by | |
14 | * the National Science Foundation. | |
15 | * | |
16 | * This program is free software; you can redistribute it and/or modify | |
17 | * it under the terms of the GNU General Public License as published by | |
18 | * the Free Software Foundation; either version 2 of the License, or | |
19 | * (at your option) any later version. | |
20 | * | |
21 | * This program is distributed in the hope that it will be useful, | |
22 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
23 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
24 | * GNU General Public License for more details. | |
25 | * | |
26 | * You should have received a copy of the GNU General Public License | |
27 | * along with this program; if not, write to the Free Software | |
28 | * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. | |
29 | * | |
30 | */ | |
1e74c110 | 31 | |
fe40a877 | 32 | /* |
4e3f29eb | 33 | * Abstract: These routines are used to generate error messages to be |
34 | * sent to clients. The error type is used to select between | |
35 | * the various message formats. (formats are stored in the | |
36 | * Config.errorDirectory) | |
fe40a877 | 37 | */ |
38 | ||
44a47c6e | 39 | #include "squid.h" |
1e74c110 | 40 | |
02922e76 | 41 | |
42 | /* local types */ | |
43 | ||
44 | typedef struct { | |
45 | int id; | |
46 | char *page_name; | |
47 | } ErrorDynamicPageInfo; | |
48 | ||
49 | /* local constant and vars */ | |
50 | ||
1d803566 | 51 | /* |
52 | * note: hard coded error messages are not appended with %S automagically | |
53 | * to give you more control on the format | |
54 | */ | |
2ac76861 | 55 | static const struct { |
1afe05c5 | 56 | int type; /* and page_id */ |
2ac76861 | 57 | const char *text; |
58 | } error_hard_text[] = { | |
1afe05c5 | 59 | |
2ac76861 | 60 | { |
61 | ERR_SQUID_SIGNATURE, | |
62 | "\n<br clear=\"all\">\n" | |
63 | "<hr noshade size=1>\n" | |
5d85cb62 | 64 | "Generated %T by %h (<a href=\"http://squid.nlanr.net/Squid/\">%s</a>)\n" |
e32b11fc | 65 | "</BODY></HTML>\n" |
1d803566 | 66 | } |
67 | }; | |
02922e76 | 68 | |
69 | static Stack ErrorDynamicPages; | |
70 | ||
71 | /* local prototypes */ | |
72 | ||
2ac76861 | 73 | static const int error_hard_text_count = sizeof(error_hard_text) / sizeof(*error_hard_text); |
02922e76 | 74 | static char **error_text = NULL; |
75 | static int error_page_count = 0; | |
9b312a19 | 76 | |
02922e76 | 77 | static char *errorTryLoadText(const char *page_name, const char *dir); |
78 | static char *errorLoadText(const char *page_name); | |
1d803566 | 79 | static const char *errorFindHardText(err_type type); |
80 | static MemBuf errorBuildContent(ErrorState * err); | |
fe40a877 | 81 | static const char *errorConvert(char token, ErrorState * err); |
9b312a19 | 82 | static CWCB errorSendComplete; |
1e74c110 | 83 | |
fe40a877 | 84 | /* |
85 | * Function: errorInitialize | |
86 | * | |
1d803566 | 87 | * Abstract: This function finds the error messages formats, and stores |
fe40a877 | 88 | * them in error_text[]; |
89 | * | |
90 | * Global effects: | |
91 | * error_text[] - is modified | |
92 | */ | |
b8d8561b | 93 | void |
0673c0ba | 94 | errorInitialize(void) |
fa966b74 | 95 | { |
02922e76 | 96 | int i; |
1d803566 | 97 | const char *text; |
02922e76 | 98 | error_page_count = ERR_MAX + ErrorDynamicPages.count; |
1afe05c5 | 99 | error_text = xcalloc(error_page_count, sizeof(char *)); |
02922e76 | 100 | for (i = ERR_NONE + 1; i < error_page_count; i++) { |
1d803566 | 101 | safe_free(error_text[i]); |
102 | /* hard-coded ? */ | |
103 | if ((text = errorFindHardText(i))) | |
104 | error_text[i] = xstrdup(text); | |
105 | else | |
1afe05c5 | 106 | /* precompiled ? */ |
02922e76 | 107 | if (i < ERR_MAX) |
108 | error_text[i] = errorLoadText(err_type_str[i]); | |
109 | /* dynamic */ | |
110 | else { | |
1afe05c5 | 111 | ErrorDynamicPageInfo *info = ErrorDynamicPages.items[i - ERR_MAX]; |
02922e76 | 112 | assert(info && info->id == i && info->page_name); |
113 | error_text[i] = errorLoadText(info->page_name); | |
114 | } | |
1d803566 | 115 | assert(error_text[i]); |
116 | } | |
117 | } | |
118 | ||
119 | static const char * | |
120 | errorFindHardText(err_type type) | |
121 | { | |
122 | int i; | |
123 | for (i = 0; i < error_hard_text_count; i++) | |
124 | if (error_hard_text[i].type == type) | |
125 | return error_hard_text[i].text; | |
126 | return NULL; | |
127 | } | |
128 | ||
129 | ||
130 | static char * | |
02922e76 | 131 | errorLoadText(const char *page_name) |
1d803566 | 132 | { |
133 | /* test configured location */ | |
02922e76 | 134 | char *text = errorTryLoadText(page_name, Config.errorDirectory); |
1d803566 | 135 | /* test default location if failed */ |
136 | if (!text && strcmp(Config.errorDirectory, DEFAULT_SQUID_ERROR_DIR)) | |
02922e76 | 137 | text = errorTryLoadText(page_name, DEFAULT_SQUID_ERROR_DIR); |
1d803566 | 138 | /* giving up if failed */ |
139 | if (!text) | |
140 | fatal("failed to find or read error text file."); | |
141 | return text; | |
142 | } | |
143 | ||
144 | static char * | |
02922e76 | 145 | errorTryLoadText(const char *page_name, const char *dir) |
1d803566 | 146 | { |
9b312a19 | 147 | int fd; |
148 | char path[MAXPATHLEN]; | |
149 | struct stat sb; | |
1d803566 | 150 | char *text; |
151 | ||
152 | snprintf(path, MAXPATHLEN, "%s/%s", | |
02922e76 | 153 | dir, page_name); |
1d803566 | 154 | fd = file_open(path, O_RDONLY, NULL, NULL, NULL); |
155 | if (fd < 0 || fstat(fd, &sb) < 0) { | |
156 | debug(4, 0) ("errorTryLoadText: '%s': %s\n", path, xstrerror()); | |
157 | if (fd >= 0) | |
158 | file_close(fd); | |
159 | return NULL; | |
160 | } | |
161 | text = xcalloc(sb.st_size + 2 + 1, 1); | |
162 | if (read(fd, text, sb.st_size) != sb.st_size) { | |
163 | debug(4, 0) ("errorTryLoadText: failed to fully read: '%s': %s\n", | |
164 | path, xstrerror()); | |
165 | xfree(text); | |
166 | text = NULL; | |
1e74c110 | 167 | } |
1d803566 | 168 | file_close(fd); |
2ac76861 | 169 | strcat(text, "%S"); /* add signature */ |
1d803566 | 170 | return text; |
6eb42cae | 171 | } |
172 | ||
02922e76 | 173 | static ErrorDynamicPageInfo * |
174 | errorDynamicPageInfoCreate(int id, const char *page_name) | |
175 | { | |
176 | ErrorDynamicPageInfo *info = xcalloc(1, sizeof(ErrorDynamicPageInfo)); | |
177 | info->id = id; | |
178 | info->page_name = xstrdup(page_name); | |
179 | return info; | |
180 | } | |
181 | ||
182 | static void | |
1afe05c5 | 183 | errorDynamicPageInfoDestroy(ErrorDynamicPageInfo * info) |
02922e76 | 184 | { |
185 | assert(info); | |
186 | xfree(info->page_name); | |
187 | xfree(info); | |
188 | } | |
189 | ||
190 | int | |
191 | errorReservePageId(const char *page_name) | |
192 | { | |
1afe05c5 | 193 | ErrorDynamicPageInfo *info = |
194 | errorDynamicPageInfoCreate(ERR_MAX + ErrorDynamicPages.count, page_name); | |
02922e76 | 195 | stackPush(&ErrorDynamicPages, info); |
196 | return info->id; | |
197 | } | |
198 | ||
53ad48e6 | 199 | void |
200 | errorFree(void) | |
201 | { | |
202 | int i; | |
02922e76 | 203 | for (i = ERR_NONE + 1; i < error_page_count; i++) |
53ad48e6 | 204 | safe_free(error_text[i]); |
02922e76 | 205 | while (ErrorDynamicPages.count) |
206 | errorDynamicPageInfoDestroy(stackPop(&ErrorDynamicPages)); | |
207 | safe_free(error_text); | |
208 | error_page_count = 0; | |
53ad48e6 | 209 | } |
210 | ||
fe40a877 | 211 | /* |
212 | * Function: errorCon | |
213 | * | |
214 | * Abstract: This function creates a ErrorState object. | |
215 | */ | |
216 | ErrorState * | |
02922e76 | 217 | errorCon(int type, http_status status) |
fe40a877 | 218 | { |
219 | ErrorState *err = xcalloc(1, sizeof(ErrorState)); | |
1afe05c5 | 220 | err->page_id = type; /* has to be reset manually if needed */ |
fe40a877 | 221 | err->type = type; |
222 | err->http_status = status; | |
223 | return err; | |
224 | } | |
225 | ||
226 | /* | |
227 | * Function: errorAppendEntry | |
228 | * | |
229 | * Arguments: err - This object is destroyed after use in this function. | |
230 | * | |
231 | * Abstract: This function generates a error page from the info contained | |
4e3f29eb | 232 | * by 'err' and then stores the text in the specified store |
233 | * entry. This function should only be called by ``server | |
234 | * side routines'' which need to communicate errors to the | |
79e0dc20 | 235 | * client side. It should also be called from client_side.c |
236 | * because we now support persistent connections, and | |
237 | * cannot assume that we can immediately write to the socket | |
238 | * for an error. | |
fe40a877 | 239 | */ |
240 | void | |
241 | errorAppendEntry(StoreEntry * entry, ErrorState * err) | |
242 | { | |
cb69b4c7 | 243 | HttpReply *rep; |
cb69b4c7 | 244 | MemObject *mem = entry->mem_obj; |
901e234d | 245 | #if 0 |
246 | /* | |
247 | * Kostas sez PUT "success" replies might not be STORE_PENDING? | |
248 | */ | |
fe40a877 | 249 | assert(entry->store_status == STORE_PENDING); |
54220df8 | 250 | #endif |
b50179a6 | 251 | assert(mem != NULL); |
4e3f29eb | 252 | assert(mem->inmem_hi == 0); |
cb69b4c7 | 253 | rep = errorBuildReply(err); |
254 | httpReplySwapOut(rep, entry); | |
255 | httpReplyDestroy(rep); | |
cb69b4c7 | 256 | mem->reply->sline.status = err->http_status; |
00691420 | 257 | storeComplete(entry); |
b50179a6 | 258 | storeNegativeCache(entry); |
259 | storeReleaseRequest(entry); | |
fe40a877 | 260 | errorStateFree(err); |
fe40a877 | 261 | } |
262 | ||
263 | /* | |
264 | * Function: errorSend | |
265 | * | |
266 | * Arguments: err - This object is destroyed after use in this function. | |
267 | * | |
268 | * Abstract: This function generates a error page from the info contained | |
269 | * by 'err' and then sends it to the client. | |
4e3f29eb | 270 | * The callback function errorSendComplete() is called after |
271 | * the page has been written to the client socket (fd). | |
272 | * errorSendComplete() deallocates 'err'. We need to add | |
273 | * 'err' to the cbdata because comm_write() requires it | |
274 | * for all callback data pointers. | |
79e0dc20 | 275 | * |
276 | * Note, normally errorSend() should only be called from | |
277 | * routines in ssl.c and pass.c, where we don't have any | |
278 | * StoreEntry's. In client_side.c we must allocate a StoreEntry | |
279 | * for errors and use errorAppendEntry() to account for | |
280 | * persistent/pipeline connections. | |
fe40a877 | 281 | */ |
282 | void | |
283 | errorSend(int fd, ErrorState * err) | |
284 | { | |
cb69b4c7 | 285 | HttpReply *rep; |
fe40a877 | 286 | debug(4, 3) ("errorSend: FD %d, err=%p\n", fd, err); |
287 | assert(fd >= 0); | |
88aad2e5 | 288 | /* |
289 | * ugh, this is how we make sure error codes get back to | |
290 | * the client side for logging and error tracking. | |
291 | */ | |
292 | if (err->request) | |
293 | err->request->err_type = err->type; | |
cb69b4c7 | 294 | /* moved in front of errorBuildBuf @?@ */ |
79a15e0a | 295 | EBIT_SET(err->flags, ERR_FLAG_CBDATA); |
3f6c0fb2 | 296 | cbdataAdd(err, MEM_NONE); |
cb69b4c7 | 297 | rep = errorBuildReply(err); |
298 | comm_write_mbuf(fd, httpReplyPack(rep), errorSendComplete, err); | |
299 | httpReplyDestroy(rep); | |
fe40a877 | 300 | } |
301 | ||
302 | /* | |
303 | * Function: errorSendComplete | |
304 | * | |
4e3f29eb | 305 | * Abstract: Called by commHandleWrite() after data has been written |
306 | * to the client socket. | |
fe40a877 | 307 | * |
308 | * Note: If there is a callback, the callback is responsible for | |
309 | * closeing the FD, otherwise we do it ourseves. | |
310 | */ | |
311 | static void | |
79a15e0a | 312 | errorSendComplete(int fd, char *bufnotused, size_t size, int errflag, void *data) |
fe40a877 | 313 | { |
314 | ErrorState *err = data; | |
315 | debug(4, 3) ("errorSendComplete: FD %d, size=%d\n", fd, size); | |
316 | if (errflag != COMM_ERR_CLOSING) { | |
317 | if (err->callback) | |
318 | err->callback(fd, err->callback_data, size); | |
319 | else | |
320 | comm_close(fd); | |
321 | } | |
322 | errorStateFree(err); | |
fe40a877 | 323 | } |
324 | ||
cb69b4c7 | 325 | void |
9b312a19 | 326 | errorStateFree(ErrorState * err) |
6eb42cae | 327 | { |
9b312a19 | 328 | requestUnlink(err->request); |
329 | safe_free(err->redirect_url); | |
330 | safe_free(err->url); | |
76a5501f | 331 | safe_free(err->host); |
5f3c4e9a | 332 | safe_free(err->dnsserver_msg); |
b5af8569 | 333 | safe_free(err->request_hdrs); |
79a15e0a | 334 | if (EBIT_TEST(err->flags, ERR_FLAG_CBDATA)) |
38d04788 | 335 | cbdataFree(err); |
bb0929d8 | 336 | else |
337 | safe_free(err); | |
1e74c110 | 338 | } |
8213067d | 339 | |
2658f489 | 340 | #define CVT_BUF_SZ 512 |
fe40a877 | 341 | |
342 | /* | |
1d803566 | 343 | * B - URL with FTP %2f hack x |
344 | * c - Squid error code x | |
345 | * d - seconds elapsed since request received x | |
fe40a877 | 346 | * e - errno x |
347 | * E - strerror() x | |
fe40a877 | 348 | * f - FTP request line x |
969c39b9 | 349 | * F - FTP reply line x |
7131112f | 350 | * g - FTP server message x |
fe40a877 | 351 | * h - cache hostname x |
969c39b9 | 352 | * H - server host name x |
fe40a877 | 353 | * i - client IP address x |
354 | * I - server IP address x | |
355 | * L - HREF link for more info/contact x | |
356 | * M - Request Method x | |
357 | * p - URL port # x | |
358 | * P - Protocol x | |
b5af8569 | 359 | * R - Full HTTP Request x |
1d803566 | 360 | * S - squid signature from ERR_SIGNATURE x |
361 | * s - caching proxy software with version x | |
fe40a877 | 362 | * t - local time x |
363 | * T - UTC x | |
969c39b9 | 364 | * U - URL without password x |
365 | * u - URL without password, %2f added to path x | |
fe40a877 | 366 | * w - cachemgr email address x |
367 | * z - dns server error message x | |
368 | */ | |
369 | ||
370 | static const char * | |
9b312a19 | 371 | errorConvert(char token, ErrorState * err) |
8213067d | 372 | { |
2658f489 | 373 | request_t *r = err->request; |
374 | static char buf[CVT_BUF_SZ]; | |
fe40a877 | 375 | const char *p = buf; |
9b312a19 | 376 | switch (token) { |
8f872bb6 | 377 | case 'B': |
378 | p = r ? ftpUrlWith2f(r) : "[no URL]"; | |
379 | break; | |
042461c3 | 380 | case 'e': |
c45ed9ad | 381 | snprintf(buf, CVT_BUF_SZ, "%d", err->xerrno); |
042461c3 | 382 | break; |
383 | case 'E': | |
23d92c64 | 384 | if (err->xerrno) |
53ad48e6 | 385 | snprintf(buf, CVT_BUF_SZ, "(%d) %s", err->xerrno, strerror(err->xerrno)); |
23d92c64 | 386 | else |
0d87e358 | 387 | snprintf(buf, CVT_BUF_SZ, "[No Error]"); |
03d7b07f | 388 | break; |
fe40a877 | 389 | case 'f': |
390 | /* FTP REQUEST LINE */ | |
391 | if (err->ftp.request) | |
392 | p = err->ftp.request; | |
393 | else | |
394 | p = "<none>"; | |
395 | break; | |
396 | case 'F': | |
397 | /* FTP REPLY LINE */ | |
398 | if (err->ftp.request) | |
399 | p = err->ftp.reply; | |
400 | else | |
401 | p = "<none>"; | |
03d7b07f | 402 | break; |
7131112f | 403 | case 'g': |
404 | /* FTP SERVER MESSAGE */ | |
5da06f20 | 405 | p = wordlistCat(err->ftp_server_msg); |
7131112f | 406 | break; |
03d7b07f | 407 | case 'h': |
408 | snprintf(buf, CVT_BUF_SZ, "%s", getMyHostname()); | |
f787fb1e | 409 | break; |
fe40a877 | 410 | case 'H': |
411 | p = r ? r->host : "[unknown host]"; | |
f787fb1e | 412 | break; |
413 | case 'i': | |
414 | snprintf(buf, CVT_BUF_SZ, "%s", inet_ntoa(err->src_addr)); | |
f6aa1a5c | 415 | break; |
f787fb1e | 416 | case 'I': |
417 | if (err->host) { | |
418 | snprintf(buf, CVT_BUF_SZ, "%s", err->host); | |
f787fb1e | 419 | } else |
fe40a877 | 420 | p = "[unknown]"; |
421 | break; | |
422 | case 'L': | |
423 | if (Config.errHtmlText) { | |
424 | snprintf(buf, CVT_BUF_SZ, "%s", Config.errHtmlText); | |
425 | } else | |
426 | p = "[not available]"; | |
427 | break; | |
428 | case 'M': | |
429 | p = r ? RequestMethodStr[r->method] : "[unkown method]"; | |
430 | break; | |
431 | case 'p': | |
432 | if (r) { | |
433 | snprintf(buf, CVT_BUF_SZ, "%d", (int) r->port); | |
434 | } else { | |
435 | p = "[unknown port]"; | |
436 | } | |
437 | break; | |
438 | case 'P': | |
439 | p = r ? ProtocolStr[r->protocol] : "[unkown protocol]"; | |
440 | break; | |
b5af8569 | 441 | case 'R': |
442 | p = err->request_hdrs ? err->request_hdrs : "[no request]"; | |
443 | break; | |
1d803566 | 444 | case 's': |
445 | p = full_appname_string; | |
446 | break; | |
447 | case 'S': | |
448 | /* signature may contain %-escapes, recursion */ | |
02922e76 | 449 | if (err->page_id != ERR_SQUID_SIGNATURE) { |
450 | const int saved_id = err->page_id; | |
1d803566 | 451 | MemBuf mb; |
02922e76 | 452 | err->page_id = ERR_SQUID_SIGNATURE; |
1d803566 | 453 | mb = errorBuildContent(err); |
454 | snprintf(buf, CVT_BUF_SZ, "%s", mb.buf); | |
455 | memBufClean(&mb); | |
02922e76 | 456 | err->page_id = saved_id; |
1d803566 | 457 | } else { |
458 | /* wow, somebody put %S into ERR_SIGNATURE, stop recursion */ | |
459 | p = "[%S]"; | |
460 | } | |
461 | break; | |
fe40a877 | 462 | case 't': |
463 | xstrncpy(buf, mkhttpdlogtime(&squid_curtime), 128); | |
f787fb1e | 464 | break; |
f8291f8f | 465 | case 'T': |
466 | snprintf(buf, CVT_BUF_SZ, "%s", mkrfc1123(squid_curtime)); | |
f8291f8f | 467 | break; |
fe40a877 | 468 | case 'U': |
b5af8569 | 469 | p = r ? urlCanonicalClean(r) : err->url ? err->url : "[no URL]"; |
b9916917 | 470 | break; |
fe40a877 | 471 | case 'w': |
472 | if (Config.adminEmail) { | |
473 | snprintf(buf, CVT_BUF_SZ, "%s", Config.adminEmail); | |
474 | } else | |
475 | p = "[unknown]"; | |
476 | break; | |
477 | case 'z': | |
478 | if (err->dnsserver_msg) | |
479 | p = err->dnsserver_msg; | |
b9916917 | 480 | else |
fe40a877 | 481 | p = "[unknown]"; |
b9916917 | 482 | break; |
e347f8e5 | 483 | case '%': |
484 | p = "%"; | |
485 | break; | |
9b312a19 | 486 | default: |
487 | p = "%UNKNOWN%"; | |
488 | break; | |
489 | } | |
9e6aef51 | 490 | assert(p != NULL); |
e102ebda | 491 | debug(4, 3) ("errorConvert: %%%c --> '%s'\n", token, p); |
9b312a19 | 492 | return p; |
8213067d | 493 | } |
e381a13d | 494 | |
cb69b4c7 | 495 | /* allocates and initializes an error response */ |
496 | HttpReply * | |
2ac76861 | 497 | errorBuildReply(ErrorState * err) |
cb69b4c7 | 498 | { |
cb69b4c7 | 499 | HttpReply *rep = httpReplyCreate(); |
1d803566 | 500 | MemBuf content = errorBuildContent(err); |
cb69b4c7 | 501 | /* no LMT for error pages; error pages expire immediately */ |
1d803566 | 502 | httpReplySetHeaders(rep, 1.0, err->http_status, NULL, "text/html", content.size, 0, squid_curtime); |
2ac76861 | 503 | httpBodySet(&rep->body, content.buf, content.size + 1, NULL); |
1d803566 | 504 | memBufClean(&content); |
cb69b4c7 | 505 | return rep; |
506 | } | |
507 | ||
1d803566 | 508 | static MemBuf |
509 | errorBuildContent(ErrorState * err) | |
cb69b4c7 | 510 | { |
1d803566 | 511 | MemBuf content; |
1d803566 | 512 | const char *m; |
513 | const char *p; | |
cb69b4c7 | 514 | const char *t; |
515 | assert(err != NULL); | |
02922e76 | 516 | assert(err->page_id > ERR_NONE && err->page_id < error_page_count); |
1d803566 | 517 | memBufDefInit(&content); |
02922e76 | 518 | m = error_text[err->page_id]; |
1d803566 | 519 | assert(m); |
cb69b4c7 | 520 | while ((p = strchr(m, '%'))) { |
2ac76861 | 521 | memBufAppend(&content, m, p - m); /* copy */ |
522 | t = errorConvert(*++p, err); /* convert */ | |
523 | memBufPrintf(&content, "%s", t); /* copy */ | |
524 | m = p + 1; /* advance */ | |
cb69b4c7 | 525 | } |
1d803566 | 526 | if (*m) |
2ac76861 | 527 | memBufPrintf(&content, "%s", m); /* copy tail */ |
1d803566 | 528 | assert(content.size == strlen(content.buf)); |
cb69b4c7 | 529 | return content; |
530 | } |