#!/usr/bin/perl -w # Copyright 2009 Apple Inc. All rights reserved. # Migrate a 10.4,10.5,10.6 configuration to 10.6 use Getopt::Std; use File::Copy; use File::Basename; use XML::Simple; use strict; my $DEBUG = 0; my $SERVERMGR_JABBER_CGI_PATH = "/usr/share/servermgrd/cgi-bin/servermgr_jabber"; my $MKTEMP_PATH = "/usr/bin/mktemp"; my $logPath = "/Library/Logs/Migration/jabbermigrator.log"; my $MKDIR_PATH = "/bin/mkdir"; my $PLISTBUDDY = "/usr/libexec/PlistBuddy"; my $CERT_PATH = "/etc/certificates"; sub usage { print "This script reads a jabberd config XML and migrates the config data into jabberd 2.1.\n"; print "It uses servermgr_jabber to perform the necessary changes to jabberd2 config files.\n\n"; print "Usage: $0 [-d] [-c path] [-s version]\n"; print "Flags:\n"; print " -d: Debug mode.\n"; print " -c : Source jabberd config file to use. Should be a \"com.apple.ichatserver.plist\" for\n"; print " 10.5, 10.6 or \"jabber.xml\" for 10.4. (REQUIRED)\n"; print " -s : Source OS version. \"10.4\", \"10.5\", and \"10.6\" supported. (REQUIRED)\n"; print " -?, -h: Show usage info.\n"; } sub log_message { open(LOGFILE, ">>$logPath") || die "$0: cannot open $logPath: $!"; my $time = localtime(); print LOGFILE "$time: ". basename($0). ": @_\n"; print "@_\n" if $DEBUG; close(LOGFILE); } sub bail { &log_message(@_); &log_message("Aborting!"); exit 1; } ############ MAIN if (! -d dirname($logPath) ) { my $logDir = dirname($logPath); qx{ $MKDIR_PATH -p $logDir }; if ($? != 0) { &log_message("\"$MKDIR_PATH -p $logDir\" returned failure status $?"); } } my %opts; getopts('ds:c:?h', \%opts); if (defined $opts{'?'} || defined $opts{'h'}) { &usage; exit 0; } if (defined $opts{'d'}) { $DEBUG = 1; } my $src_ver; if (defined $opts{'s'}) { my $tmp_src_ver = $opts{'s'}; if ($opts{'s'} =~ /^10\.4/) { &log_message("Treating source OS as 10.4.x"); $src_ver = "10.4"; } elsif ($opts{'s'} =~ /^10\.5/) { &log_message("Treating source OS as 10.5.x"); $src_ver = "10.5"; } elsif ($opts{'s'} =~ /^10\.6/) { &log_message("Treating source OS as 10.6.x"); $src_ver = "10.6"; } else { &bail("Unrecognized source OS version specified, don't know how to parse the source config file. Aborting.\n"); } } else { &bail("Source OS version was not specified, aborting.\n"); } my $source_config = ""; if (defined $opts{'c'} && $opts{'c'} ne "") { $source_config = $opts{'c'}; } else { &bail("You must specify the source configuration file to use, aborting"); } open(IN, "<$source_config") || &bail("ERROR: Cannot open source config $source_config: $!"); my @lines = ; chomp(@lines); close(IN); my $tmpname; for (my $i = 0; $i < 5; $i++) { $tmpname = qx{ $MKTEMP_PATH /tmp/jabber_migration.XXXXXXXXXXXXXXXXXXXXXXXX }; chomp($tmpname); if (-e $tmpname) { last; } if ($i == 4) { &bail("ERROR: Cannot create temporary file: $tmpname"); } } my $res; if ($src_ver eq "10.5" || $src_ver eq "10.6") { &log_message("Importing settings from file: $source_config"); my $xs1 = XML::Simple->new (KeepRoot => 1); my $doc = $xs1->XMLin($source_config); my @keys; for (my $i = 0; (defined $doc->{plist}->{dict}->{key}->[$i]); $i++) { push(@keys, $doc->{plist}->{dict}->{key}->[$i]); if ($DEBUG) { print "found key: ".$doc->{plist}->{dict}->{key}->[$i]."\n"; } } open(OUT, ">$tmpname") || &bail("ERROR: Could not open file $tmpname: $!"); print OUT <<"EOF"; command writeSettings configuration EOF # Gather certificate file data for later use my @cert_files; my $ret = opendir(CERTS_DIR, $CERT_PATH); if (! $ret) { &log_message("ERROR: unable to read dir: $CERT_PATH"); } else { @cert_files = readdir(CERTS_DIR); closedir(CERTS_DIR); chomp(@cert_files); } my $prospective_chainfile = ""; my $configured_chainfile = 0; NEXTKEY: foreach my $key (@keys) { $ret = qx { $PLISTBUDDY -x -c "Print :$key:" "$source_config" }; if ($DEBUG) { print "DEBUG: $PLISTBUDDY output: $ret\n"; } chomp($ret); my @lines = split("\n", $ret); if ($ret eq "" || $#lines < 4) { &log_message("ERROR: PlistBuddy problem getting value for key \"$key\" from file \"$source_config\", it returned:\n$ret\n"); next; } # Magic numbers used assume this output from PlistBuddy: # # # # example # # We want to filter out directory paths that have empty values, otherwise our writeSettings will fail. if (($key eq "dataLocation" || $key eq "jabberdDatabasePath" || $key eq "savedChatsLocation") && ($lines[3] eq "")) { next; } # For 10.5 sources, certificates were named differently than 10.6+. # 10.5: mycert.crtkey # 10.6: mycert.6E60F5812D4DE84616E92E1ACB9F0B35ED6A2229.concat.pem # 10.5: mycert.chcrt # 10.6: mycert.6E60F5812D4DE84616E92E1ACB9F0B35ED6A2229.chain.pem # We need to use the migrated certificates in the new configuration. # SSLCAFile (chain file) notes: # If the source has sslCAFile defined, AND it has been moved to a new filename, use the new file. # If the source has sslCAFile defined, AND and there's no new migrated version, use the original sslCAFile value. # If the source has no sslCAFile defined, but has a valid sslKeyFile, then look for a chain file # that has been created during migration based on the old sslKeyFile. # (10.5 systems that have an sslKeyFile but no corresponding chain file are going to # have a new chain file generated for them during migration, so lets use it.) my $cert_prefix; my $cert; if ($key eq "sslKeyFile") { if ($lines[3] =~ /^(.*)\.crtkey<\/string>$/) { $cert_prefix = $1; $cert_prefix =~ s/.*[\/](.*)/$1/; foreach $cert (@cert_files) { if ($cert =~ /^$cert_prefix.([A-F0-9]+).concat.pem$/) { print OUT "\t\t$key\n"; print OUT "\t\t$CERT_PATH/$cert\n"; $prospective_chainfile = "$CERT_PATH/$cert_prefix.$1.chain.pem"; next NEXTKEY; } } } } elsif ($key eq "sslCAFile") { if ($lines[3] =~ /^(.*)\.chcrt<\/string>$/) { $cert_prefix = $1; $cert_prefix =~ s/.*[\/](.*)/$1/; foreach $cert (@cert_files) { if ($cert =~ /^$cert_prefix.([A-F0-9]+).chain.pem$/) { print OUT "\t\tsslCAFile\n"; print OUT "\t\t$CERT_PATH/$cert\n"; $configured_chainfile = 1; next NEXTKEY; } } } elsif ($lines[3] !~ /^<\/string>$/) { $configured_chainfile = 1; } } print OUT "\t\t$key\n"; for (my $i = 3; $i < $#lines; $i++) { print OUT "\t\t$lines[$i]\n"; } } ## NEXTKEY if (! $configured_chainfile && $prospective_chainfile ne "" && -e $prospective_chainfile) { print OUT "\t\tsslCAFile\n"; print OUT "\t\t$prospective_chainfile\n"; } print OUT <<"EOF"; EOF &log_message("Importing settings from file: $source_config"); $res = qx{ $SERVERMGR_JABBER_CGI_PATH < $tmpname }; unlink($tmpname); } elsif ($src_ver eq "10.4") { # parse out original service.host elements to get our realms my $in_service_elem = 0; my @realms; my $realm; # realms foreach my $line (@lines) { if ($line =~ //) { &log_message("Found service tag in $source_config") if $DEBUG; $in_service_elem = 1; } if ($line =~ /([a-zA-Z0-9.]+)<\/host>/ && $in_service_elem) { &log_message("Found host tag in $source_config: $1") if $DEBUG; push(@realms, $1); } if ($line =~ /<\/service>/) { $in_service_elem = 0; } } # Get SSL certificate data my @cert_files; my $ret = opendir(CERTS_DIR, $CERT_PATH); if (! $ret) { &log_message("ERROR: unable to read dir: $CERT_PATH"); } else { @cert_files = readdir(CERTS_DIR); closedir(CERTS_DIR); chomp(@cert_files); } my $xs1 = XML::Simple->new (KeepRoot => 1); my $doc = $xs1->XMLin($source_config); my $old_cert = basename($doc->{jabber}->{io}->{ssl}->{key}->{content}); my $cert_prefix; my $cert; # Write the new file for import open(OUT, ">$tmpname") || &bail("ERROR: Could not open file $tmpname: $!"); print OUT <<"EOF"; command writeSettings configuration EOF if ($#realms >= 0) { print OUT "\t\thosts\n"; print OUT "\t\t\n"; foreach $realm (@realms) { print OUT " $realm\n"; } print OUT "\t\t"; } else { &log_message("No realms to migrate."); } if ($old_cert =~ /^(.*)\.crtkey$/) { $cert_prefix = $1; foreach $cert (@cert_files) { if ($cert =~ /^$cert_prefix.([A-F0-9]+).concat.pem$/) { print OUT "\t\tsslKeyFile\n"; print OUT "\t\t$CERT_PATH/$cert\n"; last; } } } else { &log_message("Problem matching SSL certificate path from source config: $old_cert"); } print OUT <<"EOF"; EOF &log_message("Importing settings from file: $source_config"); $res = qx{ $SERVERMGR_JABBER_CGI_PATH < $tmpname }; unlink($tmpname); } ### file ownership changes for changes in jabberd/mu-conference identity # mu-conference files should now be owned by user _jabber my $cmd_ret = qx{ serveradmin settings jabber:hosts }; chomp($cmd_ret); @lines = split("\n", $cmd_ret); foreach my $line (@lines) { if ($line =~ /^.*\"(.*)\"$/) { system("chown -R _jabber:_jabber /private/var/spool/conference.$1"); } } system("chown -R _jabber:_jabber /private/var/jabberd/log"); &log_message("New servermgr_jabber settings:\n$res"); &log_message("Upgrade completed successfully.");