#!/usr/bin/perl # # WWE - a Web-based web-page editor - http://bre.klaki.net/cgi-bin/wwe # ## Copyright (c) 2003-2005 Bjarni R. Einarsson. All rights reserved. ## 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. # ############################################################################## # Customize this to match your site's requirements. my $script_url = "http://bre.klaki.net/cgi-bin/wwe"; # The script's URL my $temp_prefix = "/home/bre/private/wwe/"; # Temp/log file prefix my $max_size = 128000; # Largest allowed size # Set this to zero to never offer to or try to save passwords. Saving # passwords only works if either the Crypt::Blowfish (preferred) or # Crypt::DES module is available. my $save_passwords = 1; my $using_cipher = '(unset)'; # This contains various "helpers" which I like to have around when editing. # Some are Iceland-specific, so you may want to customize this a bit. my $edit_helpers = join("\n", "", "", "", "", "

Helpers

", ' Merriam-Webster:
 
', ' Google search:
 
', "
", "

Spellchecking via SpellCheck.net:", "

", " ", "
", " ", ' ', "

", "

Yfirlestur með hjálp Púki.is:
", "

", "
", ' ', ' ', "

", "
"); # You shouldn't need to modify anything after this point... ##[ Constants ]################################################################ my $version = "WWE v0.4.0"; my $progurl = "$version"; my $author = 'Bjarni R. Einarsson'; my $instructions = "

What is this?

This is $progurl.

WWE is a program written to allow people who only have FTP-access to their web-sites to edit their web pages using a web browser.

Note: Download & installation info is at the bottom of this page.

How do I use it?

Simply fill out the form above and hit \"Edit\". If successful, the contents of the file you requested will be made available for editing within your browser. Otherwise the program will return an error message and let you try again.

If you are used to using FTP to upload files to your web-site, then most of the fields on the log-in page should be self explanitory. The only part which may confuse people, is the \"File Path\":

FIXME: Explain file paths...

Shortcuts, bookmarks, ...

It is possible to create a shortcut for editing a certain web-page, so you don't have to fill out the entire form each time. This is done by encoding the page location into the URL to WWE. When you edit a page with WWE, the shortcut URL for that page will be displayed near the bottom of the editor, which you can cut-and-paste into your bookmarks or even into the page source itself (so the pages can have \"edit me\" buttons which really work).

The autogenerated URLs don't contain the password, for security reasons.

Is WWE secure?

It depends!

First of all, it should be noted that the FTP protocol itself is rather insecure - it uses an unencrypted channel to transmit data back and forth, which makes evesdropping relatively simple. To make matters worse, WWE is an intermediate step in the communications - whoever installed the WWE you are using could easily evesdrop and steal the password to your web-site. So it's probably a very bad idea to use a WWE which is located on a stranger's web-site.

However, WWE can also be used to make things more secure. If an SSL enabled web server with WWE is installed by the administrator on the FTP host itself, then updating your site using the SSL secured WWE will actually be more secure than using plain-old FTP. SSL is the encryption technology used for online banking and things like that.

If this is all just a bunch of techno-mumbo-jumbo to you, but you are still concerned about the security of your web-site, then I suggest you consult your system administrator before using WWE. :-)

What about sessions and passwords?

As of WWE 0.4.0, you can ask WWE to remember your password for a certain amount of time to avoid having to log in every time you edit a page.

This is done in a relatively secure manner; passwords are encrypted using a randomly generated key and stored on the WWE server's hard drive. Your browser is given the key as a cookie. Without the secret key, the file on the WWE server's hard-drive should be very hard to decrypt, thus keeping your password secure. When you log out, both the password file and cookie will be deleted.

When using WWE over un-encrypted HTTP (not SSL), using a cookie like this will actually improve your security, since you have to trust the admin of the WWE server anyway and the password will only be sent over the 'wild-wild-web' once instead of being sent every time you edit a page.

Encryption is done using either the Blowfish (preferred) or DES algorithms, depending on what is available on the WWE server. If no encryption is available, users won't be given the option to store their passwords.

This server uses: USING_CIPHER

Editing sections (instead of entire pages)

Most HTML pages contain a mixture of \"layout\" code (tables, icons, banners) and actual content (news, articles, other text, ...). When maintaining a web site you may only want to update some of the content regularly and leave the rest (especially the layout code) alone. In such cases, finding the things you really want to edit can be a bit like searching for a needle in a haystack.

