#! /usr/bin/perl # Universal script for MJohn # 1) Wrapper part # By default it works like wrapper around John the Ripper # Sends attack descriptions to the server # 2) Daemon part # Daemon to report progress # Collects and sends attack progress to the server # Daemon should run to upload results. Start it and do not stop. # 3) Server daemon part # Server daemon to import data into request tracker # !!! Experimental: this all is subject to change very soon !!! # Dependencies: ssh, john, perl, unix (linux, *bsd, cygwin or any other) # TODO: versions. # TODO: list of perl modules (and packages). # Server dependencies: sshd, perl, unix, request tracker. # LICENSE # Copyright © 2012 Aleksey Cherepanov . # # Redistribution and use in source and binary forms, with or without # modification, are permitted. # END OF LICENSE # # Written with great help of Frank Dittrich # Limitations: order of parameters should not affect behaviour, and # many others not found yet. TODO # Configuration # TODO: review options. my %C = qw' remote con2 remote_in_dir /home/aleksey/wrapper/shared remote_in con2:/home/aleksey/wrapper/shared remote_out_dir /home/aleksey/wrapper/shared remote_out con2:/home/aleksey/wrapper/shared user alekseytest hash_files %s.john-uncracked conf ~/.john/mjohn.conf store ~/.john/mjohn-store john ~/desktop/wrapper/echojohn.sh role wrapper attack_name_generator q/name/.time() john_conf ~/desktop/magnum-jumbo/run/john.conf lair no_lair action start show nothing server_delay 10 daemon_delay 30 auto_start_daemon 0 transparent_wrapper 0 '; # TODO: remote, remote_in_dir, remote_in: one may be dropped. # TODO: option to limit folder from which files could shared to avoid # mistakes. # TODO: support $JOHN/john.conf syntax for john_conf. # TODO: add separate delay for server daemon. It should be smaller. # TODO: enable auto_start_daemon. Disabled for debugging. # TODO: role seems to be an abuse for config. Though it is nice to be # able to set default behaviour in any value. For instance if have # copies of this script with different names. Though it maybe useful # to see argv[0] to determine name and adjust behaviour accordingly # (just like busybox). use strict; use warnings; use IPC::Run qw(run timeout); use File::Copy; use YAML qw(Dump LoadFile); use Getopt::Long qw(:config pass_through); # TODO: other better? Windows support? Does this one lock pidfile? #use Proc::Daemon; use Config::Simple; use Digest::SHA qw(sha1_hex); ###################################################################### # Functions # NOTE: .rec should be first to avoid race conditions on its removal. my @extensions_for_run = qw/rec status pot log/; # # TODO: daemons should be subscribed onto changes in their folder(s). # sub user_daemon { # # TODO: option to not detach. # Proc::Daemon->new( # work_dir => $C{store}, # child_STDOUT => '>>daemon.out', # child_STDERR => '>>daemon.err', # pid_file => 'daemon.pid' # ); # } # sub start_user_daemon { # my $d = user_daemon(); # # TODO: should not we check that daemon is already run? # # TODO: nice messages. # my $p = $d->Init; # unless ($p) { # die "not implemented"; # } # } # sub stop_user_daemon { # my $d = user_daemon(); # # TODO: send TERM for the first time. # $d->Kill_Daemon; # # TODO: some informative output? # } # sub server_daemon { # # TODO: option to not detach. # Proc::Daemon->new( # work_dir => $C{store}, # child_STDOUT => '>>daemon.out', # child_STDERR => '>>daemon.err', # pid_file => 'daemon.pid' # ); # } # sub start_server_daemon { # my $d = server_daemon(); # my $p = $d->Init; # unless ($p) { # die "not implemented"; # } # } # sub stop_server_daemon { # my $d = server_daemon(); # # TODO: send TERM for the first time. # $d->Kill_Daemon; # # TODO: some informative output? # } sub expand_tilde { # TODO: windows portability. # TODO: logdir? $_[0] =~ s!^~([^/]*)!$1 ? (getpwnam($1))[7] : $ENV{HOME} || $ENV{LOGDIR}!e; } sub check_attack_name { unless (shift =~ /^([a-z0-9_-]+)\/([a-z0-9_-]+)\z/) { die "bad " . shift . "attack name, make it non-empty and use only a..z, 0..9, _, -, stopped" } } # Regexp to match sha1 my $sha1re = '[0-9a-f]{40}'; # TODO: errors? # Returns: table of pairs sha1 => type, where type is file or aref sub get_sha1_table { my %sha1; # TODO: scp maybe more preferable. #system 'scp', "$C{remote_in}/files", "$C{remote_in}/attacks", "."; # TODO: if we want to let shell expand tildes we should avoid quoting... # TODO: check for errors of ssh and ls. for (`ssh "$C{remote}" 'ls $C{remote_in_dir}/files.d'`) { if (/^($sha1re)\.(file|aref)$/) { # TODO: potentially we could have one sha1 with different extensions. $sha1{$1} = $2; } else { warn "could not parse line '$_' as sha1 reference"; } } %sha1; } # Functions end ###################################################################### # Options handling warn "ARGV: @ARGV"; # TODO: Usage info, in pod, me thinks. if ($#ARGV == -1) { if ($C{transparent_wrapper}) { system $C{john}; } else { # NOTE: transparent_wrapper is not very user friendly because # it means wrapper behaves exactly like john if there are no # specific arguments so user could be confused. # TODO: show usage info. } } # Parse wrapper's only options # Defaults + Config file + Options = Resulting configuration # Get path to config GetOptions("config-conf=s", \$C{conf}); # Read config expand_tilde($C{conf}); Config::Simple->import_from($C{conf}, \%C) or warn Config::Simple->error(); my %options; for (keys %C) { # TODO: smarter types for options? # TODO: maybe --config name=value is better? $options{"config-$_=s"} = \$C{$_}; } # TODO: config file option. --config is used by john. # Attack name from user # While attack_name_generator could provide name this option is a # convenient way to assign meaningful name to attack. # (attack_name_generator is passed to eval, so explicit name needs a # quoting while this option does not it). # TODO: usage info. my $user_supplied_attack_name = ''; $options{"aname|attack_name=s"} = \$user_supplied_attack_name; my $old_attack_name = ''; $options{"oaname|old_attack_name=s"} = \$old_attack_name; # TODO: we could take some john's options if we use short form. Though # there are no such options now. GetOptions(%options); # Expand tildes in config expect remote folders. for (keys %C) { # TODO: we could want to expand tildes like on remote. expand_tilde $C{$_} unless /remote/ } # TODO: expand relative paths. # TODO: error on '--option-show='. if ($C{show} ne 'nothing') { print "$C{$C{show}}"; # TODO: error code on non-existence. Message on stderr. exit; } # Check config sanity # TODO: check that on server side (too). For user names there could fs # permissions but attack names it is not easy to do limitation: we # could check on server and yell, but there is a time between folder # creation and check so we need either other architecture (with # staging) or clients should such folder on their own. # TODO: more checks. # NOTE: attack name could not be checked here. # User name could contain only small Latin letters and could not be # empty. # TODO: there is a user with 0 in name. unless ($C{user} =~ /^[a-z]+\z/) { die "use only small letters for user name"; } # TODO: set store up if there is not one. Or at least exit if so. # Create dir (with path) if there are no one. Clone remote repo there. # TODO: if there is a store check if its remote address equals to our. # TODO: check toplevel structure of the store. # Other options is for john, remember them. my @args = @ARGV; # Distinguish roles if ($C{role} ne 'wrapper' && @args) { # Error: unexpected args # TODO: usage. } # New name of attack # TODO: easier name, maybe with incremental counter. # TODO: this does not reflect real user+attack_name. my $attack_name = "$C{user}/"; if ($user_supplied_attack_name ne '') { $attack_name .= $user_supplied_attack_name } else { $attack_name .= eval($C{attack_name_generator}); } # TODO: check that on server side too. check_attack_name $attack_name, 'new'; # Each wrapper has its own store. my $pwd = $ENV{PWD}; # TODO: set store up if there is not one. chdir $C{store} or die "cannot cd to the store: $!, stopped"; sub get_full_path { # TODO: windows? Use library. my $arg = shift; expand_tilde $arg; $arg =~ /^\// ? $arg : "$pwd/$arg" } sub upload_run_files { my $session = shift; my $folder; if ($session =~ m!^[^/]+/[^/]+/!) { $folder = $&; } else { die "could not parse folder name"; } system 'scp', (map { "$session.$_" } @extensions_for_run), "$C{remote_out}/$folder" and warn "scp fails: $!"; unless (-r "$session.rec") { system "ssh '$C{remote}' 'rm $C{remote_out_dir}/$session.rec'"; } } if ($C{role} eq 'wrapper') { # TODO: Parse options for john # TODO: warning if autostart is disabled and daemon is not runned. die "start daemon yourself" if $C{auto_start_daemon}; #start_user_daemon if $C{auto_start_daemon}; # Get list of attacks and files my %sha1 = get_sha1_table; sub download_file_by_sha1 { my $ref = shift; my $type = $sha1{$ref}; if ($type eq 'file') { system "scp", "$C{remote_in}/files.d/$ref.file", "files.d/" and die "scp download failed"; } else { die "unsupported sha1 reference type '$ref' for file, stopped"; } } my %original_names; # Gets parameter's argument, returns path in store or false. sub replace_filename { # Argument is either filename, regular argument or sha1. # TODO: is it correct to use slashes everywhere? my $arg = shift; my $full_path = get_full_path $arg; my $new_path = ""; if (-r $full_path) { if (`sha1sum -b "$full_path"` =~ /^($sha1re) /) { my $sha1sum = $1; # TODO: separate files by type: wordlists, configs, and so on. $new_path = "files.d/$sha1sum.file"; if (-r $new_path) { # TODO: add byte to byte comparison here if you're paranoid. warn "file '$full_path' ($sha1sum) is already in the store"; } else { warn "file '$full_path' ($sha1sum) is about to be copied to the store"; # Copy file to store # TODO: do files in parameters for wrapper need copies? # TODO: fifos? ('copy' converts fifo into regular file) # TODO: what if our file already in the store? # TODO: use hardlinks if possible. # TODO: as of we do not use git, we could avoid copying. Or use symlinks. copy $full_path, $new_path or die "$!"; $original_names{$new_path} = $full_path; } # TODO: store symlink-like file into attack's subdir. } } elsif ($arg =~ /^$sha1re\z/) { # TODO: maybe some prefix for sha1 references? To make completion easier... $new_path = "files.d/$arg.file"; if (-r $new_path) { # Nothing } elsif (exists $sha1{$arg}) { # TODO: we die on fail. Better/other design? download_file_by_sha1 $arg; } else { die "no file with such sha1 '$arg', stopped" } } else { # TODO: debug output, make an option to control this. # TODO: distinguish readability and existence. warn "file '$full_path' does not exist"; return $arg; } $new_path } # Dump john's version and usage # TODO: john's version could be changed between runs. Lock? Copy? # TODO: it could be overwritten by other wrapper. #my $usage = "$attack_name/john.usage"; # TODO: there is no $attack_name folder here. my $usage = "john.usage"; system "$C{john} > $usage" and die "$!"; # NOTE: we do note die on error because some johns may not support --list option. # TODO: die if you want. It is possible to limit support now. system "$C{john} --list=hidden-options >> $usage" and warn "$!"; # Build expansion table for john's parameters my %expansion; { my %full_options; open my $f, '<', $usage; while (<$f>) { # TODO: not listed --make_check needs _ . # NOTE: --stdin and --pipe do not start at the begin of line. # TODO: check all other options to be matched by that pattern. while (/--[a-z-]+/g) { # NOTE: --wordlist is mentioned twice. # TODO: if one option is shorter than other (for # instance -a vs. -a-b) shorter one is dropped. unless (exists $full_options{$&}) { $full_options{$&} = $&; for (reverse 3 .. length $&) { my $s = substr $&, 0, $_; if (exists $expansion{$s}) { $expansion{$s} = ''; } else { $expansion{$s} = $&; } } } } } close $f; } expand_tilde $_ for @args; my @possible_hashfiles = grep { !/^-/ } @args; die "too many files with hashes" if @possible_hashfiles > 1; # TODO: $hashfile represents hash type. @args = grep { /^-/ } @args; die "hash type/file is mandatory" unless @possible_hashfiles; my $hashfile = shift @possible_hashfiles; for (@args) { # We use only one parameter-value separator (=). # John accepts only = and : as separater for parameters. # Also we use only "--". s/^--?/--/; s/^(--.*?)[=:]/$1=/; } # Expand short options. # TODO: retest after option expansion and sort? { for (@args) { if (/^--[a-z-]+/) { if (exists $expansion{$&}) { my $expanded = $expansion{$&}; if ($expanded eq '') { # If option could be expanded then test run should # fail. If we are here then our code work not like # john. die "bug"; } s/^\Q$&\E/$expanded/; } else { # NOTE: wrapper could not pass not listed options to john. die "Unknown option '$&', stopped"; } } } } # TODO: suitable only for mini contest my @formats = qw(bf dynamic_22 mssql05 phps bsdi dynamic_23 mysql-sha1 raw-md5 des dynamic_28 nt raw-sha1 dynamic_12 md5 oracle11 raw-sha512 dynamic_16 mscash2 phpass salted-sha1); die "unknown format '$hashfile', use one of: @formats; stopped" unless $hashfile ~~ @formats; # Separate cosmetic arguments from others # TODO: check they are returned back in both test and real run. my @exclude_parameters = qw(dupe-suppression save-memory mem-file-size crack-status mkpc length fix-state-delay); # TODO: some of these parameters are used for our own purposes # while others just could not be handled right. my @deprecate_parameters = qw(config stdin pipe restore session make-charset status show stdout test users groups shells salts pot format list nolog max-run-time plugin help subformat field-separator-char log-stderr); # TODO: we do not handle default value for loopback, like any other default values. my @attack_if_have_any = qw(single wordlist loopback incremental markov external); my $is_attack = 0; my @cosmetic; for (@args) { my $a = $_; for (@attack_if_have_any) { if ($a =~ /^--\Q$_\E/) { $is_attack = 1; } } for (@deprecate_parameters) { if ($a =~ /^--\Q$_\E/) { die "could not handle '$a' argument"; } } for (@exclude_parameters) { if ($a =~ /^--\Q$_\E/) { push @cosmetic, $a; } } } @args = grep { not $_ ~~ @cosmetic } @args; # TODO: just should not create attack, just run without tracking. # TODO: move it somewhere to avoid additional check $old_attack_name eq ''. if (!$is_attack and $old_attack_name eq '') { die "not an attack or batch mode used, specify attack mode"; } # TODO: we abort on failed commands while it could be useful to # share them to provide newbies with help. Now there is lesser noise. # TODO: yet another place that differs for attack reuse. my $run_name = $old_attack_name eq '' ? $attack_name : $old_attack_name; $run_name .= "/$C{user}" . time(); sub run_john { # TODO: system { $a[0] } @a ??? system $C{john}, @_ and warn "john failed"; } # TODO: copy-pasting is evil! sub test_run_john { my @cmd = ($C{john}, @_); my ($in, $out, $err); run \@cmd, \$in, \$out, \$err, timeout(100); print "\ntest run output: \n'''$out'''\ntest run errors: \n'''$err'''\n"; # TODO: put some condition here. if ($?) { unless ($err =~ /^Session stopped \(max run-time reached\)$/m) { die "test run of john failed"; } } } # NOTE: it is not really cosmetic but should not be included into attack. push @cosmetic, "--format=$hashfile"; my $file_to_crack = "$run_name.john.$hashfile"; sub get_uncracked { # TODO: reduce download and check sha1 before? Though server should calculate sha1 once system 'scp', "$C{remote}:" . sprintf($C{hash_files}, $hashfile), $file_to_crack and die "scp failed on hashfile"; } if ($old_attack_name eq '') { # New attack # Store parameters into file # Each attack has its own dir in store. # TODO: check if this attack already exists. mkdir "$C{user}" or warn "could not mkdir for user"; mkdir "$attack_name" or die "could not mkdir for attack, try another name, stopped"; # Check that all files are available and copy them into the store. # TODO: all paths should become relative to the store's root. To # make .rec files portable. for (@args) { if (/^(--.*?=)(.*)/) { # TODO: expand tildes. # TODO: handle config file too. # TODO: respect type of parameter! Do not copy file named 'all' # from original dir if we just call --incremental=all . # TODO: --make-charset does not need a copy. # Add file to the store and overwrite args with path in the store. # TODO: is it correct to use slashes everywhere? my ($parameter, $value) = ($1, $2); $_ = $parameter . replace_filename($value); } # TODO: we could need a copy of file with hashes to keep track # what hashes were attacked and what were not. Remember about # selectors like --users. } # TODO: use new patch for john to make smallest single-file config. # TODO: disabled as of broken. # my $john_conf = ''; # if (-r $C{john_conf}) { # # TODO: includes. # # TODO: it could be useful to split config file into smaller # # files to keep meaningful and common parts separated. # $john_conf = replace_filename $C{john_conf} # } else { # die "config is not readable, use --config-john_conf to specify config, stopped"; # } # TODO: inhibit batch mode. # Check attack for existence my $attack_sha1 = sha1_hex join "\0", @args; warn "attack's sha1 is $attack_sha1"; if (exists $sha1{$attack_sha1}) { if ($sha1{$attack_sha1} eq 'aref') { # Attack exists. # TODO: maybe use `ssh 'cat aref'` instead? system 'scp', "$C{remote_in}/files.d/$attack_sha1.aref", 'files.d' and die "scp failed on .aref download"; open my $f, '<', "files.d/$attack_sha1.aref" or die "could not read .aref"; my $aref = <$f>; close $f; die "this attack is already defined, its name is '$aref'. Use --oaname=$aref to use it" } else { die "sha1 should aref" } } # Attack does not exist. Define new after test. { my $attack_parameters = "$attack_name/parameters"; my $f; open $f, '>', $attack_parameters or die "could not open file $attack_parameters"; # TODO: do we need to dump wrapper's config? # TODO: what if some options are doubled? # TODO: drop decorative parameters. print $f Dump sort @args; close $f; } get_uncracked; push @args, "--session=$run_name"; push @args, "--pot=$run_name.pot"; push @args, $file_to_crack; # TODO: we could not restore cosmetic options. push @args, @cosmetic; # Short test run # TODO: when we start attack by name we should run tests too. #if ($hashfile) { if (1) { # TODO: reduce .pot to one hash for test. Dump line by line # till we load it. Then load the whole file if it is not # possible to load one line. #open my $i, '<', get_full_path $hashfile or die "$!"; open my $i, '<', $file_to_crack or die "$!"; # TODO: comment lines? my $l = <$i>; close $i; # TODO: temp file. my $test_file = "$attack_name/test" . time(); open my $o, '>', $test_file or die "$!"; print $o $l; close $o; eval { # NOTE: forced exit by time gives error code. # TODO: it is the same as for all other errors. Patch john? test_run_john @args, $test_file, "--max-run-time=1"; }; # TODO: better error handling. my $e = $@; unlink $test_file or die "$! ($e)"; # TODO: should die here. Waits fix of exit code for --max-run-time. die $e if $e; } else { # Run as is because there is no hashfile. # TODO: 0 may be enough. It seems that 0 means "while time < 1". test_run_john @args, "--max-run-time=1"; } # As soon as attack tested it is eligible to be shared. { my $aref = "files.d/$attack_sha1.aref"; open my $f, '>', $aref; print $f $attack_name; close $f; system 'scp', $aref, "$C{remote_out}/files.d" and die "scp failed, could not upload .aref"; } } else { # Old attack unless ($old_attack_name =~ /\//) { $old_attack_name = "$C{user}/$old_attack_name"; } check_attack_name $old_attack_name, 'old'; # TODO: do not we overwrite something? $attack_name = $old_attack_name; # TODO: offline mode? if (`ssh $C{remote} 'ls -d "$C{remote_in_dir}/$old_attack_name"'` eq '') { die "there is no such old attack '$old_attack_name', stopped"; } my ($user, $attack) = split /\//, $old_attack_name; mkdir $user or warn "could not create user's subdir '$user' in store, not a problem"; system 'scp', '-r', "$C{remote_in}/$old_attack_name", $user and die "scp failed"; my @remote_files = `ssh '$C{remote}' 'cd $C{remote_in_dir} && find $old_attack_name'`; for (@remote_files) { # TODO: maybe chomp instead of \n? if (/^(.*?)\.log$/ && not "$1.rec\n" ~~ @remote_files) { unlink "$1.rec" or warn "tried to unlink '$1.rec' but failed"; } } # TODO: remove this, restore does not need keys. Though we need to download files. # my @loaded_args = LoadFile("$old_attack_name/parameters"); # for (@loaded_args) { # # TODO: reg exp for parameter start (--.*?=) is used in other places too. # s/^(--.*?=).*?($sha1re).file/$1$2/; # } # push @args, @loaded_args; # Pick best run files, copy and continue with them # TODO: finished attacks. my $max_candidates = 0; my $best_session = ''; my %L = ('' => 1, 'K' => 1_000, 'M' => 1_000_000, 'G' => 1_000_000_000); for (<$attack_name/*.john.$hashfile>) { s/\.john\.$hashfile$//; if (-r "$_.rec") { my $t = $_; my $f; unless (open $f, '<', "$_.status") { warn "could not open '$_.status'"; next; } my %l = ('' => 1, 'K' => 1_000, 'M' => 1_000_000, 'G' => 1_000_000_000); while (<$f>) { if (/time: (\d+):(\d\d):(\d\d):(\d\d) /) { # NOTE: not accurate but enough good. my $time = $4 + $3 * 60 + $2 * 60 * 60 + $1 * 60 * 60 * 24; # TODO: magic number of seconds that restart is reasonable after. if ($time > 5) { if (/c\/s: (\d+)(|K|M|G)/) { my $candidates = $time * $1 * $L{$2}; if ($candidates > $max_candidates) { $max_candidates = $candidates; $best_session = $t; } } } } } close $f; } else { die "attack '$attack_name' is already finished for '$hashfile', aborted" } } if ($best_session ne '') { # Copy session for (@extensions_for_run) { copy "$best_session.$_", "$run_name.$_" or die "could not copy run file"; } # TODO: I do not need to copy file with hashes. Delete # this. Session file refers old file. It is even better. # Though we'd be able to get newest file with uncracked # hashes and get speed up. # my @hashes = <$best_session.john.*>; # # TODO: 0 is ok? # die "too many .john files" if @hashes > 1; # if (@hashes) { # $file_to_crack = "$run_name.john.$hashfile"; # copy $hashes[0], $file_to_crack or die "could not copy .john file"; # } # Patch session file... # TODO: avoid touching session file. Use separate folders # for runs and cd into them. So session is isolated and # could be just copied. { my $f; open $f, '<', "$run_name.rec" or die "could not read"; my @lines = <$f>; close $f; for (@lines) { s/^(--session=|--pot=)\Q$best_session\E/$1$run_name/; } # TODO: could not I do it in one open? open $f, '>', "$run_name.rec" or die "could not write"; print $f @lines; close $f; } @args = "--restore=$run_name"; } else { # Create new session. get_uncracked; push @args, "--session=$run_name"; } } # TODO: check that we do not dupe efforts. # TODO: force for duplicated attack. # Start john. # Push changes to server. # TODO: Pushing to server could be async. # TODO: quoting or restrictions on user/attack names: double quotes # are not allowed for user and attack names, also backslashes are not # allowed. # TODO: remember that file names could start with dash. Check all places. # TODO: one scp is better. system 'scp', '-r', $attack_name, "$C{remote_out}/$C{user}" and die "scp upload attack failed"; for (keys %original_names) { # TODO: condition does not seem to be right. unless (-r $_) { system 'scp', $_, "$C{remote_out}/files.d" and die "scp upload failed"; } } # Really run john. # TODO: do not count john without args or with help parameters as attack. # TODO: It would be nice to say what we are going to run. # TODO: pipes? # TODO: pipes and remote execution: does perl have a safe restricted mode? # TODO: it would be better to craft args list from saved parameters. # TODO: check that --session and --pot were not used before. # TODO: time is not a good unique number. Though uuid could be overkill. # TODO: add session and .pot files to git. The daemon could do it, though. # TODO: interactive status does not work. # TODO: track status: syntax ok, all hashes loaded, some hashes cracked. run_john @args; # John finished or aborted. # TODO: Report finish or abort upload_run_files $run_name; } elsif ($C{role} eq 'daemon') { if ($C{action} eq 'start') { # We traverse our folders in the store. while (1) { # TODO: should not we pull here? # NOTE: do not chdir. Work in store's root to save file links in .pot's. for (<*/*/$C{user}*.log>) { s/\.log$//; my $session = $_; # Capture/calculate speed # TODO: is it all/enough? # TODO: should not we parse --status output right here? # TODO: dirty. Use other way to redirect output. # TODO: use john used for command, not just from config. # TODO: do not we need to pass --pot and --log options here? system qq/"$C{john}" --status="$session" 2> "$session.status"/; # TODO: look into logs. # TODO: see diff on .pot file. # TODO: capture speed, eta, what else? # Upload progress info: .status, .pot, .rec, .log # TODO: real quoting. # TODO: are all that file in all our dirs? Think about it. # TODO: failed commands does not have pot/rec/log files. # TODO: there could not be .rec file. Finished session does not have it. # TODO: $! does not seem to provide meaningful message here. upload_run_files $session; } # TODO: probably we should sleep delay minus time spent on work. warn "going to sleep at " . time(); sleep $C{daemon_delay}; } } elsif ($C{action} eq 'stop') { die "not implemented"; #stop_user_daemon; } elsif ($C{action} eq 'status') { die "not implemented"; # TODO } else { die "not implemented"; # TODO: error: Unknown daemon action. } } elsif ($C{role} eq 'server-worker') { if ($C{action} eq 'start') { # TODO: This daemon shares architecture with client daemon. # We traverse all folders in the lair. while (1) { `git pull`; for (<*/>) { # TODO: skip 'files.d'. chdir $_; # We add new attack. my $attack_name = $_; # Check if we already added that attack. # TODO: remember to export vars with credentials to access rt. # TODO: inefficient. We could get list of all once. Or we # could store status in file near, though it could become not # synced with rt. # TODO: 'No matches found' could be in field. Add ^ and $ and # test. Is there a machine readable interface? # TODO: rt ls 'subject=user1_name1341851801/' # -> Query:subject='user1_name1341851801'/ . "/" is in wrong place. if (`rt ls "subject='$attack_name'"` =~ 'No matches found') { # No such attack. Add it. # TODO: cats are nice pets. Do not use them so. # TODO: text is not for parameters. Use custom fields instead! # TODO: should not I add a ticket from user which did this attack? `rt create -t ticket set 'subject=$attack_name' "text=\$(cat parameters)"`; } # TODO: We update speeds. chdir $C{lair}; } # TODO: probably we should sleep during delay minus time spent on work. sleep $C{server_delay}; } } elsif ($C{action} eq 'stop') { die "not implemented"; #stop_server_daemon; } elsif ($C{action} eq 'status') { die "not implemented"; # TODO } else { die "not implemented"; # TODO: error: Unknown daemon action. } } else { # Error: unknown role # TODO: usage } # TODO: right shebang. # TODO: avoid backticks where possible. # TODO: make sure system() does not call shell. # TODO: pod with usage below. __END__