1 /********************************************************************\
2 * BitlBee -- An IRC to other IM-networks gateway *
4 * Copyright 2002-2006 Wilmer van der Gaast and others *
5 \********************************************************************/
7 /* Storage backend that uses an XMLish format for all data. */
10 This program is free software; you can redistribute it and/or modify
11 it under the terms of the GNU General Public License as published by
12 the Free Software Foundation; either version 2 of the License, or
13 (at your option) any later version.
15 This program is distributed in the hope that it will be useful,
16 but WITHOUT ANY WARRANTY; without even the implied warranty of
17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 GNU General Public License for more details.
20 You should have received a copy of the GNU General Public License with
21 the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
22 if not, write to the Free Software Foundation, Inc., 59 Temple Place,
23 Suite 330, Boston, MA 02111-1307 USA
32 #if GLIB_CHECK_VERSION(2,8,0)
33 #include <glib/gstdio.h>
35 /* GLib < 2.8.0 doesn't have g_access, so just use the system access(). */
37 #define g_access access
42 XML_PASS_CHECK_ONLY
= -1,
48 /* To make it easier later when extending the format: */
49 #define XML_FORMAT_VERSION 1
54 char *current_setting
;
55 account_t
*current_account
;
56 irc_channel_t
*current_channel
;
57 set_t
**current_set_head
;
64 static char *xml_attr( const gchar
**attr_names
, const gchar
**attr_values
, const gchar
*key
)
68 for( i
= 0; attr_names
[i
]; i
++ )
69 if( g_strcasecmp( attr_names
[i
], key
) == 0 )
70 return (char*) attr_values
[i
];
75 static void xml_destroy_xd( gpointer data
)
77 struct xml_parsedata
*xd
= data
;
79 g_free( xd
->given_nick
);
80 g_free( xd
->given_pass
);
84 static void xml_start_element( GMarkupParseContext
*ctx
, const gchar
*element_name
, const gchar
**attr_names
, const gchar
**attr_values
, gpointer data
, GError
**error
)
86 struct xml_parsedata
*xd
= data
;
89 if( xd
->unknown_tag
> 0 )
93 else if( g_strcasecmp( element_name
, "user" ) == 0 )
95 char *nick
= xml_attr( attr_names
, attr_values
, "nick" );
96 char *pass
= xml_attr( attr_names
, attr_values
, "password" );
101 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
102 "Missing attributes for %s element", element_name
);
104 else if( ( st
= md5_verify_password( xd
->given_pass
, pass
) ) == -1 )
106 xd
->pass_st
= XML_PASS_WRONG
;
107 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
108 "Error while decoding password attribute" );
112 if( xd
->pass_st
!= XML_PASS_CHECK_ONLY
)
113 xd
->pass_st
= XML_PASS_OK
;
117 xd
->pass_st
= XML_PASS_WRONG
;
118 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
119 "Password mismatch" );
122 else if( xd
->pass_st
< XML_PASS_OK
)
124 /* Let's not parse anything else if we only have to check
127 else if( g_strcasecmp( element_name
, "account" ) == 0 )
129 char *protocol
, *handle
, *server
, *password
= NULL
, *autoconnect
, *tag
;
130 char *pass_b64
= NULL
;
131 unsigned char *pass_cr
= NULL
;
133 struct prpl
*prpl
= NULL
;
135 handle
= xml_attr( attr_names
, attr_values
, "handle" );
136 pass_b64
= xml_attr( attr_names
, attr_values
, "password" );
137 server
= xml_attr( attr_names
, attr_values
, "server" );
138 autoconnect
= xml_attr( attr_names
, attr_values
, "autoconnect" );
139 tag
= xml_attr( attr_names
, attr_values
, "tag" );
141 protocol
= xml_attr( attr_names
, attr_values
, "protocol" );
143 prpl
= find_protocol( protocol
);
145 if( !handle
|| !pass_b64
|| !protocol
)
146 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
147 "Missing attributes for %s element", element_name
);
149 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
150 "Unknown protocol: %s", protocol
);
151 else if( ( pass_len
= base64_decode( pass_b64
, (unsigned char**) &pass_cr
) ) &&
152 arc_decode( pass_cr
, pass_len
, &password
, xd
->given_pass
) >= 0 )
154 xd
->current_account
= account_add( irc
->b
, prpl
, handle
, password
);
156 set_setstr( &xd
->current_account
->set
, "server", server
);
158 set_setstr( &xd
->current_account
->set
, "auto_connect", autoconnect
);
160 set_setstr( &xd
->current_account
->set
, "tag", tag
);
164 /* Actually the _decode functions don't even return error codes,
165 but maybe they will later... */
166 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
167 "Error while decrypting account password" );
173 else if( g_strcasecmp( element_name
, "setting" ) == 0 )
177 if( xd
->current_setting
)
179 g_free( xd
->current_setting
);
180 xd
->current_setting
= NULL
;
183 if( ( setting
= xml_attr( attr_names
, attr_values
, "name" ) ) )
185 if( xd
->current_channel
!= NULL
)
186 xd
->current_set_head
= &xd
->current_channel
->set
;
187 else if( xd
->current_account
!= NULL
)
188 xd
->current_set_head
= &xd
->current_account
->set
;
190 xd
->current_set_head
= &xd
->irc
->b
->set
;
192 xd
->current_setting
= g_strdup( setting
);
195 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
196 "Missing attributes for %s element", element_name
);
198 else if( g_strcasecmp( element_name
, "buddy" ) == 0 )
202 handle
= xml_attr( attr_names
, attr_values
, "handle" );
203 nick
= xml_attr( attr_names
, attr_values
, "nick" );
205 if( xd
->current_account
&& handle
&& nick
)
207 nick_set_raw( xd
->current_account
, handle
, nick
);
211 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
212 "Missing attributes for %s element", element_name
);
215 else if( g_strcasecmp( element_name
, "channel" ) == 0 )
219 name
= xml_attr( attr_names
, attr_values
, "name" );
220 type
= xml_attr( attr_names
, attr_values
, "type" );
224 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
225 "Missing attributes for %s element", element_name
);
229 /* The channel may exist already, for example if it's &bitlbee.
230 Also, it's possible that the user just reconnected and the
231 IRC client already rejoined all channels it was in. They
232 should still get the right settings. */
233 if( ( xd
->current_channel
= irc_channel_by_name( irc
, name
) ) ||
234 ( xd
->current_channel
= irc_channel_new( irc
, name
) ) )
235 set_setstr( &xd
->current_channel
->set
, "type", type
);
237 /* Backward compatibility: Keep this around for a while for people
238 switching from BitlBee 1.2.4+. */
239 else if( g_strcasecmp( element_name
, "chat" ) == 0 )
241 char *handle
, *channel
;
243 handle
= xml_attr( attr_names
, attr_values
, "handle" );
244 channel
= xml_attr( attr_names
, attr_values
, "channel" );
246 if( xd
->current_account
&& handle
&& channel
)
250 if( ( ic
= irc_channel_new( irc
, channel
) ) &&
251 set_setstr( &ic
->set
, "type", "chat" ) &&
252 set_setstr( &ic
->set
, "chat_type", "room" ) &&
253 set_setstr( &ic
->set
, "account", xd
->current_account
->tag
) &&
254 set_setstr( &ic
->set
, "room", handle
) )
256 /* Try to pick up some settings where possible. */
257 xd
->current_channel
= ic
;
260 irc_channel_free( ic
);
264 g_set_error( error
, G_MARKUP_ERROR
, G_MARKUP_ERROR_INVALID_CONTENT
,
265 "Missing attributes for %s element", element_name
);
271 irc_rootmsg( irc
, "Warning: Unknown XML tag found in configuration file (%s). "
272 "This may happen when downgrading BitlBee versions. "
273 "This tag will be skipped and the information will be lost "
274 "once you save your settings.", element_name
);
276 g_set_error( error, G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
277 "Unkown element: %s", element_name );
282 static void xml_end_element( GMarkupParseContext
*ctx
, const gchar
*element_name
, gpointer data
, GError
**error
)
284 struct xml_parsedata
*xd
= data
;
286 if( xd
->unknown_tag
> 0 )
290 else if( g_strcasecmp( element_name
, "setting" ) == 0 && xd
->current_setting
)
292 g_free( xd
->current_setting
);
293 xd
->current_setting
= NULL
;
295 else if( g_strcasecmp( element_name
, "account" ) == 0 )
297 xd
->current_account
= NULL
;
299 else if( g_strcasecmp( element_name
, "channel" ) == 0 ||
300 g_strcasecmp( element_name
, "chat" ) == 0 )
302 xd
->current_channel
= NULL
;
306 static void xml_text( GMarkupParseContext
*ctx
, const gchar
*text_orig
, gsize text_len
, gpointer data
, GError
**error
)
308 char text
[text_len
+1];
309 struct xml_parsedata
*xd
= data
;
311 strncpy( text
, text_orig
, text_len
);
314 if( xd
->pass_st
< XML_PASS_OK
)
316 /* Let's not parse anything else if we only have to check
317 the password, or if we didn't get the chance to check it
320 else if( g_strcasecmp( g_markup_parse_context_get_element( ctx
), "setting" ) == 0 && xd
->current_setting
)
322 if( xd
->current_account
)
324 set_t
*s
= set_find( xd
->current_set_head
, xd
->current_setting
);
325 if( s
&& ( s
->flags
& ACC_SET_ONLINE_ONLY
) )
327 g_free( xd
->current_setting
);
328 xd
->current_setting
= NULL
;
332 set_setstr( xd
->current_set_head
, xd
->current_setting
, (char*) text
);
333 g_free( xd
->current_setting
);
334 xd
->current_setting
= NULL
;
338 GMarkupParser xml_parser
=
347 static void xml_init( void )
349 if( g_access( global
.conf
->configdir
, F_OK
) != 0 )
350 log_message( LOGLVL_WARNING
, "The configuration directory `%s' does not exist. Configuration won't be saved.", global
.conf
->configdir
);
351 else if( g_access( global
.conf
->configdir
, F_OK
) != 0 ||
352 g_access( global
.conf
->configdir
, W_OK
) != 0 )
353 log_message( LOGLVL_WARNING
, "Permission problem: Can't read/write from/to `%s'.", global
.conf
->configdir
);
356 static storage_status_t
xml_load_real( irc_t
*irc
, const char *my_nick
, const char *password
, xml_pass_st action
)
358 GMarkupParseContext
*ctx
;
359 struct xml_parsedata
*xd
;
364 xd
= g_new0( struct xml_parsedata
, 1 );
366 xd
->given_nick
= g_strdup( my_nick
);
367 xd
->given_pass
= g_strdup( password
);
368 xd
->pass_st
= action
;
369 nick_lc( xd
->given_nick
);
371 fn
= g_strdup_printf( "%s%s%s", global
.conf
->configdir
, xd
->given_nick
, ".xml" );
372 if( ( fd
= open( fn
, O_RDONLY
) ) < 0 )
374 xml_destroy_xd( xd
);
376 return STORAGE_NO_SUCH_USER
;
380 ctx
= g_markup_parse_context_new( &xml_parser
, 0, xd
, xml_destroy_xd
);
382 while( ( st
= read( fd
, buf
, sizeof( buf
) ) ) > 0 )
384 if( !g_markup_parse_context_parse( ctx
, buf
, st
, &gerr
) || gerr
)
386 xml_pass_st pass_st
= xd
->pass_st
;
388 g_markup_parse_context_free( ctx
);
391 if( pass_st
== XML_PASS_WRONG
)
393 g_clear_error( &gerr
);
394 return STORAGE_INVALID_PASSWORD
;
399 irc_rootmsg( irc
, "Error from XML-parser: %s", gerr
->message
);
401 g_clear_error( &gerr
);
402 return STORAGE_OTHER_ERROR
;
406 /* Just to be sure... */
407 g_clear_error( &gerr
);
409 g_markup_parse_context_free( ctx
);
412 if( action
== XML_PASS_CHECK_ONLY
)
418 static storage_status_t
xml_load( irc_t
*irc
, const char *password
)
420 return xml_load_real( irc
, irc
->user
->nick
, password
, XML_PASS_UNKNOWN
);
423 static storage_status_t
xml_check_pass( const char *my_nick
, const char *password
)
425 /* This is a little bit risky because we have to pass NULL for the
426 irc_t argument. This *should* be fine, if I didn't miss anything... */
427 return xml_load_real( NULL
, my_nick
, password
, XML_PASS_CHECK_ONLY
);
430 static int xml_printf( int fd
, int indent
, char *fmt
, ... )
434 char tabs
[9] = "\t\t\t\t\t\t\t\t";
437 /* Maybe not very clean, but who needs more than 8 levels of indentation anyway? */
438 if( write( fd
, tabs
, indent
<= 8 ? indent
: 8 ) != indent
)
441 va_start( params
, fmt
);
442 out
= g_markup_vprintf_escaped( fmt
, params
);
446 len
-= write( fd
, out
, len
);
452 static gboolean
xml_save_nick( gpointer key
, gpointer value
, gpointer data
);
454 static storage_status_t
xml_save( irc_t
*irc
, int overwrite
)
456 char path
[512], *path2
, *pass_buf
= NULL
;
460 md5_byte_t pass_md5
[21];
461 md5_state_t md5_state
;
464 path2
= g_strdup( irc
->user
->nick
);
466 g_snprintf( path
, sizeof( path
) - 2, "%s%s%s", global
.conf
->configdir
, path2
, ".xml" );
469 if( !overwrite
&& g_access( path
, F_OK
) == 0 )
470 return STORAGE_ALREADY_EXISTS
;
472 strcat( path
, ".XXXXXX" );
473 if( ( fd
= mkstemp( path
) ) < 0 )
475 irc_rootmsg( irc
, "Error while opening configuration file." );
476 return STORAGE_OTHER_ERROR
;
479 /* Generate a salted md5sum of the password. Use 5 bytes for the salt
480 (to prevent dictionary lookups of passwords) to end up with a 21-
481 byte password hash, more convenient for base64 encoding. */
482 random_bytes( pass_md5
+ 16, 5 );
483 md5_init( &md5_state
);
484 md5_append( &md5_state
, (md5_byte_t
*) irc
->password
, strlen( irc
->password
) );
485 md5_append( &md5_state
, pass_md5
+ 16, 5 ); /* Add the salt. */
486 md5_finish( &md5_state
, pass_md5
);
487 /* Save the hash in base64-encoded form. */
488 pass_buf
= base64_encode( pass_md5
, 21 );
490 if( !xml_printf( fd
, 0, "<user nick=\"%s\" password=\"%s\" version=\"%d\">\n", irc
->user
->nick
, pass_buf
, XML_FORMAT_VERSION
) )
495 for( set
= irc
->b
->set
; set
; set
= set
->next
)
496 if( set
->value
&& !( set
->flags
& SET_NOSAVE
) )
497 if( !xml_printf( fd
, 1, "<setting name=\"%s\">%s</setting>\n", set
->key
, set
->value
) )
500 for( acc
= irc
->b
->accounts
; acc
; acc
= acc
->next
)
502 unsigned char *pass_cr
;
506 pass_len
= arc_encode( acc
->pass
, strlen( acc
->pass
), (unsigned char**) &pass_cr
, irc
->password
, 12 );
507 pass_b64
= base64_encode( pass_cr
, pass_len
);
510 if( !xml_printf( fd
, 1, "<account protocol=\"%s\" handle=\"%s\" password=\"%s\" "
511 "autoconnect=\"%d\" tag=\"%s\"", acc
->prpl
->name
, acc
->user
,
512 pass_b64
, acc
->auto_connect
, acc
->tag
) )
519 if( acc
->server
&& acc
->server
[0] && !xml_printf( fd
, 0, " server=\"%s\"", acc
->server
) )
521 if( !xml_printf( fd
, 0, ">\n" ) )
524 for( set
= acc
->set
; set
; set
= set
->next
)
525 if( set
->value
&& !( set
->flags
& ACC_SET_NOSAVE
) )
526 if( !xml_printf( fd
, 2, "<setting name=\"%s\">%s</setting>\n", set
->key
, set
->value
) )
529 /* This probably looks pretty strange. g_hash_table_foreach
530 is quite a PITA already (but it can't get much better in
531 C without using #define, I'm afraid), and since it
532 doesn't seem to be possible to abort the foreach on write
533 errors, so instead let's use the _find function and
534 return TRUE on write errors. Which means, if we found
535 something, there was an error. :-) */
536 if( g_hash_table_find( acc
->nicks
, xml_save_nick
, & fd
) )
539 if( !xml_printf( fd
, 1, "</account>\n" ) )
543 for( l
= irc
->channels
; l
; l
= l
->next
)
545 irc_channel_t
*ic
= l
->data
;
547 if( ic
->flags
& IRC_CHANNEL_TEMP
)
550 if( !xml_printf( fd
, 1, "<channel name=\"%s\" type=\"%s\">\n",
551 ic
->name
, set_getstr( &ic
->set
, "type" ) ) )
554 for( set
= ic
->set
; set
; set
= set
->next
)
555 if( set
->value
&& strcmp( set
->key
, "type" ) != 0 )
556 if( !xml_printf( fd
, 2, "<setting name=\"%s\">%s</setting>\n", set
->key
, set
->value
) )
559 if( !xml_printf( fd
, 1, "</channel>\n" ) )
563 if( !xml_printf( fd
, 0, "</user>\n" ) )
569 path2
= g_strndup( path
, strlen( path
) - 7 );
570 if( rename( path
, path2
) != 0 )
572 irc_rootmsg( irc
, "Error while renaming temporary configuration file." );
577 return STORAGE_OTHER_ERROR
;
587 irc_rootmsg( irc
, "Write error. Disk full?" );
590 return STORAGE_OTHER_ERROR
;
593 static gboolean
xml_save_nick( gpointer key
, gpointer value
, gpointer data
)
595 return !xml_printf( *( (int*) data
), 2, "<buddy handle=\"%s\" nick=\"%s\" />\n", key
, value
);
598 static storage_status_t
xml_remove( const char *nick
, const char *password
)
601 storage_status_t status
;
603 status
= xml_check_pass( nick
, password
);
604 if( status
!= STORAGE_OK
)
607 lc
= g_strdup( nick
);
609 g_snprintf( s
, 511, "%s%s%s", global
.conf
->configdir
, lc
, ".xml" );
612 if( unlink( s
) == -1 )
613 return STORAGE_OTHER_ERROR
;
618 storage_t storage_xml
= {
621 .check_pass
= xml_check_pass
,
622 .remove
= xml_remove
,