Larger sites solve this sort of problem using databases and complex template-based publishing systems. For a small site, such a solution may be both too expensive and too complicated to make sense. WWE supports editing sections as a light-weight, cheap solution to this problem.

Editing sections are created by simply embedding markers within the HTML document itself, which instruct WWE whether the following text is supposed to be editable or not, and which style of editing you want to use. This is probably best explained with an example:

When this file is edited with WWE, the program will present the user with two boxes for editing, which will look something like this:

Page title

Page body


One thing to note here, is that because the \"Page body\" section was defined using \"WWE edit simple\", then the paragraph markers (<p> etc.) have been removed and replaced with empty lines. When the file is saved empty lines will automatically be converted back to paragraph boundaries. This may make it a little bit easier for people unfamiliar with HTML syntax to maintain the text on a web page.

WWE will automatically enter section-editing mode, if any <!--WWE ... --> markers are found in the edited document. To disable this behavior, check \"Ignore sections\" on the log-in page.

Can I install WWE myself?

Sure, if you know how to install Perl CGI programs. WWE requires the Net::FTP module. You will also want to install Crypt::Blowfish if you want to enable secure password storage and session support.

Get the source here, and be sure to edit the first few lines to match your local configuration. New releases are announced on Freshmeat.net.

If you add any cool new features to WWE or find any bugs, please share them with the author. Thanks!

"; ##[ Main ]#################################################################### my ($cgi, $hidden) = parse_cgi({ rows => 25, cols => 80, charset => 'iso-8859-1', }); $cgi->{user} = $cgi->{userfoo} if ($cgi->{userfoo}); my $head_common = ''; my $cookies; eval { $cookies = do_password_magic($cgi); }; # Make sure the instructions show which cipher we're using. $instructions =~ s/USING_CIPHER/$using_cipher/g; print "Content-Type: text/html\n", "Cache-Control: no-cache\n", "Expires: 0\n", $cookies, "\n"; print $@; if ($cgi->{host} && $cgi->{file} && $cgi->{user} && $cgi->{pass}) { use Net::FTP; if ($cgi->{save} && ($cgi->{text} || $cgi->{part001})) { my $data = assemble_data($cgi); save_data($cgi, $data); } else { my $empty_ok = $cgi->{template} || $cgi->{create}; my $data = get_file($cgi, $cgi->{file}, $empty_ok); $data = get_file($cgi, $cgi->{template}) if (!$data && $cgi->{template}); edit_data($cgi, $data); } } else { first_form($cgi, "Please complete the form..."); } ##[ Subroutines ]############################################################## sub first_form { my ($cgi, $error) = @_; $error = "

!! $error

" if ($error); my $inst = "
".$instructions unless ($cgi->{noinst}); my $edit_all = "checked" if ($cgi->{edit_all}); my $url = "URL:" if ($cgi->{url}); my $rem = 'Forget me.'. 'Remember me for this session.'. 'Remember me for a day.'. 'Remember me for a week.'. 'Remember me for a month.' if ($save_passwords); print join("\n", "$head_common$version: Web-based Web-site Editor! (edit web pages using your web browser)", $error, '
', '', '', $url, '', '', $rem, '', '
FTP Host:
File path:
User name:
Password:
', '    . . . . ', '   Lines:', '   Columns:', "   Ignore sections:", '
', $inst, "
$progurl by $author
", ''); exit(0); } sub edit_data { my ($cgi, $data, $error) = @_; $error = "

!! $error

" if ($error); # Top of editing page... print "$head_common$version: Web-based Web-site Editor!\n"; if ($cgi->{pass_from_cookie}) { print "
\n", "

You are logged in, using $using_cipher.   ", "

", "
"; } print "
\n"; # us to embed editing commands within the file itself to control what is # editable and what isn't... $data =~ s/\<(\/?textarea)/<$1/gi; if (($data !~ /)/m, $data); my $mode = "noedit"; my $count = 0; foreach my $part (@parts) { print edit_format($cgi, $part, \$mode, \$count); } } my @keys = ("user", "host", "file", "rows", "cols", "edit_all", "url"); push @keys, "pass" unless ($cgi->{pass_from_cookie}); foreach my $key (@keys) { print '', "\n" if (defined $cgi->{$key}); } my $url = "
Go to edited page" if ($cgi->{url}); my $short = sprintf("%s?host=%s&userfoo=%s&file=%s&url=%s&noinst=1", $script_url, map { url_encode($cgi->{$_}) } ('host', 'user', 'file', 'url') ); print join("\n", "
", "  ", "  ", $url, "
", $error, "


