/* MiddleMan filtering proxy server Copyright (C) 2002 Jason McLaughlin This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include #include #include #include #include #include #include #include #include #include #include #include "proto.h" #ifdef USE_SYSLOG #include #endif /* USE_SYSLOG */ int loglevel = 2047; char configfile[256] = "", logfile[256] = "", username[256] = "", group[256] = ""; TEMPLATES *templates = NULL; ACCESS_LIST *access_list = NULL; HEADER_LIST *header_list = NULL; FILTER_LIST *filter_list = NULL; COOKIE_LIST *cookie_list = NULL; REWRITE_LIST *rewrite_list = NULL; MIME_LIST *mime_list = NULL; REDIRECT_LIST *redirect_list = NULL; KEYWORD_LIST *keyword_list = NULL; FORWARD_LIST *forward_list = NULL; EXTERNAL *external = NULL; HASH_TABLE *dns_cache; THREADLIST threads[MAXTHREADS]; NETWORK *network = NULL; LOGBUFFER *logbuffer = NULL; int main(int argc, char **argv) { int fpid, x; char pidfile[256] = ""; struct stat fileinfo; struct passwd *pwd = NULL; struct group *grp = NULL; if (argc < 2) { show_help(argv); exit(EXIT_SUCCESS); } while ((x = getopt(argc, argv, "hp:c:l:d:u:g:")) != EOF) { switch (x) { case 'c': s_strncpy(configfile, optarg, sizeof(configfile)); break; case 'l': s_strncpy(logfile, optarg, sizeof(logfile)); break; case 'p': s_strncpy(pidfile, optarg, sizeof(pidfile)); break; case 'd': loglevel = atoi(optarg); break; case 'u': s_strncpy(username, optarg, sizeof(username)); break; case 'g': s_strncpy(group, optarg, sizeof(group)); break; case 'h': show_help(argv); exit(EXIT_SUCCESS); } } while (--argc > 0) memset(argv[argc], 0, strlen(argv[argc])); if (*configfile) { if (stat(configfile, &fileinfo) == -1) { fprintf(stderr, "couldn't stat %s\n", configfile); exit(EXIT_FAILURE); } } else { fprintf(stderr, "config file option missing\n"); exit(EXIT_FAILURE); } if (strcmp(group, "")) { grp = getgrnam(group); if (grp == NULL) { fprintf(stderr, "getgrnam: unknown group\n"); exit(EXIT_FAILURE); } x = setgid(grp->gr_gid); if (x == -1) { perror("setgid"); exit(EXIT_FAILURE); } } else { grp = getgrgid(getgid()); if (grp == NULL) { fprintf(stderr, "setgrgid: unknown group"); exit(EXIT_FAILURE); } s_strncpy(group, grp->gr_name, sizeof(group)); } if (strcmp(username, "")) { pwd = getpwnam(username); if (pwd == NULL) { fprintf(stderr, "getpwnam: unknown user\n"); exit(EXIT_FAILURE); } x = setuid(pwd->pw_uid); if (x == -1) { perror("setuid"); exit(EXIT_FAILURE); } } else { pwd = getpwuid(getuid()); if (pwd == NULL) { fprintf(stderr, "getpwuid: unknown user"); exit(EXIT_FAILURE); } s_strncpy(username, pwd->pw_name, sizeof(username)); } fpid = fork(); switch (fpid) { case -1: fprintf(stderr, "failed to fork daemon\n"); exit(EXIT_FAILURE); break; case 0: if (*pidfile) { x = pid_check(pidfile); switch (x) { case 0: break; case -1: exit(EXIT_FAILURE); default: exit(EXIT_FAILURE); } } close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); mainloop(); default: setpgid(fpid, fpid); } exit(EXIT_SUCCESS); } void show_help(char **argv) { fprintf(stderr, "MiddleMan filtering proxy server v%s (c)2002 Jason McLaughlin\n\n", MMAN_VERSION); fprintf(stderr, "Usage: %s [options]\n\n", argv[0]); fprintf(stderr, " -c : location of config file\n"); #ifndef USE_SYSLOG fprintf(stderr, " -l : file to log actvity to\n"); #endif fprintf(stderr, " -p : PID file\n"); fprintf(stderr, " -u : run as alternate user\n"); fprintf(stderr, " -g : run in altername group\n"); fprintf(stderr, " -d : set log level (default: %d)\n", loglevel); fprintf(stderr, " -h : help\n\n"); fprintf(stderr, " Add any of the following to specify logging detail:\n"); fprintf(stderr, " 1 = requests\n"); fprintf(stderr, " 2 = network\n"); fprintf(stderr, " 4 = url filtering\n"); fprintf(stderr, " 8 = header filtering\n"); fprintf(stderr, " 16 = mime filtering\n"); fprintf(stderr, " 32 = cookie filtering\n"); fprintf(stderr, " 64 = redirections\n"); fprintf(stderr, " 128 = templates\n"); fprintf(stderr, " 256 = keyword filtering\n"); fprintf(stderr, " 512 = warnings\n"); fprintf(stderr, " 1024 = errors\n"); fprintf(stderr, " 2048 = debug\n"); } /* check if pidfile exists and whether or not the pid inside is active, otherwise write current pid. */ int pid_check(char *pidfile) { int i = 0, x; FILE *fptr; fptr = fopen(pidfile, "r"); if (fptr == NULL) goto makepidfile; x = fscanf(fptr, "%d", &i); if (x == 0) { fclose(fptr); goto makepidfile; } x = kill(i, SIGCHLD); if (x == 0) { fclose(fptr); return i; } makepidfile: unlink(pidfile); fptr = fopen(pidfile, "w"); if (fptr == NULL) return TRUE; fprintf(fptr, "%u\n", (unsigned int) getpid()); fclose(fptr); return FALSE; } /* things that only need to be done once at startup */ void config() { int i; #ifdef USE_SYSLOG openlog("mman", LOG_PID, LOG_DAEMON); #endif logbuffer = xmalloc(sizeof(LOGBUFFER)); logbuffer->entries = 0; logbuffer->size = LOGBUFFERSIZE; logbuffer->head = logbuffer->tail = NULL; pthread_rwlock_init(&logbuffer->lock, NULL); signal_setup(); net_init(); for (i = 0; i < MAXTHREADS; i++) { threads[i].flags = THREAD_UNUSED; pthread_mutex_init(&threads[i].lock, NULL); } pcre_free = xfree; pcre_malloc = (void *) xmalloc; config_load(3, configfile); dns_cache = hash_create(DNS_HASH_SIZE); } int config_load(int overwrite, char *file) { XML_LIST *xml_list; xml_list = xml_load(NULL, file); if (xml_list == NULL) return FALSE; if (access_list != NULL) { pthread_rwlock_wrlock(&access_list->lock); if (overwrite) { access_list->id = 0; access_ll_free(access_list->allow); access_ll_free(access_list->deny); access_list->allow = access_list->deny = NULL; } access_load(access_list, xml_list); pthread_rwlock_unlock(&access_list->lock); } else access_list = access_load(NULL, xml_list); if (filter_list != NULL) { pthread_rwlock_wrlock(&filter_list->lock); if (overwrite) { filter_list->id = 0; filter_ll_free(filter_list->allow); filter_ll_free(filter_list->deny); filter_list->allow = filter_list->deny = NULL; FREE_AND_NULL(filter_list->dtemplate); } filter_load(filter_list, xml_list); pthread_rwlock_unlock(&filter_list->lock); } else filter_list = filter_load(NULL, xml_list); if (header_list != NULL) { pthread_rwlock_wrlock(&header_list->lock); if (overwrite) { header_list->id = 0; header_ll_free(header_list->allow); header_ll_free(header_list->deny); header_ll_free(header_list->insert); header_list->allow = header_list->deny = header_list->insert = NULL; } header_load(header_list, xml_list); pthread_rwlock_unlock(&header_list->lock); } else header_list = header_load(NULL, xml_list); if (cookie_list != NULL) { pthread_rwlock_wrlock(&cookie_list->lock); if (overwrite) { cookie_list->id = 0; cookie_ll_free(cookie_list->allow); cookie_ll_free(cookie_list->deny); cookie_list->allow = cookie_list->deny = NULL; } cookie_load(cookie_list, xml_list); pthread_rwlock_unlock(&cookie_list->lock); } else cookie_list = cookie_load(NULL, xml_list); if (rewrite_list != NULL) { pthread_rwlock_wrlock(&rewrite_list->lock); if (overwrite) { rewrite_list->id = 0; rewrite_list_free(rewrite_list->rewrite); rewrite_list->rewrite = NULL; } rewrite_load(rewrite_list, xml_list); pthread_rwlock_unlock(&rewrite_list->lock); } else rewrite_list = rewrite_load(NULL, xml_list); if (mime_list != NULL) { pthread_rwlock_wrlock(&mime_list->lock); if (overwrite) { mime_list->id = 0; mime_ll_free(mime_list->allow); mime_ll_free(mime_list->deny); mime_list->allow = mime_list->deny = NULL; FREE_AND_NULL(mime_list->dtemplate); } mime_load(mime_list, xml_list); pthread_rwlock_unlock(&mime_list->lock); } else mime_list = mime_load(NULL, xml_list); if (redirect_list != NULL) { pthread_rwlock_wrlock(&redirect_list->lock); if (overwrite) { redirect_list->id = 0; redirect_list_free(redirect_list->redirect_list); redirect_list->redirect_list = NULL; } redirect_load(redirect_list, xml_list); pthread_rwlock_unlock(&redirect_list->lock); } else redirect_list = redirect_load(NULL, xml_list); if (keyword_list != NULL) { pthread_rwlock_wrlock(&keyword_list->lock); if (overwrite) { keyword_list->id = 0; keyword_list_free(keyword_list->keyword_list); keyword_list->keyword_list = NULL; } keyword_load(keyword_list, xml_list); pthread_rwlock_unlock(&keyword_list->lock); } else keyword_list = keyword_load(NULL, xml_list); if (forward_list != NULL) { pthread_rwlock_wrlock(&forward_list->lock); if (overwrite) { forward_list->id = 0; forward_list_free(forward_list->forward_list); forward_list->forward_list = NULL; } forward_load(forward_list, xml_list); pthread_rwlock_unlock(&forward_list->lock); } else forward_list = forward_load(NULL, xml_list); if (templates != NULL) { pthread_rwlock_wrlock(&templates->lock); if (overwrite) { templates->id = 0; templates_list_free(templates->template_list); templates->template_list = NULL; FREE_AND_NULL(templates->path); } templates_load(templates, xml_list); pthread_rwlock_unlock(&templates->lock); } else templates = templates_load(NULL, xml_list); if (external != NULL) { pthread_rwlock_wrlock(&external->lock); if (overwrite) { external->id = 0; external_list_free(external->external_list); external->external_list = NULL; } external_load(external, xml_list); pthread_rwlock_unlock(&external->lock); } else external = external_load(NULL, xml_list); /* only do this at startup */ if (overwrite == 3) { if (network != NULL) { pthread_rwlock_wrlock(&network->lock); network = network_load(network, xml_list); network_check(network); pthread_rwlock_unlock(&network->lock); } else { network = network_load(NULL, xml_list); network_check(network); } } xml_list_free(xml_list); return TRUE; } /* main event loop; accept connections, check access list, then create thread to continue connection. */ void mainloop() { int x; CONNECTION *connection; config(); while (1) { connection = net_accept(-1); if (connection != NULL) { if (access_check(access_list, connection, NULL, NULL)) { x = process_new(connection); if (x != 0) { putlog(MMLOG_ERROR, "failed to create thread for %s", connection->ip); net_close(connection); } } else { putlog(MMLOG_NETWORK, "refused connect from %s on port %d", connection->ip, connection->port); net_close(connection); } } } } static void sigchld(int signo) { while (waitpid(-1,NULL,WNOHANG)>0); } /* create new thread */ int process_new(CONNECTION * connection) { int perr, thread = -1, i; pthread_t thread_id; pthread_attr_t thread_attr; for (i = 0; i < MAXTHREADS && thread == -1; i++) { pthread_mutex_lock(&threads[i].lock); if (threads[i].flags & THREAD_UNUSED) { thread = i; threads[i].flags = THREAD_IDLE; threads[i].host = NULL; threads[i].file = NULL; threads[i].method = NULL; threads[i].port = 0; threads[i].requests = 0; threads[i].ip = xstrdup(connection->ip); } pthread_mutex_unlock(&threads[i].lock); } if (thread == -1) return -1; connection->thread = thread; pthread_attr_init(&thread_attr); pthread_attr_setdetachstate(&thread_attr, PTHREAD_CREATE_DETACHED); if (getuid() == 0) pthread_attr_setschedpolicy(&thread_attr, SCHED_FIFO); signal(SIGCHLD,sigchld); if (0==(perr=fork())) { process_entry(connection); _exit(0); } perr=close(connection->client->fd) pthread_attr_destroy(&thread_attr); return perr; } /* entry function for new threads */ void process_entry(CONNECTION * connection) { int x, ret; struct HTTP_HEADER_LIST *http_header_list; struct FILTER_LIST_LIST *filter_match; char *headbuf = NULL, *ptr, buf[4096]; struct url_command_t **url_command; FILEBUF *filebuf; HEADER *header; /* write log message here so the pid matches */ putlog(MMLOG_NETWORK, "allowed connect from %s on port %d", connection->ip, connection->port); pthread_mutex_lock(&threads[connection->thread].lock); threads[connection->thread].flags = THREAD_CHEADERWAIT; threads[connection->thread].pid = (unsigned int) getpid(); pthread_mutex_unlock(&threads[connection->thread].lock); while (1) { /* reset bypass mask to the one provided by the access rule */ connection->bypass = connection->obypass; headbuf = header_get(connection, CLIENT, (connection->request) ? KEEPTIMEOUT : TIMEOUT); if (headbuf == NULL) { if (!connection->request) { putlog(MMLOG_WARN, "timeout waiting for header from %s", connection->ip); template_send(templates, "badrequest", connection, 400); } break; } /* pass the client header through the rewrite rules before parsing */ /* note: can't bypass this with a url command */ filebuf = filebuf_new(); filebuf->data = headbuf; filebuf->size = strlen(headbuf) + 1; rewrite_do(rewrite_list, connection, filebuf, REWRITE_CLIENT, TRUE); headbuf = filebuf->data; filebuf->data = NULL; filebuf_free(filebuf); connection->header = http_header_parse_request(headbuf); xfree(headbuf); if (connection->header == NULL) { if (!connection->request) { putlog(MMLOG_WARN, "invalid header reveived from %s", connection->ip); template_send(templates, "badrequest", connection, 400); } break; } /* determine if the connection should be kept alive with the information gathered so far, this is incase a template or web interface is used */ if (connection->header->type == HTTP_CONNECT) connection->keepalive_client = FALSE; else if (connection->header->keepalive == FALSE || connection->header->proxy_keepalive == FALSE) connection->keepalive_client = FALSE; else if (connection->header->proxy_keepalive == TRUE || connection->header->keepalive == TRUE) connection->keepalive_client = TRUE; else if (connection->header->version == HTTP_HTTP11) connection->keepalive_client = TRUE; else connection->keepalive_client = FALSE; if ((connection->access & ACCESS_BYPASS) && connection->header->url_command != NULL) { for (url_command = connection->header->url_command; *url_command; url_command++) { if (!strcasecmp((*url_command)->command, "bypass")) { ptr = (*url_command)->options; if (ptr != NULL) { x = TRUE; for (; *ptr; ptr++) { switch (*ptr) { case '+': x = TRUE; break; case '-': x = FALSE; break; case 'f': if (x == TRUE) connection->bypass |= FEATURE_FILTER; else connection->bypass &= ~FEATURE_FILTER; break; case 'h': if (x == TRUE) connection->bypass |= FEATURE_HEADER; else connection->bypass &= ~FEATURE_HEADER; break; case 'm': if (x == TRUE) connection->bypass |= FEATURE_MIME; else connection->bypass &= ~FEATURE_MIME; break; case 'r': if (x == TRUE) connection->bypass |= FEATURE_REDIRECT; else connection->bypass &= ~FEATURE_REDIRECT; break; case 'c': if (x == TRUE) connection->bypass |= FEATURE_COOKIES; else connection->bypass &= ~FEATURE_COOKIES; break; case 'w': if (x == TRUE) connection->bypass |= FEATURE_REWRITE; else connection->bypass &= ~FEATURE_REWRITE; break; case 'e': if (x == TRUE) connection->bypass |= FEATURE_EXTERNAL; else connection->bypass &= ~FEATURE_EXTERNAL; break; case 'p': if (x == TRUE) connection->bypass |= FEATURE_FORWARD; else connection->bypass &= ~FEATURE_FORWARD; break; case 'k': if (x == TRUE) connection->bypass |= FEATURE_KEYWORDS; else connection->bypass &= ~FEATURE_KEYWORDS; break; } } } else connection->bypass = ~0; } } } if (connection->authenticate == TRUE && connection->header->proxy_authorization == NULL) { header = header_new(); header->type = HTTP_RESP; header->code = 407; header->content_length = 0; header->proxy_authenticate = xstrdup("Basic"); header_send(header, connection, CLIENT, HEADER_RESP); http_header_free(header); goto skip; } else if (connection->header->proxy_authorization != NULL) { ptr = strchr(connection->header->proxy_authorization, ' '); if (ptr != NULL) { ptr++; ret = from64tobits(buf, ptr); buf[ret] = '\0'; putlog(MMLOG_DEBUG, "%s", buf); ptr = strchr(buf, ':'); if (ptr != NULL) { *ptr = '\0'; ret = access_check(access_list, connection, buf, ++ptr); if (ret) connection->authenticate = FALSE; } } if (connection->authenticate == TRUE) { header = header_new(); header->type = HTTP_RESP; header->code = 407; header->content_length = 0; header->proxy_authenticate = xstrdup("Basic"); header_send(header, connection, CLIENT, HEADER_RESP); http_header_free(header); goto skip; } } /* redirect the request if any matching rules found (redirect_do will fill in the host, file and port members of the connection struct if any are found) */ x = redirect_do(redirect_list, connection, REDIRECT_REQUEST); if (x == TRUE) { /* 302 redirect sent, no need to continue */ goto skip; } pthread_mutex_lock(&threads[connection->thread].lock); FREE_AND_STRDUP(threads[connection->thread].host, connection->header->host); FREE_AND_STRDUP(threads[connection->thread].file, connection->header->file); FREE_AND_STRDUP(threads[connection->thread].method, connection->header->method); threads[connection->thread].port = connection->header->port; pthread_mutex_unlock(&threads[connection->thread].lock); http_header_list = header_filter(header_list, connection); connection->header->header_filtered = http_header_list; if (connection->header->type != HTTP_CONNECT) { if (connection->header->type != HTTP_REQUEST && strcasecmp(connection->header->proto, "http")) { /* only http protocol is supported */ template_send(templates, "badprotocol", connection, 501); goto skip; } if ((connection->header->type != HTTP_REQUEST && !strcasecmp(connection->header->host, INTERFACEURL)) || (connection->header->type == HTTP_REQUEST && !strncasecmp(&connection->header->file[1], INTERFACEURL, strlen(INTERFACEURL)))) { /* request for web interface */ putlog(MMLOG_REQUEST, "request for web interface from %s", connection->ip); interface_handle_request(connection); goto skip; } if ((connection->header->type == HTTP_REQUEST && !(connection->access & ACCESS_HTTP)) || (connection->header->type == HTTP_PROXY && !(connection->access & ACCESS_PROXY))) { template_send(templates, "noaccess", connection, 404); goto skip; } } else if (!(connection->access & ACCESS_CONNECT)) { template_send(templates, "noaccess", connection, 404); goto skip; } if (connection->header->type == HTTP_REQUEST && connection->header->host == NULL) { if ((connection->access & ACCESS_TRANSPARENT) && connection->header->host_header != NULL) { /* use Host: header if it's there */ ptr = strchr(connection->header->host_header, ':'); connection->header->host = xstrndup(connection->header->host_header, (ptr != NULL) ? ptr - connection->header->host_header : strlen(connection->header->host_header)); if (ptr != NULL) connection->header->port = atoi(&ptr[1]); /* this feature causes a recursion where the proxy keeps connecting to itself if an HTTP request is made to the proxy which doesn't match a redirect rule and isn't a request for the web interface. There's no reliable way to detect this except to forbid connections to websites on the same port as the proxy */ if (connection->header->port == connection->port) { template_send(templates, "nofile", connection, 404); goto skip; } } else { /* not a request for web interface, no host header, and no matching redirect rule */ template_send(templates, "nofile", connection, 404); goto skip; } } for (url_command = connection->header->url_command; url_command && *url_command; url_command++) { if (!strcasecmp((*url_command)->command, "filter")) { filter_check_show(connection); goto skip; } } filter_match = filter_check(filter_list, connection); if (filter_match != NULL) { putlog(MMLOG_FILTER, "blocked %s%s", connection->header->host, connection->header->file); template_send(templates, (filter_match->template != NULL) ? filter_match->template : (filter_list->dtemplate != NULL) ? filter_list->dtemplate : "blocked", connection, (connection->header->type == HTTP_CONNECT) ? 404 : 200); pthread_rwlock_unlock(&filter_list->lock); goto skip; } pthread_rwlock_unlock(&filter_list->lock); if (connection->site_host != NULL && (strcasecmp(connection->header->host, connection->site_host) || connection->header->port != connection->site_port)) { /* not a request for same host/port as previous request */ if (connection->server != NULL) { sock_close(connection->server); connection->server = NULL; } } if (connection->server == NULL) { /* check if this request should be forwarded through another proxy */ /* forward_do will fill in all the necessary members of the connection struct */ connection->proxy_type = PROXY_DIRECT; forward_do(forward_list, connection); x = protocol_start(connection); if (x == -1) goto skip; FREE_AND_NULL(connection->site_host); connection->site_host = xstrdup(connection->header->host); connection->site_port = connection->header->port; } if (connection->header->type != HTTP_CONNECT) x = protocol_http(connection); else x = protocol_connect(connection); if (x < 0) putlog(MMLOG_HEADER, "error reading header from %s", connection->header->host); skip: sock_flush(connection->client); connection->request++; http_header_free(connection->header); putlog(MMLOG_DEBUG, "keepalive_client = %d", connection->keepalive_client); if (!connection->keepalive_client) { if (connection->server != NULL) { sock_close(connection->server); connection->server = NULL; } break; } else if (!connection->keepalive_server && connection->server != NULL) { sock_close(connection->server); connection->server = NULL; } pthread_mutex_lock(&threads[connection->thread].lock); threads[connection->thread].flags = THREAD_CHEADERWAIT; FREE_AND_NULL(threads[connection->thread].host); FREE_AND_NULL(threads[connection->thread].file); FREE_AND_NULL(threads[connection->thread].method); threads[connection->thread].port = 0; threads[connection->thread].requests++; pthread_mutex_unlock(&threads[connection->thread].lock); connection->header = NULL; connection->rheader = NULL; FREE_AND_NULL(connection->proxy_host); } if (connection->server != NULL) sock_close(connection->server); putlog(MMLOG_NETWORK, "%s disconnected after making %d requests", connection->ip, connection->request); pthread_mutex_lock(&threads[connection->thread].lock); threads[connection->thread].flags = THREAD_UNUSED; FREE_AND_NULL(threads[connection->thread].ip); FREE_AND_NULL(threads[connection->thread].host); FREE_AND_NULL(threads[connection->thread].file); FREE_AND_NULL(threads[connection->thread].method); pthread_mutex_unlock(&threads[connection->thread].lock); net_close(connection); return; } /* save all config settings to a file */ int config_save(char *filename) { int ret; XML_LIST *xml_list = NULL; /* reconstruct config file for all sections */ xml_list = network_xml(network, xml_list); xml_list = templates_xml(templates, xml_list); xml_list = external_xml(external, xml_list); xml_list = access_xml(access_list, xml_list); xml_list = header_xml(header_list, xml_list); xml_list = cookie_xml(cookie_list, xml_list); xml_list = redirect_xml(redirect_list, xml_list); xml_list = keyword_xml(keyword_list, xml_list); xml_list = forward_xml(forward_list, xml_list); xml_list = filter_xml(filter_list, xml_list); xml_list = mime_xml(mime_list, xml_list); xml_list = rewrite_xml(rewrite_list, xml_list); if (xml_list == NULL) return FALSE; while (xml_list->prev != NULL) xml_list = xml_list->prev; ret = xml_save(xml_list, filename); xml_list_free(xml_list); return ret; }