#!/usr/bin/perl # # Copyright 2007-2009 Timothy Kay # http://timkay.com/aws/ # # 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 3 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, see . # $program_version = "1.39"; $ec2_version = "2009-03-01"; $sqs_version = "2009-02-01"; # # Need to implement: # # ConfirmProductInstance - not tested # DescribeImageAttribute - not working "An internal error has occurred" # ModifyImageAttribute # ResetImageAttribute # # Windows support: # BundleInstance # DescribeBundleTasks # CancelBundleTasks # @cmd = ( ["ec2", "add-group addgrp", CreateSecurityGroup, [ ["", GroupName], ["d", GroupDescription], ]], ["ec2", "add-keypair addkey", CreateKeyPair, [["", KeyName]]], ["ec2", "allocate-address allad", AllocateAddress], ["ec2", "associate-address aad", AssociateAddress, [ ["", PublicIp], ["i", InstanceId], ]], ["ec2", "attach-volume attvol", AttachVolume, [ ["", VolumeId], ["i", InstanceId], ["d", Device], ]], ["ec2", "authorize auth", AuthorizeSecurityGroupIngress, [ ["", GroupName], ["P", IpProtocol], ["p", FromPort], ["f", FromPort], ["p", "ToPort"], ["t", "ToPort"], #["t", icmp type code], ["u", SourceSecurityGroupOwnerId], ["o", SourceSecurityGroupName], ["s", CidrIp], ]], ["ec2", "confirm-product-instance", ConfirmProductInstance, [ ["", ProductCode], ["i", InstanceId], ]], ["ec2", "create-snapshot csnap", CreateSnapshot, [["", VolumeId]]], ["ec2", "create-volume cvol", CreateVolume, [ ["size", Size], ["zone", AvailabilityZone], ["snapshot", SnapshotId], ]], ["ec2", "delete-group delgrp", DeleteSecurityGroup, [["", GroupName]]], ["ec2", "delete-keypair delkey", DeleteKeyPair, [["", KeyName]]], ["ec2", "delete-snapshot delsnap", DeleteSnapshot, [["", SnapshotId]]], ["ec2", "delete-volume delvol", DeleteVolume, [["", VolumeId]]], ["ec2", "deregister", DeregisterImage, [["", ImageId]]], ["ec2", "describe-addresses dad", DescribeAddresses, [["", PublicIpN]]], ["ec2", "describe-availability-zones daz", DescribeAvailabilityZones, [["", ZoneNameN]]], ["ec2", "describe-groups dgrp", DescribeSecurityGroups, [["", GroupNameN]]], ["ec2", "describe-image-attribute", DescribeImageAttribute, [ ["", ImageId], ["l", launchPermission], ["p", productCodes], ["kernel", "kernel"], ["ramdisk", "ramdisk"], ["B", "blockDeviceMapping"], ]], ["ec2", "describe-images dim", DescribeImages, [ ["", ImageIdN], ["o", OwnerN], ["x", ExecutableByN], ]], ["ec2", "describe-instances din", DescribeInstances, [["", InstanceIdN]]], ["ec2", "describe-keypairs dkey", DescribeKeyPairs, [["", KeyNameN]]], ["ec2", "describe-regions dreg", DescribeRegions], ["ec2", "describe-reserved-instances", DescribeReservedInstances, [ ["", ReservedInstanceIdN], ]], ["ec2", "describe-reserved-instances-offerings", DescribeReservedInstancesOfferings, [ ["offering", ReservedInstancesOfferingIdN], ["instance-type", InstanceType], ["availability-zone", AvailabilityZone], ["z", AvailabilityZone], ["description", ProductDescription], ]], ["ec2", "describe-snapshots dsnap", DescribeSnapshots, [["", SnapshotId]]], ["ec2", "describe-volumes dvol", DescribeVolumes, [["", VolumeIdN]]], ["ec2", "detach-volume detvol", DetachVolume, [["", VolumeId]]], ["ec2", "disassociate-address disad", DisassociateAddress, [["", PublicIp]]], ["ec2", "get-console-output gco", GetConsoleOutput, [["", InstanceId]]], ["ec2", "purchase-reserved-instance-offering", PurchaseReservedInstancesOffering, [ ["offering-id", ReservedInstancesOfferingId], ["instance-count", InstanceCount], ]], ["ec2", "reboot-instances reboot", RebootInstances, [["", InstanceIdN]]], ["ec2", "release-address rad", ReleaseAddress, [["", PublicIp]]], ["ec2", "register-image register", RegisterImage, [["", ImageLocation]]], ["ec2", "revoke", RevokeSecurityGroupIngress, [ ["", GroupName], ["P", IpProtocol], ["p", FromPort], ["f", FromPort], ["p", "ToPort"], ["t", "ToPort"], #["t", icmp type code], ["u", SourceSecurityGroupOwnerId], ["o", SourceSecurityGroupName], ["s", CidrIp], ]], ["ec2", "run-instances run-instance run", RunInstances, [ ["", ImageId, "ami-5647a33f"], ["i", InstanceType, "m1.small"], ["a", AddressingType, "public"], ["n", MinCount, 1], ["n", MaxCount, 1], ["g", SecurityGroupN, "default"], ["k", KeyName, "default"], ["z", "Placement.AvailabilityZone"], ["kernel", KernelId], ["ramdisk", RamdiskId], ["d", UserData, undef, sub {encode_base64($_[0], "")}], ["f", UserData, undef, sub {encode_base64(load_file($_[0]))}], ]], ["ec2", "terminate-instances tin", TerminateInstances, [["", InstanceIdN]]], ["s3", "ls", LS], ["s3", "get cat", GET], ["s3", "mkdir", MKDIR], ["s3", "put", PUT], ["s3", "delete rmdir rm", DELETE], ["s3", "copy cp", COPY], ["sqs", "add-permission addperm", AddPermission, [ ["" => QueueUri], [label => Label], [account => AWSAccountIdN], [action => ActionNameN], ]], ["sqs", "change-message-visibility cmv", ChangeMessageVisibility, [ ["" => QueueUri], [handle => ReceiptHandle], [timeout => VisibilityTimeout], ]], ["sqs", "create-queue cq", CreateQueue, [ ["" => QueueName], [timeout => DefaultVisibilityTimeout], ]], ["sqs", "delete-message dm", DeleteMessage, [ ["" => QueueUri], [handle => ReceiptHandle], ]], ["sqs", "delete-queue dq", DeleteQueue, [ ["" => QueueUri], ]], ["sqs", "get-queue-attributes gqa", GetQueueAttributes, [ ["" => QueueUri], [attribute => AttributeNameN], ]], ["sqs", "list-queues lq", ListQueues, [ ["" => QueueNamePrefix], ]], ["sqs", "receive-message recv", ReceiveMessage, [ ["" => QueueUri], [attribute => AttributeNameN], [n => MaxNumberOfMessages], [timeout => VisibilityTimeout], ]], ["sqs", "remove-permission remperm", RemovePermission, [ ["" => QueueUri], [label => Label], ]], ["sqs", "send-message send", SendMessage, [ ["" => QueueUri], [message => MessageBody, "", sub {encode_url($_[0])}], ]], ["sqs", "set-queue-attributes sqa", SetQueueAttributes, [ ["" => QueueUri], [attribute => "Attribute.Name"], [value => "Attribute.Value"], ]], ); $home = get_home_directory(); # Figure out $cmd. If the program is run as other than "aws", then $0 contains # the command. This way, you can link aws to the command names (with or without # ec2 or s3 prefix) and not have to type "aws". unshift @ARGV, $1 if $0 =~ /^(?:.*[\\\/])?(?:(?:ec2|s3|sqs)-?)?(.+?)(?:\..+)?$/ && $1 !~ /^l?aws/; # parse meta-parameters, leaving parameters in @argv for (split(/[\r\s]+/, load_file_silent("$home/.awsrc")), @ARGV) { if (/^--([\w\-]+?)(?:=(.*))?$/s) { my($key, $val) = ($1, $2); $key =~ s/-/_/g; if (exists $keyword{$key}) { push @argv, $_; } else { $ {$key} = defined $val? $val: 1; # --cmd0 is used to call self but without getting the command from $0 undef $cmd if $key eq "cmd0" && $val; } } elsif (/^-(\w+)$/) { if (exists $keyword{$1}) { push @argv, $_; } else { for (split(//, $1)) { s/^(\d)$/d$1/; $ {$_}++; } } } else { if ($cmd) { push @argv, $_; } else { $cmd = $_; # moved this code here, so that arguments to specific ec2, s3, and sqs commands # are active only if the particular command is indicated # make a hash of aws keywords (%keyword), which are not treated as meta-parameters CMDLAST: for (@cmd) { for my $c (split(" ", $_->[1])) { if ($cmd eq $c) { $cmd_data = $_; for (@{$cmd_data->[3]}) { (my $key = $_->[0]) =~ s/-/_/g; $keyword{$key} = undef; } last CMDLAST; } } } } } } $h ||= $help; $v ||= $verbose; $v = 2 if $vv; $v = 3 if $vvv; if ($cut) { my $columns = $ENV{COLUMNS}; ($columns) = qx[stty -a <&2] =~ /;\s*columns\s*(\d+);/s unless $columns; if ($columns) { $columns -= !!$ENV{EMACS}; open STDOUT, "|cut -c -$columns"; } } # Exercise for the reader: why is this END block here? (Hint: bug in Perl?) END {close STDOUT} print STDERR "aws version: v$program_version (ec2: $ec2_version, sqs: $sqs_version)\n" if $v; $insecsign = "--insecure" if $insecure || $insecure_signing; $insecureaws = "--insecure" if $insecureaws || $insecure_aws; $scheme = $http? "http": "https"; $silent ||= !-t; $retry ||= 3; $secrets_file ||= "$home/.awssecret"; for ([m => 60], [h => 60 * 60], [d => 24 * 60 * 60], [w => 7 * 24 * 60 * 60], [mo => 30 * 24 * 60 * 60], [y => 365.25 * 24 * 60 * 60]) { $expire_time = $1 * $_->[1] if $expire_time =~ /^(-?\d+)$_->[0]$/; } # run a sanity check if $home/.awsrc doesn't exists, or if it was requested if (!-e "$home/.awsrc" || $sanity_check) { if (!$silent) { if (!-e $secrets_file) { warn "sanity-check: \"$secrets_file\": file is missing. (Format: AccessKeyID\\nSeecretAccessKey\\n)\n"; } elsif (!-r $secrets_file) { warn "sanity-check: \"$secrets_file\": file is not readable\n"; } elsif ($ENV{OS} !~ /windows/i) { my $stat = (stat $secrets_file)[2] & 0777; if (($stat & 0477) != 0400) { my @perm = (qw(x r w)) x 4; my $perm = join("", map {my $s = shift @perm; $_? $s: "-"} (split//, (unpack("B*", pack("n", $stat))))[6 .. 15]); warn "sanity-check: \"$secrets_file\": file permissions are $perm. Should be -rw-------\n"; } } } my($curl_version) = qx[curl -V] =~ /^curl\s+([\d\.]+)/s; print "curl version: $curl_version\n" if $v >= 2; if (xcmp($curl_version, "7.12.3") < 0) { $retry = undef; warn "sanity-check: This curl (v$curl_version) does not support --retry (>= v7.12.3), so --retry is disabled\n" unless $silent; } my $aws = qx[curl -q -s $insecureaws $fail --dump-header - $scheme://s3.amazonaws.com/connection/test]; print $aws if $v >= 2; my($d, $mon, $y, $h, $m, $s) = $aws =~ /^Date: ..., (..) (...) (....) (..):(..):(..) GMT\r?$/m; if (!$d) { $aws = qx[curl -q -s --insecure $fail --dump-header - $scheme://s3.amazonaws.com/connection/test]; ($d, $mon, $y, $h, $m, $s) = $aws =~ /^Date: ..., (..) (...) (....) (..):(..):(..) GMT\r?$/m; if ($d) { warn "sanity-check: Your host SSL certificates are not working for curl.exe. Fix the problem or use --insecure-aws\n" unless $silent; } else { die "sanity-check: Problems accessing AWS (not related to SSL certificates). Check that curl is installed.\n"; } } if (eval {require Time::Local}) { $mon = {Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5, Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11}->{$mon}; my $t = Time::Local::timegm($s, $m, $h, $d, $mon, $y); $time_offset = $t - time; warn "sanity-check: Your system clock is @{[abs($time_offset)]} seconds @{[$time_offset > 0? 'behind': 'ahead']}.\n" if !$silent && abs($time_offset) > 5; } } $curl_options .= " -q -g -S"; $curl_options .= " --retry $retry" if $retry; $curl_options .= " --fail" if $fail; $curl_options .= " --verbose" if $v >= 2; $curl_options .= $progress? " --progress": " -s"; $curl_options .= " --max-time $max_time" if $max_time; $curl_options .= " --limit-rate $limit_rate" if $limit_rate; #use Digest::SHA1 qw(sha1); #use Digest::SHA::PurePerl qw(sha1); #use MIME::Base64; -- added encode_base64() below use IO::File; use File::Temp qw(tempfile); use Digest::MD5 qw(md5 md5_hex); if (exists $ENV{QUERY_STRING} || $sign) { sysread STDIN, my $data, $ENV{CONTENT_LENGTH} if $ENV{CONTENT_LENGTH}; $data ||= $sign; if ($v >= 2) { (my $pretty = $data) =~ s/\n/\\n/sg; print STDERR "data = $pretty\n"; } my($sig, $awskey) = sign($data); $data = "$sig\n$awskey\n"; if (exists $ENV{QUERY_STRING}) { print "Content-Length: @{[length($data)]}\nContent-Type: text/plain\n\n"; } print $data; exit; } if ($install) { die "Usage: .../aws --install\n" if $install && @argv; if (-w "/usr/bin") { chomp(my $dir = qx[pwd]); my $path = $0; $path = "$dir/$0" if $0 !~ /^\//; if ($dir !~ /^\/usr\/bin$/) { print STDERR "copying aws to /usr/bin/\n"; my $aws = load_file($0) or die "installation failed (can't load script)\n"; if (-e "/usr/bin/aws") { unlink "/usr/bin/aws" or die "can't unlink old /usr/bin/aws\n"; } save_file("/usr/bin/aws", $aws); die "installation failed (can't copy script)\n" unless load_file("/usr/bin/aws") eq $aws; chmod 0555, "/usr/bin/aws"; chdir "/usr/bin"; } } chmod 0555, $0; make_links($0); die "installation failed\n"; } if ($link) { die "Usage: .../aws --link[=short|long] [-bare]\n" if $link && @argv; make_links($0); } sub make_links { my($target) = @_; # # Create symlinks to this program named for all available # commands. Then the script can be invoked as "s3mkdir foo" # rather than "aws mkdir foo". (Run this command in /usr/bin # or /usr/local/bin.) # # aws -link # symlinks all command names (ec2-delete-group, ec2delgrp, ec2-describe-groups, ec2dgrp, etc.) # aws -link=short # symlinks only the short versions of command names (ec2delgrp, ec2dgrp, etc.) # aws -link=long # symlinks only the long versions of command names (ec2-delete-group, ec2-describe-groups, etc.) # # The -bare option creates symlinks without the ec2 and s3 prefixes # (delete-group, delgrp, etc.). Be careful using this option, as # commands named "ls", "mkdir", "rmdir", etc. are created. for (@cmd) { my($service, $cmd, $action) = @$_; for my $fn (split(' ', $cmd)) { my($dash) = $fn =~ /(-)/; next if $dash && $link eq "short"; next if !$dash && $link eq "long"; $fn = "$service$dash$fn" unless $bare; unlink $fn; symlink($target, $fn) or die "$fn: $!\n"; #print "symlink $fn --> $target\n"; } } exit; } if (!$cmd_data) { my $output = "$cmd: unknown command\n" if $cmd; $output .= "Usage: aws ACTION [--help]\n\twhere ACTION is one of\n"; my(%output); for (@cmd) { my($service, $cmd, $action, $param) = @$_; $output{$service} .= " $cmd"; } for my $service (sort keys %output) { $output .= "\t$service"; while ($output{$service} =~ /\s*(.{1,80})(?:\s|$)/g) { my($one) = ($1); $output .= "\t" if $output =~ /\n$/; $output .= "\t\t$one\n"; } } $output .= "aws version: v$program_version (ec2 $ec2_version, sqs $sqs_version)\n"; die $output; } { my($service, $cmd, $action, $param) = @$cmd_data; if ($h) { my $help = "Usage: aws $cmd"; for (@$param) { my($a, $key, $default) = @$_; my $x = "-$a " if $a; $x = "--$a " if length($a) > 1; my($name, $N) = $key =~ /^(.*?)(N?)$/; my $ddd = "..." if $N eq "N"; my $def = " ($default)" if $default; $help .= " [$x$name$ddd$def]"; } $help .= " BUCKET[/OBJECT] [SOURCE]" if $service eq "s3"; $help .= "\n"; print STDERR $help; exit; } my($result); if ($service eq "ec2" || $service eq "sqs") { #print STDERR "(@{[join(', ', @argv)]})\n" if $v; my(%key); my @list = (Action => $action); for (my $i = 0; $i < @argv; $i++) { my($b); if ($argv[$i] =~ /^--?(.*)$/) { ($b) = ($1); ++$i; } # # find the right param # for (@$param) { my($a, $key, $default, $cref) = @$_; next if $b ne $a; my $count = ++$key{$key}; $key =~ s/N$/\.$count/; my $data = $argv[$i]; $data = $cref->($data) if $cref; push @list, $key => $data; } } for (@$param) { my($a, $key, $default) = @$_; if ($default && $key{$key} == 0) { my $count = ++$key{$key}; $key =~ s/N$/\.$count/; push @list, $key => $default; } } print STDERR "ec2(@{[join(', ', @list)]})\n" if $v; $result = ec2($service, @list); } elsif ($service eq "s3") { my($last_marker, $marker); for (;;) { my $r = s3($action, $marker, (grep(!/^(?:x-amz-|Cache-|Content-|Expires:|If-|Range:)/i, @argv))[0, 1], grep(/^(?:x-amz-|Cache-|Content-|Expires:|If-|Range:)/i, @argv)); $r =~ s/<\?xml.*?>\r?\s*//; $result .= $r; ($marker) = $r =~ /.*(.*?)<\/Key>/; last if $r !~ /true<\/IsTruncated>/ || $marker le $last_marker; $last_marker = $marker; } } else { die; } if ($xml) { print xmlpp($result); } elsif ($result =~ / # # c1438ce900acb0db547b3708dc29ca60370d8174ee55305050d2990dcf27109c # timkay681 # # # # 3.14 # 2007-03-04T22:29:34.000Z # # # # boopsielog # # # 1000 # false # # ec201-2008-08-20-access.log.gz # 2008-08-21T03:01:51.000Z # "baa27b2e8def9acf8c2f3690e230e37a" # 2405563 # # c1438ce900acb0db547b3708dc29ca60370d8174ee55305050d2990dcf27109c # timkay681 # # STANDARD # my $isdir = $result =~ /(.*?)<\/DisplayName>/s; my(@result); while ($result =~ /<(?:Contents|Bucket)>\s*(.*?)\s*<\/(?:Contents|Bucket)>/sg) { my($item) = ($1); my $key = dentity($item =~ /<(?:Key|Name)>(.*?)<\/(?:Key|Name)>/s); my($size) = $item =~ /(.*?)<\/Size>/s; my($mod) = $item =~ /<(?:LastModified|CreationDate)>(.*?)<\/(?:LastModified|CreationDate)>/s; my($owner) = $item =~ /(.*?)<\/DisplayName>/s; for ($mod) { s/T/ /g; s/\.000Z//; } push @result, [$item, $key, $size, $mod, $owner || $owner1]; } if ($t) { @result = sort {$a->[3] cmp $b->[3]} @result; } if ($r) { @result = reverse @result; } for (@result) { my($item, $key, $size, $mod, $owner) = (@$_); if ($l) { $key = printable($key); if ($isdir) { print "drwx------ 2 $owner 0 $mod $key\n"; } else { printf "-rw------- 1 $owner %9d $mod $key\n", $size; } } elsif ($d1) { printf "$key\n"; } elsif ($simple) { printf "%9d\t$mod\t$key\n", $size; } elsif ($exec) { #local $_ = sprintf "%9d\t$mod\t$key\n", $size; #local @_ = ($size, $mod, $key); my($bucket, $prefix) = split(/\//, $argv[0], 2); eval $exec; last if $? & 127; # if the user hits control-c during a system() call... } } } elsif ($result =~ /(.*?)<\/keyMaterial>/s, "\n"; } elsif ($result =~ /(.*?)<\/output>/s); } elsif ($result =~ /)/sg) { my($result) = ($1); while ($result =~ /()/sg) { my($result) = ($1); my($instanceId) = $result =~ /(.*?)<\/instanceId>/s; my($instanceState) = map {/(.*?)<\/name>/s} $result =~ /(.*?)<\/instanceState>/s; my($dnsName) = $result =~ /(.*?)<\/dnsName>/s; push @instanceId, $instanceId; push @instanceState, $instanceState; push @dnsName, $dnsName; } } my($pending); for (my $i = 0; $i < @instanceId; $i++) { $pending += $instanceState[$i] eq "pending"; print "$instanceId[$i]\t$instanceState[$i]\t$dnsName[$i]\n"; } last unless $wait && $pending; sleep $wait; $result = qx[$0 --cmd0 --xml --region=$region describe-instances @instanceId]; } } elsif ($result =~ / "Queue URLs", empty => "no queues\n"}); } elsif ($result =~ /[1] if $_->[0] eq "ReceiptHandle"; $body = decode_url($_->[1]) if $_->[0] eq "Body"; } print "$handle\t$body\n" if $handle; } else { print ary2tab($ary, {title => "Messages", empty => "no messages\n"}); } } elsif ($result =~ / Name, value => Value}), {title => "Attributes", empty => "no attributes\n"}); } else { print xml2tab($result) || xmlpp($result); } exit $exit_code; } sub xml2ary { my($tag, $result, $param, @result) = @_; for ($result =~ /<$tag.*?>(.*?)<\/$tag>/sg) { while (/<(.*?)>(.*?)<\/\1>/sg) { my($key, $val1) = ($1, $2); my($val); while ($val1 =~ /<(.+?)>(.*?)<\/\1>/sg) { if ($1 eq $param->{key}) { $key = $2; } elsif ($1 eq $param->{value}) { $val = $2; } else { $val .= " " if $val; $val .= "$1=$2"; } } $val = $val1 unless length($val); push @result, [$key, $val]; } } \@result; } sub ary2tab { my($ary, $param) = @_; return $param->{empty} if exists $param->{empty} && !@$ary; my(@width); for (@$ary) { if (ref $_ eq SCALAR) { $_ = [$_]; } if (ref $_ eq ARRAY) { for (my $i = 0; $i < @$_; $i++) { $width[$i] = length($_->[$i]) if $width[$i] < length($_->[$i]); } } } if ($param->{title}) { my $width = -1; $width += 2 + $_ for @width; my $l = int(($width - length($param->{title})) / 2); my $r = $width - length($param->{title}) - $l; $output .= "+" . "-" x (@width - 1); $output .= "-" x (2 + $_) for @width; $output .= "+\n"; $output .= "| " . " " x $l . $param->{title} . " " x $r . " |\n"; } $output .= "+" . "-" x (2 + $_) for @width; $output .= "+\n"; for (@$ary) { for (my $i = 0; $i < @width; $i++) { $output .= "| " . $_->[$i] . " " x (1 + $width[$i] - length($_->[$i])); } $output .= "|\n"; } $output .= "+" . "-" x (2 + $_) for @width; $output .= "+\n"; } sub xml2tab { my($xml) = @_; my($output); $xml =~ s/^<\?xml.*?>(\r?\n)*//; my @xml = grep !/^\s*$/, split(/(<.*?>)/, $xml); my(@tag, @depth); my $depth = 0; for (my $i = 0; $i < @xml; $i++) { if ($xml[$i] =~ /^<(\w+)\/>$/) { next; } elsif ($xml[$i] =~ /^<(\w+)/) { my($tag) = ($1); $tag[$i] = $tag; $depth[$i] = ++$depth; } elsif ($xml[$i] =~ /^<\/(\w+)/) { my($tag) = ($1); for (my $j = $i - 1; $j >= 0; $j--) { next if $depth[$j] > $depth; next if $tag[$j] ne $tag; $depth = $depth[$j] - 1; last; } } else { $tag[$i] = $xml[$i]; $depth[$i] = 99; } } my(@parent, $depth, %head, @head, @table, $col); my $skipre = qr/^(?:amiLaunchIndex|ETag|HostId|ipPermissions|Owner)$/; for (my $i = 0; $i <= @xml; $i++) { $parent[$depth[$i]] = $tag[$i]; if (@head && $i == @xml || $depth[$i] && $depth[$i] < $depth) { for (@table) { $_ = [map {printable(dentity($_))} @$_{@head}]; } unshift @table, [@head]; my(@width); for (@table) { for (my $i = 0; $i < @head; $i++) { if ($head[$i] =~ /^(?:privateDnsName|dnsName)$/) { chomp(my $me = qx[me $_->[$i]]); $_->[$i] = $me if $me; } my $length = length($_->[$i]); $width[$i] = $length if $width[$i] < $length; } } my $sep = "+"; for (my $i = 0; $i < @head; $i++) { next if $head[$i] =~ /$skipre/; $sep .= "-" x (2 + $width[$i]) . "+"; } for (my $j = 0; $j < @table; $j++) { $output .= "$sep\n" if $j < 2; for (my $i = 0; $i < @head; $i++) { next if $head[$i] =~ /$skipre/; my $len = length($table[$j]->[$i]); my $pad = $width[$i] - $len; my $l = 1 + int($pad / 2); # center justify $l = 1 if $j; # left justify all but first row my $r = 2 + $pad - $l; $output .= "|" . " " x $l . $table[$j]->[$i] . " " x $r; } $output .= "|\n"; } $output .= "$sep\n"; $depth = 0; %head = (); @head = (); @table = (); } my $tag2 = "$parent[$depth[$i] - 1]-$tag[$i]"; if ($tag[$i] =~ /^(?:LocationConstraint|Grant |AttachVolumeResponse|Bucket|Contents|AuthorizeSecurityGroupIngressResponse|CopyObjectResult |CreateKeyPairResponse|CreateSecurityGroupResponse|CreateSnapshotResponse|CreateVolumeResponse |DeleteSecurityGroupResponse|DeleteKeyPairResponse|DeleteSnapshotResponse|DeleteVolumeResponse |DetachVolumeResponse|Error|GetConsoleOutputResponse|ListBucketResult|RebootInstancesResponse |RevokeSecurityGroupIngressResponse|AllocateAddressResponse|ReleaseAddressResponse|AssociateAddressResponse|DescribeRegionsResponse |CreateQueueResponse|ResponseMetadata )$/x || $tag2 =~ /^(?:addressesSet-item|availabilityZoneInfo-item|imagesSet-item|instancesSet-item |ipPermissions-item|keySet-item|reservedInstancesOfferingsSet-item|securityGroupInfo-item|volumeSet-item|snapshotSet-item|regionInfo-item |ReceiveMessageResult-Message )$/x || $i == @xml) { $depth = $depth[$i]; ###push @table, {"" => $tag[$i]}; push @table, {}; } next unless $depth; if ($depth[$i] == $depth + 1) { $col = $tag[$i]; push @head, $col unless exists $head{$col}; $head{$col} = undef; } if ($depth[$i] >= $depth + 2) { $table[$#table]->{$col} .= " " if $table[$#table]->{$col} && $depth[$i] < 99; $table[$#table]->{$col} .= $tag[$i]; $table[$#table]->{$col} .= "=" if $depth[$i] < 99; } } if (!@table || $dump_xml) { print STDERR "$xml\n"; for (my $i = 0; $i < @xml; $i++) { next unless $tag[$i]; print STDERR $depth[$i], " " x $depth[$i], "$tag[$i]\n"; } } $output; } sub xmlpp { my($xml) = @_; my($indent, @path, $defer, @defer, $result) = "\t"; for ($xml =~ /<.*?>|[^<]*/sg) { if (/^<\/(\w+)/ || /^<(!\[endif)/) # $/) # .../> or ...?> { $result .= "@{[$indent x @path]}@{[$defer =~ /^\s*(.*?)\s*$/s]}\n" if $defer; push @path, @defer; $result .= "@{[$indent x @path]}@{[/^\s*(.*?)\s*$/s]}\n" if $_; $defer = ""; @defer = (); } elsif (/^(?:[^<]|\s]+)/; } } $result .= "@{[$indent x @path]}@{[$defer =~ /^\s*(.*?)\s*$/s]}\n" if $defer; $result; } sub s3 { my($verb, $marker, $name, $file, @header) = @_; my $expires = time + ($expire_time || 30) + $time_offset; $file ||= $name if $verb eq PUT && $ENV{S3_DIR}; $name = "$ENV{S3_DIR}/$name" if $ENV{S3_DIR}; $name =~ s/^([^\?\/]+)(\?|$ )/$1\/$2/xs; $name .= $file if $verb eq PUT && $name =~ /\/$/; # add a Content-Type header using mime.types if ($verb eq PUT) { my($found_content_type, $found_content_md5); for (@header) { $found_content_type++ if /^content-type:/i; $found_content_md5++ if /^content-md5:/i; } if (!$found_content_type) { my($ext) = $name =~ /\.(\w+)$/; if ($ext) { local(@ARGV); for (qw(mime.types /etc/mime.types)) { push @ARGV, $_ if -e $_; } if (@ARGV) { while (<>) { my($type, @ext) = split(/\s+/); if (grep /^$ext$/, @ext) { push @header, "Content-Type: $type"; print STDERR "setting $header[$#header]\n" if $v; last; } } } } } if (!$found_content_md5 && $md5) { # Too memory intensive: #my $md5 = encode_base64(md5(load_file($file)), ""); # Uses Digest::MD5::File that isn't in base perl: # (Use this choice for Windows, after installing the package) #use Digest::MD5::File qw(file_md5); #my $md5 = encode_base64(file_md5($file), ""); # Just right: my $md5 = encode_base64(pack("H*", (split(" ", qx[md5sum @{[cq($file)]}]))[0]), ""); push @header, "Content-MD5: $md5"; print STDERR "setting $header[$#header]\n" if $v; } } $set_acl = "public-read" if $public; $set_acl = "private" if $private; push @header, "x-amz-acl: $set_acl" if $verb eq PUT && $set_acl; # read from stdin when # aws put target # aws put target - # but not # aws put target?acl # what about # aws put target?location my($temp_fh); if ($verb eq PUT && ($file eq "-" || $file eq "" && $name !~ /\?acl$/)) { # and not when a terminal die "$0: will not to read from terminal (use \"-\" for filename to force)\n" if -t && $file ne "-"; ($temp_fh, $file) = tempfile(UNLINK => 1); while (STDIN->read(my $buf, 16_384)) { print $temp_fh $buf; } $temp_fh->flush; } # added a case for "mkdir", so that "$name .= $file" gets defeated # in the mkdir case - We don't want the file we are uploading to be # appended to the name because we are creating the bucket, and the # name is the location constraint file. $verb = PUT if $verb eq MKDIR; # added a case for "copy", so that the source moves to a header if ($verb eq COPY) { if ($name =~ /\/$/) { my($what) = $file =~ /([^\/]+)$/; $name .= $what; } if ($file !~ /^\//) { (my $where = $name) =~ s/\/[^\/]+$/\//; $file = "/$where$file"; } push @header, "x-amz-copy-source: @{[encode_url($file)]}"; undef $file; $verb = PUT; } my($prefix); # added a case for "ls", so that a prefix can be specified # (otherwise, the prefix looks like an object name) if ($verb eq LS) { $name =~ s/^\///; ($name, $prefix) = split(/\//, $name, 2); $name .= "/" if $name; $prefix ||= $file; undef $file; $verb = GET; } my($ub, $uo, $uq) = $name =~ /^(.+?)(?:\/(.*?))?(\?(?:acl|location|logging|bittorrent))?$/s; my $uname = encode_url($ub) . "/" . encode_url($uo) . $uq if $name; if ($v >= 2) { print "name = $name\n"; print "ub = $ub\n"; print "uo = $uo\n"; print "uq = $uq\n"; print "uname = $uname\n"; } my($vhost, $vname) = ("s3.amazonaws.com", $uname); ($vhost, $vname) = ($dns_alias? $1: "$1.$vhost", $2) if $uname =~ /^([0-9a-z][\.\-0-9a-z]{1,61}[0-9a-z])(?:\/(.*))?$/; print STDERR "vhost=$vhost vname=$vname\n" if $v; my $isGETobj = $verb eq GET && $uname =~ /\/./ && $uname !~ /\?/; if ($isGETobj && !$fail && !$request) { my $data = "HEAD\n\n\n$expires\n/$uname"; my($sig, $awskey) = sign($data); my $url = "$scheme://$vhost/$vname@{[$vname =~ /\?/? '&': '?']}AWSAccessKeyId=@{[encode_url($awskey)]}&Expires=$expires&Signature=@{[encode_url($sig)]}"; my $cmd = qq[curl $curl_options $insecureaws --header "Expect: " --head @{[cq($url)]}]; print STDERR "$cmd\n" if $v; my $head = qx[$cmd]; print STDERR $head if $v; my($code) = $head =~ /^HTTP\/\d+\.\d+\s+(\d+\s+.*?)\r?\n/s; if ($code !~ /^2\d\d\s/) { print STDERR "$code\n" unless $v; $exit_code = 22; return; } } my($content_type, $content_md5); for (@header) { if (/^(.*?):\s*(.*)$/) { $content_type = $2 if lc $1 eq "content-type"; $content_md5 = $2 if lc $1 eq "content-md5"; } } my $header = join("\n", sort(map {s/^(.*?):\s*/\L$1:/s; $_} grep /^x-amz-/, @header), "") if @header; my $data = "$verb\n$content_md5\n$content_type\n$expires\n$header/$uname"; my($sig, $awskey) = sign($data); my $url = "$scheme://$vhost/$vname@{[$vname =~ /\?/? '&': '?']}AWSAccessKeyId=@{[encode_url($awskey)]}&Expires=$expires&Signature=@{[encode_url($sig)]}"; $url .= "&marker=$marker" if $marker; $url .= "&prefix=$prefix" if $prefix; return $url if $request; my($content); $content = "--upload-file @{[cq($file)]}" if $file; if ($verb eq GET && $file) { if ($file =~ /\/$/ || -d $file) { $file .= "/" if $file !~ /\/$/; #Why doesn't #1 work? #$file .= "#1"; my($name) = $name =~ /(?:.*\/)?(.*)$/; $file .= $name; } $content = "--create-dirs --output @{[cq($file)]}"; } my $header = join(" --header ", undef, map {cq($_)} @header); # exec() is used because we can, but it doesn't work under Windows: # curl.exe runs asynchronously, and control is returned to the caller # before the file transfer request is complete. Thus, for Windows, # no exec(). if ($isGETobj && !$md5 && $ENV{OS} !~ /windows/i) { my $cmd = qq[curl $curl_options $insecureaws --header "Expect: " $header --request $verb $content --location @{[cq($url)]}]; print STDERR "exec $cmd\n" if $v; exec $cmd; die; } my $cmd = qq[curl $curl_options $insecureaws --header "Expect: " $header --request $verb --dump-header - $content --location @{[cq($url)]}]; print STDERR "$cmd\n" if $v; my $item = qx[$cmd]; exit $? >> 8 if $? && $fail; my($head, $body) = $item =~ /^(.*?\r?\n)\r?\n(.*)$/s; print STDERR $head if $v; my($code) = $head =~ /^HTTP\/\d+\.\d+\s+(\d+\s+.*?)\r?\n/s; if ($code !~ /^2\d\d\s/) { print STDERR "$code\n" unless $v; $exit_code = 22; return if $fail; } if ($md5) { my($want) = $head =~ /^ETag:\s*"(.*?)"/m; if ($want) { my($have); if ($body) { $have = md5_hex($body); } else { $have = (split(" ", qx[md5sum @{[cq($file)]}]))[0]; } print STDERR "MD5: checksum is $want\n" if $v; if ($want ne $have) { print STDERR "MD5: checksum failed ($want at amz != $have here)\n"; exit 1 if $fail; } } } $body; } sub ec2 { my $service = shift; $expire_time ||= 30; # force it to use Expires rather than Timestamp, so it expires more quickly my($sec, $min, $hour, $mday, $mon, $year, undef, undef, undef) = gmtime(time + $time_offset + $expire_time); my $zulu = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ", 1900 + $year, $mon + 1, $mday, $hour, $min, $sec; my($version); $version .= $ec2_version if $service eq "ec2"; $version .= $sqs_version if $service eq "sqs"; my %data = (AWSAccessKeyId => _, SignatureMethod => HmacSHA1, SignatureVersion => 2, Version => $version, ($expire_time? Expires: Timestamp) => $zulu, @_); if ($service eq "ec2") { $endpoint = {eu => "eu-west-1", us => "us-east-1"}->{lc $region} || $region; $endpoint .= "." if $endpoint; $endpoint .= "ec2.amazonaws.com"; } if ($service eq "sqs") { $endpoint = "queue.amazonaws.com"; } $queue ||= "/"; my($url); for (sort keys %data) { if ($service eq "sqs" && $_ eq "QueueUri") { $queue = $data{$_}; next; } $url .= "&" if $url; $url .= "$_=@{[encode_url($data{$_})]}"; } my($sig, $awskey) = sign("GET\n$endpoint\n$queue\n$url"); $url =~ s/(AWSAccessKeyId=?)_/$1$awskey/; $url = "$scheme://$endpoint$queue?Signature=@{[encode_url($sig)]}&$url"; return $url if $request; local($/); # return a string regardless of wantarray qx[curl $curl_options $insecureaws @{[cq($url)]}]; } sub encode_url { my($s) = @_; $s =~ s/([^\-\.0-9a-z\_\~])/%@{[uc unpack(H2,$1)]}/ig; $s; } sub decode_url { my($s) = @_; $s =~ s/%(..)/@{[uc pack(H2,$1)]}/ig; $s; } sub dentity { my($s) = @_; for ($s) { s/&\#x([0-9a-f]{1,2});/pack(C, hex($1))/iseg; s/&(.*?);/{quot => '"', amp => "&", apos => "'", lt => "<", gt => ">"}->{$1} || "&$1;"/seg; } $s; } sub printable { my($s) = @_; $s =~ s/[\x00-\x1f\x7f]/\?/sg; $s; } sub load_file { my $fn = shift; my $io = new IO::File($fn) or die "$fn: $!\n"; local($/); <$io>; } sub save_file { my $nfn = my $fn = shift; $nfn = ">$fn" if $fn !~ /^\s*[\>\|]/; my $out = IO::File->new($nfn) or die "$fn: $!\n"; print $out join("", @_); } sub load_file_silent { my $fn = shift; my $io = new IO::File($fn) or return; local($/); <$io>; } sub get_home_directory { return "$ENV{HOMEDRIVE}$ENV{HOMEPATH}" || "C:" if $ENV{OS} =~ /windows/i; (getpwuid($<))[7]; } sub sign { my($data) = @_; my($awskey, $secret, $signurl) = @ENV{qw(AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SIGN_URL)}; unless ($awskey || $secret || $signurl) { ($awskey, $secret, $signurl) = split(' ', load_file($secrets_file)); } die if $signurl =~ /\'/; $data =~ s/(AWSAccessKeyId=?)_/$1$awskey/g; if ($v) { (my $pretty = $data) =~ s/\n/\\n/sg; print STDERR "data = $pretty\n"; } if (!exists $ENV{QUERY_STRING} && $signurl) { (my $pretty = $data) =~ s/\n/\\n/sg; (my $url = $signurl) =~ s/\/\/.*?\@/\/\/\*\*\*\*\*\*\:\*\*\*\*\*\*\@/; print STDERR "signing [$pretty] via $url\n" if $v; my $s = qx[curl $curl_options $insecsign --data @{[cq($data)]} @{[cq($signurl)]}]; #S9pr7y07SGtgt7OKjMxMYBy+LCk= #1B5JPHYQCXW13GWKHAG2 die "bad signature [$s] from remote signing service (perhaps the password is bad?)\n$s\n" unless $s =~ /^[A-Z0-9\+\/\=\n]+$/i; return (split(/\n/, $s)); } (encode_base64(hmac($data, $secret, \&sha1_sha1), ""), $awskey); } sub cq { # quote for sending to curl via shell my($s) = @_; return "\"$s\"" if $ENV{OS} =~ /windows/i; $s =~ s/\'/\'\\\'\'/g; "'$s'"; } sub curlq { # quote for sending URL to curl via shell my($s) = @_; return "\"$s\"" if $ENV{OS} =~ /windows/i; $s =~ s/[\'\ \+\#]/%@{[unpack(H2, $1)]}/g; $s } sub xcmp { my($a, $b) = @_? @_: ($a, $b); my @a = split(//, $a); my @b = split(//, $b); for (;;) { return @a - @b unless @a && @b; last if $a[0] cmp $b[0]; shift @a; shift @b; } my $cmp = $a[0] cmp $b[0]; for (;;) { return ($a[0] =~ /\d/) - ($b[0] =~ /\d/) if ($a[0] =~ /\d/) - ($b[0] =~ /\d/); last unless (shift @a) =~ /\d/ && (shift @b) =~ /\d/; } return $cmp; } # # hmac() was taken from the Digest::HMAC CPAN module # Copyright 1998-2001 Gisle Aas. # Copyright 1998 Graham Barr. # This library is free software; you can redistribute it and/or # modify it under the same terms as Perl itself. # sub hmac { my($data, $key, $hash_func, $block_size) = @_; $block_size ||= 64; $key = &$hash_func($key) if length($key) > $block_size; my $k_ipad = $key ^ (chr(0x36) x $block_size); my $k_opad = $key ^ (chr(0x5c) x $block_size); &$hash_func($k_opad, &$hash_func($k_ipad, $data)); } # # end of hmac() # # # sha1() was taken from http://www.movable-type.co.uk/scripts/SHA-1.html # Copyright 2002-2005 Chris Veness # You are welcome to re-use these scripts [without any warranty express or implied] # provided you retain my copyright notice and when possible a link to my website. # # Conversion from Javascript # Copyright 2007 Timothy Kay # sub sha1_sha1 { # integer arithment should be mod 32 use integer; my $msg = join("", @_); #constants [4.2.1] my @K = (0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6); # PREPROCESSING $msg .= pack(C, 0x80); # add trailing '1' bit to string [5.1.1] # convert string msg into 512-bit/16-integer blocks arrays of ints [5.2.1] my @M = unpack("N*", $msg . pack C3); # how many integers are needed (to make complete 512-bit blocks), including two words with length my $N = 16 * int((@M + 2 + 15) / 16); # add length (in bits) into final pair of 32-bit integers (big-endian) [5.1.1] @M[$N - 2, $N - 1] = (sha1_lsr(8 * length($msg), 29), 8 * (length($msg) - 1)); # set initial hash value [5.3.1] my @H = (0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476, 0xc3d2e1f0); # HASH COMPUTATION [6.1.2] for (my $i = 0; $i < $N; $i += 16) { # 1 - prepare message schedule 'W' my @W = @M[$i..$i + 15]; # 2 - initialise five working variables a, b, c, d, e with previous hash value my($a, $b, $c, $d, $e) = @H; # 3 - main loop for (my $t = 0; $t < 80; $t++) { $W[$t] = sha1_rotl($W[$t - 3] ^ $W[$t - 8] ^ $W[$t - 14] ^ $W[$t - 16], 1) if $t >= 16; my $s = int($t / 20); # seq for blocks of 'f' functions and 'K' constants my $T = sha1_rotl($a, 5) + sha1_f($s, $b, $c, $d) + $e + $K[$s] + $W[$t]; ($e, $d, $c, $b, $a) = ($d, $c, sha1_rotl($b, 30), $a, $T); } # 4 - compute the new intermediate hash value $H[0] += $a; $H[1] += $b; $H[2] += $c; $H[3] += $d; $H[4] += $e; } pack("N*", @H); } # # function 'f' [4.1.1] # sub sha1_f { my($s, $x, $y, $z) = @_; return ($x & $y) ^ (~$x & $z) if $s == 0; return $x ^ $y ^ $z if $s == 1 || $s == 3; return ($x & $y) ^ ($x & $z) ^ ($y & $z) if $s == 2; } # # rotate left (circular left shift) value x by n positions [3.2.5] # sub sha1_rotl { my($x, $n) = @_; ($x << $n) | (($x & 0xffffffff) >> (32 - $n)); } # # logical shift right value x by n positions # done using floating point, so that it works for more than 32 bits # sub sha1_lsr { no integer; my($x, $n) = @_; $x / 2 ** $n; } # # end of sha1() # # # Jim Dannemiller says MIME::Base64 was missing from the Perl installation # on a small Linux handheld, so I added this code here instead of including # MIME::Base64. # # Copyright 1995-1999, 2001-2004 Gisle Aas. # # This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. # # Distantly based on LWP::Base64 written by Martijn Koster and Joerg Reichelt # and code posted to comp.lang.perl <3pd2lp$6gf@wsinti07.win.tue.nl> by Hans Mulder # sub encode_base64 ($;$) { if ($] >= 5.006) { require bytes; if (bytes::length($_[0]) > length($_[0]) || ($] >= 5.008 && $_[0] =~ /[^\0-\xFF]/)) { require Carp; Carp::croak("The Base64 encoding is only defined for bytes"); } } use integer; my $eol = $_[1]; $eol = "\n" unless defined $eol; my $res = pack("u", $_[0]); # Remove first character of each line, remove newlines $res =~ s/^.//mg; $res =~ s/\n//g; $res =~ tr|` -_|AA-Za-z0-9+/|; # `# help emacs # fix padding at the end my $padding = (3 - length($_[0]) % 3) % 3; $res =~ s/.{$padding}$/'=' x $padding/e if $padding; # break encoded string into lines of no more than 76 characters each if (length $eol) { $res =~ s/(.{1,76})/$1$eol/g; } return $res; } sub decode_base64 ($) { local($^W) = 0; # unpack("u",...) gives bogus warning in 5.00[123] use integer; my $str = shift; $str =~ tr|A-Za-z0-9+=/||cd; # remove non-base64 chars if (length($str) % 4) { require Carp; Carp::carp("Length of base64 data not a multiple of 4") } $str =~ s/=+$//; # remove padding $str =~ tr|A-Za-z0-9+/| -_|; # convert to uuencoded format return "" unless length $str; ## I guess this could be written as #return unpack("u", join('', map( chr(32 + length($_)*3/4) . $_, #$str =~ /(.{1,60})/gs) ) ); ## but I do not like that... my $uustr = ''; my ($i, $l); $l = length($str) - 60; for ($i = 0; $i <= $l; $i += 60) { $uustr .= "M" . substr($str, $i, 60); } $str = substr($str, $i); # and any leftover chars if ($str ne "") { $uustr .= chr(32 + length($str)*3/4) . $str; } return unpack ("u", $uustr); } # # end of encode_base64() #