", $edit_helpers, "
", "Editing shortcut: $short", "
$progurl by $author
", ''); exit(0); } sub edit_format { my ($cgi, $data, $moderef, $countref) = @_; $$countref = sprintf("%3.3d", $$countref+1); if ($data =~ //i) { $$moderef = $1; my $partname = $2; return "\n" . "

$partname

"; } elsif ($$moderef =~ /noedit/i) { return "\n"; } elsif ($$moderef =~ /(html|text|raw)/i) { my $rows = 1 + int(length($data) / $cgi->{cols}) + grep(/\n/, split(//, $data)); $rows = 30 if ($rows > 30); return "\n" . "\n"; } elsif ($$moderef =~ /simple/i) { $data =~ s/\s+/ /gs; $data =~ s/\s*<\/p>\s*//igs; $data =~ s/\s*
\s*/\n/igs; $data =~ s/\s*

\s*/\n\n/igs; $data =~ s/^\s*(.*?)\s*$/$1/s; my $rows = 1 + int(length($data) / $cgi->{cols}) + grep(/\n/, split(//, $data)); return "\n" . "\n"; } } sub assemble_data { my $cgi = shift; if ($cgi->{part001}) { my $data = ''; foreach my $pnum (sort(map { s/^part//; $_ } grep(/^part/, keys(%{ $cgi })))) { my $mode = $cgi->{"mode$pnum"}; my $text = $cgi->{"part$pnum"}; if ($mode =~ /simple/) { $text =~ s/\r//g; $text =~ s/^\s*(.*?)\s*$/$1/s; $text =~ s/[ \t]*\n\n[ \t]*/<\/p>

/gs; $text =~ s/[ \t]*\n[ \t]*/
/gs; $text =~ s/(<(\/p|br)>)/$1\n/gi; $text = "\n

$text

\n"; } elsif ($mode =~ /text/) { $text = entity_encode($text); } $data .= $text; } return $data; } else { return $cgi->{text}; } } # Load a file from the FTP server into the form... sub get_file { my ($cgi, $filepath, $missing_ok) = @_; umask 0077; my $ftp = new Net::FTP $cgi->{host}; first_form($cgi, "Error: $@") unless ($ftp); unless ($ftp->login($cgi->{user}, $cgi->{pass})) { logline("Bad login: $cgi->{user}\@$cgi->{host} pass: $cgi->{pass}"); first_form($cgi, "Failed to log-on. Check the username and password."); } my ($file, $path) = ($filepath, $filepath); $file =~ s,^.*/,,g; $path =~ s,[^\/]+$,,; $ftp->binary(); if ($path) { first_form($cgi, "No such directory: $path") unless $ftp->cwd($path); } my $size = $ftp->size($file); first_form($cgi, "That file is too big ($size) for editing!") if (($size > $max_size) && ($size < 1000000000000)); first_form($cgi, "File not found or access denied for: $file") unless $ftp->get($file, $temp_prefix.$$) or $missing_ok; $ftp->quit(); logline("GET $cgi->{user}\@$cgi->{host}:$cgi->{file}"); open(DATA, "<$temp_prefix$$"); my $data = join('', ); close(DATA); unlink($temp_prefix.$$); return $data; } # Upload changes to FTP server... sub save_data { my ($cgi, $data) = @_; umask 0077; open(DATA, ">$temp_prefix$$"); print DATA $data; close(DATA); edit_data($cgi, $data, "Error: $@") unless my $ftp = new Net::FTP $cgi->{host}; edit_data($cgi, $data, "Failed to log-on! Please try again.") unless $ftp->login($cgi->{user}, $cgi->{pass}); my ($file, $path) = ($cgi->{file}, $cgi->{file}); $file =~ s,^.*/,,g; $path =~ s,[^\/]+$,,; $ftp->binary(); if ($path) { edit_data($cgi, $data, "The path has become invalid: $path") unless $ftp->cwd($path); } edit_data($cgi, $data, "Access denied for: $file") unless $ftp->put($temp_prefix.$$, $file); $ftp->quit(); unlink($temp_prefix.$$); logline("PUT $cgi->{user}\@$cgi->{host}:$cgi->{file}"); edit_data($cgi, $data, "The changes have been saved! (".localtime().")") } sub do_password_magic { my ($cgi) = @_; my $retval = ""; return $retval unless ($save_passwords); # Do we have a password and a request to "remember me?" if ($cgi->{"pass"} && $cgi->{"remember_me"}) { my $days = $cgi->{"remember_me"}; my $id = sprintf("%x%x%x", time(), $$, rand()*0xFFFF); my $key = chr(rand()*256).chr(rand()*256) . chr(rand()*256).chr(rand()*256) . chr(rand()*256).chr(rand()*256) . chr(rand()*256).chr(rand()*256); # This above is too predictable a key, try and get a better one from # /dev/urandom if possible. if (open(RAND, "{"pass"}; while ($pass =~ s/^(.{1,8})//) { my $data = $1.("\0" x 8); $ciphertext .= $cipher->encrypt(substr($data, 0, 8)); } # Save to disk... open (SAVE, ">$temp_prefix$id"); print SAVE unpack("h*", $ciphertext); close(SAVE); my $expires = sprintf("; expires=%s", http_time(time() + ($days*24*3600))) if ($days =~ /^\d+$/); $cgi->{pass_from_cookie} = 1; return sprintf("Set-Cookie: wwe=%s:%s; path=/%s\n", $id, unpack("h*", $key), $expires); } # Else, do we have a cookie and want to look up the password? elsif ((!$cgi->{"pass"}) && ($ENV{HTTP_COOKIE} =~ /wwe/)) { my ($file, $key); foreach my $cookie (split(/;\s+/, $ENV{HTTP_COOKIE})) { my ($n, $f, $k) = split(/[=:]/, $cookie); next unless ($n eq "wwe"); next unless ("$f$k" =~ /^[a-z\d]+$/i); $file = $f; $key = $k; last; } # If the user wants to log out, then log out if ($file && (($cgi->{log_out}) || ($cgi->{remember_me} eq "0"))) { unlink("$temp_prefix$file"); $file = undef; } # Else, log in! if ($file && $key && open (SAVE, "<$temp_prefix$file")) { my $ciphertext = pack("h*", ); close(SAVE); # Decript password my $cipher = get_cipher(pack("h*", $key)) || return ""; my $pass = ''; while ($ciphertext =~ s/^(.{1,8})//) { my $data = $1.("\0" x 8); $pass .= $cipher->decrypt(substr($data, 0, 8)); } $pass =~ s/\0+$//; $cgi->{pass} = $pass; $cgi->{pass_from_cookie} = 1; return ""; } } # If we get this far, just check if ciphers are available on this machine. get_cipher(pack("H16", "0123456789ABCDEF")); return $retval; } sub get_cipher { my $key = shift; my $cipher = undef; foreach my $alg ("Blowfish", "DES") { eval 'use Crypt::'.$alg.'; $cipher = new Crypt::'.$alg.' $key;'; $using_cipher = $alg; return $cipher if ((!$@) && ($cipher)); } $using_cipher = "(encryption unavailable, please install Crypt::Blowfish)"; $save_passwords = 0; return undef; } sub entity_encode { my $data = shift; $data =~ s/\&/&/gs; $data =~ s/\/>/gs; $data =~ s/\"/"/gs; $data =~ s/\'/'/gs; return $data; } sub url_encode { my $data = shift; $data =~ s/([^A-Za-z0-9_=\/\\:,\!\?\.-])/sprintf("%%%2.2x", ord($1))/egi; $data =~ tr/ /+/; return $data; } sub parse_cgi { my $defaults = shift || { }; my @set = split(/&/,join('&', $ENV{QUERY_STRING}, join('',))); my $cgi = { }; my $hidden = ""; %{ $cgi } = %{ $defaults }; foreach my $param (@set) { my ($name, $value) = split(/=/,$param); next unless ($value); $value =~ tr/+/ /; $value =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; $cgi->{$name} = $value; $hidden .= '' unless ($name =~ /submit/i); } return ($cgi, $hidden); } sub http_time { my $time = shift; my $ht = gmtime($time); $ht =~ s/^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*$/$1, $3-$2-$5 $4 GMT/; return $ht; } sub logline { my $message = shift; open(LOG, ">>".$temp_prefix."log") || return; chomp $message; print LOG scalar localtime, "; ", ($ENV{REMOTE_HOST}||$ENV{REMOTE_ADDR}), "; ", $message, "\n"; close(LOG); } # vi:ts=4 expandtab