ikiwiki-hosting/0000755000000000000000000000000012232570331011053 5ustar ikiwiki-hosting/iki-git-hook-update.c0000644000000000000000000000223012211430070014757 0ustar /* * git update hook that allows trusted sources to make any change, * while untrusted sources may only push changes that seem safe. * * (iki-git-shell sets UNTRUSTED_COMMIT=1 for commits pushed from external * sources) * * In C for speed. * */ #include #include #include #include int main (int argc, char **argv) { char *untrusted=getenv("UNTRUSTED_COMMIT"); if (argc != 4) { fprintf(stderr, "expected: \n"); exit(1); } if (! untrusted || strcmp(untrusted, "1") != 0) { exit(0); } if (strcmp(argv[3], "0000000000000000000000000000000000000000") == 0) { /* don't allow deletion of master branch or the setup * branch, which would hose the site */ if (strcmp(argv[1], "refs/heads/master") == 0 || strcmp(argv[1], "refs/heads/setup") == 0) { fprintf(stderr, "you are not allowed to delete %s\n", argv[1]); exit(1); } } /* Use ikisite to check modifications of the setup branch. */ if (strcmp(argv[1], "refs/heads/setup") == 0) { unsetenv("GIT_DIR"); execlp("ikisite", "ikisite", "checksetup", argv[3], NULL); perror("ikisite"); exit(1); } exit(0); } ikiwiki-hosting/iki-ssh-unsafe0000755000000000000000000000016212211430070013616 0ustar #!/bin/sh # Used for git push to remotes, without host key checking. exec ssh -o StrictHostKeyChecking=false "$@" ikiwiki-hosting/iki-git-shell.c0000644000000000000000000000540612232570331013666 0ustar /* * Wrapper for git-shell that only allows it to access ~/source.git. * * Also allows running a very few other special commands: * * logview tails site logs * logdump dumps site logs * * In C for speed. * * Use in .ssh/authorized_keys: * command="iki-git-shell",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,no-user-rc * */ #define _GNU_SOURCE #include #include #include #include #include #include /* A "parked" flag file in the repo disables remote access. * The file content explains why. */ void checkparked (const char *repo) { char *flagfile; FILE *f; if (asprintf(&flagfile, "%s/parked", repo) == -1) { perror("asprintf"); exit(1); } if ((f=fopen(flagfile, "r")) != NULL) { int bufsize=1024; char *buf=malloc(bufsize); while (fread(buf, bufsize, 1, f) > 0) { fwrite(buf, bufsize, 1, stderr); } exit(1); } } void logs (const char *param) { execlp("ikisite", "ikisite", "logs", param, NULL); perror("ikisite"); exit(1); } int main (int argc, char **argv) { char *repo, *command, *subcommand, *s; struct passwd *passwd; passwd=getpwuid(geteuid()); if (! passwd || ! passwd->pw_dir || ! strlen(passwd->pw_dir)) { fprintf(stderr, "error: failed to determine home directory\n"); exit(1); } if (asprintf(&repo, "%s/source.git", passwd->pw_dir) == -1) { perror("asprintf"); exit(1); } checkparked(repo); /* * The original command passed to ssh will be * "git subcommand path" or "git-subcommand path". * Pass the subcommand on to git-shell, along with the path to the * repository. */ command=getenv("SSH_ORIGINAL_COMMAND"); if (! command) { fprintf(stderr, "error: SSH_ORIGINAL_COMMAND not set\n"); exit(1); } s=strtok(command, " -"); /* handle "git-" and "git " */ if (! s) { fprintf(stderr, "error: missing command\n"); exit(1); } if (strcmp(s, "logview") == 0) { logs("--tail"); exit(0); } if (strcmp(s, "logdump") == 0) { logs("--dump"); exit(0); } if (strcmp(s, "git") != 0) { fprintf(stderr, "error: unknown command \"%s\"\n", s); exit(1); } s=strtok(NULL, " "); if (! s || ! strlen(s)) { fprintf(stderr, "error: missing git subcommand\n"); } if (asprintf(&subcommand, "git-%s", s) == -1) { perror("asprintf"); exit(1); } /* git-shell currently needs some wacky quoting around the * subcommand and repo that its man page does not show */ if (asprintf(&subcommand, "%s '%s'", subcommand, repo) == -1) { perror("asprintf"); exit(1); } /* this variable is used to communicate to iki-git-hook-update * that the commit is coming in from an external source */ setenv("UNTRUSTED_COMMIT", "1", 1); execlp("git-shell", "git-shell", "-c", subcommand, NULL); perror("git-shell"); exit(1); } ikiwiki-hosting/autosetup/0000755000000000000000000000000012232566773013123 5ustar ikiwiki-hosting/autosetup/README0000644000000000000000000000046312211430070013757 0ustar This directory holds ikiwiki setup automator files. These files are used to generate the ikiwiki setup files for sites. By default, auto.setup is used, but if --type=foo is passed to ikisite, it will prefer to use auto-foo.setup. This way, specific setup templates can be used for different types of sites. ikiwiki-hosting/autosetup/auto.setup0000644000000000000000000000400112232566773015150 0ustar #!/usr/bin/perl # Ikiwiki setup automator. # # This setup file causes ikiwiki to create a wiki, check it into revision # control, generate a setup file for the new wiki, and set everything up. # # This is modified to run noninteractively; configuration # is passed in via the environment. # # BTW, when you change something in this file, you probably will also want to # use `ikisite changesetup` to make the same change to existing, running # sites. The configuration part of the file is written using YAML to ease # doing so. use IkiWiki::Setup::HostingAutomator(<<'EOF'); --- force_overwrite: 1 setuptype: Yaml wikiname: '$ENV{IKIWIKI_WIKINAME}' adminuser: - '$ENV{IKIWIKI_ADMIN}' adminemail: '$ENV{IKIWIKI_ADMINEMAIL}' owner: '$ENV{IKIWIKI_OWNER}' created: '$ENV{IKIWIKI_CREATED}' hostname: '$ENV{IKIWIKI_HOSTNAME}' rcs: '$ENV{IKIWIKI_VCS}' libdir: '$ENV{HOME}/.ikiwiki' srcdir: '$ENV{IKIWIKI_SRCDIR}' repository: '$ENV{IKIWIKI_REPOSITORY}' destdir: '$ENV{IKIWIKI_DESTDIR}' dumpsetup: '$ENV{HOME}/ikiwiki.setup' url: 'http://$ENV{IKIWIKI_HOSTNAME}' cgiurl: 'http://$ENV{IKIWIKI_HOSTNAME}/ikiwiki.cgi' historyurl: 'http://source.$ENV{IKIWIKI_HOSTNAME}/?p=source.git;a=history;f=[[file]];hb=HEAD' diffurl: 'http://source.$ENV{IKIWIKI_HOSTNAME}/?p=source.git;a=blobdiff;f=[[file]];h=[[sha1_to]];hp=[[sha1_from]];hb=[[sha1_commit]];hpb=[[sha1_parent]]' cgi_wrapper: '$ENV{IKIWIKI_CGIDIR}/ikiwiki.cgi' openid_realm: '$ENV{IKIWIKI_OPENID_REALM}' openid_cgiurl: 'http://$ENV{IKIWIKI_HOSTNAME}/ikiwiki.cgi' add_plugins: - goodstuff - websetup - 404 - ikiwikihosting - recentchangesdiff rss: 1 atom: 1 syslog: 1 hardlink: 1 websetup_force_plugins: - httpauth - openid - mdwn - wmd - aggregate websetup_unsafe: - url - cgiurl - verbose - syslog - usedirs - prefix_directives - indexpages - repositories websetup_show_unsafe: 0 cgi_wrappermode: $ENV{IKIWIKI_CGI_WRAPPERMODE} git_wrappermode: $ENV{IKIWIKI_GIT_WRAPPERMODE} untrusted_committers: - $ENV{IKIWIKI_GITDAEMONUSER} anonpush: 1 ENV: TMPDIR: '$ENV{HOME}/tmp' timezone: GMT EOF ikiwiki-hosting/autosetup/auto-blog.setup0000644000000000000000000000454412232566773016105 0ustar #!/usr/bin/perl # Ikiwiki setup automator -- blog version. # # This setup file causes ikiwiki to create a blog, check it into revision # control, generate a setup file for the new blog, and set everything up. # # This is modified to run noninteractively; configuration # is passed in via the environment. # # BTW, when you change something in this file, you probably will also want to # use `ikisite changesetup` to make the same change to existing, running # sites. The configuration part of the file is written using YAML to ease # doing so. use IkiWiki::Setup::HostingAutomator(<<'EOF'); --- force_overwrite: 1 setuptype: Yaml wikiname: '$ENV{IKIWIKI_WIKINAME}' adminuser: - '$ENV{IKIWIKI_ADMIN}' adminemail: '$ENV{IKIWIKI_ADMINEMAIL}' owner: '$ENV{IKIWIKI_OWNER}' created: '$ENV{IKIWIKI_CREATED}' hostname: '$ENV{IKIWIKI_HOSTNAME}' rcs: '$ENV{IKIWIKI_VCS}' libdir: '$ENV{HOME}/.ikiwiki' srcdir: '$ENV{IKIWIKI_SRCDIR}' repository: '$ENV{IKIWIKI_REPOSITORY}' destdir: '$ENV{IKIWIKI_DESTDIR}' dumpsetup: '$ENV{HOME}/ikiwiki.setup' url: 'http://$ENV{IKIWIKI_HOSTNAME}' cgiurl: 'http://$ENV{IKIWIKI_HOSTNAME}/ikiwiki.cgi' historyurl: 'http://source.$ENV{IKIWIKI_HOSTNAME}/?p=source.git;a=history;f=[[file]];hb=HEAD' diffurl: 'http://source.$ENV{IKIWIKI_HOSTNAME}/?p=source.git;a=blobdiff;f=[[file]];h=[[sha1_to]];hp=[[sha1_from]];hb=[[sha1_commit]];hpb=[[sha1_parent]]' cgi_wrapper: '$ENV{IKIWIKI_CGIDIR}/ikiwiki.cgi' openid_realm: '$ENV{IKIWIKI_OPENID_REALM}' openid_cgiurl: 'http://$ENV{IKIWIKI_HOSTNAME}/ikiwiki.cgi' add_plugins: - goodstuff - websetup - lockedit - 404 - comments - blogspam - calendar - sidebar - ikiwikihosting - recentchangesdiff rss: 1 atom: 1 syslog: 1 hardlink: 1 example: blog tagbase: tags comments_pagespec: 'posts/* and !*/Discussion' blogspam_pagespec: 'postcomment(*)' archive_pagespec: 'page(posts/*) and !*/Discussion' htmlscrubber_skip: '* and !comment(*) and !*/Discussion' global_sidebars: 0 discussion: 0 locked_pages: '* and !postcomment(*)' websetup_force_plugins: - httpauth - openid - mdwn - wmd - aggregate websetup_unsafe: - url - cgiurl - verbose - syslog - usedirs - prefix_directives - indexpages - repositories websetup_show_unsafe: 0 cgi_wrappermode: $ENV{IKIWIKI_CGI_WRAPPERMODE} git_wrappermode: $ENV{IKIWIKI_GIT_WRAPPERMODE} untrusted_committers: - $ENV{IKIWIKI_GITDAEMONUSER} ENV: TMPDIR: '$ENV{HOME}/tmp' timezone: GMT EOF ikiwiki-hosting/ikiwiki-hosting-web-backup0000755000000000000000000000471212211430070016124 0ustar #!/bin/bash # Backs up all sites. failed="" . /etc/ikiwiki-hosting/ikiwiki-hosting.conf LOCKFILE=/var/run/ikiwiki-hosting-web-backup-lockfile # Use lockfile to avoid multiple jobs running. # (bash needed because exec 200>file is a bashism) exec 200>$LOCKFILE if ! flock --nonblock 200; then echo "another ikiwiki-hosting-web-backup is already running" >&2 exit 1 fi trap cleanup EXIT INT cleanup () { rm -f $LOCKFILE || true if [ -n "$backup_ssh_cache" ]; then kill %1 # stop any backgrounded ssh master process fi } # start master process for ssh connection caching if [ -n "$backup_ssh_cache" ]; then ssh -nMN "$backup_ssh_cache" & fi for site in $(ikisite list); do if ( [ -n "$num_backups" ] && [ "$num_backups" -gt 0 ] ) || [ -n "$backup_rsync_urls" ]; then bdir="/var/backups/ikiwiki-hosting-web/$site" mkdir -p "$bdir" # savelog has a minimim -c of 2 if [ -e "$bdir/backup" ] && [ -n "$num_backups" ] && [ "$num_backups" -gt 2 ]; then savelog -c "$num_backups" "$bdir/backup" fi if ! ikisite backup "$site" --filename="$bdir"/backup; then echo "ikisite backup $site failed!" >&2 failed=1 fi # rsync backups to somewhere if [ -n "$backup_rsync_urls" ]; then for url in $backup_rsync_urls; do if ! rsync -az $backup_rsync_options "$bdir/backup" "$url$site"; then failed=1 fi done fi # maybe we don't want to keep backups locally.. if [ -z "$num_backups" ] || [ "$num_backups" = 0 ]; then rm -f "$bdir/backup" fi # delete any obsolete version of the site in the morgue # (might exist if it got deleted and then recreated) if [ -n "$morguedir" ]; then rm -f "$morguedir/$site.backup" fi fi done if [ -n "$morguedir" ] && [ -d "$morguedir" ] && [ -n "$backup_rsync_urls" ]; then # backup the morgue to any configured rsync urls. for url in $backup_rsync_urls; do if ! rsync -az $backup_rsync_options "$morguedir/" "${url}morgue/"; then failed=1 fi done # For each site in the morgue, zero out any old backup # of it that might exist on the remote. This is done to avoid # deleted sites being restored if the backups should be used. # (We can't properly delete them the way that we're using rsync.) for file in $(find "$morguedir" -type f); do site="$(basename "$file" | sed 's/\.backup$//')" touch "$morguedir/empty" if ! rsync -a $backup_rsync_options "$morguedir/empty" "$url$site"; then failed=1 fi rm -f "$morguedir/empty" done fi if [ "$failed" ]; then exit 1 else exit 0 fi ikiwiki-hosting/gitweb/0000755000000000000000000000000012211430070012324 5ustar ikiwiki-hosting/gitweb/robots.txt0000644000000000000000000000003212211430070014370 0ustar User-agent: * Disallow: / ikiwiki-hosting/ikisite0000755000000000000000000023523112232570331012450 0ustar #!/usr/bin/perl package IkiWiki::Hosting; use warnings; use strict; use IkiWiki; use IkiWiki::Hosting; sub meta_create { required => [qw{hostname}], options => [qw{type=s vcs=s wikiname=s owner=s admin=s adminemail=s createnonce!}], description => "create new site", section => "primary", } sub create { my $hostname=lc(shift); my %options=(%config, @_); assert_root(); my $nonce=$ENV{IKISITE_NONCE}; # stash for later assert_wrapper_unsafe(); # All inputs are verified, so that ikisite create can be run # from a suid wrapper. if (! exists $options{admin}) { error "must specify --admin=openid"; } if (! defined IkiWiki::openiduser($options{admin})) { error "$options{admin} is not a valid openid"; } my $owner=exists $options{owner} ? $options{owner} : $options{admin}; if (! defined IkiWiki::openiduser($owner)) { error "$owner is not a valid openid"; } if ($options{vcs} !~ /^(git)$/) { error "unsupported vcs $options{vcs}"; } # This will also fail if the hostname is invalid. locksite($hostname); assert_siteexists($hostname, 0); # Avoid path traversal etc when looking up autosetup file. $options{type}=~s/[^-a-zA-Z0-9]//g if defined $options{type}; if (! isemail($options{adminemail})) { $options{adminemail}=$config{adminemail}; } if (! defined $options{wikiname} || ! length $options{wikiname}) { $options{wikiname}=wikiname($hostname); } # Create user usercreate($hostname); my $home = homedir($hostname); # Enable DNS early, to help give it time to propigate. enabledns($hostname); # Setup ikiwiki. It creates the VCS repository, and builds # the site. Configuration is passed in via the environment. $ENV{IKIWIKI_WIKINAME}=$options{wikiname}; $ENV{IKIWIKI_HOSTNAME}=$hostname; $ENV{IKIWIKI_VCS}=$options{vcs}; $ENV{IKIWIKI_REPOSITORY}=$home."/source.$options{vcs}"; $ENV{IKIWIKI_ADMIN}=$options{admin}; $ENV{IKIWIKI_OWNER}=$owner; $ENV{IKIWIKI_CREATED}=time; $ENV{IKIWIKI_ADMINEMAIL}=$options{adminemail}; $ENV{IKIWIKI_SRCDIR}=srcdir($hostname); $ENV{IKIWIKI_DESTDIR}=destdir($hostname); $ENV{IKIWIKI_CGIDIR}=cgidir($hostname); $ENV{IKIWIKI_LOGDIR}=logdir($hostname); $ENV{IKIWIKI_OPENID_REALM}=$config{openid_realm}; $ENV{IKIWIKI_GITDAEMONUSER}=$config{gitdaemonuser}; # This is normally 06755, but for new users (with suexec) we can't use # suid/sgid $ENV{IKIWIKI_CGI_WRAPPERMODE}="0755"; $ENV{IKIWIKI_GIT_WRAPPERMODE}="6755"; my $autosetup=defined $options{type} ? "$config{autosetupdir}/auto-$options{type}.setup" : undef; if (! defined $autosetup || ! -e $autosetup) { $autosetup="$config{autosetupdir}/auto.setup"; } add_wikilist($hostname); eval q{use Cwd q{abs_path}}; $autosetup=abs_path($autosetup); runas(username($hostname), sub { shell("ikiwiki", "-setup", $autosetup); chmod(0600, "$home/ikiwiki.setup") || error "chmod $home/ikiwiki.setup: $!"; chmod(0700, "$home/source") || error "chmod $home/source: $!"; }); if ($options{vcs} eq "git") { githooks($ENV{IKIWIKI_REPOSITORY}); } createsetupbranch($hostname); calendar($hostname); enable($hostname); accountinglog("create", $hostname, $owner); if ($options{createnonce}) { # If IKISITE_NONCE was set to a non-dummy nonce # initially, create a nonce with that value. if (defined $nonce && $nonce ne 'dummy') { return createnonce($hostname, value => $nonce); } else { return createnonce($hostname); } } else { return 1; } } sub meta_delete { required => [qw{hostname}], options => [qw{}], description => "delete a site", section => "primary", } sub delete { my $hostname=lc(shift); assert_root(); locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_safe($hostname); accountinglog("delete", $hostname); disable($hostname); ikiwikiclean($hostname); if ($config{morguedir}) { backup($hostname, morgue => 1); } userdelete($hostname); } sub meta_backup { required => [qw{hostname}], options => [qw{filename=s morgue stdout}], description => "back up a site to the specified output", section => "primary", } sub backup { my $hostname=lc(shift); my %options=@_; assert_root(); locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_denied(); my $file; if ($options{stdout}) { $file="/dev/stdout"; } elsif ($options{morgue}) { if (! $config{morguedir}) { error "morguedir not set (configure in $config{ikisite_conffile})"; } shell("mkdir", "-p", $config{morguedir}); $file=$config{morguedir}."/$hostname.backup"; } elsif (defined $options{filename}) { eval q{use Cwd q{abs_path}}; error $@ if $@; $file=abs_path($options{filename}); } else { error "must specify --filename=value or --morgue or --stdout" } # Ensure setup and wiki state are committed to the setup branch. # Note that since this pushes into the repo, it triggers ikiwiki # to run, and thus it must come before lockwiki, to avoid a # deadlock. commitsetup($hostname); # Lock the wiki to avoid it being in an inconsistent state # during backup. $config{wikistatedir}=srcdir($hostname)."/.ikiwiki"; IkiWiki::lockwiki(); # Generate VCS dump. my $vcs=getsetup($hostname, "rcs"); my $dump=homedir($hostname)."/$vcs.dump"; my @tobackup="$vcs.dump"; if (-e $dump) { unlink($dump) || error("unlink $dump: $!"); } if ($vcs eq "git") { chdir(repository($hostname)) || error "chdir repository: $!"; shell("git", "bundle", "create", $dump, "--all"); # git bundle does not preserve git's config file, # so that will be backed up separately. push @tobackup, repository($hostname)."/config"; } else { error "backup not implemented for $vcs"; } foreach my $ikifile (qw{indexdb userdb sessions.db transient}) { my $f=srcdir($hostname)."/.ikiwiki/$ikifile"; push @tobackup, $f if -e $f; } foreach my $extrafile (glob(homedir($hostname)."/.ssh/id_*"), homedir($hostname)."/.ssh/known_hosts", glob(homedir($hostname)."/apache/*"),) { push @tobackup, $extrafile if -e $extrafile; } # Copy rootconfig into home directory so it can be included # in backup. Nuke ~/rootconfig if it already exists; # the user should not be allowed to add files to rootconfig! my $tmprootconfig=homedir($hostname)."/rootconfig/"; my @rootconfigs=glob(rootconfig($hostname)."/*"); if (@rootconfigs) { if (! mkdir($tmprootconfig)) { system("rm", "-rf", $tmprootconfig); if (! mkdir("rootconfig")) { error "failed to create $tmprootconfig directory: $!"; } } foreach my $config (@rootconfigs) { shell("cp", "-a", $config, $tmprootconfig."/"); } push @tobackup, $tmprootconfig; } # Store files in backup tarball. chdir(homedir($hostname)) || error "chdir HOME: $!"; my $umask=umask(0277); # owner read-only backup file my $homedir=homedir($hostname); shell("tar", "cf", $file, map { # avoid full paths to home directory in tarball my $file=$_; $file=~s/\Q$homedir\E\///; $file; } @tobackup, "--owner=root", "--group=root"); umask($umask); # Cleanup. unlink($dump); shell("rm", "-rf", $tmprootconfig); IkiWiki::unlockwiki(); return; # avoid writing status to stdout } sub meta_restore { required => [qw{hostname}], options => [qw{filename=s morgue! stdin! no-setup! no-sshkeys!}], description => "restore a site from the specified input", section => "primary", } sub restore { my $hostname=lc(shift); my %options=@_; assert_root(); locksite($hostname); assert_siteexists($hostname, 0); assert_wrapper_denied(); my $file; if ($options{stdin}) { $file="/dev/stdin"; } elsif ($options{morgue}) { if (! $config{morguedir}) { error "morguedir not set (configure in $config{ikisite_conffile})"; } $file=$config{morguedir}."/$hostname.backup"; } elsif (defined $options{filename}) { eval q{use Cwd q{abs_path}}; error $@ if $@; $file=abs_path($options{filename}); } else { error "must specify --filename=value or --morgue or --stdin" } if (! -e $file) { error "$file does not exist"; } usercreate($hostname); # Enable DNS early, to help give it time to propigate. enabledns($hostname) unless $options{"no-setup"}; # Extract the VCS dump from the backup tarball. chdir(homedir($hostname)) || error "chdir HOME: $!"; shell("tar", "xf", $file, "--wildcards", "*.dump"); my $dump=glob("*.dump"); my ($vcs)=$dump=~/(.*)\.dump/; my $repository="source.$vcs"; # Extract repository from VCS dump and then check out the source # and setup branches from VCS. Since VCS typically have issues # around checking out directly into an existing home directory, # setup is extracted into a home-tmp. if ($vcs eq "git") { shell("git", "clone", $dump, $repository, "--bare"); # denyNonFastforwards and sharedrepository are normally # set by git init --shared; replicate that in our bare clone. chdir($repository) || error "chdir repository: $!"; shell("git", "config", "core.sharedrepository", 1); shell("git", "config", "receive.denyNonFastforwards", "true"); chdir(homedir($hostname)) || error "chdir HOME: $!"; githooks($repository); shell("git", "clone", $repository, "source"); shell("git", "clone", $repository, "home-tmp", "--branch", "setup"); chmod(0700, "source") || error "chmod source: $!"; chmod(0700, "home-tmp/.git") || error "chmod home-tmp/.git: $!"; chmod(0600, "home-tmp/ikiwiki.setup") || error "chmod home-tmp/ikiwiki.setup: $!"; chmod(0600, "home-tmp/.gitignore") || error "chmod home-tmp/.gitignore: $!" if -e "home-tmp/.gitignore"; } else { error "restore not implemented for $vcs"; } unlink($dump) || error "unlink: $!"; # XXX Using rsync to merge the tree in means more disk IO # than strictly necessary. But is easy. shell("rsync", "-a", "--exclude=.ssh/", "home-tmp/", "."); # Extract remainder of backup tarball. shell("tar", "xf", $file, "--exclude", $dump); # Move rootconfig into place. if (-d "rootconfig") { shell("rm", "-rf", rootconfig($hostname)); shell("mv", "rootconfig", rootconfig($hostname)); } # Make all extracted files be owned by the user. my $username=username($hostname); shell("chown", "$username:$username", "-R", "."); # apache/ should be readable by www-data. if (-d "apache") { shell("chgrp", "www-data", "apache"); chmod(0750, "apache") || error "chmod apache: $!"; } if (! $options{'no-sshkeys'}) { # Restore ssh last, to ensure the git repo is set up # before any attempt is made to push changes into it. foreach my $file (glob("home-tmp/.ssh/*")) { if (-f $file) { shell("mv", "-f", $file, ".ssh/".IkiWiki::basename($file)); } } # ssh requires this file be mode 400, and most # VCS do not preserve the mode. shell("chmod", "400", ".ssh/authorized_keys") if -e ".ssh/authorized_keys"; } else { # record that the ssh keys were dropped commitsetup($hostname); } shell("rm", "-rf", "home-tmp"); if (! $options{"no-setup"}) { ikiwikisetup($hostname); enable($hostname); accountinglog("create", $hostname, getsetup($hostname, "owner")); } # This is the same as IkiWiki::Setup::Automator does when # creating a new site. runas($username, sub { mkdir(homedir($hostname)."/.ikiwiki"); open (WIKILIST, ">", homedir($hostname)."/.ikiwiki/wikilist") || die "wikilist: $!"; print WIKILIST $username." ".homedir($hostname)."/ikiwiki.setup\n"; close WIKILIST; }); add_wikilist($hostname); return 1; } sub meta_branch { required => [qw{oldhostname newhostname}], options => [qw{owner=s admin=s ... adminemail=s wikiname=s createnonce!}], description => "generate a branch of a site with a new hostname, optionally with a different admin", section => "primary", } sub branch { my $oldhostname=lc(shift); my $newhostname=lc(shift); my %options=@_; assert_root(); my $nonce=$ENV{IKISITE_NONCE}; # stash for later assert_wrapper_unsafe(); # All inputs are verified, so that ikisite create can be run # from a suid wrapper. my $adminchanged=0; my %oldadmin=map { $_ => 1 } @{getsetup($oldhostname, "adminuser")}; my $owner=$options{owner}; if (exists $options{admin}) { foreach my $admin (@{$options{admin}}) { if (! defined IkiWiki::openiduser($admin)) { error "$admin is not a valid openid"; } if (! exists $oldadmin{$admin}) { $adminchanged=1; } $owner=$admin unless defined $owner; } } if (defined $owner && ! defined IkiWiki::openiduser($owner)) { error "$owner is not a valid openid"; } # These will also fail if the hostnames are invalid. locksite($oldhostname); assert_siteexists($oldhostname, 1); locksite($newhostname); assert_siteexists($newhostname, 0); # The adminemail must be changed when the admin is changed. # If it's not valid, use the default value. if (exists $options{adminemail} && ! isemail($options{adminemail})) { $options{adminemail}=$config{adminemail}; } if (! exists $options{adminemail} && $adminchanged) { $options{adminemail}=$config{adminemail}; } eval q{use File::Temp}; my ($tempfh, $tempfile) = File::Temp::tempfile(UNLINK => 1); backup($oldhostname, filename => $tempfile); restore($newhostname, filename => $tempfile, "no-setup" => 1, # when admin is changed, do not copy ssh keys from # oldhostname "no-sshkeys" => ($adminchanged ? 1 : 0)); # Enable DNS early, to help give it time to propigate. enabledns($newhostname); my $vcs=getsetup($oldhostname, "rcs"); my $vcswrapper; # XXX this duplicates code from IkiWiki::Setup::Automator if ($vcs eq 'git') { $vcswrapper=repository($newhostname)."/hooks/post-update"; } else { error "branch not implemented for $vcs"; } # The srcdir may be inside a subdirectory of the # repo. If so, this should be preserved when branching. my $oldsrcdir=srcdir($oldhostname); my $oldhomedir=homedir($oldhostname); $oldsrcdir=~s/^\Q$oldhomedir\E\//\//; my $srcdir=homedir($newhostname).$oldsrcdir; my $historyurl=getsetup($oldhostname, "historyurl"); $historyurl=~s/source\.\Q$oldhostname\E/source.$newhostname/i if defined $historyurl; my $diffurl=getsetup($oldhostname, "diffurl"); $diffurl=~s/source\.\Q$oldhostname\E/source.$newhostname/i if defined $diffurl; my @settings; push @settings, set => [ "wikiname" ."=". (exists $options{wikiname} ? $options{wikiname} : wikiname($newhostname)), "srcdir" ."=". $srcdir, "destdir" ."=". destdir($newhostname), "url" ."=". "http://$newhostname", "cgiurl" ."=". "http://$newhostname/ikiwiki.cgi", "openid_cgiurl" ."=". "http://$newhostname/ikiwiki.cgi", "cgi_wrapper" ."=". cgidir($newhostname)."/ikiwiki.cgi", "libdir" ."=". homedir($newhostname)."/.ikiwiki", $vcs."_wrapper" ."=". $vcswrapper, (defined $historyurl ? "historyurl" ."=". $historyurl : ()), (defined $diffurl ? "diffurl" ."=". $diffurl : ()), (exists $options{adminemail} ? "adminemail=$options{adminemail}" : ()), (defined $owner ? "owner=$owner" : ()), "created" ."=". time, "hostname" ."=". $newhostname, "parent" ."=". $oldhostname, ]; eval q{use YAML::Syck}; die $@ if $@; push @settings, 'set-yaml' => [ "urlalias=".Dump([]), ($options{admin} ? "adminuser=".Dump($options{admin}) : ()), "ENV=".Dump({TMPDIR => homedir($newhostname)."/tmp"}), ]; # avoid git pushes from branch unless explicitly turned back on push @settings, 'disable-plugin' => ['gitpush']; changesetup($newhostname, rebuild => 1, commit => 1, message => "branched $newhostname from $oldhostname", @settings, ); if ($options{admin}) { # TODO modify .ikiwiki/userdb to add new admin, if not an openid? } enable($newhostname); accountinglog("create", $newhostname, defined $owner ? $owner : getsetup($newhostname, "owner")); if ($options{createnonce}) { # If IKISITE_NONCE was set to a non-dummy nonce # initially, create a nonce with that value. if (defined $nonce && $nonce ne 'dummy') { return createnonce($newhostname, value => $nonce); } else { return createnonce($newhostname); } } else { return 1; } } sub meta_rename { required => [qw{oldhostname newhostname}], options => [qw{}], description => "rename a site to use a different hostname", section => "primary", } sub rename { my $oldhostname=lc(shift); my $newhostname=lc(shift); assert_root(); locksite($oldhostname); assert_siteexists($oldhostname, 1); locksite($newhostname); assert_siteexists($newhostname, 0); assert_wrapper_denied(); branch($oldhostname, $newhostname); IkiWiki::Hosting::delete($oldhostname); # silly perl, not the builtin delete.. return 1; } sub meta_compact { required => [qw{hostname}], options => [qw{aggressive!}], description => "log rotation and vcs cleanup for a site", section => "primary", } sub compact { my $hostname=lc(shift); my %options=@_; assert_root(); locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_denied(); # log rotation my $log_period=getsetup($hostname, "log_period") || 7; chdir(logdir($hostname)) || error "chdir logs: $!"; shell("savelog", "-p", "-n", "-t", "-c", $log_period, "ikisite.log", "error.log", "access.log"); # Note that apache still has logs open, but this is ok since # the logs are only renamed to .0, so it can still write to them. # So, apache is not HUP'd -- but it would be a good idea to # HUP apache after running a compact cron job. # VCS optimisations if (getsetup($hostname, "rcs") eq "git") { runas(username($hostname), sub { foreach my $repo (repository($hostname), srcdir($hostname)) { chdir($repo) || error "chdir $repo: $!"; shell("git", "gc", $options{aggressive} ? "--aggressive" : ()); } }); } return 1; } sub meta_list { required => [qw{}], options => [qw{admin=s ... owner=s ... extended! aggregatedue!}], description => "lists all sites that exist on the current host, or match a condition", section => "query", } sub list { my %options=@_; # Root is needed to read setup files in some circumstances. assert_root(); # All inputs (and outputs) are safe, so this can be run from # a suid wrapper. assert_wrapper_unsafe(); my @list; my %seen; setpwent(); while (my $user=(getpwent())[0]) { my $hostname=userlookup(user => $user); next unless defined $hostname && length $hostname; $seen{$hostname}=1 if $options{extended}; my $owner; my %admins; if ($options{extended} || $options{owner} || $options{admin}) { # avoid list crashing if setup file is broken eval { $owner=getsetup($hostname, "owner") }; next if $@; %admins=map { $_ => 1 } @{getsetup($hostname, "adminuser")}; } my $isowner=defined $options{owner} && grep { $_ eq $owner } @{$options{owner}}; next if defined $options{owner} && ! $isowner && ! defined $options{admin}; my $isadmin=defined $options{admin} && grep { $admins{$_} } @{$options{admin}}; next if defined $options{admin} && ! $isowner && ! $isadmin; if ($options{aggregatedue}) { my $timestamp=eval { readfile(srcdir($hostname)."/.ikiwiki/aggregatetime") }; next unless defined $timestamp && length $timestamp && $timestamp <= time(); } if (! $options{extended}) { push @list, $hostname; } else { # Extended mode includes the information the # controlpanel module needs, as well as some # information needed by other programs. my @admins=@{getsetup($hostname, "adminuser")}; my $unfinished=0; # unfinished site will have dummy admin if (@admins && ! grep { $_ ne "http://none/" } @admins) { $unfinished=1; } push @list, { isowner => $isowner, isadmin => $isadmin, site_owner => $owner, site_hostname => $hostname, site_url => getsetup($hostname, "url"), site_cgiurl => getsetup($hostname, "cgiurl"), site_wikiname => getsetup($hostname, "wikiname"), site_created => getsetup($hostname, "created"), site_parent => getsetup($hostname, "parent"), isunfinished => $unfinished, }; } } return @list unless $options{extended}; # Filter no longer existing parents. foreach my $siteinfo (@list) { my $parent=$siteinfo->{site_parent}; if (defined $parent && ! $seen{$parent}) { $siteinfo->{site_parent} = undef; } } return \@list; # complex data structure } sub meta_userlookup { required => [qw{}], options => [qw{user=s}], description => "returns the internal hostname corresponding to a unix username", section => "query", } { my %prefixes; sub userlookup { my %options=@_; assert_wrapper_unsafe(); if (! exists $options{user} || ! length $options{user}) { error "specify user to lookup with --user"; } if (! keys %prefixes) { foreach my $key (keys %config) { if ($key =~ /^prefix_(.*)/) { $prefixes{$1}=$config{$key}; } } } foreach my $prefix (keys %prefixes) { if ($options{user}=~/^\Q$prefix\E-(.*)/) { return "$1.$prefixes{$prefix}"; } elsif ($options{user}=~/^\Q$prefix\E5-(.*)/) { # md5'd username; look up real hostname # in setup file, and ensure it md5sums # to the same username. my $homedir=(getpwnam($options{user}))[7]; if (defined $homedir && -e "$homedir/ikiwiki.setup") { foreach my $url (urllist($options{user})) { my $host=$url->host; if ($host=~/^(([a-z0-9][-a-z0-9]+)\.)\Q$prefixes{$prefix}\E$/ && $options{user} eq username($host)) { return $url->host; } } } print STDERR "cannot determine hostname for user $options{user}\n"; } } return ""; } } sub meta_sitelookup { required => [qw{}], options => [qw{site=s}], description => "returns the internal hostname for an url or external hostname", section => "query", } sub sitelookup { my %options=@_; # Root is needed to read setup files in some circumstances. assert_root(); assert_wrapper_unsafe(); if (! exists $options{site} || ! length $options{site}) { error "specify site to lookup with --site"; } eval q{use URI}; my $url=URI->new($options{site}); my $host=$url->can("host") ? $url->host : $options{site}; if (eval { sitexists($host) } && ! $@) { return $host; } foreach my $hostname (list()) { foreach my $url (urllist(username($hostname))) { if (lc $url->host eq lc $host) { return $hostname; } } } error "$host not found"; } sub meta_siteexists { required => [qw{hostname}], options => [qw{}], description => "returns true if the hostname already exists", section => "query", } sub siteexists { my $hostname=lc(shift); my $user=username($hostname); # TODO Assumes one server. To handle multiple servers, will need to # query DNS or a database of sites. my $uid=getpwnam($user); return defined $uid ? IkiWiki::SuccessReason->new("$hostname exists") : IkiWiki::FailReason->new("$hostname does not exist (user $user not found)"); } sub meta_username { required => [qw{hostname}], options => [qw{}], description => "returns the unix username for a site", section => "query", } sub username { my $hostname=lc(shift); # paranoia: make sure the hostname looks like a FQDN if ($hostname !~ /^([a-z0-9][-a-z0-9]*)\.\w+(\.\w+)+$/) { error "illegal hostname \"$hostname\""; } if (length $hostname > 252) { # why not 253? Trailing dot.. error "hostname too long \"$hostname\""; } # since unix usernames are limited to 32 characters (see utmp(5)) # chop off the subdomain of the hostname, and use that as the # username my ($subdomain, $topdomain)=$hostname=~/^([a-z0-9][-a-z0-9]*)\.(.*)/; # add unique prefix to avoid collisions with system users and # other top domains my $prefix; foreach my $key (keys %config) { next unless defined $config{$key}; next unless $config{$key} eq $topdomain; $prefix=$key; $prefix=~s/^prefix_//; last; } if (! $prefix) { error "unknown prefix for $topdomain (configure in $config{ikisite_conffile})"; } my $username=$prefix.'-'.$subdomain; if (length($username) > 32) { # Fall back to using a hash for longer subdomains. eval q{use Digest::MD5}; die $@ if $@; my $md5=Digest::MD5::md5_hex($subdomain); $username=$prefix.'5-'.substr($md5, 0, 16).'-'. substr($subdomain, 0, 12); } return $username; } sub meta_homedir { required => [qw{hostname}], options => [qw{}], description => "returns the home directory for a site", section => "query", } sub homedir { my $hostname=lc(shift); return (getpwnam(username($hostname)))[7]; } sub meta_srcdir { required => [qw{hostname}], options => [qw{}], description => "returns the ikiwiki srcdir for a site", section => "query", } sub srcdir { my $hostname=shift; # Normally it is ~/srcdir, but the setup file can be modified # to only use a subdirectory of that. if (-e homedir($hostname)."/ikiwiki.setup") { return getsetup($hostname, "srcdir"); } else { return homedir($hostname)."/source"; } } sub meta_destdir { required => [qw{hostname}], options => [qw{}], description => "returns the ikiwiki destdir for a site", section => "query", } sub destdir { my $hostname=shift; return homedir($hostname)."/public_html"; } sub meta_cgidir { required => [qw{hostname}], options => [qw{}], description => "returns the CGI directory for a site", section => "query", } sub cgidir { my $hostname=shift; my $newly_created=shift; my $dir = "/var/www/".username($hostname); if ($newly_created || -d $dir) { return $dir; } return homedir($hostname)."/public_html"; } sub meta_logdir { required => [qw{hostname}], options => [qw{}], description => "returns the server log directory for a site", section => "query", } sub logdir { my $hostname=shift; my $newly_created=shift; my $dir = userlogdir(username($hostname)); if ($newly_created || -d $dir) { return $dir; } my $home=homedir($hostname); return unless defined $home; return $home."/logs"; } sub userlogdir { my $username=shift; return "/var/log/ikiwiki-hosting/".$username; } sub meta_rootconfig { required => [qw{hostname}], options => [qw{}], description => "returns the root configuration directory of the site", section => "query", } sub rootconfig { my $hostname=lc(shift); return $config{ikisite_rootconfigdir}."/".username($hostname); } sub meta_wikiname { required => [qw{hostname}], options => [qw{}], description => "calculates the default wikiname to use for a site", section => "query", } sub wikiname { my $hostname=shift; my ($wikiname)=$hostname=~/^([a-zA-Z0-9][-a-zA-Z0-9_]*)\./; return $wikiname; } sub meta_repository { required => [qw{hostname}], options => [qw{}], description => "returns the vcs repository for a site", section => "query", } sub repository { my $hostname=lc(shift); return homedir($hostname)."/source.".getsetup($hostname, "rcs"); } sub meta_getsetup { required => [qw{hostname key}], options => [qw{}], description => "returns a value from the site's ikiwiki.setup", section => "query", } my ($getsetup_cache_setup, $getsetup_homedir_old); # memoized for speed sub getsetup { my $hostname=shift; my $key=shift; my %options=@_; # The suid wrapper can only be used to access a few values. if ($key eq 'branchable' || $key eq 'adminuser') { assert_wrapper_unsafe(); } else { assert_wrapper_denied(); } my $homedir; # this option is used internally, when the hostname is not known if (exists $options{user}) { $homedir=(getpwnam($options{user}))[7]; } else { $hostname=lc($hostname); assert_siteexists($hostname, 1); $homedir=homedir($hostname); } if (! defined $getsetup_homedir_old || $homedir ne $getsetup_homedir_old) { $getsetup_cache_setup=loadsetup_safe($homedir."/ikiwiki.setup"); # Only keep cache if setup was successfully loaded. if (keys %$getsetup_cache_setup) { $getsetup_homedir_old=$homedir; } } return $getsetup_cache_setup->{$key}; } sub meta_changesetup { required => [qw{hostname}], options => [qw{set=key=value ... set-yaml=key=value ... enable-plugin=s ... disable-plugin=s ... rebuild! commit! message=s}], description => "changes settings in the site's ikiwiki.setup", section => "utility", } sub changesetup { my $hostname=lc(shift); my %options=@_; locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_safe($hostname); runas(username($hostname), sub { my $cgi_wrapper=getsetup($hostname, "cgi_wrapper"); my $srcdir=srcdir($hostname); my @settings; my $changedesc="changed "; foreach my $type (qw{set set-yaml}) { next unless $options{$type}; while (@{$options{$type}}) { my ($key, $value)=split("=", shift @{$options{$type}}, 2); if (! length $key) { error "no key specified"; } if (! defined $value) { error "no value specified for $key"; } $changedesc.="$key to '$value' ;"; if ($key eq "cgi_wrapper") { $cgi_wrapper=$value; } elsif ($key eq "srcdir") { $srcdir=$value; } push @settings, "--$type", "$key=$value"; } } if ($options{"enable-plugin"} || $options{"disable-plugin"}) { my @add_plugins=@{getsetup($hostname, "add_plugins")}; my @disable_plugins=@{getsetup($hostname, "disable_plugins")}; foreach my $plugin (@{$options{"enable-plugin"}}) { $changedesc.="$plugin (enabled) ;"; @disable_plugins=grep { $_ ne $plugin } @disable_plugins; push @add_plugins, $plugin unless grep { $_ eq $plugin } @add_plugins; } foreach my $plugin (@{$options{"disable-plugin"}}) { $changedesc.="$plugin (disabled) ;"; @add_plugins=grep { $_ ne $plugin } @add_plugins; push @disable_plugins, $plugin unless grep { $_ eq $plugin } @disable_plugins; } eval q{use YAML::Syck}; die $@ if $@; push @settings, "--set-yaml", "add_plugins=". Dump(\@add_plugins); push @settings, "--set-yaml", "disable_plugins=". Dump(\@disable_plugins); } $changedesc=~s/ ;$//; $changedesc=$options{message} if exists $options{message}; # There's a potential race with websetup; if a websetup # change is taking place at the same time, ikiwiki will # be running with the old configuration, and will write # a version of that back out to the setup file, overwriting # our changes. # # Simply calling lockwiki() will not help, because ikiwiki # could still start up, with the old configuration. # Instead, we need to prevent the ikiwiki cgi from running # (at least for websetup) while the setup file is being # changed. # # To accomplish that, let's first take the cgilock, # to ensure no cgi is currently running. Note that this assumes # that perl flock() really calls flock(2), which is currently # the case in Debian, but not guaranteed of all perls. $config{wikistatedir}="$srcdir/.ikiwiki"; if (-d $config{wikistatedir}) { open(CGILOCK, ">", "$config{wikistatedir}/cgilock") || error("$config{wikistatedir}/cgilock: $!"); flock(CGILOCK, 2) || error("flock: $!"); } # Now, replace the cgi wrapper with a program that waits # for the cgilock to free up, and then re-execs the cgi # wrapper, loading the new wrapper we will generate, # with the changed configuration. if (-e $cgi_wrapper) { link($cgi_wrapper, $cgi_wrapper.".old") || error("$cgi_wrapper.old: $!"); open(OUT, ">", $cgi_wrapper.".tmp") || error("$cgi_wrapper.tmp: $!"); print OUT qq{#!/usr/bin/perl # Temporary wrapper inserted by ikisite changesetup. open(LOCK, ">", "$config{wikistatedir}/cgilock") || die "error opening $config{wikistatedir}/cgilock"; flock(LOCK, 2) || die "flock error"; close LOCK; # When this is reached, the new wrapper has # replaced this program. exec(\$0, \@ARGV); die "exec: \$!"; }; close OUT || error("close: $!"); shell("chmod", "755", $cgi_wrapper.".tmp"); my $user=username($hostname); shell("chown", "$user:$user", $cgi_wrapper.".tmp"); CORE::rename($cgi_wrapper.".tmp", $cgi_wrapper) || error("rename: $!"); } my $setupfile=homedir($hostname)."/ikiwiki.setup"; my $oldsetup=readfile($setupfile); eval { shell("ikiwiki", "-setup", $setupfile, "-dumpsetup", $setupfile, @settings); ikiwikisetup($hostname, refresh => 1, wrappers => 1); }; if ($@) { # error unwind if (-e $cgi_wrapper.".old") { CORE::rename($cgi_wrapper.".old", $cgi_wrapper) || error("rename: $!"); } writefile($setupfile, "", $oldsetup); error $@; } unlink $cgi_wrapper.".old"; close CGILOCK; # Now any CGIs that were waiting to start up can run. if ($options{rebuild}) { ikiwikisetup($hostname); } if ($options{commit}) { commitsetup($hostname, message => $changedesc); } }); $getsetup_homedir_old=undef; # memoized setup values no longer valid return 1; } sub meta_domains { required => [qw{hostname}], options => [qw{external=s alias=s ...}], description => "configure a site's external domain name and aliases", section => "utility", } sub domains { my $hostname=lc(shift); my %options=@_; assert_root(); locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_safe($hostname); # This subcommand can be run from a suid wrapper so inputs # cannot be trusted; check and sanitize. my @urlalias; my @settings; my $rebuild=0; if (defined $options{external}) { if (isinternaldomain($options{external})) { error "$options{external} is a reserved hostname"; } # The external domain name does need to be configured # correctly in the DNS. my $address=host_address_or_cname($options{external}, $hostname); if (! defined $address) { print STDERR "DNS not correctly configured for $options{external}. (Should point to $hostname)\n"; exit 2; # special code } if ($address ne $hostname) { if (! grep { defined $_ && $_ eq $address } site_addresses()) { print STDERR "DNS not configured correctly for $options{external} - $address\n"; exit 3; # special code } else { print STDERR "warning: DNS for $options{external} hardcodes IP $address\n"; } } # It can't be a name used by another site. my $exists=eval{ sitelookup(site => $options{external}) }; if (! $@ && defined $exists && $exists ne $hostname) { error "$options{external} is in use by $exists"; } if (getsetup($hostname, "url") ne "http://$options{external}/") { push @settings, 'set' => [ "url=http://$options{external}", "cgiurl=http://$options{external}/ikiwiki.cgi", ]; $rebuild=1; } # When using an external domain, the internal hostname # must be listed as one of the urlaliases. push @urlalias, "http://$hostname/"; } else { push @settings, 'set' => [ "url=http://$hostname", "cgiurl=http://$hostname/ikiwiki.cgi", ]; } my %seen; foreach my $alias (@{$options{alias}}) { $alias=lc($alias); # Sanitize aliases to avoid path traversal, etc. if ($alias !~ /^[-.a-zA-Z0-9]+$/) { error "bad alias \"$alias\": illegal hostname"; } # Prevent aliases being set to (possibly unused) internal # site names. if ($alias ne $hostname && isinternaldomain($alias)) { error "bad alias \"$alias\": reserved hostname"; } # Aliases should not be in use by other sites. my $site=eval{ sitelookup(site => $alias) }; if (defined $site && !$@ && $site ne $hostname) { error "alias \"$alias\": already in use by $site"; } # Note that aliases do not have to be set in the DNS. # Eg, www.foo.com may be an alias but not set up yet, # and should still be accepted. push @urlalias, "http://$alias/" unless $seen{$alias}; $seen{$alias}=1; } eval q{use YAML::Syck}; die $@ if $@; push @settings, 'set-yaml' => [ "urlalias=".Dump(\@urlalias), ]; disable($hostname, temporary => 1); changesetup($hostname, rebuild => 0, # not yet; slow commit => 1, message => "configured domains", @settings, ); enable($hostname); if ($rebuild) { # A full site rebuild can take a long time for # large sites, so is postponed until after the site's # changed domain has been configured. # # Unfortunatly, a long rebuild also blocks other ikiwiki # cgis from running. That needs to be dealt with in # ikiwiki, not here. ikiwikisetup($hostname); } } sub meta_usercreate { required => [qw{hostname}], options => [qw{}], description => "creates the unix user for a site", section => "utility", } sub usercreate { my $hostname=lc(shift); assert_root(); my $lockfile=locksite($hostname); assert_wrapper_denied(); my $user=username($hostname); my @useradd_opts = ( "--base-dir", $config{useradd_basedir} ) if ($config{useradd_basedir}) ; shell("useradd", $user, "--create-home", "--skel", "/dev/null", # disable /etc/skel files @useradd_opts, ); mkdir(rootconfig($hostname)); my $uid=int getpwnam($user); my $gid=int getgrnam($user); # The user needs to own the site lock file, so ikisite # processes running as it can do locking. chown($uid, $gid, $lockfile) || error "chown $lockfile: $!"; # create skeleton my $home=homedir($hostname); mkdir("$home/.ssh") || error "mkdir $home/.ssh: $!"; mkdir("$home/tmp") || error "mkdir $home/tmp: $!"; foreach my $file (".ssh", "tmp") { chown($uid, $gid, "$home/$file") || error "chown $home/$file: $!"; } chmod(0700, "$home/tmp") || error "chmod $home/tmp: $!"; my $logdir=logdir($hostname, 1); mkdir($logdir) || error "mkdir $logdir: $!"; writefile("ikisite.log", $logdir, ""); foreach my $file (".", "ikisite.log") { chown(0, $gid, "$logdir/$file") || error "chown $logdir/$file: $!"; } chmod(0750, "$logdir") || error "chmod $logdir: $!"; my $cgidir=cgidir($hostname, 1); mkdir($cgidir) || error "mkdir $cgidir: $!"; chown($uid, $gid, "$cgidir") || error "chown $cgidir: $!"; chmod(0755, "$cgidir") || error "chmod $cgidir: $!"; # configure default username and email for git commits runas(username($hostname), sub { shell(qw{git config --global user.name admin}); shell(qw{git config --global user.email}, $config{adminemail}); chmod(0600, "$home/.gitconfig") || error "chmod $home/.gitconfig: $!"; }); } sub meta_userdelete { required => [qw{hostname}], options => [qw{}], description => "removes the unix user for a site", section => "utility", } sub userdelete { my $hostname=lc(shift); assert_root(); my $lockfile=locksite($hostname); assert_wrapper_denied(); shell("rm", "-rf", logdir($hostname)); shell("rm", "-rf", cgidir($hostname)); my $user=username($hostname); my $status=shell_exitcode("userdel", "-rf", $user); # userdel -f returns exit code 12 if it cannot delete the mail # spool, which typically will not exist. Work around this bug. if ($status != 0 && ($status >> 8) != 12) { error("userdel failed"); } shell("rm", "-rf", rootconfig($hostname)); unlink($lockfile); } sub meta_enable { required => [qw{hostname}], options => [qw{}], description => "enables user access to a site", section => "utility", } sub enable { my $hostname=lc(shift); assert_root(); locksite($hostname); assert_wrapper_denied(); my $user=username($hostname); my $home=homedir($hostname); my @hostnames=map { $_->host } urllist($user); # Lock down the public_html directory so only the site user and # www-data can access it. my $uid=int getpwnam($user); my $www_data_gid=int getgrnam("www-data"); chown($uid, $www_data_gid, "$home/public_html") || error "chown $home/public_html: $!"; chmod(0750, "$home/public_html") || error "chmod $home/public_html: $!"; my $vcs=getsetup($hostname, "rcs"); if ($vcs eq "git") { # Setup git-daemon. foreach my $h (@hostnames) { my $dest="$config{gitdaemondir}/$h.git"; unlink($dest); # The symlink is always made, but git-daemon will only # serve the site if its repository contains # git-daemon-export-ok. symlink(repository($hostname), $dest); } # Allow git-daemon to read and write to the repo via ACL. readconfig(); eval { shell("setfacl", "-R", "-m", "d:g:$config{gitdaemonuser}:rwX,d:g:$user:rwX,g:$config{gitdaemonuser}:rwX,g:$user:rwX", "$home/source.git") }; if ($@) { print STDERR "warning: setfacl failed, anonpush will not work (perhaps the filesystem is not mounted with option 'acl'?)\n"; } # Create file if site currently allows branching. # The branchable plugin will also create/remove it # as needed when the setting is changed. my $branchable=getsetup($hostname, "branchable"); my $flagfile=repository($hostname)."/git-daemon-export-ok"; if ($branchable) { open(FLAG, ">", $flagfile); close FLAG; shell("chown", "$user:$user", $flagfile); chmod(02755, "$home/source.git") || error "chmod $home/source.git: $!"; } else { unlink($flagfile); chmod(02750, "$home/source.git") || error "chmod $home/source.git: $!"; } # Setup gitweb. # An environment variable to points gitweb to # this file. We make it a symlink pointing at the real # file, and only enable the link for branchable sites. my $gitweb_conf_file=srcdir($hostname)."/.ikiwiki/gitweb.conf"; # To avoid privilege escalation via gitweb.conf, we run gitweb # via a suexec'able CGI script. my $cgidir=cgidir($hostname); my $gitweb = "$cgidir/gitweb.cgi"; my $uid=int getpwnam($user); my $gid=int getgrnam($user); open(GITWEB, ">", "$gitweb") || die "open $gitweb: $!"; print GITWEB "#!/bin/sh\n"; print GITWEB "GITWEB_CONFIG=$gitweb_conf_file\n"; print GITWEB "export GITWEB_CONFIG\n"; print GITWEB "exec /usr/lib/cgi-bin/gitweb.cgi \"\$@\"\n"; close GITWEB; chown($uid, $gid, $gitweb) || error "chown $gitweb: $!"; chmod(0755, $gitweb) || error "chmod $gitweb: $!"; my $gitweb_projectslist=srcdir($hostname)."/.ikiwiki/gitweb-projectslist"; my $git_description=repository($hostname)."/description"; outtemplate("$gitweb_conf_file.real", "gitweb.conf.tmpl", user => $user, home => $home, destdir => destdir($hostname), cgidir => cgidir($hostname), logdir => logdir($hostname), hostname => $hostname, projectslist => $gitweb_projectslist, ); shell("chown", "$user:$user", "$gitweb_conf_file.real"); if ($branchable) { unlink($gitweb_conf_file); # force refresh symlink symlink("$gitweb_conf_file.real", $gitweb_conf_file); } else { unlink($gitweb_conf_file); } open(DESC, ">", $git_description) || die "$git_description: $!"; print DESC "source repository for $hostname\n"; close DESC; shell("chown", "$user:$user", $git_description); open(PLIST, ">", $gitweb_projectslist) || die "$gitweb_projectslist: $!"; print PLIST "source.git source\n"; close PLIST; shell("chown", "$user:$user", $gitweb_projectslist); } # Get the url from the setup file. The hostname in this url # may be different from the internal site hostname; configure # apache to serve the url's hostname. eval q{use URI}; error $@ if $@; # ensure url has a trailing slash (apache requires it) my $u=getsetup($hostname, "url")."/"; $u=~s://$:/:; my $url=URI->new($u); my $ssl_cert_file=rootconfig($hostname)."/ssl.crt"; my $ssl_key_file=rootconfig($hostname)."/ssl.key"; my $ssl_chain_file=rootconfig($hostname)."/ssl.chain"; # Check that any user provided key file is not password protected, # as that makes apache startup hang. (Also checks that it's valid.) if (-e $ssl_key_file) { if (shell_exitcode("openssl", "rsa", "-in" => $ssl_key_file, "-out" => $ssl_key_file, "-passin" => "pass:dummy", "-passout" => "pass:dummy") != 0) { $ssl_key_file=''; $ssl_cert_file=''; $ssl_chain_file=''; } } # Use wildcard cert if there is no site specific one, only # if the site is not using its own domain name. if (defined $config{wildcard_ssl_cert} && length $config{wildcard_ssl_cert} && defined $config{wildcard_ssl_key} && length $config{wildcard_ssl_key} && ! -e $ssl_cert_file && ! -e $ssl_key_file) { my @domains = split(' ', $config{domains}); my $wildcard_ok=1; foreach my $h (@hostnames) { my $w_ok=0; foreach my $d (@domains) { if ($h =~ /^.+\.\Q$d\E$/i || $h eq $d) { $w_ok=1; } } $wildcard_ok = $wildcard_ok && $w_ok; } if ($wildcard_ok) { $ssl_cert_file=$config{wildcard_ssl_cert}; $ssl_key_file=$config{wildcard_ssl_key}; if (defined $config{wildcard_ssl_chain} && length $config{wildcard_ssl_chain}) { $ssl_chain_file=$config{wildcard_ssl_chain}; } else { $ssl_chain_file=''; } } } my $ssl_enabled=-e $ssl_key_file && -e $ssl_cert_file; my @ssl_template_vars=( ssl_cert_file => $ssl_cert_file, ssl_key_file => $ssl_key_file, ssl_enabled => $ssl_enabled, ); if (-e $ssl_chain_file) { push @ssl_template_vars, ( ssl_chain_file => $ssl_chain_file, ssl_chain => 1, ); } # This is the url that alias urls redirect to. my $redirurl=$url; if ($ssl_enabled) { $redirurl->scheme("https"); } # generate apache config file my @apache_template_vars=( suexec => (cgidir($hostname) =~ m!^/var/www!), user => $user, hostname => $url->host, home => homedir($hostname), destdir => destdir($hostname), cgidir => cgidir($hostname), logdir => logdir($hostname), source_hostname => "source.$hostname", @ssl_template_vars ); # If an apache.conf.tmpl is available, # it will be added into the apache config file. my $apache_conf_tmpl=""; if (-f rootconfig($hostname)."/apache.conf.tmpl") { my @bits=stat(_); if ($bits[4] == 0 && $bits[5] == 0) { require HTML::Template; my $template=HTML::Template->new( filename => rootconfig($hostname)."/apache.conf.tmpl", die_on_bad_params => 0, ); $template->param(@apache_template_vars); $apache_conf_tmpl=$template->output; } else { print STDERR "warning: ignoring apache.conf.tmpl; not owned by root\n"; } } # write and enable apache config file my $apache_site="ikisite-".$url->host; my $apache_conf_file="/etc/apache2/sites-available/$apache_site"; outtemplate($apache_conf_file, "apache-site.tmpl", @apache_template_vars, apache_conf_tmpl => $apache_conf_tmpl, ); my %setup; $setup{$url->host}=1; shell("a2ensite", $apache_site); # generate apache config files for alias urls, that redirect to the # main url foreach my $alias (@hostnames) { next if $setup{$alias}; $setup{$alias}=1; $apache_site="ikisite-$alias"; $apache_conf_file="/etc/apache2/sites-available/$apache_site"; outtemplate($apache_conf_file, "apache-sitealias.tmpl", suexec => (cgidir($hostname) =~ m!^/var/www!), user => $user, hostname => $url->host, home => homedir($hostname), destdir => destdir($hostname), cgidir => cgidir($hostname), logdir => logdir($hostname), alias => $alias, # Value escaped to prevent leakage # into RewriteEngine regexp. url_escaped => quotemeta($redirurl), @ssl_template_vars ); shell("a2ensite", $apache_site); } # reload apache config eval { shell("apache2ctl", "graceful") }; if ($@) { # avoid leaving apache in a broken state foreach my $site (keys %setup) { shell("a2dissite", $site); } shell("apache2ctl", "graceful"); error "apache2ctl graceful failed"; } enabledns($hostname); return 1; } sub meta_disable { required => [qw{hostname}], options => [qw{temporary!}], description => "disables user access to a site, without removing it", section => "utility", } sub disable { my $hostname=lc(shift); my %options=@_; assert_root(); locksite($hostname); assert_wrapper_denied(); disabledns($hostname) unless $options{temporary}; my @urls=urllist(username($hostname)); my $vcs=getsetup($hostname, "rcs"); if ($vcs eq "git") { foreach my $h (map { $_->host } @urls) { unlink("$config{gitdaemondir}/$h.git"); } } # remove apache config file(s) my $reload=0; foreach my $url (@urls) { my $apache_site="ikisite-".$url->host; my $apache_conf_file="/etc/apache2/sites-available/$apache_site"; if (-e $apache_conf_file) { # inside guard because a2dissite fails if the config # file does not exist, and this needs to be idempotent shell("a2dissite", $apache_site); unlink($apache_conf_file); $reload=1; } } shell("apache2ctl", "graceful") if $reload && ! $options{temporary}; return 1; } sub meta_enabledns { required => [qw{hostname}], options => [qw{}], description => "adds a hostname to DNS", section => "utility", } sub enabledns { my $hostname=lc(shift); assert_root(); # Enabling dns is a fairly safe operation, and sites need to be # able to call it when ipv6_disabled is changed. assert_wrapper_unsafe(); locksite($hostname); if ($config{allow_ipv4} || $config{allow_ipv6}) { my @commands; # A site's setup file can be used to disable ipv6. # But, the setup file may not yet exist, if an early dns # setup is being done. In that case, ipv6 is assumed not # to be disabled, and enabledns is expected to be run # a second time once the setup file is in place, and # can disable it then. my $ipv6_disabled = eval { getsetup($hostname, "ipv6_disabled") }; my ($ipv4, $ipv6)=site_addresses(); foreach my $h ($hostname, "source.$hostname") { if ($ipv6_disabled || ! defined $ipv6 || ! $config{allow_ipv6}) { push @commands, "update delete $h AAAA"; } else { push @commands, "update add $h $config{ttl} IN AAAA $ipv6"; } push @commands, "update add $h $config{ttl} IN A $ipv4" if defined $ipv4 && $config{allow_ipv4}; } if (! @commands) { error("failed to determine IP address"); } nsupdate(@commands); } } sub meta_disabledns { required => [qw{hostname}], options => [qw{}], description => "removes a hostname from DNS", section => "utility", } sub disabledns { my $hostname=lc(shift); assert_root(); locksite($hostname); assert_wrapper_denied(); if ($config{allow_ipv4} || $config{allow_ipv6}) { nsupdate( "update delete $hostname", "update delete source.$hostname", ); } } sub meta_ikiwikisetup { required => [qw{hostname}], options => [qw{refresh! wrappers!}], description => "calls ikiwiki to setup a site", section => "utility", } sub ikiwikisetup { my $hostname=lc(shift); my %options=@_; locksite($hostname); assert_wrapper_denied(); runas(username($hostname), sub { my $setupfile=homedir($hostname)."/ikiwiki.setup"; shell("ikiwiki", "-setup", $setupfile, ($options{refresh} ? "-refresh" : ()), ($options{wrappers} ? "-wrappers" : ())); }); } sub meta_ikiwikiclean { required => [qw{hostname}], options => [qw{}], description => "removes ikiwiki generated files", section => "utility", } sub ikiwikiclean { my $hostname=lc(shift); locksite($hostname); assert_wrapper_denied(); remove_wikilist($hostname); runas(username($hostname), sub { my $setupfile=homedir($hostname)."/ikiwiki.setup"; shell("ikiwiki", "-setup", $setupfile, "-clean"); return 1; }); } sub meta_createsetupbranch { required => [qw{hostname}], options => [qw{}], description => "creates \"setup\" branch in VCS", section => "utility", } sub createsetupbranch { my $hostname=lc(shift); locksite($hostname); assert_wrapper_denied(); runas(username($hostname), sub { chdir(homedir($hostname)) || error "chdir HOME: $!"; my $vcs=getsetup($hostname, "rcs"); my @ignores=( ".ikiwiki", "public_html", "source", "source.".$vcs, "logs", ".ikisite-nonce", ); if ($vcs eq "git") { push @ignores, ".gitconfig"; if (-d ".git") { error ".git directory already exists"; } shell("git", "init"); chmod(0700, ".git") || error "chmod .git: $!"; # convince git to start committing to setup branch writefile(".git/HEAD", ".", "ref: refs/heads/setup\n"); writefile(".gitignore", ".", join("\n", @ignores)."\n"); chmod(0600, ".gitignore") || error "chmod .gitignore: $!"; shell("git", "remote", "add", "origin", repository($hostname)); commitsetup($hostname, message => "initial commit of setup files"); } else { error "createsetupbranch not implemented for $vcs"; } return 1; }); } sub meta_commitsetup { required => [qw{hostname}], options => [qw{message=s}], description => "commits to \"setup\" branch in VCS", section => "utility", } sub commitsetup { my $hostname=lc(shift); my %options=@_; locksite($hostname); assert_wrapper_denied(); runas(username($hostname), sub { chdir(homedir($hostname)) || error "chdir HOME: $!"; my $vcs=getsetup($hostname, "rcs"); my @files=( "ikiwiki.setup", ".ssh/authorized_keys", "apache.conf.tmpl", ); my $message=$options{message} ? $options{message} : "commit of setup files"; if ($vcs eq "git") { push @files, ".gitignore"; foreach my $file (@files) { if (-e $file) { shell("git", "add", $file); } else { # the file may have never been added, # so --ignore-unmatch shell("git", "rm", "--ignore-unmatch", $file); } } my $changed=`git status --porcelain | grep -v '^\?\?'`; if (length $changed) { shell("git", "commit", "-m", $message); shell("git", "push", "origin", "setup"); } } else { error "commitsetup not implemented for $vcs"; } return 1; }); } sub meta_observe { required => [qw{hostname}], options => [qw{force!}], description => "informs observersites about site creation or removal", section => "utility", } sub observe { my $hostname=lc(shift); my %options=@_; assert_wrapper_denied(); readconfig(); return unless defined $config{observersites}; my $exists=siteexists($hostname); foreach my $observer (split(' ', $config{observersites})) { next if $observer eq $hostname; next unless siteexists($observer); runas(username($observer), sub { # Load observer site's ikiwiki setup file, # and plugins, so rcs_* functions can be used. require IkiWiki::Setup; %config=(%config, IkiWiki::defaultconfig()); # Note use of safe mode to avoid running perl # format setup files. IkiWiki::Setup::load(homedir($observer)."/ikiwiki.setup", 1); IkiWiki::loadplugins(); IkiWiki::checkconfig(); chdir(srcdir($observer)); my $templatefile="templates/observersite.tmpl"; return unless -e $templatefile; my $e="existing/$hostname"; my $r="removed/$hostname"; my $page=$exists ? $e : $r; my $other=$exists ? $r : $e; my $staged=0; # Handle moving page if it already existed. if (-e $other.".mdwn") { IkiWiki::rcs_rename($other.".mdwn", $page.".mdwn"); $staged=1; # reset timestamp system("touch", $page.".mdwn"); # just in case $other was not checked into # git, remove it unlink($other.".mdwn"); } # Handle moving any subpages. if (-d $other) { IkiWiki::rcs_rename($other, $page); $staged=1; } # Ensure page exists, but avoid destructive # overwrites, unless in force mode. if (! -e $page.".mdwn" || $options{force}) { eval q{use HTML::Template}; error $@ if $@; my $template=HTML::Template->new( filename => $templatefile, ); $template->param(hostname => $hostname); writefile($page.".mdwn", srcdir($observer), $template->output); IkiWiki::rcs_add($page.".mdwn"); $staged=1; } if ($staged) { IkiWiki::rcs_commit_staged( message => "automatic update", ); } }); } } sub meta_updatecustomersite { required => [qw{hostname}], options => [], description => "updates customersite checkout", section => "utility", } sub updatecustomersite { my $hostname=lc(shift); locksite($hostname); assert_wrapper_unsafe(); runas(username($hostname), sub { eval q{use IkiWiki::Customer}; if (! defined IkiWiki::Customer::basedir(1)) { error "no customersite checked out; cannot update"; } if (! IkiWiki::Customer::update()) { error "update failed"; } return 1; }); } sub meta_add_wikilist { required => [qw{hostname}], options => [], description => "adds hostname's user to /etc/ikiwiki/wikilist", section => "utility", } sub add_wikilist { my $hostname=lc(shift); ikiwiki_update_wikilist($hostname, 0); } sub meta_remove_wikilist { required => [qw{hostname}], options => [], description => "removes hostname's user to /etc/ikiwiki/wikilist", section => "utility", } sub remove_wikilist { my $hostname=lc(shift); ikiwiki_update_wikilist($hostname, 1); } sub ikiwiki_update_wikilist { my $hostname=lc(shift); my $remove=shift; assert_root(); assert_siteexists($hostname, 1); my $username=username($hostname); my $wikilist="/etc/ikiwiki/wikilist"; if (! -e $wikilist) { print "$wikilist does not exist\n"; return; } my $changed=0; my $seen=0; my @lines; open (my $list, "<$wikilist") || die "read $wikilist: $!"; while (<$list>) { chomp; if (/^\s*([^\s]+)\s*$/) { my $user=$1; if ($user eq $username) { if (! $remove) { $seen=1; push @lines, $_; } else { $changed=1; } } else { push @lines, $_; } } else { push @lines, $_; } } if (! $seen && ! $remove) { push @lines, $username; $changed=1; } if ($changed) { close $list || die "error reading $list: $!\n"; open ($list, ">$wikilist") || die "cannot write to $wikilist\n"; foreach (@lines) { print $list "$_\n"; } close $list || die "error writing $wikilist: $!\n"; } return 1; } sub meta_sshkeys { required => [qw{hostname}], options => [qw{}], description => "lists enabled ssh keys", section => "query", } sub sshkeys { my $hostname=lc(shift); assert_siteexists($hostname, 1); assert_wrapper_denied(); my @ret; my $user=username($hostname); my $sshdir=homedir($hostname)."/.ssh"; my $authorized_keys=$sshdir."/authorized_keys"; if (! -e $authorized_keys) { return (); } open(IN, "<", $authorized_keys) || error("open $authorized_keys: $!"); while () { chomp; my $k=parse_sshkey($_); if (defined $k) { push @ret, $k; } } close IN; return @ret; } sub meta_enablesshkey { required => [qw{hostname}], options => [qw{key=s}], description => "adds the specified ssh key to .ssh/authorized_keys", section => "utility", } sub enablesshkey { my $hostname=lc(shift); my %options=@_; locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_denied(); if (! exists $options{key}) { error("must specify a key with --key="); } my $key=parse_sshkey($options{key}); if (! defined $key) { error "invalid or unsupported ssh key"; } my $user=username($hostname); my $sshdir=homedir($hostname)."/.ssh"; my $authorized_keys=$sshdir."/authorized_keys"; if (! -d $sshdir) { shell("mkdir", "-p", $sshdir); shell("chown", "$user:$user", $sshdir); } # regenerate file, adding key if it is not already present my %seen; $seen{$key}=1; if (-e $authorized_keys) { open(IN, "<", $authorized_keys) || error("open $authorized_keys: $!"); while () { chomp; my $k=parse_sshkey($_); $seen{$k}=1 if defined $k; } close IN; } my $vcs=getsetup($hostname, "rcs"); open(OUT, ">", $authorized_keys.".tmp") || error("open: $authorized_keys.tmp: $!"); foreach my $key (sort keys %seen) { print OUT sshkey_line($key, $vcs); } close OUT || error("close: $!"); shell("chmod", "400", $authorized_keys.".tmp"); shell("chown", "$user:$user", $authorized_keys.".tmp"); CORE::rename($authorized_keys.".tmp", $authorized_keys) || error("rename; $!"); return 1; } sub meta_disablesshkey { required => [qw{hostname}], options => [qw{key=s all}], description => "removes the specified ssh key (or all keys)", section => "utility", } sub disablesshkey { my $hostname=lc(shift); my %options=@_; locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_denied(); my $key; if (exists $options{all}) { $key=undef; } elsif (exists $options{key}) { $key=parse_sshkey($options{key}); } else { error("must specify --all or a key with --key="); } my $user=username($hostname); my $sshdir=homedir($hostname)."/.ssh"; my $authorized_keys=$sshdir."/authorized_keys"; return 1 if ! -e $authorized_keys; if (! -d $sshdir) { shell("mkdir", "-p", $sshdir); shell("chown", "$user:$user", $sshdir); } # regenerate file, removing matching keys open(IN, "<", $authorized_keys) || error("open $authorized_keys: $!"); open(OUT, ">", $authorized_keys.".tmp") || error("open: $authorized_keys.tmp: $!"); my $vcs=getsetup($hostname, "rcs"); while () { chomp; my $k=parse_sshkey($_); if (defined $k && defined $key && $k ne $key) { print OUT sshkey_line($k, $vcs); } } close IN; close OUT || error("close: $!"); shell("chmod", "400", $authorized_keys.".tmp"); shell("chown", "$user:$user", $authorized_keys.".tmp"); CORE::rename($authorized_keys.".tmp", $authorized_keys) || error("rename; $!"); return 1; } sub meta_calendar { required => [qw{hostname}], options => [qw{force! startyear=s endyear=s}], description => "runs ikiwiki-calendar if the site has the calendar plugin enabled", section => "utility", } sub calendar { my $hostname=lc(shift); my %options=@_; locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_denied(); if ($options{endyear} && ! $options{startyear}) { error "cannot specify endyear without startyear"; } if (plugin_enabled($hostname, "calendar") && defined getsetup($hostname, "archivebase") && length getsetup($hostname, "archivebase")) { runas(username($hostname), sub { shell("ikiwiki-calendar", ($options{force} ? "-f" : ()), homedir($hostname)."/ikiwiki.setup", ($options{startyear} ? $options{startyear} : ()), ($options{endyear} ? $options{endyear} : ()), ); }); } } sub meta_checksetup { required => [qw{setupref}], options => [qw{}], description => "checks that the specified ref contains only safe changes to the setup branch", section => "utility", } sub checksetup { my $setupref=shift; my $hostname=userlookup(user => getpwuid($<)); if (! defined $hostname) { error "unable to determine internal hostname"; } # Make a temporary checkout of the ref. my $homedir=homedir($hostname); eval q{use File::Temp}; error $@ if $@; my $tmpcheckout=File::Temp::tempdir(CLEANUP => 1, DIR => "$homedir/tmp"); my $newsetup="$tmpcheckout/ikiwiki.setup"; my $vcs=getsetup($hostname, "rcs"); if ($vcs eq "git") { # doing a shared clone makes the setupref, which has # not landed on any branch, be available for checkout shell("git", "clone", "--quiet", "--shared", "--no-checkout", repository($hostname), $tmpcheckout); chdir($tmpcheckout) || error "chdir $tmpcheckout: $!"; shell("git", "checkout", "--quiet", $setupref, "-b", "setup"); } else { error "checksetup not implemented for $vcs"; } # Only allow changes to ikiwiki.setup. my @files=map { chomp; $_ } `git diff --name-only remotes/origin/setup...`; chdir($homedir) || error "chdir $tmpcheckout: $!"; if (grep { $_ ne "ikiwiki.setup" } @files) { error "rejecting change to setup branch: modification of files other than ikiwiki.setup is not allowed"; } # Make sure that ikiwiki.setup didn't turn into a symlink or other # special file, and was not removed. if (-l $newsetup || ! -f $newsetup || ! -e $newsetup) { error "rejecting change to setup branch: ikiwiki.setup must remain a regular file"; } # paranoia if (-x $newsetup || -X $newsetup) { error "rejecting change to setup branch: ikiwiki.setup cannot be executable"; } # Load new and current setup file into temporary hashes. my $config_new=eval { loadsetup_safe($newsetup) }; if ($@ || ! defined $config_new) { error "rejecting change to setup branch: failed to parse ikiwiki.setup as YAML formatted file (check syntax)\n$@"; } my $config_old=loadsetup_safe($homedir."/ikiwiki.setup"); # Compare to see which settings changed. eval q{use Data::Compare}; error $@ if $@; my %changed; my %changed_plugins; foreach my $key (keys %$config_new, keys %$config_old) { if (! Compare($config_new->{$key}, $config_old->{$key})) { if ($key eq 'add_plugins' || $key eq 'disable_plugins') { my %old=map { $_ => 1 } @{$config_old->{$key}}; my %new=map { $_ => 1 } @{$config_new->{$key}}; foreach my $k (keys %new, keys %old) { if (! Compare($new{$k}, $old{$k})) { $changed_plugins{$k}=1; } } } else { $changed{$key}=1; } } } # Check that each setting that changed is one that ikiwiki # considers safe. Take into account any overrides configured # into the websetup plugin. # Also, see if a full rebuild is called for by the changes made. my $rebuild_needed=0; eval q{use IkiWiki::Setup}; error $@ if $@; # bootstrap enough of ikiwiki to load plugins my %oldconfig=%config; %config=(%config,IkiWiki::defaultconfig(), %$config_old); IkiWiki::checkconfig(); my @setup=([main => [IkiWiki::getsetup()]], IkiWiki::Setup::getsetup()); %config=%oldconfig; my @unsafe_changes; my $websetup_unsafe=getsetup($hostname, "websetup_unsafe"); CHANGE: foreach my $setting (keys %changed) { if (! ref $websetup_unsafe || ! grep { $_ eq $setting } @$websetup_unsafe) { foreach my $pair (@setup) { my %setup=@{$pair->[1]}; foreach my $key (keys %setup) { if ($key eq $setting) { $rebuild_needed=1 unless Compare($setup{$key}->{rebuild}, 0); next CHANGE if $setup{$key}->{safe}; } } } } push @unsafe_changes, $setting; } if (@unsafe_changes) { print STDERR "the following settings cannot be changed:\n"; print STDERR "\t$_\n" foreach @unsafe_changes; } my @unsafe_plugin_changes; my $websetup_force_plugins=getsetup($hostname, "websetup_force_plugins"); PCHANGE: foreach my $p (keys %changed_plugins) { if (! ref $websetup_force_plugins || ! grep { $_ eq $p } @$websetup_force_plugins) { foreach my $pair (@setup) { next if $pair->[0] ne $p; my %setup=@{$pair->[1]}; $rebuild_needed=1 unless Compare($setup{plugin}->{rebuild}, 0); next PCHANGE if $setup{plugin}->{safe}; } } push @unsafe_plugin_changes, $p; } if (@unsafe_plugin_changes) { print STDERR "the following plugins cannot be enabled/disabled:\n"; print STDERR "\t$_\n" foreach @unsafe_plugin_changes; } error "rejecting change to setup branch" if @unsafe_changes || @unsafe_plugin_changes; if (%changed || %changed_plugins) { # Check out setup file in toplevel. This is slightly tricky # as the commit has not landed in the bare git repo yet -- # but it is available in the tmpcheckout. shell("git", "pull", "-q", $tmpcheckout, "setup"); # Refresh or rebuild site to reflect setup changes. print STDERR "Updating site to reflect setup changes...\n"; shell("ikiwiki", "-setup", "ikiwiki.setup", "-v", ($rebuild_needed ? ("-rebuild") : ("-refresh", "-wrappers")) ); } exit 0; } sub meta_sudo { required => [qw{hostname ...}], options => [], description => "runs sudo as site user (example: ikisite sudo \$SITE -- ls -l)", section => "utility", } sub sudo { my $hostname=lc(shift); assert_root(); # note: no locksite, as commands that themselves lock may be run inside the sudo assert_siteexists($hostname, 1); assert_wrapper_denied(); $ENV{HOME}=homedir($hostname); chdir($ENV{HOME}) || error "chdir: $!"; shell("sudo", "-u", username($hostname), (@_ ? @_ : ($ENV{SHELL} || "/bin/sh"))); return undef; # don't print return code } sub meta_analog { required => [qw{hostname ...}], options => [], description => "runs analog, generates report to stdout", section => "utility", } sub analog { my $hostname=lc(shift); assert_siteexists($hostname, 1); assert_wrapper_unsafe(); if (! $config{allow_analog_reports}) { assert_root(); } chdir(logdir($hostname)) || die "chdir: $!"; system("analog", "+CREFREPEXCLUDE http://${hostname}/*", "+a", "+f", "+D", glob("access.log*"), @_); return undef; # don't print return code } sub meta_logview { required => [qw{hostname ...}], options => [], description => "tail -f of all log files", section => "utility", } sub logview { my $hostname=lc(shift); assert_root(); assert_siteexists($hostname, 1); assert_wrapper_unsafe(); chdir(logdir($hostname)) || die "chdir: $!"; system("tail", "-f", glob("*.log"), @_); return undef; # don't print return code } # Run by iki-git-shell, don't trust inputs or stdio. sub meta_logs { required => [qw{}], options => [qw{dump tail}], description => "tails or dumps apache access.log for current user's site", section => "utility", } sub logs { my %options=@_; assert_wrapper_unsafe(); my $username=(getpwuid($<))[0]; if (! defined $username || ! length $username) { error "unable to determine username"; } chdir(userlogdir($username)) || die "chdir: $!"; if ($options{dump}) { foreach my $n (reverse(1..9)) { if (-e "access.log.$n.gz") { system("zcat", "access.log.$n.gz"); } } foreach my $l ("access.log.0", "access.log") { if (-e $l) { system("cat", $l); } } } else { print "Viewing logs.. press ctrl-c to exit.\n\n"; system("tail", "-f", "access.log"); } return undef; # don't print return code } sub meta_createnonce { required => [qw{hostname}], options => [qw{value=s}], description => "returns a nonce for ikisite-wrapper", section => "utility", } sub createnonce { my $hostname=shift; my %options=@_; locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_denied(); my $nonce=defined $options{value} ? $options{value} : time().':'.`uuid -v4`; chomp $nonce; my $noncefile=homedir($hostname)."/.ikisite-nonce"; open(NONCE, ">>", $noncefile) || die "$noncefile: $!"; my $username=username($hostname); shell("chown", "$username:$username", $noncefile); shell("chmod", "600", $noncefile); print NONCE "$nonce\n"; close NONCE; return $nonce; } sub meta_deletenonce { required => [qw{hostname}], options => [qw{nonce=s}], description => "invalidates the specified nonce", section => "utility", } sub deletenonce { my $hostname=lc(shift); my %options=@_; assert_root(); locksite($hostname); assert_siteexists($hostname, 1); assert_wrapper_safe($hostname); if (! defined $options{nonce}) { error "--nonce required"; } my $noncefile=homedir($hostname)."/.ikisite-nonce"; open(NONCE, "<", $noncefile) || die "$noncefile: $!"; my %nonces; while () { chomp; $nonces{$_}=1; } close NONCE; delete $nonces{$options{nonce}}; open(NONCE, ">", $noncefile) || die "$noncefile: $!"; my $username=username($hostname); shell("chown", "$username:$username", $noncefile); shell("chmod", "600", $noncefile); print NONCE "$_\n" foreach keys %nonces; close NONCE; return 1; } sub meta_checksite { required => [qw{hostname}], options => [qw{wait hasnonce}], description => "checks if site is done being created", section => "query", } sub checksite { my $hostname=lc(shift); my %options=@_; assert_root(); assert_siteexists($hostname, 1); assert_wrapper_unsafe(); my $checknonce = sub { if ($options{hasnonce}) { my $noncefile=homedir($hostname)."/.ikisite-nonce"; if (! -e $noncefile) { return IkiWiki::SuccessReason->new("$hostname lacks nonce"); } } return IkiWiki::SuccessReason->new("$hostname is ready"); }; if ($options{wait}) { # blocking wait locksite($hostname); return $checknonce->(); } else { # nonblocking lock test my $lockfile="$config{lockdir}/".username($hostname); open(CHECKLOCK, '>', $lockfile) || error ("cannot write to $lockfile: $!"); if (! flock(CHECKLOCK, 2 | 4)) { # LOCK_EX | LOCK_NB close CHECKLOCK; return IkiWiki::FailReason->new("$hostname is locked"); } else { close CHECKLOCK; return $checknonce->(); } } } sub meta_upgrade { required => [qw{hostname}], options => [], description => "upgrade a site to new layout", section => "primary", } sub upgrade { my $hostname=lc(shift); assert_root(); assert_siteexists($hostname, 1); assert_wrapper_denied(); my $home=homedir($hostname); my @cleanup; my $user=username($hostname); my $uid=int getpwnam($user); my $gid=int getgrnam($user); # logs moved if (-d "$home/logs") { my $newlogdir=logdir($hostname, 1); if (! -d $newlogdir) { mkdir($newlogdir) || error "mkdir $newlogdir: $!"; chmod(0750, "$newlogdir") || error "chmod $newlogdir: $!"; } foreach my $file (<$home/logs/*>) { my $basefile=IkiWiki::basename($file); my $dest="$newlogdir/$basefile"; if (! -e $dest) { shell("mv", "-f", $file, $dest); } else { print STDERR "warning: $file not moved to $basefile, which already exists\n"; } } rmdir("$home/logs"); # Only ikisite log is writable by the site user's gid. foreach my $file (".", "ikisite.log") { chown(0, $gid, "$newlogdir/$file") || error "chown $newlogdir/$file: $!"; } } # cgi moved, and is no longer suid, as suidexec is used. my $oldcgi=$home."/public_html/ikiwiki.cgi"; if (-e $oldcgi) { push @cleanup, $oldcgi; my $cgidir=cgidir($hostname, 1); mkdir($cgidir) || error "mkdir $cgidir: $!"; chown($uid, $gid, "$cgidir") || error "chown $cgidir: $!"; chmod(0755, "$cgidir") || error "chmod $cgidir: $!"; eval q{use YAML::Syck}; die $@ if $@; readconfig(); changesetup($hostname, rebuild => 0, commit => 1, message => "ikisite upgrade", set => [ "cgi_wrapper" ."=". $cgidir."/ikiwiki.cgi", "cgi_wrappermode" ."=". "0755", ], # gitdaemonuser became an untrusted committer 'set-yaml' => [ "untrusted_committers=".Dump([$config{gitdaemonuser}]), ], ); changesetup($hostname, rebuild => 0, commit => 1, message => "ikisite upgrade", set => [ "cgi_wrapper" ."=". $cgidir."/ikiwiki.cgi", "cgi_wrappermode" ."=". "0755", ], ); } # File perms were locked down. foreach my $file ("ikiwiki.setup", ".gitconfig", ".gitignore") { if (-e "$home/$file") { chmod(0600, "$home/$file") || error "chmod $home/$file: $!"; } } # Dir perms were locked down. foreach my $dir (".git", "source") { if (-e "$home/$dir") { chmod(0700, "$home/$dir") || error "chmod $home/$dir: $!"; } } foreach my $dir ("apache") { if (-e "$home/$dir") { shell("chown", "$user:www-data", "$home/$dir"); chmod(0750, "$home/$dir") || error "chmod $home/$dir: $!"; } } # New rootconfig directory created. my $rootconfig=rootconfig($hostname); if (! -d $rootconfig) { mkdir($rootconfig) || error "mkdir $rootconfig: $!"; my $old=homedir($hostname)."/apache.conf.tmpl"; if (-e $old && ! -e "$rootconfig/apache.conf.tmpl") { # Safe because the file will only be used if owned # by root. shell("mv", "-f", $old, $rootconfig); } } # Disable and re-enable to write new apache configs, etc. disable($hostname, temporary => 1); enable($hostname); foreach my $file (@cleanup) { unlink($file) || error "unlink $file: $!"; } } ############################################################################ my %locks; sub locksite { my $hostname=shift; my $lockfile="$config{lockdir}/".username($hostname); if (! exists $locks{$hostname}) { open($locks{$hostname}, '>', $lockfile) || error ("cannot write to $lockfile: $!"); if (! flock($locks{$hostname}, 2)) { # LOCK_EX error("flock: $!"); } } return $lockfile; } sub urllist { my $username=shift; my @urls; my $urlalias=getsetup(undef, "urlalias", user => $username); if (ref $urlalias) { push @urls, @$urlalias; } push @urls, getsetup(undef, "url", user => $username); eval q{use URI}; error $@ if $@; return grep { $_->can("host") } map { URI->new($_) } @urls; } sub githooks { my $repository=shift; unlink("$repository/hooks/update"); symlink("/usr/bin/iki-git-hook-update", "$repository/hooks/update") || error("symlink: $!"); } sub parse_sshkey { my $line=shift; return undef if $line=~/^\s*#/; # Parse key out of line, which may be just the key, # or may be an authorized_keys line, and may have surrounding whitespace. # Retain one word of optional comment part. if ($line =~ /^(?:.* )?\s*(ssh-\w+\s+[A-Za-z0-9+\/=]+(?:\s+[^\s]*)?)\s*.*$/) { return $1; } else { return undef; } } sub sshkey_line { my $key=shift; my $vcs=shift; if ($vcs eq 'git') { return "command=\"iki-git-shell\",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty,no-user-rc $key\n"; } else { error("unsupported vcs $vcs"); } } sub assert_siteexists { my $hostname=shift; my $should=shift; my $exists=siteexists($hostname); if ($exists != $should) { if ($exists) { error "$hostname already exists"; } else { if (ref($exists) eq 'IkiWiki::FailReason') { error "$exists"; } else { error "$hostname does not exist"; } } } } sub assert_wrapper_denied { if (exists $ENV{IKISITE_NONCE}) { error "subcommand cannot be run via wrapper"; } } sub assert_wrapper_unsafe { my $hostname=shift; delete $ENV{IKISITE_NONCE}; } sub assert_wrapper_safe { my $hostname=shift; # IKISITE_NONCE, if set, must be equal to a line from the # .ikisite-nonce file. This allows suid wrappers to pass the # variable through to check that their caller knows a valid nonce # for the site. if (exists $ENV{IKISITE_NONCE}) { my $noncefile=homedir($hostname)."/.ikisite-nonce"; open(NONCE, "<", $noncefile) || die "$noncefile: $!"; while () { chomp; if ($_ eq $ENV{IKISITE_NONCE}) { close NONCE; delete $ENV{IKISITE_NONCE}; return 1; } } close NONCE; error "nonce check failure"; } } sub isemail { my $email=shift; return unless defined $email && length $email; # This is not intended to be a complete RFC compliant email address # check. It only makes sure the email contains only legal # characters. return $email =~ /[^-+@_.a-zA-Z0-9]/; } sub isinternaldomain { my $domain=shift; while (length $domain) { if (defined eval { username($domain) } && !$@) { return 1; } $domain=~s/^[^.]+(\.|$)//; # strip subdomains } return 0; } sub plugin_enabled { my $hostname=shift; my $plugin=shift; my @add_plugins=@{getsetup($hostname, "add_plugins")}; my @disable_plugins=@{getsetup($hostname, "disable_plugins")}; return 0 if grep { $_ eq $plugin } @disable_plugins; return 1 if grep { $_ eq $plugin } @add_plugins; return undef; # might be enabled by default or pulled in via plugin bundle } sub actionlog { my $message=shift; my $subcommand=shift; my $hostname=shift; # hostname is always the first required parameter return unless defined $hostname && length $hostname; my $username=eval { username($hostname) }; return unless defined $username; my $logdir=eval { logdir($hostname) }; return unless defined $logdir; my $localhost=eval q{use Sys::Hostname; hostname()} || "somehost"; open(LOG, ">>", "$logdir/ikisite.log") || return; print LOG gmtime()." $localhost: $subcommand: $message\n"; close LOG; } # used only for recording a few specific actions: # site creation and deletion sub accountinglog ($$;$) { my $action=shift; # "create" or "delete" my $hostname=shift; my $openid=shift; # only for create readconfig(); open(ACCOUNTING_LOG, ">>", $config{accountinglog}) || error "$config{accountinglog}: $!"; if ($action eq "create") { print ACCOUNTING_LOG gmtime(time)." create $hostname $openid\n"; } elsif ($action eq "delete") { print ACCOUNTING_LOG gmtime(time)." delete $hostname\n"; } else { error "accountinglog: unsupported action $action"; } close ACCOUNTING_LOG; # Inform observers as background job to avoid delaying eg, # site creation. my $pid=IkiWiki::Hosting::daemonize(); if ($pid) { return; } # Avoid keeping sites locked while daemonized. foreach my $site (keys %locks) { close($locks{$site}); } # Wait a minute to let anything that is being done settle. sleep(60); observe($hostname); exit; } sub nsupdate { # chdir done here because nsupdate expects the other key in the pwd chdir "$config{keydir}/dns" || error "chdir $config{keydir}/dns: $!"; my @privkey=glob("K**.private"); if (! @privkey) { error("cannot find private key in $config{keydir}/dns"); } # Any key in the directory is assumed to work, so just take the first. debug("nsupdate -k $privkey[0] <) { s{(\\?)\[\[([^\s\|\]]+)(\|[^\s\]]+)?\]\]}{$1 ? "[[$2]]" : $2}eg; s/\`//g; s/^\s*\./\\&./g; if (/^#\s/) { s/^#\s/.SH /; <>; # blank; } s/^[ \n]+//; s/^\t/ /; s/-/\\-/g; s/^Warning:.*//g; s/^$/.PP\n/; s/^\*\s+(.*)/.IP "$1"/; next if $_ eq ".PP\n" && $skippara; if (/^.IP /) { $inlist=1; $spippara=0; } elsif (/.SH/) { $skippara=0; $inlist=0; } elsif (/^\./) { $skippara=1; } else { $skippara=0; } if ($inlist && $_ eq ".PP\n") { $_=".IP\n"; } print $_; } ikiwiki-hosting/html/0000755000000000000000000000000012232567070012025 5ustar ikiwiki-hosting/ikidns0000755000000000000000000000757412211430070012267 0ustar #!/usr/bin/perl package IkiWiki::Hosting; use warnings; use strict; use IkiWiki; use IkiWiki::Hosting; sub meta_createkey { required => [qw{client-hostname}], options => [qw{}], description => "creates key for a client, and outputs the key filenames", section => "primary", } sub createkey { my $client=shift; assert_root(); assert_client_haskey($client, 0); shell("mkdir", "-p", "$config{keydir}/dns"); shell("chmod", "700", $config{keydir}); chdir "$config{keydir}/dns" || die "chdir $config{keydir}/dns: $!"; my $keyid=`dnssec-keygen -b 512 -a HMAC-MD5 -n HOST "$client."`; chomp $keyid; if (! length $keyid) { error "dnssec-keygen failed"; } setupbind(); return "$config{keydir}/dns/$keyid.key", "$config{keydir}/dns/$keyid.private"; } sub meta_deletekey { required => [qw{client-hostname}], options => [qw{}], description => "removes key files for a client", section => "primary", } sub deletekey { my $client=shift; assert_root(); assert_client_haskey($client, 1); my @keys=(dns_private_keys($client), dns_keys($client)); shell("rm", "-f", @keys); setupbind(); } sub meta_setupbind { required => [qw{}], options => [qw{}], description => "generates bind config files that allow updates using keys", section => "utility", } sub setupbind { assert_root(); # use template to generate named.conf file my $named_conf="/etc/bind/ikisite.conf"; my @keys = map { { key => dns_keyid($_), secret => dns_keysecret($_), } } dns_private_keys(); my @domains; foreach my $domain (split ' ', $config{domains}) { my $basedir="/etc/bind/$domain"; my $zonefile="$basedir/zonefile"; shell("mkdir", "-p", $basedir); # bind needs permission to write to the basedir shell("chown", "root:bind", $basedir); shell("chmod", "770", $basedir); if (! -e $zonefile) { # use template to generate stub zone file outtemplate($zonefile, "zonefile.tmpl", domain => $domain, ); } push @domains, { domain => $domain, zonefile => $zonefile, keys => \@keys, }; } my $template=IkiWiki::Hosting::ikisite_template("named.conf.tmpl"); $template->param( keys => \@keys, dns_domains => \@domains, ); open(OUT, ">", $named_conf) || error "$named_conf: $!"; # contains keys, so needs to be only readable by bind shell("chown", "root:bind", $named_conf); shell("chmod", "640", $named_conf); print OUT $template->output; close OUT; # add include to named.conf.local my $named_conf_comment="// automatically added by ikidns"; open(NAMED_CONF_IN, "<", "/etc/bind/named.conf.local") || error("named.conf.local: $!"); open(NAMED_CONF_OUT, ">", "/etc/bind/named.conf.local.tmp") || error("named.conf.local.tmp: $!"); while () { chomp; print NAMED_CONF_OUT "$_\n" unless /^include\s+\".*\";\s*\Q$named_conf_comment\E/; } close NAMED_CONF_IN; print NAMED_CONF_OUT "include \"$named_conf\"; $named_conf_comment\n"; close NAMED_CONF_OUT || error "close named.conf.tmp: $!"; shell("mv", "-f", "/etc/bind/named.conf.local.tmp", "/etc/bind/named.conf.local"); shell("/etc/init.d/bind9", "restart"); } ############################################################################# sub assert_client_haskey { my $client=shift; my $true=shift; my @keys=(dns_private_keys($client), dns_keys($client)); if (! $true && @keys) { error("dns key already exists for $client"); } elsif ($true && ! @keys) { error("dns key does not exist for $client"); } } sub dns_private_keys { my $client=shift || "*"; my @list=glob("$config{keydir}/dns/K$client.+*.private"); return @list; } sub dns_keys { my $client=shift || "*"; my @list=glob("$config{keydir}/dns/K$client.+*.key"); return @list; } sub dns_keyid { my $keyfile=shift; $keyfile=IkiWiki::basename($keyfile); $keyfile=~s/^K//; $keyfile=~s/\+.*//; return $keyfile; } sub dns_keysecret { my $keyfile=shift; my $file=readfile($keyfile); my ($key)=$file=~/^Key:\s+(.*)$/m; return $key; } main(); ikiwiki-hosting/ikiwiki-hosting.conf0000644000000000000000000000775212232570331015046 0ustar # ikiwiki hosting configuration file. # Admin contact email. adminemail=root@localhost # List supported domains here. domains="example.com ikiwiki.net" # Username prefixes used to ensure unique usernames are used for # sites under different top level domains. Keep the prefixes short! prefix_e=example.com prefix_i=ikiwiki.net # Filenames for a default SSL certificate to use for all sites that # do not have their own certificate. This has to be a wildcard SSL # cert if enabled. wildcard_ssl_cert= wildcard_ssl_key= wildcard_ssl_chain= # Configure a realm based on the primary supported domain, that users # will log into via openid. openid_realm="http://*.example.com/" # The default version control system to use. vcs=git # Set to 1 to allow sites to be auto-assigned ipv4 addresses in the DNS. allow_ipv4=0 # Set to 1 to allow sites to be auto-assigned ipv6 addresses in the DNS. allow_ipv6=0 # (Disable all of the above to disable auto-assignment of DNS addresses.) # by default, ikiwiki-hosting looks at the first IP on the interface # that has the default gateway, but this may fail if you're behind NAT # or else. those allow you to hardcode the IPs # hardcode_ipv4=10.0.0.1 # hardcode_ipv6=fr00::0 # This is the DNS TTL to use when adding a hostname for a site. ttl=28800 # Set to 1 to allow site admins to view analog reports. allow_analog_reports=0 # Deleted sites can optionally be automatically backed up to a morgue directory. #morguedir=/var/backups/ikiwiki-hosting-web/morgue # Sites are backed up to /var/backups daily, by default. This controls # how many days worth of backups to keep. Set to 0 to disable storing # backups locally. num_backups=3 # Locations that rsync should transfer site backups to. # (In order to keep track of deleted sites, it's best to enable # morguedir when configuring this.) #backup_rsync_urls= # Options to pass to rsync when it is backing up a site. #backup_rsync_options= # To enable ssh connection caching when making the possibly many # rsyncs used, ssh must be started in master mode. This does so # for the specified ssh login. Note that ssh needs to be configured # to use a ControlPath in order for this to work. #backup_ssh_cache=user@host # Templates are read from this directory. ikisite_templatedir=/etc/ikiwiki-hosting/templates # Site configuration that site users cannot influence is read from # per-user subdirectories of this directory. ikisite_rootconfigdir=/etc/ikiwiki-hosting/config # Ikiwiki auto setup files are read from this directory. autosetupdir=/etc/ikiwiki-hosting/autosetup # Various keys are stored in this directory. keydir=/etc/ikiwiki-hosting/keys # Per-site lock files are stored in this directory. lockdir=/var/lib/ikiwiki-hosting-web/lock # This can be used to set an alternative home directory in which # to create new sites. #adduser_basedir=/some/other/home # Git daemon looks for vhosts in this directory. gitdaemondir=/var/lib/ikiwiki-hosting-web/git # Git daemon runs as this user. gitdaemonuser=ikiwiki-anon # Long-lived log, records site create and deletion only, for accounting. accountinglog=/var/log/ikiwiki-hosting/accounting.log # Hostname of site used to hold customer data. #customersite=customers.example.com # Hostname of site with controlpanel and makesite plugins enabled. #controlsite=www.example.com # List of sites that will be updated when sites are added/removed. # These sites should each have a template named observersite.tmpl, which # will be filled out to create pages under sites/existing/ and # sites/removed/ #observersites="www.example.com" # Url to ikiwiki CGI for the control site. #controlsitecgiurl="http://www.example.com/ikiwiki.cgi" # Where to send a user on a newly created site to welcome them to it # and let them jumpstart with, typically, editing it. # (Never done for branched sites, and not done if commented out.) #welcome_redir="ikiwiki.cgi?page=index&do=edit&welcome=welcome" # Where to send a user on a newly created blog. #welcome_redir_blog="ikiwiki.cgi?page=posts/first_post&do=edit&welcome=welcomeblog" ikiwiki-hosting/templates/0000755000000000000000000000000012232570331013051 5ustar ikiwiki-hosting/templates/welcomeblog.tmpl0000644000000000000000000000035412232566773016267 0ustar

Welcome to your new site!    To get started, make your first post. (skip this step)

ikiwiki-hosting/templates/apache-site.tmpl0000644000000000000000000000472512232570331016142 0ustar # Apache config file for # Automatically generated by ikisite; do not modify directly. ServerAdmin ServerName :80 SuexecUserGroup UserDir disabled DocumentRoot DirectoryIndex index.html index AllowOverride None > Options Indexes MultiViews ExecCGI AllowOverride None Order allow,deny allow from all Options ExecCGI AllowOverride None Order allow,deny allow from all ScriptAlias /ikiwiki.cgi /ikiwiki.cgi ErrorLog /error.log LogLevel warn CustomLog /access.log combined AddHandler cgi-script .cgi ErrorDocument 404 "/ikiwiki.cgi" ServerAdmin ServerName :443 SSLEngine on SSLCertificateFile SSLCertificateKeyFile SSLCertificateChainFile SuexecUserGroup UserDir disabled DocumentRoot DirectoryIndex index.html index AllowOverride None > Options Indexes MultiViews ExecCGI AllowOverride None Order allow,deny allow from all Options ExecCGI AllowOverride None Order allow,deny allow from all ScriptAlias /ikiwiki.cgi /ikiwiki.cgi ErrorLog /error.log LogLevel warn CustomLog /access.log combined AddHandler cgi-script .cgi ErrorDocument 404 "/ikiwiki.cgi" ServerAdmin ServerName :80 SuexecUserGroup UserDir disabled DocumentRoot /usr/share/gitweb ScriptAlias /index.cgi /gitweb.cgi ErrorLog /error.log LogLevel warn CustomLog /access.log combined ikiwiki-hosting/templates/zonefile.tmpl0000644000000000000000000000101712232566773015600 0ustar ; ; Stub BIND data file for . Modify as needed. ; $ORIGIN . $TTL 28800 ; 8 hours IN SOA ns1.. root.. ( 2 ; serial 14400 ; refresh (4 hours) 3600 ; retry (1 hour) 2419200 ; expire (4 weeks) 0 ; minimum (no caching) ) NS ns1.. NS ns2.. MX 0 . A ; configure here $ORIGIN . ns1 A ; configure here ns2 A ; configure here ikiwiki-hosting/templates/gitweb.conf.tmpl0000644000000000000000000000050512232566773016173 0ustar # path containing git repository (source.git) $projectroot = ""; $projects_list = ""; # link back to the site $home_link_str=""; $home_link="http:///"; # paranoia: a good default $prevent_xss=1; # nice feature $feature{'blame'}{'default'} = [1]; ikiwiki-hosting/templates/missingsite.tmpl0000644000000000000000000000025012232566773016321 0ustar site does not exist

The site does not currently exist.

ikiwiki-hosting/templates/setupdns.tmpl0000644000000000000000000000256212232570034015621 0ustar

Here you can configure the domain names used for this site.

To make your site be available at , you need to purchase that domain name. I can't do that for you, but you can buy the domain at sites like GoDaddy or Gandi. When you buy the domain, configure it to point to .

To make your site be available at , you need to visit your DNS Registrar and configure the domain to point to .

DNS successfully configured.





ikiwiki-hosting/templates/setupsshkeys.tmpl0000644000000000000000000000264712232570331016532 0ustar

Here you can configure the SSH keys that are allowed to send changes to this site's git repository. You can add not only your ssh key, but the keys of others you trust to modify this site.

As well as editing the site with git, these keys let you run a few useful commands:

  • To view the webserver access log in real time, run: ssh logview
  • To download the webserver access log for your site, run: ssh logdump > access.log

Your ssh key is typically in the file `.ssh/id_rsa.pub` or `.ssh/id_dsa.pub` in your home directory. If you don't yet have a ssh key, you can make one using the ssh-keygen program.

Currently enabled SSH keys:



ikiwiki-hosting/templates/makesite.tmpl0000644000000000000000000000705012232570331015553 0ustar
Your site has been created. is being created right now.

Billing

To pay for your site, you need to sign up for the plan:
( month free trial)

checked /> *
*

*

Domain

To make your site be available at , you need to purchase that domain name. I can't do that for you, but you can buy the domain at sites like GoDaddy or Gandi. When you buy the domain, configure it to point to . Looks like already exists. If you own that domain, you need to visit your DNS Registrar, and configure the domain so it points to .

  Sorry, the DNS for is still not right ...

Or, you can postpone using the domain, and set it up later, after you've used your site for a while.

ikiwiki-hosting/templates/controlpanel.tmpl0000644000000000000000000000561012232570210016445 0ustar
Your web hosting plan: ()
Sites used: of
Balance: $ due credit

Your sites:
Other sites you administer:
BranchedCreated , from
You currently have no sites.
ikiwiki-hosting/templates/named.conf.tmpl0000644000000000000000000000056112232566773016000 0ustar # Automatically generated by ikidns; do not edit directly. key { algorithm "HMAC-MD5"; secret ""; }; zone "" { type master; file ""; allow-update { key ; }; }; ikiwiki-hosting/templates/parkedsite.tmpl0000644000000000000000000000044212211430070016072 0ustar <TMPL_VAR WIKINAME> (not available)

This site is currently unavailable.

ikiwiki-hosting/templates/apache-sitealias.tmpl0000644000000000000000000000207512232570331017150 0ustar # Apache config file for # Automatically generated by ikisite; do not modify directly. ServerAdmin ServerName :80 SuexecUserGroup UserDir disabled RewriteEngine On RewriteRule ^/(.*) $1 [L,R,NE] ErrorLog /error.log LogLevel warn CustomLog /access.log combined ServerAdmin ServerName :443 SSLEngine on SSLCertificateFile SSLCertificateKeyFile SSLCertificateChainFile SuexecUserGroup UserDir disabled RewriteEngine On RewriteRule ^/(.*) $1 [L,R,NE] ErrorLog /error.log LogLevel warn CustomLog /access.log combined ikiwiki-hosting/templates/branchable.tmpl0000644000000000000000000000300712232570210016024 0ustar

Version Control

This site is version controlled using Git. Anyone can access its anonymous git repository:

git clone 
Modifications to the site can be pushed to the anonymous git repository, but will be subject to verification.

Users with configured ssh keys can modify the git repository:

git clone  

Branching This Site

Anyone can make a branch of this site. Only the owner and admins of this site may make a branch of it. The branch will start out as an exact copy of the site, and you can then change it as desired.

I want a branch of named


If this is your site and you want to disableenable branching and anonymous git access, you can configure that on the Setup Page.

ikiwiki-hosting/templates/welcome.tmpl0000644000000000000000000000035312232566773015422 0ustar

Welcome to your new site!    To get started, edit the front page. (skip this step)

ikiwiki-hosting/Makefile0000644000000000000000000000342412232566773012535 0ustar BINDIR=$(DESTDIR)/usr/bin ETCDIR=$(DESTDIR)/etc MODDIR=$(DESTDIR)/usr/share/perl5/ CFLAGS=-O2 -Wall -g BINS=iki-git-shell iki-git-hook-update ikisite-wrapper SCRIPTS=ikisite ikidns ikiwiki-hosting-web-daily ikiwiki-hosting-web-backup \ ikisite-delete-unfinished-site iki-ssh-unsafe MANS=ikisite ikidns ikisite-wrapper ikiwiki-hosting-web-daily \ ikiwiki-hosting-web-backup ikisite-delete-unfinished-site \ iki-git-hook-update iki-git-shell iki-ssh-unsafe IKIWIKI=ikiwiki --wikiname "ikiwiki hosting internals" \ --no-usedirs --underlaydir=/dev/null \ --plugin goodstuff \ --disable-plugin smiley --disable-plugin shortcut \ --disable-plugin relativedate --disable-plugin toggle \ --disable-plugin openid \ doc html include ./ikiwiki-hosting.conf all: build test build: $(BINS) $(IKIWIKI) for man in $(MANS); do \ ./mdwn2man $$man 1 doc/$$man.mdwn > $$man.1; \ done test: perl "-MExtUtils::Command::MM" "-e" "test_harness(0, '.')" t/*.t clean: $(IKIWIKI) --clean rm -f $(BINS) for man in $(MANS); do \ rm -f $$man.1; \ done install: install -d $(BINDIR) install -m 0755 $(SCRIPTS) $(BINS) $(BINDIR) install -d $(ETCDIR)/ikiwiki-hosting/config install -m 0644 ikiwiki-hosting.conf $(ETCDIR)/ikiwiki-hosting install -d $(ETCDIR)/ikiwiki-hosting/autosetup cp autosetup/* $(ETCDIR)/ikiwiki-hosting/autosetup install -d $(ETCDIR)/ikiwiki-hosting/templates cp templates/* $(ETCDIR)/ikiwiki-hosting/templates install -d $(MODDIR)/IkiWiki/Plugin $(MODDIR)/IkiWiki/Setup install -m 0644 IkiWiki/*.pm $(MODDIR)/IkiWiki/ install -m 0644 IkiWiki/Plugin/*.pm $(MODDIR)/IkiWiki/Plugin/ install -m 0644 IkiWiki/Setup/*.pm $(MODDIR)/IkiWiki/Setup/ mkdir -p $(DESTDIR)/$(lockdir) mkdir -p $(DESTDIR)/$(gitdaemondir) mkdir -p $(DESTDIR)/$(shell dirname $(accountinglog)) ikiwiki-hosting/t/0000755000000000000000000000000012232566773011335 5ustar ikiwiki-hosting/t/business.t0000755000000000000000000000103712232566773013361 0ustar #!/usr/bin/perl use strict; use warnings; no warnings 'redefine'; use IkiWiki::Business; use Test::More 'no_plan'; # dollar formatting is(IkiWiki::Business::show_dollars(0), "0.00"); is(IkiWiki::Business::show_dollars(100), "1.00"); is(IkiWiki::Business::show_dollars(999), "9.99"); is(IkiWiki::Business::show_dollars(-1999), "-19.99"); is(IkiWiki::Business::show_dollars(5764), "57.64"); foreach my $n (0..9) { is(IkiWiki::Business::show_dollars($n), "0.0$n"); } foreach my $n (10..99) { is(IkiWiki::Business::show_dollars($n), "0.$n"); } ikiwiki-hosting/t/makesite.t0000755000000000000000000001020512211430070013276 0ustar #!/usr/bin/perl use strict; use warnings; no warnings 'redefine'; use IkiWiki::Plugin::makesite; use Test::More 'no_plan'; # Synthetic test functions. sub IkiWiki::Hosting::site_addresses { return "2.2.2.2", "fe80::4878:f:21a:2"; } sub IkiWiki::Hosting::host_address_or_cname { my $host=shift; my $wanted_cname=shift; if ($host eq 'one.com' || $host eq 'www.one.com') { return "1.1.1.1"; } elsif ($host eq 'two.com' || $host eq 'www.two.com' || $host eq 'two.two.com') { return "2.2.2.2"; } elsif ($host eq 'four.two.com') { return "4.4.2.2"; } else { return undef; } } sub IkiWiki::Plugin::makesite::unique_internal_hostname { my $base=shift; my $domain=shift; return "$base.$domain"; } is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("foo", "example.com")], [$IkiWiki::Plugin::makesite::DNS_READY, "foo.example.com", undef, "foo"], "user requests hostname only"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames(" Foo Bar! ", "example.com")], [$IkiWiki::Plugin::makesite::DNS_READY, "foobar.example.com", undef, "Foo Bar!"], "user entered wikiname, munged to internal hostname"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("one.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_WRONG, "one-com.example.com", "one.com", "one", "www.one.com"], "user entered existing external hostname, dns needs fixing"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("www.one.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_WRONG, "one-com.example.com", "www.one.com", "one", "one.com"], "user entered existing external hostname with www prefix, dns needs fixing"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("two.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_READY, "two-com.example.com", "two.com", "two", "www.two.com"], "user entered existing external hostname, dns is correct"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("www.two.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_READY, "two-com.example.com", "www.two.com", "two", "two.com"], "user entered existing external hostname with www prefix, dns is correct"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("three.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_NEEDED, "three-com.example.com", "three.com", "three", "www.three.com"], "user entered domain that does not yet exist"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("www.three.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_NEEDED, "three-com.example.com", "www.three.com", "three", "three.com"], "user entered domain that does not yet exist with www prefix"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("three.co.uk", "example.com")], [$IkiWiki::Plugin::makesite::DNS_NEEDED, "three-co-uk.example.com", "three.co.uk", "three", "www.three.co.uk"], "user entered domain that does not yet exist, under multilevel TLD"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("www.three.co.uk", "example.com")], [$IkiWiki::Plugin::makesite::DNS_NEEDED, "three-co-uk.example.com", "www.three.co.uk", "three", "three.co.uk"], "user entered domain that does not yet exist, under multilevel TLD, with www prefix"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("me.two.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_NEEDED, "me-two-com.example.com", "me.two.com", "me", "www.me.two.com"], "user entered nonexistant subdomain of existing domain"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("me.four.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_NEEDED, "me-four-com.example.com", "me.four.com", "me", "www.me.four.com"], "user entered nonexistant subdomain of nonexistant domain"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("four.two.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_WRONG, "four-two-com.example.com", "four.two.com", "four", "www.four.two.com"], "user entered existant subdomain of existing domain, but dns address needs fixed"); is_deeply([IkiWiki::Plugin::makesite::gen_hostnames("two.two.com", "example.com")], [$IkiWiki::Plugin::makesite::DNS_READY, "two-two-com.example.com", "two.two.com", "two", "www.two.two.com"], "user entered existant subdomain of existing domain, and dns is ok"); ikiwiki-hosting/ikiwiki-hosting-web-daily0000755000000000000000000000107012211430070015753 0ustar #!/bin/sh # Run by daily cron job. failed="" . /etc/ikiwiki-hosting/ikiwiki-hosting.conf for site in $(ikisite list); do # Rotate logs, gc git repos, etc. if ! ikisite compact "$site"; then echo "ikisite compact $site failed" >&2 failed=1 fi # Update calendars for sites that have the calendar plugin enabled. if ! ikisite calendar "$site"; then echo "ikisite calendar $site failed" >&2 failed=1 fi done # Reload apache so it will start logging to the new site log files. /etc/init.d/apache2 reload if [ "$failed" ]; then exit 1 else exit 0 fi ikiwiki-hosting/ikisite-delete-unfinished-site0000755000000000000000000000244212211430070016770 0ustar #!/usr/bin/perl # Finds sites whose registration was not completed, and deletes # after a timeout period. my $one_day=24 * 60 * 60; my $timeout=2 * $one_day; use warnings; use strict; use IkiWiki::Hosting; use Getopt::Long; sub usage { die "usage: $0 openid|--all [--dry-run]\n"; } my $all=0; my $dry_run=0; GetOptions( "all" => \$all, "dry-run" => \$dry_run, ) || usage(); IkiWiki::Hosting::readconfig(); if ($> != getpwnam("root")) { die "$0 must be run by root\n"; } my @siteinfo; if ($all) { usage() if @ARGV; @siteinfo=(@{IkiWiki::Hosting::yamlgetshell( "ikisite", "list", "--extended")}); } else { usage() unless @ARGV; foreach my $openid (@ARGV) { push @siteinfo, (@{IkiWiki::Hosting::yamlgetshell( "ikisite", "list", "--extended", "--owner=$openid")}); } } foreach my $site (@siteinfo) { next unless $site->{isunfinished}; # Skip sites just created, to avoid deleting a site a user is still # setting up. if ($site->{site_created} + $timeout > time()) { if ($site->{site_created} + $timeout <= time() + $one_day) { print "tomorrow, will delete unfinished site ".$site->{site_hostname}."\n"; } next; } print "deleting unfinished site ".$site->{site_hostname}."\n"; if (! $dry_run) { IkiWiki::Hosting::shell("ikisite", "delete", $site->{site_hostname}) } } ikiwiki-hosting/debian/0000755000000000000000000000000012232570340012275 5ustar ikiwiki-hosting/debian/copyright0000644000000000000000000010623412211430070014226 0ustar Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Files: * Copyright: © 2010-2011 Joey Hess License: AGPL-3+ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 . Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. . Preamble . The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. . The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. . When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. . Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. . A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. . The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. . An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. . The precise terms and conditions for copying, distribution and modification follow. . TERMS AND CONDITIONS . 0. Definitions. . "This License" refers to version 3 of the GNU Affero General Public License. . "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. . "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. . To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. . A "covered work" means either the unmodified Program or a work based on the Program. . To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. . To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. . An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. . 1. Source Code. . The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. . A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. . The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. . The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. . The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. . The Corresponding Source for a work in source code form is that same work. . 2. Basic Permissions. . All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. . You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. . Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. . 3. Protecting Users' Legal Rights From Anti-Circumvention Law. . No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. . When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. . 4. Conveying Verbatim Copies. . You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. . You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. . 5. Conveying Modified Source Versions. . You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: . a) The work must carry prominent notices stating that you modified it, and giving a relevant date. . b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". . c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. . d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. . A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. . 6. Conveying Non-Source Forms. . You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: . a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. . b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. . c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. . d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. . e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. . A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. . A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. . "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. . If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). . The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. . Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. . 7. Additional Terms. . "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. . When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. . Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: . a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or . b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or . c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or . d) Limiting the use for publicity purposes of names of licensors or authors of the material; or . e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or . f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. . All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. . If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. . Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. . 8. Termination. . You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). . However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. . Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. . Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. . 9. Acceptance Not Required for Having Copies. . You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. . 10. Automatic Licensing of Downstream Recipients. . Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. . An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. . You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. . 11. Patents. . A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". . A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. . Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. . In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. . If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. . If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. . A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. . Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. . 12. No Surrender of Others' Freedom. . If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. . 13. Remote Network Interaction; Use with the GNU General Public License. . Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. . Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. . 14. Revised Versions of this License. . The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. . Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. . If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. . Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. . 15. Disclaimer of Warranty. . THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. . 16. Limitation of Liability. . IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. . 17. Interpretation of Sections 15 and 16. . If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. . END OF TERMS AND CONDITIONS . How to Apply These Terms to Your New Programs . If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. . To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. . Copyright (C) . This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. . You should have received a copy of the GNU Affero General Public License along with this program. If not, see . . Also add information on how to contact you by electronic and paper mail. . If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. . You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ikiwiki-hosting/debian/changelog0000644000000000000000000001566712232570340014166 0ustar ikiwiki-hosting (0.20131025) unstable; urgency=high * Exclude the site from showing up as a referrer in the analog report. * Fix XSS in site creation interface. Thanks, Gopal Bisht. -- Joey Hess Fri, 25 Oct 2013 18:17:44 -0400 ikiwiki-hosting (0.20130926) unstable; urgency=low * ikisite now contains its own /etc/ikiwiki/wikilist update subcommands, avoiding the need for ikiwiki-update-wikilist to be made suid in order to keep ikiwiki-mass-rebuild working. * https can be enabled for a site by dropping a SSL key and certificate into /etc/ikiwiki-hosting/config/$username/ssl.{key,crt} and running ikisite enable. * Also, a wildcard SSL certificate can be configured to be used by sites that do not have their own DNS. -- Joey Hess Mon, 26 Aug 2013 01:18:52 -0400 ikiwiki-hosting (0.20130504) unstable; urgency=low * One word of the comment at the end of ssh keys is now preserved. * ikisite logs: New command that can tail or dump the apache access.log. Designed to be run remotely. * iki-git-shell: Allow the remote user to specify a command of "logview" or "logdump", to tail or dump the access.log. * Site admins can now view analog reports, if allow_analog_reports is set in ikiwiki-hosting.conf. * ikisite-calendar is not run for sites that do no have archivebase configured, allowing use of the calendar plugin without archive page generation when desired. -- Joey Hess Sat, 04 May 2013 23:51:34 -0400 ikiwiki-hosting (0.20120527) unstable; urgency=low * Add cron.d job to run ikiwiki aggregation every 5 minutes for sites that need it. I thought I had merged this from Branchable's tweaks earlier. * Add welcome banner support after making a new site, enabled by uncommenting the welcome_redir setting. -- Joey Hess Sun, 27 May 2012 17:23:40 -0400 ikiwiki-hosting (0.20120526) unstable; urgency=low * makesite.tmpl: Typo fix. * Conflict with the parallel package, which diverts away the moreutils parallel and would break the RSS/Atom aggregation cron job. -- Joey Hess Sat, 26 May 2012 15:14:17 -0400 ikiwiki-hosting (0.20120425) unstable; urgency=low * Add the ability to hardcode the site's IP address in ikiwiki-hosting.conf, rather than looking at interfaces. Thanks, Antoine Beaupré. * Enable gitweb blame feature. * Add libgravatar-url-perl to depends. * Move removal code for /etc/ikiwiki-hosting/keys/dns/ from ikiwiki-hosting-web to ikiwiki-hosting-common, which creates it. Closes: #670432 -- Joey Hess Wed, 25 Apr 2012 12:22:12 -0400 ikiwiki-hosting (0.20120131) unstable; urgency=low * Fix quoting issue in use of which to determine if package is installed. Closes: #658063 -- Joey Hess Tue, 31 Jan 2012 15:53:19 -0400 ikiwiki-hosting (0.20120125) unstable; urgency=low * Add the adduser_basedir configuration file setting, which can be used to create sites someplace other than /home. Thanks, Philip Hands. * Don't use savelog -C, it spews an ls error message. * ikisite checksetup: Bugfix, when plugins are added or removed and there are no other changes, the site was not updated. * Use invoke-rc.d. Closes: #657336 -- Joey Hess Wed, 25 Jan 2012 15:00:37 -0400 ikiwiki-hosting (0.20111005) unstable; urgency=low * ikisite-wrapper: Allow getsetup subcommand to access the branchable and adminuser values, which are needed when branching. -- Joey Hess Wed, 05 Oct 2011 13:32:12 -0400 ikiwiki-hosting (0.20110926) unstable; urgency=low * Further hardening: Use setsid when running code as a site user. * Add libtext-multimarkdown-perl to depends, needed for multimarkdown support (see #630705). * Fix disablesshkey. -- Joey Hess Mon, 26 Sep 2011 14:01:06 -0400 ikiwiki-hosting (0.20110608) unstable; urgency=low * Set timezone to GMT in auto setup files, to avoid random system timezimes from leaking out to existing sites when changesetup or upgrade is run. * gitpush: Push non-master branches too. * Configure git-daemon to know about external domain names of sites. * missingsite: Stop providing an index.cgi, just use apache.conf.tmpl for the missingsite to DirectoryIndex index.html ikiwiki.cgi * More portable environment clearing. * ikisite analog: Output to stdout, not stderr. * ikisite logview: Tails logs. -- Joey Hess Wed, 08 Jun 2011 10:29:25 -0400 ikiwiki-hosting (0.20110515) unstable; urgency=low * Improve security robustness, blocking escalation from site users to httpd user, by moving apache log directory out of users home directory to /var/log/ikiwiki-hosting/, and using suexec with cgi programs moved to /var/www. Thanks, Simon McVittie * Lock down permissions of ikiwiki.setup, .git, .gitconfig, .gitignore, public_html/, source/, apache/. * Lock down source.git, unless branchability is enabled. * The apache.conf.tmpl files are no longer read from the user's home directory, but instead from /etc/ikiwiki-hosting/config/$username/. * Note that previously created sites will continue using the old locations and permissions. Using "ikisite upgrade" to upgrade them is highly recommended. * Added support for anonymous git push. It will only work if the home directory of a site is on a filesystem that supports POSIX ACLs, otherwise git-daemon won't be able to write to the source.git directory. * Anonymous git push enabled by default for new wikis, not for blogs or existing sites. * Support ipv6-only operation. * Add gitpush plugin, which can be used to push changes to a site on to other git repositories. * Remove dns key directory on purge. Closes: #625817 * Don't run cron jobs once removed. Closes: #625815 -- Joey Hess Sun, 15 May 2011 16:23:42 -0400 ikiwiki-hosting (0.20110424) unstable; urgency=low * Remove unused dependency on libdigest-sha1-perl. Closes: #623957 -- Joey Hess Sun, 24 Apr 2011 16:02:16 -0400 ikiwiki-hosting (0.20110420) unstable; urgency=low * ikisite sudo: Use SHELL if set; /bin/sh as dash is a horrible interactive shell. * better handling of www special case when making a site * ikiwiki-hosting-web-backup: Fix removal of morgued sites from primary backup. * ikisite checklock renamed to checksite, and can check that a requested nonce has been created, to notice if site creation crashed part way through. * Include copy of entire AGPL in debian/copyright due to absurd policy requirements that it not be in a separate file, despite all common licenses being shipped in separate files in Debian. -- Joey Hess Wed, 20 Apr 2011 15:52:38 -0400 ikiwiki-hosting (0.20110401) unstable; urgency=low * Initial release to Debian. -- Joey Hess Fri, 01 Apr 2011 20:41:11 -0400 ikiwiki-hosting/debian/ikiwiki-hosting-web.init0000755000000000000000000000544512211430070017051 0ustar #!/bin/sh ### BEGIN INIT INFO # Provides: ikiwiki-hosting-web # Required-Start: $remote_fs $syslog # Required-Stop: $remote_fs $syslog # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: git damon # Description: git daemon init script for use with ikiwiki-hosting-web ### END INIT INFO . /etc/ikiwiki-hosting/ikiwiki-hosting.conf PATH=/sbin:/usr/sbin:/bin:/usr/bin DESC="ikiwiki-hosting-web" NAME="ikiwiki-hosting-web" DAEMON="/usr/lib/git-core/git-daemon" # git daemon is run in virtual host mode, so for git://example.com/*, # it looks in /var/lib/ikiwiki-hosting-web/git/example.com DAEMON_ARGS="--reuseaddr --interpolated-path=$gitdaemondir/%H" PIDFILE=/var/run/$NAME.pid SCRIPTNAME=/etc/init.d/$NAME # Exit if the package is not installed [ -x "$DAEMON" ] || exit 0 # Read configuration variable file if it is present [ -r /etc/default/$NAME ] && . /etc/default/$NAME # Define LSB log_* functions. # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. . /lib/lsb/init-functions # # Function that starts the daemon/service # do_start() { # Return # 0 if daemon has been started # 1 if daemon was already running # 2 if daemon could not be started start-stop-daemon --start --chuid $gitdaemonuser:$gitdaemonuser --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \ || return 1 start-stop-daemon --start --chuid $gitdaemonuser:$gitdaemonuser --quiet --make-pidfile --pidfile $PIDFILE --background --exec $DAEMON -- \ $DAEMON_ARGS \ || return 2 } # # Function that stops the daemon/service # do_stop() { # Return # 0 if daemon has been stopped # 1 if daemon was already stopped # 2 if daemon could not be stopped # other if a failure occurred start-stop-daemon --stop --chuid $gitdaemonuser:$gitdaemonuser --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --exec $DAEMON RETVAL="$?" [ "$RETVAL" = 2 ] && return 2 return "$RETVAL" } case "$1" in start) [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" do_start case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; stop) [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" do_stop case "$?" in 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; esac ;; status) status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? ;; restart|force-reload) log_daemon_msg "Restarting $DESC" "$NAME" do_stop case "$?" in 0|1) do_start case "$?" in 0) log_end_msg 0 ;; 1) log_end_msg 1 ;; # Old process is still running *) log_end_msg 1 ;; # Failed to start esac ;; *) # Failed to stop log_end_msg 1 ;; esac ;; *) echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 exit 3 ;; esac : ikiwiki-hosting/debian/ikiwiki-hosting-dns.manpages0000644000000000000000000000001112211430070017665 0ustar ikidns.1 ikiwiki-hosting/debian/rules0000755000000000000000000000003512211430070013343 0ustar #!/usr/bin/make -f %: dh $@ ikiwiki-hosting/debian/ikiwiki-hosting-web.cron.daily0000755000000000000000000000116112232566773020166 0ustar #!/bin/sh # Compact and maintain all sites. This is a low-priority background job. # It can be a bit noisy, so chronic is used to only show output on error. if [ -x "`which ikiwiki-hosting-web-daily 2>/dev/null`" ]; then nice -n 19 ionice -c 3 chronic ikiwiki-hosting-web-daily fi # Backup sites once a day. if [ -x "`which ikiwiki-hosting-web-backup 2>/dev/null`" ]; then nice -n 19 ionice -c 3 chronic ikiwiki-hosting-web-backup fi # Clean up unfinished sites. This produces output you probably want to see. if [ -x "`which ikisite-delete-unfinished-site 2>/dev/null`" ]; then ikisite-delete-unfinished-site --all fi ikiwiki-hosting/debian/ikiwiki-hosting-dns.install0000644000000000000000000000001712211430070017546 0ustar usr/bin/ikidns ikiwiki-hosting/debian/ikiwiki-hosting-web.postinst0000755000000000000000000000073212232570331017773 0ustar #!/bin/sh set -e if [ "$1" = configure ]; then . /etc/ikiwiki-hosting/ikiwiki-hosting.conf || true if [ -n "$gitdaemondir" ] && [ -n "$gitdaemonuser" ]; then adduser --quiet --system --group --no-create-home --home "$gitdaemondir" "$gitdaemonuser" fi fi #DEBHELPER# a2enmod suexec a2enmod userdir # only so it can be forced disabled, ironically a2enmod ssl if which invoke-rc.d >/dev/null 2>&1; then invoke-rc.d apache2 restart else /etc/init.d/apache2 restart fi ikiwiki-hosting/debian/ikiwiki-hosting-web.install0000644000000000000000000000047212232566773017573 0ustar usr/bin/ikisite usr/bin/ikisite-wrapper usr/bin/ikisite-delete-unfinished-site usr/bin/ikiwiki-hosting-web-daily usr/bin/ikiwiki-hosting-web-backup usr/bin/iki-git-shell usr/bin/iki-git-hook-update usr/bin/iki-ssh-unsafe etc/ikiwiki-hosting/autosetup var/lib/ikiwiki-hosting-web gitweb/robots.txt usr/share/gitweb ikiwiki-hosting/debian/ikiwiki-hosting-web.manpages0000644000000000000000000000025512211430070017670 0ustar ikisite.1 ikisite-wrapper.1 ikisite-delete-unfinished-site.1 ikiwiki-hosting-web-daily.1 ikiwiki-hosting-web-backup.1 iki-git-shell.1 iki-git-hook-update.1 iki-ssh-unsafe.1 ikiwiki-hosting/debian/ikiwiki-hosting-web.dirs0000644000000000000000000000007712211430070017040 0ustar var/backups/ikiwiki-hosting-web/morgue var/log/ikiwiki-hosting ikiwiki-hosting/debian/ikiwiki-hosting-web.cron.d0000644000000000000000000000165112232566773017310 0ustar # Frequent cron jobs for ikiwiki-hosting-web. # Trigger ikiwiki aggregation for sites where it is due. This runs every # five minutes, so anyone attempting a more frequent update rate will # be throttled back to that. (The default aggregation rate is every 15 # minutes; so a factor of 15 is desirable.) # # Note that aggregation jobs may stall if a remote system cannot be # contacted or is slow. So it's desirable to run several at a time. # They're run niced, so the main limit is the number of jobs in # memory. # # Also, note that ikiwiki does its own locking to prevent a site being # aggregated more than once at a time, so no locking is needed here. MAXLOAD=5 MAXJOBS=5 NICE="nice -n 19 ionice -c 3" AGGREGATE="$NICE ikisite sudo {} -- ikiwiki --setup ikiwiki.setup --aggregate --refresh -v --syslog" */5 * * * * root ikisite list --aggregatedue | xargs parallel -l $MAXLOAD -j $MAXJOBS -i sh -c "$AGGREGATE" -- >/dev/null 2>&1 ikiwiki-hosting/debian/ikiwiki-hosting-common.postinst0000755000000000000000000000021112211430070020466 0ustar #!/bin/sh set -e #DEBHELPER# # DNS server keys go in here. mkdir -p /etc/ikiwiki-hosting/keys/dns/ chmod 700 /etc/ikiwiki-hosting/keys/ ikiwiki-hosting/debian/ikiwiki-hosting-common.postrm0000644000000000000000000000017112232566773020160 0ustar #!/bin/sh set -e #DEBHELPER# if [ "$1" = purge ]; then rmdir -p /etc/ikiwiki-hosting/keys/dns/ 2>/dev/null || true fi ikiwiki-hosting/debian/control0000644000000000000000000000572012232570331013704 0ustar Source: ikiwiki-hosting Section: admin Priority: optional Build-Depends: ikiwiki, debhelper (>= 9) Maintainer: Joey Hess Standards-Version: 3.9.3 Vcs-Git: git://ikiwiki-hosting.branchable.com/ Homepage: http://ikiwiki-hosting.branchable.com/ Package: ikiwiki-hosting-common Architecture: all Depends: ${misc:Depends}, ${perl:Depends} Description: ikiwiki hosting: common files A hosting interface for ikiwiki. Facilitates management of many separate ikiwiki sites, with capabilities including web-based signup to create new sites, easy support for branching sites, deleting sites, and transferring sites between servers. Ikiwiki-hosting was developed for Branchable.com. . This package contains common files for all ikiwiki hosting servers, and documentation. Package: ikiwiki-hosting-web Architecture: any Depends: ${misc:Depends}, ${perl:Depends}, ${shlibs:Depends}, ikiwiki-hosting-common, ikiwiki (>= 3.20100723), gcc | c-compiler, libc6-dev | libc-dev, git (>= 1:1.7.0), libxml-simple-perl, libnet-openid-consumer-perl (>= 1.03), liblwpx-paranoidagent-perl, libtimedate-perl, libdatetime-perl, libcgi-formbuilder-perl (>= 3.05), libcgi-session-perl (>= 4.14-1), libmail-sendmail-perl, libauthen-passphrase-perl, librpc-xml-perl, libtext-wikiformat-perl, python-docutils, polygen, libhtml-tree-perl, libxml-feed-perl, libmailtools-perl, perlmagick, libfile-mimeinfo-perl, libcrypt-ssleay-perl, liblocale-gettext-perl (>= 1.05-1), libtext-typography-perl, libtext-csv-perl, libtext-wikicreole-perl, libtext-textile-perl, libhighlight-perl, apache2, apache2-suexec, dnsutils, gitweb (>= 1:1.7.0), libyaml-perl, libyaml-syck-perl, uuid, dnsutils, libtext-markdown-perl (>= 1.0.26-1~bpo50+1), libtext-multimarkdown-perl, moreutils (>= 0.43), libdata-compare-perl, libnet-inet6glue-perl, libgravatar-url-perl, acl, adduser, openssl Recommends: analog Conflicts: parallel Description: ikiwiki hosting: web server A hosting interface for ikiwiki. Facilitates management of many separate ikiwiki sites, with capabilities including web-based signup to create new sites, easy support for branching sites, deleting sites, and transferring sites between servers. Ikiwiki-hosting was developed for Branchable.com. . This package contains the ikisite program, and related things to install on each web server. Package: ikiwiki-hosting-dns Architecture: all Depends: ${misc:Depends}, ${perl:Depends}, ikiwiki-hosting-common, bind9 Description: ikiwiki hosting: dns server A hosting interface for ikiwiki. Facilitates management of many separate ikiwiki sites, with capabilities including web-based signup to create new sites, easy support for branching sites, deleting sites, and transferring sites between servers. Ikiwiki-hosting was developed for Branchable.com. . This package should be installed on the master DNS server, only if you will be allowing ikiwiki-hosting to automatically manage DNS for sites. It contains the ikidns program. ikiwiki-hosting/debian/ikiwiki-hosting-common.install0000644000000000000000000000017212211430070020254 0ustar etc/ikiwiki-hosting/ikiwiki-hosting.conf etc/ikiwiki-hosting/templates etc/ikiwiki-hosting/config usr/share/perl5 var/log ikiwiki-hosting/debian/ikiwiki-hosting-common.docs0000644000000000000000000000000712211430070017533 0ustar html/* ikiwiki-hosting/debian/compat0000644000000000000000000000000212232570331013473 0ustar 9 ikiwiki-hosting/ikisite-wrapper.c0000644000000000000000000000375212211430070014335 0ustar /* * Suid wrapper for ikisite. Can run subcommands that allow themselves to * be run via a wrapper. * * To ensure only site owners can change their sites, * this wrapper expects the environment variable IKISITE_NONCE to be * set to contain a previously created nonce for the site. * * An exception is the create, branch, list, checksite, sitelookup, * enabledns, and updatecustomersite subcommands, which do not need a * nonce to be set. * */ #include #include #include #include #include extern char **environ; int main (int argc, char **argv) { int i; char *args[argc+2]; char *nonce=getenv("IKISITE_NONCE"); if (!nonce) { if (! argv[1] || strcmp(argv[1], "create") == 0 || strcmp(argv[1], "branch") == 0 || strcmp(argv[1], "list") == 0 || strcmp(argv[1], "sitelookup") == 0 || strcmp(argv[1], "getsetup") == 0 || strcmp(argv[1], "updatecustomersite") == 0 || strcmp(argv[1], "checksite") == 0 || strcmp(argv[1], "enabledns") == 0) { /* use a dummy value so ikisite still can tell * it is being run from the wrapper */ nonce="dummy"; } else { fprintf(stderr, "IKISITE_NONCE not set\n"); exit(1); } } /* suid safety */ #ifdef __TINYC__ /* old tcc versions do not support modifying environ directly */ if (clearenv() != 0) { perror("clearenv"); exit(1); } #else /* this is more portable on freebsd/osx, which lack clearenv */ *environ = NULL; #endif if (setenv("PATH", "/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin", 1) != 0) { perror("setenv"); exit(1); } if (setenv("IKISITE_NONCE", nonce, 1) != 0) { perror("setenv"); exit(1); } if (setregid(0, 0) != 0) { perror("setregid"); exit(1); } if (setreuid(0, 0) != 0) { perror("setreuid"); exit(1); } /* Pass argv to ikisite. */ args[0]="/usr/bin/ikisite"; for (i=1; i "getsetup", id => "branchable", call => \&getsetup); hook(type => "checkconfig", id => "branchable", call => \&checkconfig, first => 1); hook(type => "sessioncgi", id => "branchable", call => \&sessioncgi); hook(type => "pageactions", id => "branchable", call => \&pageactions, # Want it to be the last thing on the action bar. last => 1); hook(type => "genwrapper", id => "branchable", call => \&genwrapper, last => 1); hook(type => "disable", id => "branchable", call => \&disable); } sub getsetup () { return plugin => { safe => 0, rebuild => undef, section => "core", }, branchable => { type => "boolean", example => 1, description => "Allow anyone to branch, check out, and copy this site?", safe => 1, rebuild => 1, # disables diffurl and historyurl }, anonpush => { type => "boolean", example => 0, description => "Allow anyone to git push verified changes to this site?", safe => 1, rebuild => 0, }, branchable_action => { type => "boolean", example => 1, description => "Display \"Branchable\" link on action bar?", safe => 1, rebuild => 1, # update action bar }, } sub checkconfig { $config{branchable_action}=1 unless defined $config{branchable_action}; # Automatically configure repositories for repolist plugin. # (This hook needs to run first to do so.) $config{repositories}=[grep defined, (gitsshurl(), gitanonurl())]; # Sites that are not branchable do not have gitweb configured, so # automatically disable the historyurl and diffurl. # TODO: store old values and re-enable if branchable is enabled # Also, non-branchability disabled anonpush. if (! $config{branchable}) { delete $config{historyurl}; delete $config{diffurl}; $config{anonpush}=0; } # Sites with anonpush enabled have a git_test_receive_wrapper. if ($config{anonpush}) { my $repo=find_git_repository(); if (defined $repo) { $config{git_test_receive_wrapper}="$repo/hooks/pre-receive"; } else { $config{anonpush}=0; delete $config{git_test_receive_wrapper}; } } else { delete $config{git_test_receive_wrapper}; } } my $inbranchable; my $link; sub pageactions (@) { return $link if defined $link; return if $inbranchable; return unless $config{branchable_action}; return $link=' "branchable").'">'. "Branchable".''; } sub sessioncgi ($$) { my $cgi=shift; my $session=shift; return unless defined $cgi->param("do"); if ($cgi->param("do") eq "branchable") { showbranchable($cgi, $session); } } sub showbranchable ($$) { my $cgi=shift; my $session=shift; $inbranchable=1; eval q{use IkiWiki::Hosting}; error $@ if $@; IkiWiki::Hosting::readconfig(); my $template=IkiWiki::Hosting::ikisite_template("branchable.tmpl"); $template->param(wikiname => $config{wikiname}); $template->param(url => $config{url}); $template->param(cgiurl => $config{cgiurl}); $template->param(setupurl => IkiWiki::cgiurl(do => "setup"). # jump right to the branchable plugin setup "#setup_core_plugin_branchable"); $template->param(gitsshurl => gitsshurl()); $template->param(gitanonurl => gitanonurl()); $template->param(hostname => $config{hostname}); $template->param(branchable => $config{branchable}); $template->param(anonpush => $config{anonpush}); $template->param(controlsitecgiurl => $config{controlsitecgiurl}); IkiWiki::printheader($session); print IkiWiki::cgitemplate($cgi, 'Branchable', $template->output); exit 0; } sub genwrapper { # This hook is called whenever wrappers are being built, # and that happens when configuration has changed. # (This hook is set to run last, in order to run after # the creation of the git_test_receive_wrapper.) setbranchable(); # C code to inject into wrapper. return ""; } sub disable { # When this plugin is disabled, clean up any previously # configured branchability. $config{branchable}=0; $config{anonpush}=0; setbranchable(); } sub setbranchable { # When the branchable setting is changed, we need to # touch or remove the git-daemon-export-ok flag file, # and the symlink to gitweb.conf, and tweak the permissions # of the git repo. my $repo=find_git_repository(); if (defined $repo) { my $flagfile="$repo/git-daemon-export-ok"; my $gitweb_conf_file=$config{srcdir}."/.ikiwiki/gitweb.conf"; if ($config{branchable}) { open(FLAG, ">", $flagfile); close FLAG; unlink($gitweb_conf_file); # force refresh symlink symlink("$gitweb_conf_file.real", $gitweb_conf_file); chmod(02775, $repo) || error "chmod $repo: $!"; } else { unlink($flagfile); unlink($gitweb_conf_file); chmod(0700, $repo) || error "chmod $repo: $!"; } } if (defined $repo && $config{anonpush}) { # Allow git daemon to push. system("cd $repo && git config daemon.receivepack true"); } else { system("cd $repo && git config daemon.receivepack false"); # Delete unnecesary hook for speed. if (defined $repo) { unlink("$repo/hooks/pre-receive"); } } } # Parses git_wrapper to find out where the git repository is. sub find_git_repository { if ($config{rcs} eq 'git' && $config{git_wrapper}=~m!^(.*)/hooks/post-update$!) { return $1; } else { return undef; } } sub gitsshurl { # Ikiwiki will be running as the site's username, my $username=(getpwuid($<))[0]; if (defined $username && length $username && defined $config{hostname} && length $config{hostname}) { return "ssh://$username\@$config{hostname}/"; } else { return undef; } } sub gitanonurl { if ($config{branchable} && defined $config{hostname} && length $config{hostname}) { return "git://$config{hostname}/"; } else { return undef; } } 1 ikiwiki-hosting/IkiWiki/Plugin/gitpush.pm0000644000000000000000000000346112211430070015666 0ustar #!/usr/bin/perl package IkiWiki::Plugin::gitpush; use warnings; use strict; use IkiWiki 3.00; sub import { hook(type => "getsetup", id => "gitpush", call => \&getsetup); hook(type => "refresh", id => "gitpush", call => \&refresh); hook(type => "savestate", id => "gitpush", call => \&savestate); } sub getsetup () { my $keyurl=length $config{cgiurl} ? IkiWiki::cgiurl(do => "sshkey") : ""; return plugin => { safe => 1, rebuild => 0, section => "core", }, git_push_to => { type => "string", example => [], description => "git repository urls that changes are pushed to", htmldescription => "git repository urls that changes are pushed to (using this ssh key)", safe => 1, rebuild => 0, }, } # Pushing is deferred to the end to avoid running at the same time # as site building. my $need_push; sub refresh { $need_push=1; } sub savestate { if ($need_push && $config{rcs} eq 'git' && ref($config{git_push_to}) eq 'ARRAY' && length @{$config{git_push_to}}) { # daemonise defined(my $pid = fork) or error("Can't fork: $!"); return if $pid; chdir '/'; open STDIN, '/dev/null'; open STDOUT, '>/dev/null'; POSIX::setsid() or error("Can't start a new session: $!"); open STDERR, '>&STDOUT' or error("Can't dup stdout: $!"); # Don't need to keep a lock on the wiki as a daemon. IkiWiki::unlockwiki(); # Low priority job. eval q{use POSIX; POSIX::nice(10)}; # Ensure we have a ssh key generated to use. IkiWiki::Plugin::ikiwikihosting::get_ssh_public_key(); # Avoid ssh host key checking prompts. $ENV{GIT_SSH}="iki-ssh-unsafe"; chdir "$ENV{HOME}/source.git/" || die $!; foreach my $url (@{$config{git_push_to}}) { next unless length $url; system('git', 'push', '--quiet', '--mirror', $url); } exit 0; } } 1 ikiwiki-hosting/IkiWiki/Plugin/missingsite.pm0000644000000000000000000000257612232566773016576 0ustar #!/usr/bin/perl # Special purpose plugin that takes advantage of ikiwiki being configured # as the 404 handler, and *always* displays missingsite.tmpl. # # One special-purpose site per web server can be configured to use this # plugin, and set as apache's default site. This way, attempts to access # sites that have been deleted or are not created yet will display a # useful error message. # # Put this in the apache config template for the missingsite (eg, in # /etc/ikiwiki-hosting/config/b-missingsite/apache.conf.tmpl): # DirectoryIndex index.html ikiwiki.cgi package IkiWiki::Plugin::missingsite; use warnings; use strict; use IkiWiki 3.00; use IkiWiki::Hosting; sub import { hook(type => "cgi", id => 'missingsite', call => \&cgi); hook(type => "getsetup", id => 'missingsite', call => \&getsetup); hook(type => "genwrapper", id => 'missingsite', call => \&genwrapper); } sub getsetup () { return plugin => { safe => 0, rebuild => 0, } } sub cgi ($) { my $cgi=shift; print STDERR "missing site: $ENV{SERVER_NAME}\n"; my $template=IkiWiki::Hosting::ikisite_template("missingsite.tmpl"); $template->param(SERVER_NAME => $ENV{SERVER_NAME}); IkiWiki::cgi_custom_failure( $cgi, "404 Not Found", $template->output, ); } sub genwrapper () { # Pass SERVER_NAME through the wrapper. return < "none", description => "Simple"}, {name => "blueview", description => "BlueView"}, {name => "actiontabs", description => "ActionTabs"}, {name => "goldtype", description => "GoldType"}, ); sub import { hook(type => "sessioncgi", id => "makesite", call => \&sessioncgi); } sub sessioncgi ($$) { my $cgi=shift; my $session=shift; return unless defined $cgi->param("do"); if ($cgi->param("do") eq "makesite") { makesite($cgi, $session); } elsif ($cgi->param("do") eq "branchsite") { makesite($cgi, $session, 1); } } sub makesite ($$;$) { my $cgi=shift; my $session=shift; my $branching=shift; IkiWiki::needsignin($cgi, $session); # Drop ikiwiki lock to avoid blocking other users during site # creation. IkiWiki::unlockwiki(); # Only openid logins are supported. my $openid=$session->param("name"); if (! defined IkiWiki::openiduser($session->param("name"))) { error "not logged in using openid" } my $internal_branchof; if ($branching) { $internal_branchof=$cgi->param("branchof"); my $branchable=IkiWiki::Hosting::getshell("ikisite-wrapper", "getsetup", $internal_branchof, "branchable"); if (! $branchable) { my $admins=IkiWiki::Hosting::yamlgetshell("ikisite-wrapper", "getsetup", $internal_branchof, "adminuser"); if (! ref $admins || ! grep { $_ eq $session->param("name") } @$admins) { error "You are not allowed to branch this site."; } } } # Get values from form and validate. my ($dns_state, $internal_hostname, $external_hostname, $wikiname, @alias) = gen_hostnames($cgi->param("hostname"), $cgi->param("domain"), $cgi->param("internal_hostname")); my $type=lc($cgi->param("type")) if defined $cgi->param("type"); my $nonce=$cgi->param("nonce"); my $creating=$cgi->param("creating"); my $ready=$cgi->param("ready"); my $site_theme=$cgi->param("site_theme"); my $dns_ok=1 if (($dns_state == $DNS_READY) || $cgi->param("submit_nodns")); # The session may have an email address, taken from openid. my $email=$session->param("email"); if (defined $cgi->param("email")) { $email=$cgi->param("email"); } else { eval q{use IkiWiki::Customer}; error $@ if $@; $email=IkiWiki::Customer::customer_get($openid, "email"); } if (defined $email) { eval q{use Email::Address}; if (! $@) { # This email address parser should be fully RFC822 # compliant, and can even extract email addresses # out of longer strings. # (But no check is done that the domain is valid.) $email = (Email::Address->parse($email))[0]; } else { # Most stupid fallback imaginable. ($email) = $email=~/(.*@.*)/; } $email=undef unless defined $email && length $email; } my $acceptplan=$cgi->param("acceptplan"); my $newplan=undef; if (! IkiWiki::Business::needchangeplan($openid, $internal_hostname)) { $acceptplan=1; } else { # Determine price plan. For now, the user doesn't choose a # plan, but is just set to the cheapest plan suitable for # the sites they have plus the one we're making. $newplan=IkiWiki::Business::suggestedplan($openid, $internal_hostname); if (defined $cgi->param("suggestedplan") && $cgi->param("suggestedplan") ne $newplan) { # User accepted previously suggested plan, # but it's changed somehow. $acceptplan=0; } } # Since site creation takes a while, it will be done in the # background, while the user is presented with a setup screen. if (! $ready && ! $creating) { $creating=1; # Precalculate a nonce to use when the site is # created. This allows it to be modified to # finalize its setup later. $nonce=time().':'.`uuid -v4`; chomp $nonce; my $pid=IkiWiki::Hosting::daemonize(); if (!$pid) { # Now we're a daemon.. my @action=("create", $internal_hostname); if (defined $internal_branchof) { @action=("branch", $internal_branchof, $internal_hostname); } $ENV{IKISITE_NONCE}=$nonce; IkiWiki::Hosting::getshell("ikisite-wrapper", @action, "--owner=$openid", "--admin=http://none/", # temporary admin until setup finishes "--wikiname=$wikiname", "--createnonce", ((defined $type && length $type) ? "--type=$type" : ()), ((defined $email && length $email) ? "--adminemail=$email" : ()), ); if ($? != 0) { error sprintf("failure creating %s", encode_entities($internal_hostname)); } else { exit(0); } } } else { # If all required user input is done, just wait for the # background site creation to finish. my $canwait=$acceptplan && $dns_ok && defined $email; # Check if background site creation is done by checking if # the site exists, is no longer locked, and has the # requested nonce created. IkiWiki::Hosting::getshell("ikisite-wrapper", "checksite", $internal_hostname, "--hasnonce", ($canwait ? "--wait" : ())); if ($? == 0) { $creating=0; $ready=1; } elsif ($canwait) { error sprintf("failure creating %s", encode_entities($internal_hostname)); } } # Modify site, setting alias and/or external hostname. if ($ready && ! $creating && $acceptplan && defined $email && ((@alias || ($dns_state == $DNS_READY && defined $external_hostname)))) { $ENV{IKISITE_NONCE}=$nonce; # This rebuilds the site; pretty slow. But, not as slow as # site creation, so I have not tried to background it yet. IkiWiki::Hosting::getshell("ikisite-wrapper", "domains", $internal_hostname, (($dns_state == $DNS_READY && defined $external_hostname) ? "--external=$external_hostname" : ()), map { "--alias=$_" } @alias, ); if ($dns_state == $DNS_READY && defined $external_hostname && $? != 0) { # Above can fail, if the DNS is not set right etc. $dns_state=$DNS_NEEDED } } # Record when a customer signs up. if ($acceptplan && defined $email) { eval q{use IkiWiki::Customer}; error $@ if $@; IkiWiki::Customer::customer_lock_write($openid, sub { IkiWiki::Customer::customer_set($openid, "currentplan", $newplan) if defined $newplan; IkiWiki::Customer::customer_set($openid, "email", $email); # Ikiwiki also collect a nickname from openid. # (We'd rather have a full name, but it will do.) # Avoid overwriting name if it's already set, as # the value from openid is not very good. if (defined $session->param("nickname") && ! defined IkiWiki::Customer::customer_get($openid, "name")) { IkiWiki::Customer::customer_set($openid, "name", $session->param("nickname")); } my %emails=map { $_ => 1 } (IkiWiki::Customer::customer_get($openid, "email_list"), $email); IkiWiki::Customer::customer_set($openid, "email_list", keys %emails); my %openids=map { $_ => 1 } IkiWiki::Customer::openid_list($openid); IkiWiki::Customer::customer_set($openid, "openid_list", keys %openids); my $startdate=IkiWiki::Customer::customer_get($openid, "startdate"); if (! defined $startdate || $startdate == 0) { IkiWiki::Customer::customer_set($openid, "startdate", time); } my $balance=IkiWiki::Customer::customer_get($openid, "balance"); if (! defined $balance) { IkiWiki::Customer::customer_set($openid, "balance", 0); } IkiWiki::Customer::customer_commit($openid); }); } # Redirect to newly created site once everything is ok. if ($ready && ! $creating && $acceptplan && defined $email && $dns_ok) { # But first, save the email address, which may have been # changed since site creation was started. And set the # openid as the site admin now that they have signed up. # And if a theme was selected, set it. $ENV{IKISITE_NONCE}=$nonce; eval q{use YAML::Syck}; die $@ if $@; IkiWiki::Hosting::getshell("ikisite-wrapper", "changesetup", $internal_hostname, "--set", "adminemail=$email", "--set-yaml", "adminuser=".Dump([$openid]), ((defined $site_theme && length $site_theme && $site_theme ne "none") ? ("--set", "theme=$site_theme") : ()), ); my $redir_url; if (defined $external_hostname && $dns_state == $DNS_READY) { $redir_url="http://$external_hostname/"; } else { $redir_url="http://$internal_hostname/"; } if (! $branching) { IkiWiki::Hosting::readconfig(); my $r=$config{"welcome_redir_$type"} || $config{"welcome_redir"}; $redir_url.=$r if defined $r; } IkiWiki::redirect($cgi, $redir_url); # Setup complete; nonce no longer needed. if (defined $nonce && length $nonce) { IkiWiki::Hosting::getshell("ikisite-wrapper", "deletenonce", $internal_hostname, "--nonce=$nonce"); } exit(0); } # Display site setup screen. my $template=IkiWiki::Hosting::ikisite_template("makesite.tmpl"); # If necessary prompt user to set up DNS for their site. # FIXME: It would be better if the user logged in as admin to their # new site, and finished the DNS setup in there, rather than # it all happening in here. But that needs openid singlesignin # for the user to not have to manually login, so I hacked this # into here in the meantime. if ($dns_state == $DNS_NEEDED) { $template->param(dns_needed => 1); } elsif ($dns_state == $DNS_WRONG) { $template->param(dns_wrong => 1); } else { $template->param(dns_ok => 1); } $template->param(wikiname => $wikiname); $template->param(hostname => $cgi->param("hostname")); $template->param(domain => $cgi->param("domain")); $template->param(internal_hostname => $internal_hostname); $template->param(external_hostname => $external_hostname); $template->param(nonce => $nonce) if length $nonce; $template->param(creating => $creating); $template->param(ready => $ready); $template->param(retried => 1) if $cgi->param("retry"); $template->param(type => $type) if defined $type; $template->param(branching => 1) if $branching; $template->param(branchof => $internal_branchof) if $branching; if (defined $newplan) { $template->param(suggestedplan => $newplan); $template->param(suggestedplan_description => IkiWiki::Business::planinfo($newplan)->{description}); $template->param(suggestedplan_name => IkiWiki::Business::planinfo($newplan)->{name}); my $trial=IkiWiki::Business::planinfo($newplan)->{monthsfreetrial}; $template->param(suggestedplan_monthsfreetrial => $trial) if $trial; } $template->param(acceptplan => $acceptplan ? 1 : 0); if (! $acceptplan && $cgi->param("retry")) { $template->param(acceptplan_error => "Required"); } if (defined $site_theme && length $site_theme) { $template->param(site_theme => $site_theme); } else { $template->param(themes => [ # Add type parameter to theme list to allow # links to example sites of the right type. map { $_->{type}=defined $type ? $type : "wiki"; $_; } @themes ]); } if (defined $email && $acceptplan) { $template->param(billing_ok => 1); } if (defined $email) { $template->param(email => $email); } else { $template->param(email_error => "Required"); } IkiWiki::printheader($session); print IkiWiki::cgitemplate($cgi, "site setup", $template->output); exit(0); } sub unique_internal_hostname { my ($base, $defaultdomain)=@_; my $counter=0; while (1) { my $try=$base.($counter ? ($counter+1) : "").".".$defaultdomain; # ikisite sitexists does not need to run as root IkiWiki::Hosting::getshell("ikisite", "siteexists", $try); if ($? == 0) { $counter++; } elsif ($? == 255) { # 255 is what ikisite siteexists throws on data validation error error sprintf("not allowed to use domain name %s", encode_entities($base)); } else { return $try; } } } sub gen_hostnames { # The "hostname" can be any of: A FQDN, a hostname under # the defaultdomain, or a wikiname. From this, derive an internal # hostname, an external hostname and other aliases, and look up # dns state. # # If an internal hostname has already been allocated, it can # optionally be provided. my ($hostname, $defaultdomain, $internal_hostname, $force_www)=@_; my $orig_hostname=$hostname; my ($dns_state, $external_hostname, $wikiname, @alias); # ikisite validates all data later, but early cleanup and validation # of user-supplied data allows nicer error messages. $hostname =~ s/^\s+//; $hostname =~ s/\s+$//; my $www_stripped=($hostname =~ s/^www\.//i) unless $force_www; $wikiname=$hostname; $wikiname=~s/^([^.]+).*/$1/; $hostname=lc($hostname); $defaultdomain=lc($defaultdomain); if (! length $hostname) { error "hostname not set"; } if (! length $defaultdomain) { error "empty defaultdomain"; } $hostname =~ s/(^|\.)\Q$defaultdomain\E$//; if (! length $hostname) { error "illegal hostname"; } $defaultdomain =~ s/[^-.a-z0-9]//g; if (! length $defaultdomain) { error "illegal defaultdomain"; } if (! defined $internal_hostname) { # Base internal hostname on what the user entered, but with # anything problimatic munged. If they entered a FQDN like # example.com, it will be example-com. A wikiname of "My Wiki" # will yield mywiki. my $base=$hostname; $base =~ s/\./-/g; $base =~ s/[^-a-z0-9]//g; if (! length $base) { error "illegal hostname"; } $internal_hostname=unique_internal_hostname($base, $defaultdomain); } my $address=IkiWiki::Hosting::host_address_or_cname($hostname, $internal_hostname); if (defined $address && ($address eq $internal_hostname || grep { defined $_ && $_ eq $address } IkiWiki::Hosting::site_addresses())) { # Hostname already exists, and its address is already # pointed at internal_hostname. $external_hostname=$hostname; $dns_state=$DNS_READY; } elsif (defined $address) { # Hostname already exists, addresss is not set right. $external_hostname=$hostname; $dns_state=$DNS_WRONG; } elsif ($hostname=~/([^.]+)\.(.+)$/ && defined IkiWiki::Hosting::host_address_or_cname($2)) { # Hostname does not exist, but looks like it was intended # to be a DNS address under a domain that has DNS $external_hostname=$hostname; $dns_state=$DNS_NEEDED; } elsif ($hostname=~/(.+)\.\w+$/) { # Hostname does not exist, but it seems to be a DNS address # with a TLD. $external_hostname=$hostname; $dns_state=$DNS_NEEDED; } else { # No separate external hostname. $external_hostname=undef; $dns_state=$DNS_READY; } if ($www_stripped && $dns_state != $DNS_READY) { # Retry without stripping www, just in case # www.hostname.com is set up right and hostname.com is not. my ($dns_state2, @rest) = gen_hostnames( $orig_hostname, $defaultdomain, $internal_hostname, 1); if ($dns_state2 == $DNS_READY) { return ($dns_state2, @rest); } } if ($www_stripped) { @alias=$external_hostname; $external_hostname="www.$external_hostname"; } else { push @alias, "www.$external_hostname" if defined $external_hostname; } return ($dns_state, $internal_hostname, $external_hostname, $wikiname, @alias); } 1 ikiwiki-hosting/IkiWiki/Plugin/parked.pm0000644000000000000000000000571512211430070015455 0ustar #!/usr/bin/perl # Special purpose plugin that causes a site to be parked. A parked site # cannot be branched, and has no accessible content. Remote VCS access is # disabled too. package IkiWiki::Plugin::parked; use warnings; use strict; use IkiWiki 3.00; use IkiWiki::Hosting; sub import { hook(type => "getsetup", id => 'parked', call => \&getsetup); hook(type => "refresh", id => 'parked', call => \&refresh); hook(type => "genwrapper", id => "parked", call => \&genwrapper, first => 1); hook(type => "disable", id => 'parked', call => \&disable); hook(type => "cgi", id => 'parked', call => \&cgi); # Cache both versions of the parked message. parked_message(); parked_message(1); } sub getsetup () { return plugin => { safe => 0, rebuild => 1, }, parked_message => { type => "string", description => "An optional message explaining why this site is parked.", safe => 0, rebuild => 1, }, } my %cached_message; sub parked_message ($) { my $html=shift() ? 1 : 0; if (! defined $cached_message{$html}) { my $template=IkiWiki::Hosting::ikisite_template("parkedsite.tmpl"); $template->param(html => $html); $template->param(wikiname => $config{wikiname}); $template->param(parked_message => $config{parked_message}) if defined $config{parked_message} && length $config{parked_message}; $cached_message{$html}=$template->output; } return $cached_message{$html}; } my $real_writefile; sub refresh { # Intercept file writes when building the site. if (! defined $real_writefile) { $real_writefile=\&IkiWiki::writefile; inject(name => "IkiWiki::writefile", call => \&writefile_parked); } # Avoid hardlinking raw files, it bypasses our writefile. $config{hardlink}=0; } sub genwrapper { # Write flag file to disable remote git push and pull. writefile("$ENV{HOME}/source.git/parked", "", parked_message(0)); # Set here so the branchable plugin will disable branchability # for parked sites, even when the setup has branchable enabled. # (For it to take effect, this hook is run first.) $config{branchable}=0; # C code to add to wrapper. return "" } sub disable { # Enable remote git push and pull. unlink("$ENV{HOME}/source.git/parked"); } # Replacing these files with a parked message could cause bad things to # happen, so don't. my @keyfiles=qw{robots.txt favicon.ico}; # This is a wrapper injected around ikiwiki's real writefile. # It causes the parked message to be output to files rendered # to the destdir. sub writefile_parked ($$$;$$) { my $file=shift; my $destdir=shift; my $content=shift; my $binary=shift; my $writer=shift; if ($destdir eq $config{destdir} && exists $destsources{$file} && ! grep { $_ eq $file } @keyfiles) { my $html=($file=~/\.html$/); $real_writefile->($file, $destdir, parked_message($html)); } else { $real_writefile->($file, $destdir, $content, $binary, $writer); } } sub cgi ($) { my $cgi=shift; print "Content-type: text/html\n\n"; print parked_message(1); exit; } 1; ikiwiki-hosting/IkiWiki/Plugin/controlpanel.pm0000644000000000000000000000620712211430070016704 0ustar #!/usr/bin/perl # Allows do=controlpanel to be used to view a control panel of all sites. package IkiWiki::Plugin::controlpanel; use warnings; use strict; use IkiWiki 3.00; sub import { hook(type => "sessioncgi", id => "controlpanel", call => \&sessioncgi); } sub sessioncgi ($$) { my $cgi=shift; my $session=shift; return unless defined $cgi->param("do") && $cgi->param("do") eq "controlpanel"; IkiWiki::needsignin($cgi, $session); # Drop ikiwiki lock to avoid blocking other users. IkiWiki::unlockwiki(); eval q{use IkiWiki::Hosting}; error $@ if $@; eval q{use URI}; error $@ if $@; # A single user can have multiple openids. my $openid=$session->param("name"); eval q{use IkiWiki::Customer}; error $@ if $@; my @openids=IkiWiki::Customer::openid_list($openid); my (@owned_sites, @adminned_sites); my $num=0; foreach my $siteinfo (@{IkiWiki::Hosting::yamlgetshell( "ikisite-wrapper", "list", "--extended", (map { "--admin=$_"} @openids), (map { "--owner=$_"} @openids), )}) { $siteinfo->{cgiurl}=$config{cgiurl}; $siteinfo->{num}=$num++; # dress up raw timestamp $siteinfo->{site_created}=displaytime($siteinfo->{site_created}); my $url=URI->new($siteinfo->{site_url}); $siteinfo->{site_domain}=$url->host(); if ($siteinfo->{isowner}) { push @owned_sites, $siteinfo; } else { push @adminned_sites, $siteinfo; } } if (@owned_sites) { @owned_sites=sortsites(@owned_sites); $owned_sites[0]->{first_owned}=1; $owned_sites[$#owned_sites]->{last_owned}=1; } if (@adminned_sites) { @adminned_sites=sortsites(@adminned_sites); $adminned_sites[0]->{first_adminned}=1; $adminned_sites[$#adminned_sites]->{last_adminned}=1; } my $template=IkiWiki::Hosting::ikisite_template("controlpanel.tmpl"); $template->param(sites => [@owned_sites, @adminned_sites]); if (@owned_sites) { # Add price plan and balance info. eval q{use IkiWiki::Business}; error $@ if $@; my $plan=IkiWiki::Business::currentplan($openid); if ($plan) { $template->param(site_owned_count => int @owned_sites); $template->param("plan_$plan" => 1); $template->param(plan_name => IkiWiki::Business::planinfo($plan)->{name}); $template->param(plan_description => IkiWiki::Business::planinfo($plan)->{description}); $template->param(plan_maxsites => IkiWiki::Business::planinfo($plan)->{maxsites}); my $paypal_id=IkiWiki::Business::planinfo($plan)->{paypal_id}; $template->param(paypal_id => $paypal_id) if defined $paypal_id; } $template=IkiWiki::Business::balanceinfo($openid, $template); } # Embed the makesite page. my $page="makesite"; my $file = $pagesources{$page}; if (defined $file) { my $type = pagetype($file); my $content=IkiWiki::htmlize($page, "", $type, IkiWiki::linkify($page, "", IkiWiki::preprocess($page, "", IkiWiki::filter($page, $page, readfile(srcfile($file)))))); $template->param(makesite => $content); } IkiWiki::printheader($session); print IkiWiki::cgitemplate($cgi, "Control Panel", $template->output); exit 0; } sub sortsites { sort { lc $a->{site_wikiname} cmp lc $b->{site_wikiname} } @_; } 1 ikiwiki-hosting/IkiWiki/Plugin/ikiwikihosting.pm0000644000000000000000000003001112232570331017236 0ustar #!/usr/bin/perl # Adds config file settings to record ikiwiki-hosting information, # and contains essential hosting functionality. package IkiWiki::Plugin::ikiwikihosting; use warnings; use strict; use IkiWiki 3.00; use URI::Escape; sub import { hook(type => "getsetup", id => "ikiwikihosting", call => \&getsetup); hook(type => "sessioncgi", id => "ikiwikihosting", call => \&sessioncgi); hook(type => "formbuilder_setup", id => "ikiwikihosting", call => \&formbuilder_setup, last => 1); hook(type => "checkconfig", id => "ikiwikihosting", call => \&checkconfig); hook(type => "genwrapper", id => "ikiwikihosting", call => \&genwrapper); } sub getsetup () { return plugin => { safe => 0, rebuild => undef, section => "core", }, urlalias => { type => "string", example => [], description => "list of urls that alias to the main url", safe => 0, rebuild => 0, }, owner => { type => "string", description => "openid of primary site owner", safe => 0, rebuild => 0, }, parent => { type => "string", description => "optional hostname of site this one was branched from", safe => 0, rebuild => 0, }, hostname => { type => "string", description => "internal hostname of this site", safe => 0, rebuild => 0, }, created => { type => "integer", description => "site creation datestamp", safe => 0, rebuild => 0, }, log_period => { type => "integer", example => 7, description => "how many days to retain logs", safe => 1, rebuild => 0, }, ipv6_disabled => { type => "boolean", example => 1, description => "disable IPv6?", safe => 1, rebuild => 0, }, } sub sessioncgi ($$) { my $cgi=shift; my $session=shift; return unless defined $cgi->param("do"); if ($cgi->param("do") eq "deletesite") { deletesite($cgi, $session); } elsif ($cgi->param("do") eq "setupdns") { setupdns($cgi, $session); } elsif ($cgi->param("do") eq "setupsshkeys") { setupsshkeys($cgi, $session); } elsif ($cgi->param("do") eq "sshkey") { print "Content-Type: text/plain\n\n"; print readfile(get_ssh_public_key())."\n"; exit 0; } } sub is_admin_or_owner ($) { my $session=shift; IkiWiki::is_admin($session->param("name")) || $config{owner} eq $session->param("name"); } sub formbuilder_setup (@) { my %params=@_; my $form=$params{form}; my $cgi=$params{cgi}; if ($form->title eq "preferences" && is_admin_or_owner($params{session})) { if ($config{controlsitecgiurl}) { push @{$params{buttons}}, "Control Panel"; if ($form->submitted && $form->submitted eq "Control Panel") { controlpanelredir($cgi); } } eval q{use IkiWiki::Hosting}; error $@ if $@; IkiWiki::Hosting::readconfig(); if ($config{allow_analog_reports}) { push @{$params{buttons}}, "Statistics"; if ($form->submitted && $form->submitted eq "Statistics") { analog_report($cgi, $params{session}); } } } if ($form->title eq "setup" && is_admin_or_owner($params{session})) { push @{$params{buttons}}, "DNS"; if ($form->submitted && $form->submitted eq "DNS") { setupdns($cgi, $params{session}); } push @{$params{buttons}}, "SSH keys"; if ($form->submitted && $form->submitted eq "SSH keys") { setupsshkeys($cgi, $params{session}); } } if (defined $cgi->param('welcome')) { eval q{use IkiWiki::Hosting}; error $@ if $@; my $t=IkiWiki::Hosting::ikisite_template($cgi->param('welcome').".tmpl"); $form->tmpl_param(message => $t->output); } } sub deletesite ($$){ my $cgi=shift; my $session=shift; IkiWiki::needsignin($cgi, $session); if (! is_admin_or_owner($session)) { error "You are not logged in as an admin or site owner."; } eval q{use CGI::FormBuilder}; error($@) if $@; my $f = CGI::FormBuilder->new( name => "deletesite", header => 0, charset => "utf-8", method => 'POST', javascript => 0, params => $cgi, action => $config{cgiurl}, stylesheet => 1, fields => [qw{do}], ); $f->field(name => "do", type => "hidden", value => "deletesite", force => 1); $f->field(name => "sid", type => "hidden", value => $session->id, force => 1); $f->title("Delete this site?"); my @buttons=("Delete Entire Site", "Cancel"); if ($f->submitted eq $buttons[1]) { # Cancel controlpanelredir($cgi); } elsif ($f->submitted eq $buttons[0] && $f->validate) { IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid')); eval q{use IkiWiki::Hosting}; error $@ if $@; # This is running as the site user, but ikisite delete # needs to run as root. So, use ikisite-wrapper to run # it, getting a nonce first. $ENV{IKISITE_NONCE}=IkiWiki::Hosting::getshell("ikisite", "createnonce", $config{hostname}); # Drop the lock, to avoid any interference with site # deletion. IkiWiki::unlockwiki(); IkiWiki::Hosting::shell("ikisite-wrapper", "delete", $config{hostname}); # Have to take the user somewhere now that their # site is gone.. controlpanelredir($cgi); } else { IkiWiki::showform($f, \@buttons, $session, $cgi); } exit 0; } sub setupdns ($$){ my $cgi=shift; my $session=shift; IkiWiki::needsignin($cgi, $session); if (! is_admin_or_owner($session)) { error "You are not logged in as an admin or site owner."; } eval q{use IkiWiki::Hosting}; error $@ if $@; my $template=IkiWiki::Hosting::ikisite_template("setupdns.tmpl"); $template->param(internal_hostname => $config{hostname}); $template->param(sid => $session->id); if ($cgi->param('submit')) { IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid')); my $external=$cgi->param('external'); my @alias=split(' ', $cgi->param('alias')); my %alias=map { $_ => 1 } @alias; if (lc $external ne lc $config{hostname}) { if ($external =~ /^www\.(.*)$/i) { $alias{$1}=1; } else { $alias{"www.$external"}=1; } } # This is running as the site user, but ikisite domains # needs to run as root. So, use ikisite-wrapper to run # it, getting a nonce first. $ENV{IKISITE_NONCE}=IkiWiki::Hosting::getshell("ikisite", "createnonce", $config{hostname}); # Drop the lock, to avoid interference with site # rebuild. IkiWiki::unlockwiki(); # Gather any error output from ikisite domains, to display # to the user. eval q{use IPC::Open3}; error $@ if $@; my $pid=open3(undef, \*STDIN, \*STDIN, "ikisite-wrapper", "domains", $config{hostname}, (lc $external ne lc $config{hostname} ? "--external=$external" : ()), (map { "--alias=$_" } keys %alias), ); my $error; { local $/=undef; $error=; $error=~s/\n/
/g; } waitpid( $pid, 0); my $ret=$?; IkiWiki::Hosting::shell("ikisite-wrapper", "deletenonce", $config{hostname}, "--nonce=$ENV{IKISITE_NONCE}"); delete $ENV{IKISITE_NONCE}; if (! $ret) { $template->param(dns_ok => 1); $template->param(external_hostname => external_hostname($config{hostname})); $template->param(alias => join("\n", alias($config{hostname}))."\n"); } else { $template->param(error => $error); # preserve bad input values to allow correcting $template->param(external_hostname => $external); $template->param(alias => join("\n", @alias)."\n"); # special exit codes used for missing and wrong dns if ($ret >> 8 == 2) { $template->param(dns_needed => 1); } elsif ($ret >> 8 == 3) { $template->param(dns_wrong => 1); } } } else { $template->param(first => 1); $template->param(external_hostname => external_hostname($config{hostname})); $template->param(alias => join("\n", alias($config{hostname}))."\n"); } IkiWiki::printheader($session); print IkiWiki::cgitemplate($cgi, "DNS", $template->output); exit 0; } sub setupsshkeys ($$) { my $cgi=shift; my $session=shift; IkiWiki::needsignin($cgi, $session); if (! is_admin_or_owner($session)) { error "You are not logged in as an admin or site owner."; } eval q{use IkiWiki::Hosting}; error $@ if $@; my $template=IkiWiki::Hosting::ikisite_template("setupsshkeys.tmpl"); $template->param(sid => $session->id); my $changed=0; if ($cgi->param('submit')) { IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid')); if (defined $cgi->param("add") && length $cgi->param("add")) { eval { IkiWiki::Hosting::shell("ikisite", "enablesshkey", $config{hostname}, "--key=".$cgi->param("add"), ) }; if ($@) { $template->param(add_error => "Unable to add SSH key. Check format."); $template->param(add => $cgi->param("add")); } else { $changed=1; } } } if (defined $cgi->param("delete") && length $cgi->param("delete")) { print STDERR "delete $@ ".$cgi->param("delete")."\n"; eval { IkiWiki::Hosting::shell("ikisite", "disablesshkey", $config{hostname}, "--key=".$cgi->param("delete"), ) }; if ($@) { $template->param(delete_error => "Unable to delete SSH key."); } else { $changed=1; } } my @keys=split "\n", IkiWiki::Hosting::getshell("ikisite", "sshkeys", $config{hostname}); $template->param(keys => [map { key => $_, key_urlencoded => uri_escape($_), }, grep defined, @keys]); # Ikiwiki will be running as the site's username, my $username=(getpwuid($<))[0]; if (defined $username && length $username && defined $config{hostname} && length $config{hostname}) { $template->param(sshaddress => "$username\@$config{hostname}"); } IkiWiki::printheader($session); print IkiWiki::cgitemplate($cgi, "SSH keys", $template->output); handlechangedsetup() if $changed; exit 0; } sub controlpanelredir ($) { my $cgi=shift; eval q{use IkiWiki::Hosting}; error $@ if $@; IkiWiki::Hosting::readconfig(); unless ($config{controlsitecgiurl}) { error "This wiki does not have a control panel."; } IkiWiki::redirect($cgi, $config{controlsitecgiurl}."?do=controlpanel"); } sub external_hostname ($) { my $internal_hostname=shift; eval q{use URI}; error $@ if $@; eval q{use IkiWiki::Hosting}; error $@ if $@; my $url=IkiWiki::Hosting::getshell("ikisite", "getsetup", $internal_hostname, "url"); my $u=URI->new($url); if (! $u->can("host")) { error "bad url for $internal_hostname: $url\n"; } return $u->host; } sub alias ($) { my $internal_hostname=shift; eval q{use URI}; error $@ if $@; eval q{use IkiWiki::Hosting}; error $@ if $@; my $alias=IkiWiki::Hosting::yamlgetshell("ikisite", "getsetup", $internal_hostname, "urlalias"); my @alias; if ($alias) { foreach my $u (map { URI->new($_) } @$alias) { if ($u->can("host")) { push @alias, $u->host; } } } return @alias; } sub checkconfig { if (! defined $config{ipv6_disabled}) { $config{ipv6_disabled}=0; } } sub genwrapper { # This hook is called whenever wrappers are being built, # and that happens when configuration has changed. handlechangedsetup(); # C code to inject into wrapper. return ""; } sub handlechangedsetup { # Locking requires this be done in the background. eval q{use IkiWiki::Hosting}; error $@ if $@; my $pid=IkiWiki::Hosting::daemonize(); if ($pid) { return; } # Drop lock; ikisite takes the lock itself. IkiWiki::unlockwiki(); # If ipv6 was enabled or disabled, need to refresh dns. IkiWiki::Hosting::getshell("ikisite-wrapper", "enabledns", $config{hostname}); # Commit any changes to the setup file into the setup branch. IkiWiki::Hosting::getshell("ikisite", "commitsetup", $config{hostname}); exit; } # Returns path of ssh public key. # If no key exists, one is created. (Can be a little slow.) sub get_ssh_public_key { my @keys; my $findkeys=sub { @keys=glob("$ENV{HOME}/.ssh/id_*.pub") }; $findkeys->(); if (! @keys) { my $ret=system("ssh-keygen", "-q", "-t", "rsa", "-f", "$ENV{HOME}/.ssh/id_rsa", "-N", "", "-C", $config{wikiname}); $findkeys->(); if ($ret !=0 || ! @keys) { error "ssh-keygen failed"; } } return shift(@keys); } sub analog_report ($$) { my $cgi=shift; my $session=shift; IkiWiki::needsignin($cgi, $session); if (! is_admin_or_owner($session)) { error "You are not logged in as an admin or site owner."; } print "Content-type: text/html\n\n"; system("ikisite", $config{hostname}, "analog", "--" , "-a" # html output , "+CHOSTNAME 'this site'" ); exit 0; } 1 ikiwiki-hosting/IkiWiki/Customer.pm0000644000000000000000000002621312211430070014546 0ustar #!/usr/bin/perl # Handles access to customer data. package IkiWiki::Customer; use warnings; use strict; use IkiWiki; use IkiWiki::Hosting; use Fcntl qw{:flock}; use File::Path qw{mkpath}; use Memoize; # Currently, customer data is stored in a customersite. # It could just as easily be a database. # # The customersite's repo is checked out into ~/customersite. Assuming # that directory exists, or if a checkout is successfully done, returns the # directory. memoize("basedir"); sub basedir (;$) { my $nocheckout=shift; IkiWiki::Hosting::readconfig(); my $customersite=$IkiWiki::Hosting::config{customersite}; if (! defined $customersite || ! length $customersite) { return; } my $dir=(getpwuid($<))[7]."/customersite"; return $dir if -d $dir; return if $nocheckout; # Try to checkout a copy of the customersite's git repo, # using ssh transport. my $username=IkiWiki::Hosting::getshell("ikisite", "username", $customersite); return unless defined $username && length $username; my $gitsshurl="ssh://$username\@$customersite/"; my $origumask=umask(077); eval {IkiWiki::Hosting::shell("git", "clone", $gitsshurl, $dir)}; umask($origumask); if ($@ || ! -d $dir) { # This is bad, try to make the admins aware of the problem. IkiWiki::Hosting::syslog("failed to clone $gitsshurl to $dir", "IkiWiki::Customer"); return; } else { return $dir; } } # Locks a given directory inside the customersite and executes the passed sub. sub dir_lock ($$$) { my $locktype=shift; my $dir=shift; my $sub=shift; if ($dir eq ".") { # To operate on the top directory, must take a global lock. globallock($locktype, $sub); } else { my $base=basedir(); return unless defined $base; # First take shared global lock, then individual # Directory lock. globallock(LOCK_SH, sub { _dolock($locktype, "$base/$dir", $sub); }); } } # Locks a directory for reading and executes the passed sub. sub dir_lock_read ($$) { my $dir=shift; my $sub=shift; dir_lock(LOCK_SH, $dir, $sub); } # Locks a directory for writing and executes the passed sub. sub dir_lock_write ($$) { my $dir=shift; my $sub=shift; dir_lock(LOCK_EX, $dir, $sub); } # Takes a global lock of the whole customersite. This is used to # block things like git pushes that cannot respect per-customer locks. sub globallock ($$) { my $locktype=shift; my $sub=shift; my $base=basedir(); return unless defined $base; _dolock($locktype, $base, $sub); } my %locks; sub _dolock ($$$) { my $locktype=shift; my $dir=shift; my $sub=shift; # Taking an exclusive lock when a shared lock is already held would # deadlock. if ($locktype == LOCK_EX && exists $locks{$dir}[LOCK_SH]) { die "_dolock: deadlock detected while locking $dir for LOCK_EX"; } # Avoid taking a lock if an equivilant one is already held. my $lock; if (! exists $locks{$dir}[$locktype] && ! exists $locks{$dir}[LOCK_EX]) { mkpath($dir); open ($lock, ">", "$dir/lock") || die $!; flock($lock, $locktype) || die $!; $locks{$dir}[$locktype]=$lock; } my $ret=eval { $sub->() }; my $fail=$@; if ($lock) { close $lock; delete $locks{$dir}[$locktype]; } if ($fail) { die $fail; } return $ret; } # Returns a field from a specified directory of the customersite. sub dir_get ($$) { my $dir=shift; my $field=shift; my @ret; dir_lock(LOCK_SH, $dir, sub { my $base=basedir(); return unless defined $base; if (open (my $fh, "<", "$base/$dir/$field")) { @ret=map { chomp $_; $_ } <$fh>; close $fh; } }); return wantarray ? @ret : $ret[0]; } # Sets a field in a specified directory of the customersite. sub dir_set ($$@) { my $dir=shift; my $field=shift; my @value=@_; dir_lock(LOCK_EX, $dir, sub { my $base=basedir(); return unless defined $base; open (my $fh, ">", "$base/$dir/$field") || die $!; print $fh join("\n", @value)."\n"; close $fh || die $!; }); } # Commits all changes made to a directory of the customersite # into the git repository. sub dir_commit ($;$) { my $dir=shift; my $message=shift; my $base=basedir(); return unless defined $base; eval q{use Cwd}; die $@ if $@; my $origdir=getcwd(); chdir("$base/$dir") || die $!; dir_lock(LOCK_EX, $dir, sub { IkiWiki::Hosting::shell("git", "add", "."); # empty commits needed in case no changes were really made IkiWiki::Hosting::shell("git", "commit", "--allow-empty", "-m", "updated $dir". (defined $message ? "\n\n$message" : "")); }); # Pushing is done in the background. Do not chdir daemon to / my $pid=IkiWiki::Hosting::daemonize(1); if ($pid) { chdir($origdir); return; } # To avoid deadlocks, this background process needs to drop all # possible locks. # Drop all existing locks on the customerdb. foreach my $key (keys %locks) { foreach my $lock (@{$locks{$key}}) { close $lock if defined $lock; } } # Drop all ikiwiki locks. IkiWiki::enable_commit_hook(); IkiWiki::unlockwiki(); eval {IkiWiki::Hosting::shell("git", "push", "origin", "master")}; if ($@) { # A push is most likely to fail due to being diverged from # origin. Or it could be a network problem. Try to update. if (update()) { # Update worked; try again to push. eval {IkiWiki::Hosting::shell("git", "push", "origin", "master")}; if ($@) { IkiWiki::Hosting::syslog("git push origin master failed", "IkiWiki::Customer"); } } } exit; } # Updates the whole customersite by doing a git pull. # Avoids leaving the site in a conflicted state. sub update () { my $base=basedir(1); return unless defined $base; eval q{use Cwd}; die $@ if $@; my $origdir=getcwd(); chdir($base) || die $!; # See if the origin can be contacted, by trying to fetch from it. eval {IkiWiki::Hosting::shell("git", "fetch", "origin")}; if ($@) { # This is sorta bad, though it may be a transient # problem. Try to make the admins aware of the problem. IkiWiki::Hosting::syslog("failed to fetch customersite ($@)", "IkiWiki::Customer"); chdir($origdir); return 0; } # Global lock, because git merge can touch any files. globallock(LOCK_EX, sub { eval {IkiWiki::Hosting::shell("git", "merge", "origin", "master")}; if ($@) { # Blast! A conflict. For now, resolving it is # beyond us, so whine for mommy to help, and # for now, reset our working copy so conflict # markers don't screw things up! IkiWiki::Hosting::syslog("customersite is in a conflicted state, please fix ($@)", "IkiWiki::Customer"); eval {IkiWiki::Hosting::shell("git", "reset", "--hard")}; if ($@) { # Should never happen. IkiWiki::Hosting::syslog("failed to use git reset --hard; conflict markers present in customersite! ($@)", "IkiWiki::Customer"); } chdir($origdir); return 0; } chdir($origdir); return 1; }); } # Converts an openid into a sha1 hash. sub customerid ($) { my $openid=shift; eval q{use Digest::SHA}; die $@ if $@; return Digest::SHA::sha1_hex($openid); } # Returns the directory used to store information about a particular customer, # given their openid. # # A directory matching the customerid of their openid is preferred, but # failing that, it searches for a directory with an openid_list containing # the openid. memoize("customerdir"); sub customerdir ($) { my $openid=shift; my $base=basedir(); return unless defined $base; my $dir="customer/".customerid($openid); return $dir if -d "$base/$dir"; foreach my $d (glob("$base/customer/*")) { my @openids=read_list_file($d, "openid_list"); if (grep { $_ eq $openid } @openids) { $d=~s/^\Q$base\/\E//; return $d; } } return $dir; } # Finds openids that contain a given field with the specified value. sub openids_by_field ($$;$) { my $field=shift; my $value=shift; my $ignorecase=shift; if ($ignorecase) { $value=lc $value; } my $base=basedir(); return () unless defined $base; my @ret; foreach my $d (glob("$base/customer/*")) { my @vals=read_list_file($d, $field); if (grep { ($ignorecase ? lc $_ : $_) eq $value } @vals) { my @openids=read_list_file($d, "openid_list"); push @ret, $openids[0] if @openids; } } return @ret; } # Reads a file as a list of multiple lines. # # XXX Currently the file is not locked, so there is a small potential # for a race here. There is some potential for deadlock if locking the # file here. sub read_list_file ($$) { my $dir=shift; my $file=shift; my @list; if (open (my $fh, "<", "$dir/$file")) { @list=grep { length $_ } map { chomp $_; $_ } <$fh>; close $fh; } return @list; } # Locks a customer for reading and executes the passed sub. sub customer_lock_read ($$) { my $openid=shift; my $sub=shift; dir_lock(LOCK_SH, customerdir($openid), $sub); } # Locks a customer for writing and executes the passed sub. sub customer_lock_write ($$) { my $openid=shift; my $sub=shift; dir_lock(LOCK_EX, customerdir($openid), $sub); } # Returns a given field of a customer's data, given their openid. sub customer_get ($$) { my $openid=shift; my $field=shift; dir_get(customerdir($openid), $field); } # Sets a field of a customer's data. sub customer_set ($$@) { my $openid=shift; my $field=shift; my @value=@_; dir_set(customerdir($openid), $field, @value); } # A customer may have multiple openids, given one this returns # the whole list. sub openid_list ($) { my $openid=shift; my @openids=read_list_file(basedir()."/".customerdir($openid), "openid_list"); push @openids, $openid; # in case the list doesn't exist yet return @openids; } # Commits all changes made to a customer into the git repository. sub customer_commit ($;$) { my $openid=shift; my $message=shift; dir_commit(customerdir($openid), $message); } # Sends an email to a customer (or to stdout in dry run mode). # The message body is parsed for Subject and From lines. (Also optionally, # for To, Bcc, and References lines.) sub customer_sendmail ($$;$) { my $openid=shift; my $message=shift; my $dry_run=shift; $message=~s/^(?:To: (.*))//m; my $to=$1; $message=~s/^(?:From: (.*))//m; my $from=$1; $message=~s/^(?:Bcc: (.*))//m; my $bcc=$1; $message=~s/^(?:Subject: (.*))//m; my $subject=$1; $message=~s/^(?:References: (.*))//m; my $references=$1; # avoid extra blank lines from HTML::Template $message=~s/\n\n\n/\n\n/sg; $message=~s/^\n+//s; $message=~s/\n+$/\n/s; $to=customer_get($openid, "email") if ! defined $to || ! length $to; return unless defined $to && length $to; if ($dry_run) { print "(Not sending email; in dry run mode.)\n"; print "\n"; print "To: $to\n"; print "From: $from\n"; print "Bcc: $bcc\n" if defined $bcc && length $bcc; print "References: $references\n" if defined $references && length $references; print "Subject: $subject\n"; print "\n"; print $message."\n\n"; } else { eval q{use Mail::Sendmail}; error $@ if $@; my $ret=sendmail( To => $to, From => $from, ((defined $bcc && length $bcc) ? (Bcc => $bcc) : ()), ((defined $references && length $references) ? (References => $references) : ()), Subject => $subject, Message => $message, ); if (! $ret) { die "failed to send email: $!"; } } } # Used by programs that must run as a user which has a # ~/customersite checkout. sub assert_has_customersite () { my $base=basedir(1); if (! defined $base) { die "$0 requires ~/customersite checkout exist\n"; } } 1 ikiwiki-hosting/IkiWiki/Setup/0000755000000000000000000000000012211430070013503 5ustar ikiwiki-hosting/IkiWiki/Setup/HostingAutomator.pm0000644000000000000000000000064412211430070017354 0ustar #!/usr/bin/perl package IkiWiki::Setup::HostingAutomator; use warnings; use strict; use YAML; sub import { my $class=shift; my $setup=shift; # Expand embedded $ENV{foo}, escaping single quotes for YAML. $setup=~s{\$ENV{(.*?)}}{ my $v=$ENV{$1}; $v=~s/\n//g; # should never have newlines $v=~s/'/''/g; $v; }eg; require IkiWiki::Setup::Automator; IkiWiki::Setup::Automator->import(%{Load($setup)}); } 1 ikiwiki-hosting/IkiWiki/Business.pm0000644000000000000000000003431212232566773014566 0ustar #!/usr/bin/perl # stub business logic package IkiWiki::Business; use warnings; use strict; use IkiWiki::Hosting; our $tax=0.00; # for 1%, use 0.01 # Pricing plans may be defined here to enable the business logic. # # Example of a plan: # #basic => { # name => "Default", # description => '$9.99 per month for 1 site (first month free)', # permonth => 999, # cents # peryear => 5999, # cents # maxsites => 1, # upgradefor => [], # monthsfreetrial => 1, # active => 1, # default => 1, # sendbills => 1, # }, # # Note that the description field should not exceed 47 characters. # # Plans should never be deleted from this list when users are still # using them. If a plan is changed, and existing users need to be # grandfathered in under the old terms, a new plan should instead be # added with the new terms, and the old plan left, flagged as not active. # # Only one plan can be the default plan, and only active plans should # have upgradefor set -- this allows the interface to select active plans # for new users, and allow users to upgrade to other active plans. my %plans=( # None are defined by default, so price plan stuff is disabled. ); # Given a plan name, returns its info hash. sub planinfo ($) { my $plan=shift; return $plans{$plan}; } # Based on the number of sites a user has, return their # suggested best-fit price plan. # # The optional second parameter is a site name that should # be assumed to exist whether or not it really does. # (Maybe this site is being created.) sub suggestedplan ($;$) { my $openid=shift; my $assumesite=shift; return undef unless %plans; my $currentsites=currentsites($openid, $assumesite); my @candidate_plans=sortprice(grep { $plans{$_}{active} && $plans{$_}{maxsites} >= $currentsites } upgradeset($openid, 1)); return $candidate_plans[0] if @candidate_plans; # just in case, fallback to the default plan. foreach my $plan (keys %plans) { return $plan if $plans{$plan}{default} && $plans{$plan}{active}; } die "internal error; no default price plan!"; } # Returns true if the user needs to change from their current plan # to cover their sites. (Of if the user has no current plan.) sub needchangeplan ($;$) { my $openid=shift; my $assumesite=shift; return 0 unless %plans; my $currentsites=currentsites($openid, $assumesite); my $currentplan=currentplan($openid); return 1 unless defined $currentplan; if ($plans{$currentplan}{maxsites} < $currentsites) { return 1; } else { return 0; } } # Returns the current price plan for a user. # Returns undef if the user is not currently enrolled in a plan. sub currentplan ($) { my $openid=shift; return undef unless %plans; eval q{use IkiWiki::Customer}; die $@ if $@; my $plan=IkiWiki::Customer::customer_get($openid, "currentplan"); return $plan; } # Returns a list of sites a user currently owns. # In a scalar context, returns the count of sites owned. my $currentsites_cache; my $currentsites_cache_owner; sub currentsites ($;$) { my $openid=shift; my $assumesite=shift; my @owned; if (defined $currentsites_cache_owner && $currentsites_cache_owner eq $openid) { @owned=@$currentsites_cache; } else { # A single user may have multiple openids. eval q{use IkiWiki::Customer}; die $@ if $@; my @openids=IkiWiki::Customer::openid_list($openid); @owned=split(' ', IkiWiki::Hosting::getshell("ikisite-wrapper", "list", map {"--owner=$_"} @openids)); $currentsites_cache=\@owned; $currentsites_cache_owner=$openid; } if (defined $assumesite) { @owned=((grep { $_ ne $assumesite } @owned), $assumesite); } return @owned; } # Given a user, returns how many additional sites they can make, under # their current plan. sub sitesavailable ($) { my $openid=shift; return undef unless %plans; my $currentplan=currentplan($openid); return 0 unless defined $currentplan; my $maxsites=$plans{$currentplan}{maxsites}; my @currentsites=currentsites($openid); if (@currentsites > $maxsites) { # could happen if user's plan was downgraded, or something return 0; } else { return $maxsites - @currentsites; } } # Given a user, returns the set of plans the user could upgrade to. sub upgradeset ($;$) { my $openid=shift; my $includecurrent=shift; return () unless %plans; my $currentplan=currentplan($openid); if (! defined $currentplan) { return keys %plans; } my @ret; foreach my $plan (keys %plans) { next if $plan eq $currentplan && ! $includecurrent; if ($plan eq $currentplan || grep { $_ eq $currentplan } @{$plans{$plan}{upgradefor}}) { push @ret, $plan; } } return @ret; } # Given a list of plans, sorts them by price. sub sortprice (@) { sort { $plans{$a}{permonth} <=> $plans{$b}{permonth}, } @_ } # Returns a date a certian number of months from the passed date. # The date will be the same day of the month, when possible. sub monthfrom ($;$) { my $date=shift; my $months=shift || 1; eval q{use DateTime; use DateTime::Duration}; die $@ if $@; my $d=DateTime->from_epoch(epoch => $date); my $offset=DateTime::Duration->new(months => $months, end_of_month => "preserve"); return $d->add_duration($offset)->epoch; } # Returns when the next bill should be sent for a user. sub nextbilldate ($) { my $openid=shift; eval q{use IkiWiki::Customer}; die $@ if $@; my $bill_enddate=IkiWiki::Customer::customer_get($openid, "bill_enddate"); if (! defined $bill_enddate) { # no bill sent yet; one should be sent immediatly return time; } # Next bill is normally sent just after the end of the billing # period for the previous bill. return $bill_enddate; } # Checks if it's time to send a reminder bill to a user. sub billreminderdue ($) { my $openid=shift; eval q{use IkiWiki::Customer}; die $@ if $@; my $balance=IkiWiki::Customer::customer_get($openid, "balance"); if (! defined $balance || $balance <= 0) { return 0; } my $lastbilldate=IkiWiki::Customer::customer_get($openid, "lastbilldate"); if (! defined $lastbilldate) { # Customer has never been billed, so no reminder is due. return 0; } # Currently reminder bills are hardcoded 1 week after original bill. eval q{use DateTime; use DateTime::Duration}; die $@ if $@; my $d=DateTime->from_epoch(epoch => $lastbilldate); my $offset=DateTime::Duration->new(weeks => 1); my $when=$d->add_duration($offset)->epoch; return 0 if time < $when; # not due yet my $lastreminderdate=IkiWiki::Customer::customer_get($openid, "lastreminderdate"); if (defined $lastreminderdate && $lastreminderdate >= $when) { # Bill reminder has already been sent. return 0; } return 1; } sub curryear () { return (gmtime(time))[5]+1900; } sub currmonth () { return (gmtime(time))[4]+1; } # Generates and returns a new, unique invoice id. # # Invoice ids are of the form: BR-001/2010; # the invoice numbering resets at the start of each year. sub geninvoiceid ($;$) { my $openid=shift; my $dry_run=shift; my $invoice = IkiWiki::Customer::dir_get("invoice", "last"); my ($prefix, $num, $year)=$invoice=~/^(.*?)-(\d+)\/(\d+)$/ if defined $invoice; if (! defined $num || ! defined $year || $year ne curryear()) { $year=curryear(); $num=0; } if (! defined $prefix) { $prefix='BR'; } $invoice=sprintf("%s-%03i/%s", $prefix, $num+1, $year); $invoice.="-dryrun" if $dry_run; if (! $dry_run) { IkiWiki::Customer::dir_lock_write("invoice", sub { IkiWiki::Customer::dir_set("invoice", "last", $invoice); IkiWiki::Customer::dir_commit("invoice"); }); } return $invoice; } # Saves an invoice for a customer for accountant review. # # Invoice BR-XXX/YYYY is stored in the customersite, in the file # invoice/YYYY-MM/BR-XXX. sub saveinvoice ($$$$) { my $openid=shift; my $invoicetext=shift; my $dry_run=shift; my $overwrite=shift; return if $dry_run; my $invoiceid=IkiWiki::Customer::customer_get($openid, "lastinvoiceid"); if (! defined $invoiceid) { die "saveinvoice: failed to find lastinvoiceid"; } my ($year) = $invoiceid =~ m/\/(\d\d\d\d)$/; $invoiceid =~ s/\/$year$//; my $month = currmonth(); my $dir="invoice/$year-$month"; if (! $overwrite) { my $old = IkiWiki::Customer::dir_get($dir, $invoiceid); if (defined $old && length $old) { # Don't overwrite the first copy of an invoice that is # sent. # Assumes that later copies do not change any relevant # information for accounting. (Ie, that the price, # dates, etc are stable.) return 0; } } IkiWiki::Customer::dir_lock_write($dir, sub { IkiWiki::Customer::dir_set($dir, $invoiceid, $invoicetext); IkiWiki::Customer::dir_commit($dir); }); return 1; } # Bills a user for the next month of service, updating their balance # appropriately. Suitable to be called from a daily (or more frequent) # cron job. # # The passed template is filled out with info about the bill. If the user # is in fact billed (or needs to be sent a reminder), the template will # be returned. Returns undef if no billing is currently needed for a # user. Throws an error if the customer should be billed, but cannot # due to some problem. sub bill ($$;$) { my $openid=shift; my $template=shift; my $dry_run=shift; eval q{use IkiWiki::Customer}; die $@ if $@; return if IkiWiki::Customer::customer_get($openid, "disable_billing"); $template->param(openid => $openid); foreach my $field (qw{name email}) { my $val=IkiWiki::Customer::customer_get($openid, $field); unless (defined $val && length $val) { die "cannot bill $openid: missing $field\n"; } $template->param($field => $val); } my $nextbilldate=nextbilldate($openid); my $plan; my $trial=0; if ($nextbilldate <= time) { # Time for a new bill. my $now=time; my $startdate = IkiWiki::Customer::customer_get($openid, "startdate"); my $lastbilldate = IkiWiki::Customer::customer_get($openid, "lastbilldate"); my $lastbill_enddate = IkiWiki::Customer::customer_get($openid, "bill_enddate"); $plan = currentplan($openid); if (! defined $startdate || ! defined $plan) { die "cannot bill $openid; missing startdate or currentplan\n"; } my $new=0; if (monthfrom($startdate) >= $now) { $template->param(new => 1); $new=1; } my $bill_startdate=($lastbill_enddate || $startdate); my $bill_enddate=monthfrom($bill_startdate); if ($bill_enddate <= $now) { $bill_enddate=monthfrom($now); } $template->param(bill_startdate => $bill_startdate); $template->param(bill_enddate => $bill_enddate); $template->param(lastbilldate => $now); my $invoiceid; my $balance=IkiWiki::Customer::customer_get($openid, "balance") || 0; my $origbalance=$balance; if ($plans{$plan}{monthsfreetrial} && monthfrom($startdate, $plans{$plan}{monthsfreetrial}) > $nextbilldate) { $template->param(trial => 1); $trial=1; } else { $balance+=$plans{$plan}{permonth}; if ($plans{$plan}{permonth} > 0) { $invoiceid=geninvoiceid($openid, $dry_run); $template->param(invoiceid => $invoiceid); } } if (! $dry_run) { IkiWiki::Customer::customer_lock_write($openid, sub { IkiWiki::Customer::customer_set($openid, "previous_balance", $origbalance); IkiWiki::Customer::customer_set($openid, "balance", $balance); IkiWiki::Customer::customer_set($openid, "lastbilldate", $now); IkiWiki::Customer::customer_set($openid, "lastplan", $plan); if (defined $invoiceid) { IkiWiki::Customer::customer_set($openid, "lastinvoiceid", $invoiceid); } IkiWiki::Customer::customer_set($openid, "bill_startdate", $bill_startdate); IkiWiki::Customer::customer_set($openid, "bill_enddate", $bill_enddate); IkiWiki::Customer::customer_commit($openid); }); } } elsif (billreminderdue($openid)) { # Time for a reminder bill. $template->param(reminder => 1); $template->param(bill_startdate => IkiWiki::Customer::customer_get($openid, "bill_startdate")); $template->param(bill_enddate => IkiWiki::Customer::customer_get($openid, "bill_enddate")); $template->param(lastbilldate => IkiWiki::Customer::customer_get($openid, "lastbilldate")); $template->param(invoiceid => IkiWiki::Customer::customer_get($openid, "lastinvoiceid")); if (! $dry_run) { IkiWiki::Customer::customer_lock_write($openid, sub { IkiWiki::Customer::customer_set($openid, "lastreminderdate", time); IkiWiki::Customer::customer_commit($openid); }); } $plan = IkiWiki::Customer::customer_get($openid, "lastplan"); } else { return undef; } return undef if ! $plans{$plan}{sendbills}; $template->param(plan_name => $plans{$plan}{name}); $template->param(plan_description => $plans{$plan}{description}); $template->param(plan_maxsites => $plans{$plan}{maxsites}); my $price=$plans{$plan}{permonth}; if (defined $price && $price > 0) { $price=0 if $trial; my $price_tax=calculate_tax($price); $template->param(permonth_tax => show_dollars($price_tax)); $template->param(permonth_pretax => show_dollars($price - $price_tax)); $template->param(permonth_total => show_dollars($price)); $template->param(tax_percent => $tax * 100); my $yprice=$plans{$plan}{peryear}; $template->param(peryear_total => show_dollars($yprice)); } return balanceinfo($openid, $template); } # The passed template is filled out with info about the user's balance, # and returned. sub balanceinfo ($$) { my $openid=shift; my $template=shift; foreach my $field (qw{balance previous_balance}) { my $value=IkiWiki::Customer::customer_get($openid, $field); next unless defined $value; $template->param("has_$field" => 1); $template->param($field => show_dollars(abs($value))); $template->param("zero_$field" => ($value == 0)); $template->param("negative_$field" => ($value < 0)); $template->param("positive_$field" => ($value > 0)); } return $template; } sub show_dollars ($) { my $cents=shift; my $dollars=int($cents/100); my $remainder=abs($cents-($dollars*100)); return sprintf("%01i.%02i", $dollars, $remainder); } # Given a total price in cents, returns the number of cents of that # price that are tax. # # XXX Fractional cents are rounded up, to ensure that we don't pay # slightly too little tax. An accountant could probably improve the # rounding to avoid us paying too much tax. sub calculate_tax ($) { my $price=shift; return int($price * $tax + 0.9); # int(foo+0.9) = ceil(foo) } # Short, human-readable date. sub shortdate ($) { my $date=shift; POSIX::setlocale(&POSIX::LC_TIME, "C"); return POSIX::strftime("%d %B %Y", gmtime($date)); } 1 ikiwiki-hosting/IkiWiki/Hosting.pm0000644000000000000000000002342612211430070014363 0ustar #!/usr/bin/perl package IkiWiki::Hosting; use warnings; use strict; use Getopt::Long; use IkiWiki; $config{ikisite_conffile}="/etc/ikiwiki-hosting/ikiwiki-hosting.conf"; sub assert_user { my $username=shift; my $uid=getpwnam($username); if (! defined $uid || $uid != $>) { error "this must be ran as user $username"; } } sub assert_root { assert_user("root"); } sub runas { my $username=shift; my $sub=shift; my ($uid, $gid, $homedir) = (getpwnam($username))[2, 3, 7]; if (! defined $uid || ! defined $gid) { error "cannot determine uid/gid for $username"; } if ($uid == $>) { return $sub->(); } eval q{use YAML::Syck}; die $@ if $@; $YAML::Syck::ImplicitUnicode=1; my $pid = open(RUNAS_CHILD, "-|"); if (! defined $pid) { error "Can't fork: $!"; } if (! $pid) { $)="$gid $gid"; $(=$gid; $<=$uid; $>=$uid; if ($< != $uid || $> != $uid || $) ne "$gid $gid" || $( != $gid) { error "failed to drop permissions to $username"; } $ENV{HOME}=$homedir; $ENV{USER}=$username; eval q{use POSIX 'setsid'}; die $@ if $@; if (setsid() == -1) { die "setsid: $!"; } print Dump($sub->()); exit 0; } else { local $/=undef; my $child_yaml=; if (! close(RUNAS_CHILD)) { if ($? >> 8 != 0) { exit $? >> 8; } else { exit 127; } } return Load($child_yaml); } } sub ikisite_template { my $templatefile=shift; readconfig(); require HTML::Template; my $template=HTML::Template->new( loop_context_vars => 1, die_on_bad_params => 0, filename => $config{ikisite_templatedir}."/".$templatefile, ); $template->param(%config, @_); return $template; } sub outtemplate { my $outfile=shift; my $templatefile=shift; debug("expand $templatefile -> $outfile"); my $template=ikisite_template($templatefile, @_); writefile($outfile, "", $template->output); } sub site_addresses { # Looks up the site's main addresses; those that have a default # route. if ($config{hardcode_ipv4} && $config{hardcode_ipv6}) { return ($config{hardcode_ipv4}, $config{hardcode_ipv6}); } my ($defiface) = `ip route` =~ m/^default via .* dev ([a-zA-Z0-9]+)/m; if (! $defiface) { # Maybe ipv6 only. ($defiface) = `ip -6 route` =~ m/^default via .* dev ([a-zA-Z0-9]+)/m; if (! $defiface) { error("cannot see default route, so failed to determine host IP address"); } } my $show=`ip addr show $defiface`; my ($ipv4) = $show =~ m/inet (\d+\.\d+\.\d+\.\d+).*scope global/m; my ($ipv6) = $show =~ m/inet6 ([a-zA-Z0-9:]+).*scope global/m; if ($config{hardcode_ipv4}) { $ipv4 = $config{hardcode_ipv4}; } if ($config{hardcode_ipv6}) { $ipv6 = $config{hardcode_ipv6}; } return ($ipv4, $ipv6); } sub host_address_or_cname { # Looks up a host in the DNS, and returns the (first) IP address, # or a desired CNAME, or undef if neither is found. my $host=shift; my $wanted_cname=lc(shift); # dig +trace is used to examine the DNS in order to avoid # cached NXDOMAIN from earlier tests. We want to see the current # state of the DNS. # dig has to be run a second time to check for ipv6 AAAA addresses. foreach my $atype (qw{A AAAA}) { open(DIG, "-|", "dig", "-t$atype", "+nofail", "+trace", "$host.") || error "dig: $!"; while () { next if /^;/; chomp; if (defined $wanted_cname && /\s+\d+\s+IN\s+CNAME\s+(.*)\.$/ && lc($1) eq $wanted_cname) { close DIG; return $wanted_cname; } elsif (/\s+\d+\s+IN\s+(?:A|AAAA)\s+(.*)$/) { close DIG; return $1; } } close DIG; } return undef; } sub shell_exitcode { my $command=shift; # redirect all shell output to stderr to avoid polluting # the command's own output open(SAVEDOUT, ">&STDOUT"); open(STDOUT, ">&STDERR"); debug(join(" ", $command, @_)); my $ret=system($command, @_); open(STDOUT, ">&SAVEDOUT"); close(SAVEDOUT); return $ret } sub shell { my $command=shift; my $ret=shell_exitcode($command, @_); if ($ret ne 0) { error "$command failed"; } return 1; } sub getshell { my $command=shift; if (! open (DOSHELL, "-|", $command, @_)) { $?=1; return; } my $out; { local $/=undef; $out=; } if (close DOSHELL) { $?=0; } else { $?=1; } chomp $out; return $out; } sub yamlgetshell { my $yaml=getshell(@_)."\n"; my $status=$?; eval q{use YAML::Syck}; die $@ if $@; $YAML::Syck::ImplicitUnicode=1; my $parsed=Load($yaml); $?=$status; return $parsed; } my $config_read; sub readconfig { return if $config_read; $config_read=1; open(IN, "<", $config{ikisite_conffile}) || error "$config{ikisite_conffile}: $!"; while () { chomp; s/^\s*//; s/^#.*//; next unless length; my ($key, $value)=split("=", $_, 2); if (! defined $key || ! length $key || ! defined $value) { error "parse error in $config{ikisite_conffile}: $_"; } if ($value=~/^"(.*)"/) { $value=$1; } elsif ($value=~/^'(.*)'/) { $value=$1; } $config{$key}=$value; } close IN; } sub get_subcommand_meta { my $subcommand=shift; my $symbol="meta_$subcommand"; return undef unless exists $IkiWiki::Hosting::{$symbol}; no strict 'refs'; return { $symbol->(), subcommand => $subcommand, } } sub get_subcommands_meta { my @ret; foreach my $symbol (sort keys %IkiWiki::Hosting::) { my ($subcommand)=$symbol=~/^meta_(.*)/; next unless defined $subcommand; push @ret, get_subcommand_meta($subcommand); } return @ret; } sub sorted_subcommands_meta { sort { $a->{section} cmp $b->{section} || $a->{subcommand} cmp $b->{subcommand} } get_subcommands_meta() } sub usage_subcommand { my $subcommand=shift; my $message=shift; my $standalone=!shift; print STDERR "$message\n\n" if $message; my $meta=get_subcommand_meta($subcommand); if ($standalone) { print STDERR "usage: $0 $meta->{subcommand} "; } else { print STDERR "\t$meta->{subcommand} "; } foreach my $required (@{$meta->{required}}) { print STDERR "$required "; } foreach my $option (@{$meta->{options}}) { if ($option eq '...') { print STDERR "$option "; next; } # convert Getopt::Long to human readable option unless ($option=~s/=\w$/=value/) { $option=~s/=/ /; } $option=~s/!$//; print STDERR "[--$option] "; } print STDERR "\n\t\t$meta->{description}\n"; exit 1 if $standalone; } sub usage { print STDERR "@_\n\n" if @_; print STDERR "usage: $0 [--config=file] [--verbose] subcommand [options]\n"; my $section=""; foreach my $meta (sorted_subcommands_meta()) { if ($section ne $meta->{section}) { $section=$meta->{section}; print STDERR "\n$section subcommands:\n"; } usage_subcommand($meta->{subcommand}, "", 1); } exit 1; } # used for free-form logging my $syslog_open; sub syslog { my $message=shift; my $subcommand=shift; eval q{use Sys::Syslog}; error $@ if $@; unless ($syslog_open) { Sys::Syslog::setlogsock('unix'); openlog($0, '', 'user'); $syslog_open=1; } syslog('debug', "$subcommand: $message"); } sub dispatch { my $logger=shift; my $subcommand=shift; my @options=@_; # run subcommand, handle result $logger->("started", $subcommand, @options); no strict "refs"; my @result=eval { $subcommand->(@options); }; if ($@) { my $err=$@; $logger->("error: $err", $subcommand, @options); error "error: $err"; } elsif (ref($result[0]) eq 'IkiWiki::FailReason') { $logger->("failure: @result", $subcommand, @options); print "@result\n"; exit 1; } elsif (ref($result[0]) eq 'IkiWiki::SuccessReason') { $logger->("success: @result", $subcommand, @options); print "@result\n"; exit 0; } elsif (@result && defined $result[0]) { $logger->("success: @result", $subcommand, @options); if (! ref $result[0]) { print join("\n", @result)."\n"; } else { eval q{use YAML::Syck}; error $@ if $@; print Dump(@result); } exit 0; } else { $logger->("success", $subcommand, @options); exit 0; } } sub daemonize (;$) { my $nochdir=shift; eval q{use POSIX 'setsid'}; die $@ if $@; defined(my $pid = fork) or die "fork: $!"; return $pid if $pid; if (! $nochdir) { chdir '/' or die "chdir: $!"; } open STDIN, '/dev/null' or die "/dev/null: $!"; open STDOUT, '>/dev/null' or die "/dev/null: $!"; open STDERR, '>/dev/null' or die "/dev/null: $!"; if (setsid() == -1) { die "setsid: $!"; } return 0; } sub main { my $logger=shift || \&syslog; # work out which argument is the subcommand my $subcommand; for (my $i=0; $i <= $#ARGV; $i++) { if ($ARGV[$i]=~/^\w+$/) { $subcommand=splice(@ARGV, $i, 1); last; } } usage() unless $subcommand; my $meta=get_subcommand_meta($subcommand); usage("unknown subcommand \"$subcommand\"") unless $meta; # option processing my %options; my @getopts; for (my $x=0; $x < @{$meta->{options}}; $x++) { my $option=$meta->{options}->[$x]; if ($option=~/([^=]*)=(\w\w+)/) { $option="$1=s"; } my $dest=$option; $dest=~s/=.*//; $dest=~s/!$//; if ($x+1 < @{$meta->{options}} && $meta->{options}->[$x+1] eq '...') { # option can be repeated push @getopts, ($option => sub { push @{$options{$dest}}, $_[1] }); $x++; } else { push @getopts, ($option => \$options{$dest}); } } my $ret=GetOptions(@getopts, "config=s" => \$config{ikisite_conffile}, "v|verbose!" => \$config{verbose}, ); if (! $ret) { usage_subcommand($subcommand); } # getopt sets option keys to undef if the option is not specified; # remove instead foreach my $key (keys %options) { delete $options{$key} if ! defined $options{$key}; } # remainder of @ARGV is required parameters my @required; foreach my $required (@{$meta->{required}}) { if ($required eq "...") { push @required, @ARGV; @ARGV=(); } else { push @required, shift @ARGV || usage_subcommand($subcommand, "missing $required"); } } if (@ARGV) { usage_subcommand($subcommand, "unknown parameter(s): @ARGV"); } readconfig(); dispatch($logger, $subcommand, @required, %options); } 1 ikiwiki-hosting/doc/0000755000000000000000000000000012232570343011623 5ustar ikiwiki-hosting/doc/design.mdwn0000644000000000000000000000374612232566773014011 0ustar Each site has a hostname, which is a subdomain of one of our domains. For example, foo.branchable.com. Each site has an owner, which is an openid. Typically the admin is set to the same openid as the owner, but a site may have other admins who do not own it, and it's also possible for its owner not to be an admin at all. A site may also have one or more aliases; domain names that CNAME to its hostname. The primary url of a site uses either its hostname, or one of its aliases. Ikiwiki is configured to use this in its url and cgiurl. From the hostname, a unique [[username]] is derived. Ikiwiki runs as that user, and data files associated with the site are stored in the user's home directory. Outside the home directory, runtime files for the site are managed in `/var/www/`, `/var/log/ikiwiki-hosting/`, and [[apacheconfig]]. The home directory has these contents: * `public_html` - the site's html is here (but its cgis are created in /var/www so suidexec can be used) * `source.git` - bare git repo (or similar for other VCS) * `source` - checkout of the wiki's source * `ikiwiki.setup` - config file for the site * `apache` - directory contains optional apache configs ssl certs, etc) * `tmp` - per-user temp directory The home directory *itself* is a checkout of a different branch of the VCS repository, the "setup" branch. Into that branch are checked the `ikiwiki.setup` file and other configuration and state files. (But not ssh keys, htpassword files, etc.) Remote users may not directly commit to that branch. The [[ikisite]] program handles creating, deleting, and other administration of these sites. A [[backupformat]] encapsulates the above into a single file for backups and transfer. A [[customersite]] holds information about customers. ## locking ikisite needs to lock sites as it is creating, deleting, or modifying them, to prevent concurrency issues. This is done using a directory in `/var`, the `lockdir`. The lock files in the lockdir correspond to each site's username. ikiwiki-hosting/doc/bugs/0000755000000000000000000000000012232570331012560 5ustar ikiwiki-hosting/doc/bugs/ikisite-wrapper_fails_to_build_on_osx_10.6.7.mdwn0000644000000000000000000000355112211430070024017 0ustar I decided to just give ikiwiki-hosting a spin on my mac for evaluation purposes for now, and it currently fails to build out of the box for me, here's a small patch to let it build and the tests pass as well, but it is not a good and general clearenv() subsitute. I've yet to experiment with the software. > I'm a bit surprised/confused by this being necessary, since ikiwiki > itself uses clearenv in its wrapper. Why does the wrapper's C code > that runs clearenv apparently work on OSX, but not the code here? > > Ah, I see, ikiwiki only uses clearenv in tcc mode, while manipulating > environ directly otherwise, and that is said to work on freebsd, so > probably also OSX. I've done the same thing here now, let me know if it > doesn't work. [[done]] --[[Joey]]
From eee70a464c9a577f69b0e3a1cafec579c398eb12 Mon Sep 17 00:00:00 2001
From: Jimmy Tang 
Date: Sun, 29 May 2011 16:36:45 +0100
Subject: [PATCH] clearenv() is not available on OSX 10.6.7

This patch is offers a simple clearenv alternative on OSX, and is
directly taken from

http://svn.deepdarc.com/code/miredo-osx/tags/prerelease-1/miredo/compat/clearenv.c

It is only used when built on OSX.
---
 ikisite-wrapper.c |   15 +++++++++++++++
 1 files changed, 15 insertions(+), 0 deletions(-)

diff --git a/ikisite-wrapper.c b/ikisite-wrapper.c
index f966515..8b64468 100644
--- a/ikisite-wrapper.c
+++ b/ikisite-wrapper.c
@@ -18,7 +18,13 @@
 #include 
 #include 
 
+#ifdef __APPLE__
+# include 
+# define environ (*_NSGetEnviron())
+int clearenv(void);
+#else
 extern char **environ;
+#endif
 
 int main (int argc, char **argv) {
        int i;
@@ -78,3 +84,12 @@ int main (int argc, char **argv) {
        perror(args[0]);
        exit(1);
 }
+
+
+#ifdef __APPLE__
+int clearenv(void)
+{
+       *environ = NULL;
+       return 0;
+}
+#endif
-- 
1.7.5.2
ikiwiki-hosting/doc/bugs/branching_broken.mdwn0000644000000000000000000000031612211430070016732 0ustar Noticed some breakage in handling branching of existing sites. * Branching an external site took over its internal dns. * Some things in ikiwiki.setup remained set to values for the parent site. [[done]] ikiwiki-hosting/doc/bugs/alternative_home_basedir.mdwn0000644000000000000000000000441212211430070020457 0ustar I don't enable suid bits on /home on at least one system where I'd like to run ikiwiki-hosting, so for those things that need suid I create their homes on another mount (/home/suid/). That being the case, and since there are bits of the created ikiwiki sites that need suid, I've knocked up this patch:
From 44c545ac28f641f462bfcaac328b62b50b1e4d09 Mon Sep 17 00:00:00 2001
From: Philip Hands 
Date: Thu, 24 Nov 2011 20:21:16 +0000
Subject: [PATCH] allow sites to be created in an alternative home basedir

---
 ikisite              |    3 +++
 ikiwiki-hosting.conf |    3 +++
 2 files changed, 6 insertions(+), 0 deletions(-)

diff --git a/ikisite b/ikisite
index 7678ea2..adc5df3 100755
--- a/ikisite
+++ b/ikisite
@@ -1291,9 +1291,12 @@ sub usercreate {
 	assert_wrapper_denied();
 
 	my $user=username($hostname);
+	my @useradd_opts = ( "--base-dir", $config{useradd_basedir} )
+		if ($config{useradd_basedir}) ;
 	shell("useradd", $user, 
 		"--create-home",
 		"--skel", "/dev/null", # disable /etc/skel files
+		@useradd_opts,
 	);
 
 	mkdir(rootconfig($hostname));
diff --git a/ikiwiki-hosting.conf b/ikiwiki-hosting.conf
index 0e54189..82952eb 100644
--- a/ikiwiki-hosting.conf
+++ b/ikiwiki-hosting.conf
@@ -65,6 +65,9 @@ keydir=/etc/ikiwiki-hosting/keys
 # Per-site lock files are stored in this directory.
 lockdir=/var/lib/ikiwiki-hosting-web/lock
 
+# This can be used to set an alternative home directory in which to create new sites
+#adduser_basedir=/some/other/home
+
 # Git daemon looks for vhosts in this directory.
 gitdaemondir=/var/lib/ikiwiki-hosting-web/git
 
-- 
1.7.2.5
Which does the job, but I'd imagine could be done more neatly, or with a better variable name. An alternative way of fixing the specific problem might be to be able to hive off the few suid needing files into their own path, so that the /home stuff didn't need any suid bits set anyway, but even then some other people may find it useful to segregate the homes of ikiwiki users from real users on their machines -[[fil]] > I've applied this patch. I don't see any benefit to splitting out the > suid bits from the rest of the home directory, and there's a very nice > benefit in being able to move the site home directories to someplace > other than /home. So, [[done]] --[[Joey]] ikiwiki-hosting/doc/bugs/wiki_creation_impossible.mdwn0000644000000000000000000000220612232566773020543 0ustar I have successfully created a few wikis with this in the past (on 15-12-2011 to be exact), but now this seems impossible. All I get is this error: [[!format txt """ root@marcos:~# env LANG=C ikisite --verbose create rococo.anarcat.ath.cx --admin https://id.koumbit.net/anarcat useradd a-rococo --create-home --skel /dev/null git config --global user.name admin fatal: failed to stat '.': Permission denied error: git failed """]] > Ah - it turns out that you can't run this command from a private directory (like `/root`) as ikiwiki calls `git config --global user.name admin` without changing directory first. `git`, in turn, does a `stat(".")` for some obscure reason, and fails miserably: > > [pid 597] getcwd("/root", 4096) = 6 > [pid 597] stat(".", 0x7fffdc602e60) = -1 EACCES (Permission denied) > [pid 597] write(2, "fatal: failed to stat '.': Permi"..., 45fatal: failed to stat '.': Permission denied > ) = 45 > > A simple workaround: `cd /tmp` before running the command. > > So this should be fairly simple to fix: just change the directory first. It's is probably safe to `chdir($HOME)` anyways... --[[anarcat]] ikiwiki-hosting/doc/bugs/external_hostname_validation.mdwn0000644000000000000000000000253512211430070021376 0ustar [[todo/external_hostnames]] need to be validated when changed -- we'll want to ensure that the DNS is properly set up to point to the site hostname. We'll probably want to at least warn about hardcoded IPs. Validation will also help ensure that two sites don't try to generate conflicting apache config files, but by itself it's not sufficient. Consider: * The user setups up a CNAME example.com pointing to site A. * The user adds example.com to site A. * The dns is changed to example.com now points to site B. * The user adds example.com to site B. Now ikisite will be generating conflicting apache config files. This scenario needs to be caught somehow, and doing so seems to call for a site-wide database. ---- On the ikisite side, I think a new subcommand is needed that handles setting external hostnames. It can do some of the validation. It is also needed to allow changing/adding an external hostname and adding/removing apache config files w/o having to 'ikisite disable' first, and 'ikisite enable' afterwards. ---- Update: `ikisite domains` sets up external hostnames/aliases, and does check the DNS. It also uses `ikisite sitelookup` to ensure that no other site is already configured to use the same external hostname. While `ikisite sitelookup` still doesn't support multiserver use, I think what I did today is enough to call this bug [[done]]. ikiwiki-hosting/doc/bugs/done.mdwn0000644000000000000000000000014512211430070014364 0ustar Fixed bugs: [[!inline pages="bugs/* and !bugs/*/* and link(bugs/done) and !bugs/done" archive=yes]] ikiwiki-hosting/doc/bugs/analog_shows_garbage_referrers.mdwn0000644000000000000000000000160612232570331021665 0ustar Because there's no exclusion pattern for referrers, most of the referrers mentionned in the report are from the site itself. This is easily fixed: [[!format diff """ commit 38cc74a79775aed357bd15f81e40906887d8cbcc Author: Antoine Beaupré Date: Sat Sep 7 19:42:27 2013 -0400 analog: exclude the main URL from referrer report diff --git a/ikisite b/ikisite index eb38ac5..e3c9b21 100755 --- a/ikisite +++ b/ikisite @@ -2439,7 +2439,7 @@ sub analog { } chdir(logdir($hostname)) || die "chdir: $!"; - system("analog", "+a", "+f", "+D", glob("access.log*"), @_); + system("analog", "+CREFREPEXCLUDE http://${hostname}/*", "+a", "+f", "+D", glob("access.log*"), @_); return undef; # don't print return code } """]] This is 38cc74a in my git repo (`git://src.anarc.at/ikiwiki-hosting.git`). --[[anarcat]] > [[applied|done]] --[[Joey]] ikiwiki-hosting/doc/bugs/branching_themed_site_and_picking_no_theme.mdwn0000644000000000000000000000020512211430070024145 0ustar If a site with a theme is branched, and the user selects the "Simple" (ie, none) theme, the branch inherits the parent site's theme. ikiwiki-hosting/doc/bugs/site_creation_failure_not_noticed.mdwn0000644000000000000000000000077612211430070022375 0ustar Since site creation over the web happens in the background, there's a problem in detecting when the site creation fails. Instead of a useful error message, the user will be dumped into a site that failed to be created, or branched, and is in some unknown broken state. The code currently just checks that the site exists, and is not locked, and assumed that means ikiwiki set it up correctly. There should at least be some positive sign checked that ikiwiki finished successfully. > [[done]]; nonce checked ikiwiki-hosting/doc/bugs/git_push_too_limited.mdwn0000644000000000000000000000106212211430070017650 0ustar `iki-git-hook-update` limits what pushes from external are allowed to do, which is necessary, but it may limit too much. It does not allow adding new branches, deleting branches; may not allow manipulating tags (need to check). It makes sense to not allow any modification of the setup branch, and it should prevent outright deletion of the master branch. I think all other branch changes should be safe, and allowed. --[[Joey]] > [[fixed|done]]. Note that for untrusted git push, ikiwiki's own checks > will disallow changes to any branch other than master. ././@LongLink0000644000000000000000000000016212232570344011643 Lustar rootrootikiwiki-hosting/doc/bugs/__34__Currently_enabled_SSH_keys:__34___shows_only_first_139_characters_of_each_key.mdwnikiwiki-hosting/doc/bugs/__34__Currently_enabled_SSH_keys:__34___shows_only_first_139_characters_of_0000644000000000000000000000171612232566773031051 0ustar At least at http://free-thursday.pieni.net/ikiwiki.cgi the "SSH keys" page shows only the first 139 characters of each SSH key. I'm using iceweasel in 1024x768 resolution and there are not scrollbars visible. Please contact me at timo.lindfors@iki.fi > I have access to the same wiki, and do not see the problem Timo sees. I see 380 chars of the SSH keys, and do have a scrollbar. > Weird. --liw > The html used to display the keys is an unstyled `
`. I'd expect
> a browser to add a scrollbar to the whole page when part of it is too
> wide. Or to put a scrollbar on the individual pre with the too-long line,
> like chromium does. 
> 
> But I can reproduce the problem described
> with iceweasel 3.5.15. Instead, if you click on the line of a key, 
> you can use left/right arrows to scroll just that line, with no visible
> UI! (What a strange UI decision the mozilla people made!) --[[Joey]] 

>> This seems to be fixed in current iceweasel. [[done]] --[[Joey]] 
ikiwiki-hosting/doc/bugs/backup_aggregate_data.mdwn0000644000000000000000000000115212211430070017702 0ustar  The [[design/backupformat]] does include `.ikiwiki/aggregate`
so  sites using aggregation, when restored, will think everything
has been aggregated already, and will (probably, need to check) not
re-create the aggregated files, which are *not* included.

Should the aggregate state file be included in the backup? If so,
what about the source files aggregate stores, which are not in git by
default, and so also not included in the backup?

[[done]], not including aggregate in backup. Only downside really is that
on restore a site won't have aggregated data for 5 minutes until the cron
job triggers re-aggregation.
ikiwiki-hosting/doc/bugs/anonymous_pushes_may_make_source.git_repo_unwritable.mdwn0000644000000000000000000000472512232570331026364 0ustar  I am not sure the circumstances which allowed this to happen

~~~~
r-wiki@marcos:~/source.git$ ls -al objects/
total 48
drwxrwxr-x 12 r-wiki       r-wiki       4096 avr 12 10:21 .
drwxrwsr-x  7 r-wiki       r-wiki       4096 avr 13 07:57 ..
drwxrwsr-x  2 ikiwiki-anon ikiwiki-anon 4096 avr  8 08:41 09
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 41
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 56
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 5d
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 6b
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 8b
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 9e
drwxrwsr-x  2 r-wiki       r-wiki       4096 avr 12 10:21 f3
drwxrwxr-x  2 r-wiki       r-wiki       4096 avr 13 07:57 info
drwxrwxr-x  2 r-wiki       r-wiki       4096 avr 13 07:57 pack
~~~~

But it broke editing the wiki from the web, in the sense that the history didn't get push to the `source.git` directory, which in turns means that synchronised wikis are not updated and other hooks not ran:

~~~~
r-wiki@marcos:~/source$ git push
Counting objects: 11, done.
Delta compression using up to 2 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (8/8), 1.98 KiB, done.
Total 8 (delta 5), reused 5 (delta 2)
error: insufficient permission for adding an object to repository database ./objects

fatal: failed to write object
error: unpack failed: unpack-objects abnormal exit
To /home/r-wiki/source.git
 ! [remote rejected] master -> master (n/a (unpacker error))
error: failed to push some refs to '/home/r-wiki/source.git'
~~~~

I think the problem is someone pushed a blob that had the first `09` hash prefix through the git daemon, while all the other hash prefix directories have been created by the web interface. The workaround I found is:

    chmod g+s objects
    chown :r-wiki 09 09/*

... but that's hardly convenient. :) -- [[anarcat]]

> `ikiwiki-anon` is the user used for anonymous git push.
> See "permissions mess" in [[todo/anonymous_git_push]] for a discussion of
> this problem, and its solution, which is to use a filesystem supporting
> ACLs. `ikisite enable` runs an appropriate `setfacl` for you,
> and warns if it fails. We use this at Branchable and it seems to work
> fine. --[[Joey]]

> > Awesome, so just adding acl to the mount point (add installing the acl package, but I already had that :) fixes the problem, after running `ikisite enable` again on the site, thanks and [[done]]! --[[anarcat]]
ikiwiki-hosting/doc/bugs/ikisite_calendar_old_posts.mdwn0000644000000000000000000000031412211430070021015 0ustar  ikisite calendar only adds calendar files for new years. However, a user
might import data from years past. It should probably find the oldest year
with content and add all years from it to the present. 
ikiwiki-hosting/doc/bugs/dns_caching.mdwn0000644000000000000000000000105712211430070015702 0ustar  If you go to $foo.branchable.com, for example to check if it exists, and
then create it, the NXDOMAIN is cached. Could be a problem later. Is there
a DNS TTL adjustment that can amelorate this? Should we have a wildcard
entry to avoid NXDOMAIN? (we could then redirect from the wildcarded host to
the actual server to deal with cached dns)

[[fixed|done]]; setting the SOA minimum to 0 fixes this by avoiding
NXDOMAIN caching, at the expense of some server load. 

(Still seeing NXDOMAIN be cached, but for just a few minutes at least
in testing. --[[Joey]])
ikiwiki-hosting/doc/bugs/domains_with_multiple_dots.mdwn0000644000000000000000000000452512211430070021076 0ustar  I'm trying out running a copy of ikiwiki-hosting myself, in a subdomain of
`pseudorandom.co.uk` (non-commercially - I'm not trying to compete with
Branchable!). However, ikisite refuses to create subdomains, because it
assumes that entries in `domains` contain exactly one dot (so `example.com`
would be fine, but `example.co.uk` wouldn't work). This patch lets it
create domains.

[[Patch]] (which can also be cherry-picked from
`git://git.pseudorandom.co.uk/git/smcv/ikiwiki-hosting.git`):

> This is not entirely safe to apply. The problem is that
> `source.$foo.branchable.com` is a reserved domain name. If a user can 
> create that site for someone else's $foo, they can at a mimimum make
> apache unhappy with conflicting vhosts, and at the worse, engage in evil.
> 
> To do this securely, it needs to look at the `prefix_?` keys in
> `%config` and only allow one-level subdomains of domains configured
> there. --[[Joey]]

>> The next thing ikisite does is to split into hostname and domain parts,
>> using `prefix_?` as you advised: the check I'm patching here seems to
>> just be a quick way to refuse completely invalid hostnames? The user
>> can only create `source.foo.branchable.com` if a `prefix` points to
>> `foo.branchable.com` (i.e. admin error). I've tried out `source.foo`
>> for an existing and nonexistent `foo`, and I just get the error
>> `unknown prefix for foo.wiki.pseudorandom.co.uk`. --[[smcv]]

>>> Ah, good, so it does. Cherry-picked this (and most of your general
>>> changesets). [[done]] --[[Joey]]

    From 75db93d2dca5c95f0244c073c764915ee92154e3 Mon Sep 17 00:00:00 2001
    From: Simon McVittie 
    Date: Fri, 24 Sep 2010 21:39:56 +0100
    Subject: [PATCH] ikisite: allow base hostnames with more than one dot (like example.co.uk)
    
    ---
     ikisite |    2 +-
     1 files changed, 1 insertions(+), 1 deletions(-)
    
    diff --git a/ikisite b/ikisite
    index b1e9cb5..dcac43f 100755
    --- a/ikisite
    +++ b/ikisite
    @@ -747,7 +747,7 @@ sub username {
     	my $hostname=lc(shift);
     	
     	# paranoia: make sure the hostname looks like a FQDN
    -	if ($hostname !~ /^([a-z0-9][-a-z0-9]*)\.\w+\.\w+$/) {
    +	if ($hostname !~ /^([a-z0-9][-a-z0-9]*)\.\w+(\.\w+)+$/) {
     		error "illegal hostname \"$hostname\"";
     	}
     	if (length $hostname > 252) { # why not 253? Trailing dot..
    -- 
    1.7.1
ikiwiki-hosting/doc/bugs/ipv6_should_be_priority.mdwn0000644000000000000000000001053512211430070020314 0ustar  I am trying to add an external alias for my site and, because of silly NAT, the IPv4 interface doesn't match what DNS (as opposed to /etc/hosts) returns. IPv6 works fine though, but since ikiwiki-hosting first looks for IPv4 records, it fails to pick that up. I have a crude hack to fix this, but it doesn't seem like the good approach. "Worked for me" though. ;)

Example, without the patch:

    root@marcos:/home/a-wiki# ikisite domains wiki.anarcat.ath.cx --external=anarcat.ath.cx --alias=wiki.anarcat.ath.cx  
    DNS not configured correctly for anarcat.ath.cx - 72.0.72.144

Note that I had to patch to even get that debugging information:

[[!format diff """
diff --git a/ikisite b/ikisite
index adc5df3..82a29cb 100755
--- a/ikisite
+++ b/ikisite
@@ -1193,7 +1193,7 @@ sub domains {
                }
                if ($address ne $hostname) {
                        if (! grep { defined $_ && $_ eq $address } site_addresses()) {
-                               print STDERR "DNS not configured correctly for $options{external}\n";
+                               print STDERR "DNS not configured correctly for $options{external} - got $address\n";
                                exit 3; # special code
                        }
                        else {
"""]]

With the following patch, the thing actually works:

[[!format diff """
diff --git a/IkiWiki/Hosting.pm b/IkiWiki/Hosting.pm
index b802f21..08365d2 100644
--- a/IkiWiki/Hosting.pm
+++ b/IkiWiki/Hosting.pm
@@ -128,7 +128,7 @@ sub host_address_or_cname {
        # cached NXDOMAIN from earlier tests. We want to see the current
        # state of the DNS.
        # dig has to be run a second time to check for ipv6 AAAA addresses.
-       foreach my $atype (qw{A AAAA}) {
+       foreach my $atype (qw{AAAA A}) {
                open(DIG, "-|", "dig", "-t$atype", "+nofail", "+trace", "$host.") || error "dig: $!";
                while () {
                        next if /^;/;
"""]]

    root@marcos:/home/a-wiki# ikisite domains wiki.anarcat.ath.cx --external=anarcat.ath.cx --alias=wiki.anarcat.ath.cx  
    warning: DNS for anarcat.ath.cx hardcodes IP 2001:1928:1:9:beae:c5ff:fe89:e238
    [...]

As I said, this is a rather naive patch. Maybe this kind of overall prioritisation should be a config option? Or should we have a way to override the IP detection? Not sure how to handle this, really... Maybe the simplest fix is to have a --force argument... -- [[anarcat]]

----

The following patch allows overriding the IP detection mechanism:

[[!format diff """
From: =?UTF-8?q?Antoine=20Beaupr=C3=A9?= 
Date: Sat, 17 Dec 2011 14:13:29 -0500
Subject: [PATCH] allow hardcoding the IPs

---
 IkiWiki/Hosting.pm   |    9 +++++++++
 ikiwiki-hosting.conf |    6 ++++++
 2 files changed, 15 insertions(+), 0 deletions(-)

diff --git a/IkiWiki/Hosting.pm b/IkiWiki/Hosting.pm
index b802f21..f09dd3b 100644
--- a/IkiWiki/Hosting.pm
+++ b/IkiWiki/Hosting.pm
@@ -104,6 +104,9 @@ sub outtemplate {
 sub site_addresses {
 	# Looks up the site's main addresses; those that have a default
 	# route.
+	if ($config{hardcode_ipv4} && $config{hardcode_ipv6}) {
+		return ($config{hardcode_ipv4}, $config{hardcode_ipv6});
+	}
 	my ($defiface) = `ip route` =~ m/^default via .* dev ([a-zA-Z0-9]+)/m;
 	if (! $defiface) {
 		# Maybe ipv6 only.
@@ -115,6 +118,12 @@ sub site_addresses {
 	my $show=`ip addr show $defiface`;
 	my ($ipv4) = $show =~ m/inet (\d+\.\d+\.\d+\.\d+).*scope global/m;
 	my ($ipv6) = $show =~ m/inet6 ([a-zA-Z0-9:]+).*scope global/m;
+	if ($config{hardcode_ipv4}) {
+		$ipv4 = $config{hardcode_ipv4};
+	}
+	if ($config{hardcode_ipv6}) {
+		$ipv6 = $config{hardcode_ipv6};
+	}
 	return ($ipv4, $ipv6);
 }
 
diff --git a/ikiwiki-hosting.conf b/ikiwiki-hosting.conf
index f0f124f..ad8e14c 100644
--- a/ikiwiki-hosting.conf
+++ b/ikiwiki-hosting.conf
@@ -24,6 +24,12 @@ allow_ipv4=0
 allow_ipv6=0
 # (Disable all of the above to disable auto-assignment of DNS addresses.)
 
+# by default, ikiwiki-hosting looks at the first IP on the interface
+# that has the default gateway, but this may fail if you're behind NAT
+# or else. those allow you to hardcode the IPs
+# hardcode_ipv4=10.0.0.1
+# hardcode_ipv6=fr00::0
+
 # This is the DNS TTL to use when adding a hostname for a site.
 ttl=28800
 
-- 
1.7.7.3


"""]]]

I am running with this patch in production now, seems to work okay. --[[anarcat]]

> [[applied|done]] --[[Joey]] 
ikiwiki-hosting/doc/ikidns.mdwn0000644000000000000000000000224312211430070013761 0ustar  # NAME

ikidns - ikiwiki-hosting dns helper

# SYNOPSIS

ikidns subcommand options

# DESCRIPTION

Each web server needs to be able to update the DNS when a site is brought
up on it. They do so by talking to the master DNS server, using nsupdate(1)
and a key.

ikidns handles management of the keys, and configures the master DNS server
to accept them for each configured toplevel domain name.

# FILES

/etc/bind/$domain/db/zonefile is the zone file for a domain. A stub file
will be generated from a template if it doesn't exist, but probably needs
to be configured. As nsupdate(1) is used to add subdomains, bind
will first add them to a journal file, and later update this file to list them.

/etc/ikiwiki-hosting/ikiwiki-hosting.conf is the config file read by
default.

/etc/ikiwiki-hosting/templates/ holds templates of bind config and zone files

# EXAMPLES

	ikidns setupbind

	scp $(ikidns createkey $newserver) $newserver:/etc/ikiwiki-hosting/keys/dns/

# SEE ALSO

* [[ikisite]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/security/0000755000000000000000000000000012232566773013506 5ustar  ikiwiki-hosting/doc/security/push_setup_branch_deny_was_broken.mdwn0000644000000000000000000000057712211430070023324 0ustar  Ikisite created a broken symlink for the git update hook, so pushes to the
setup branch were not denied. This could possibly lead to unauthorized code
execution, although it would have to be paired with a site branch or
backup/restore.

This is fixed, but any existing sites need to have their update hook manually
fixed to link to /usr/bin/iki-git-hook-update

[[done]] --[[Joey]] 
ikiwiki-hosting/doc/security/push_setup_branch.mdwn0000644000000000000000000000006312211430070020061 0ustar  Need to deny pushes to the setup branch.

[[done]]
ikiwiki-hosting/doc/security/homedir_perms.mdwn0000644000000000000000000000252712211430070017211 0ustar  Home directory perms currently allow global reading of most files. Lock
down to 700?

> I think I agree with 0700. Let's do that and see what breaks. --liw

> The first thing to break will be apache, so this will need to be done
> with care.. Probably safest to only lock down `source`, `source.git`,
> `~/.git`, `~/ikiwiki.setup`.
>
> Hmm, the git daemon and gitweb run unprivilidged and need access to
> `source/.ikiwiki` (gitweb config files could move elsewhere) and
> `source.git` .. --[[Joey]]

>> Would 0711 on the home dir work? Ah, but everyone will have
>> files with well-known names. --[[liw]]

---

Things that need to be readable by other than the site user:

* `~/apache.conf` (owned by root:root, and should not contain sensative
  stuff; the same info is filled into /etc/apache2 config files anyway)

Things that are already appropriatly locked-down:

* logs
* `~/.ssh/authorized_keys`
* `~/.ikisite-nonce` (transient file)
* backups
* `~/ikiwiki.setup`
* `~/.gitconfig`, `~/.gitignore`
* `~/.git/`
* `~/tmp/`
* `~/public_html` (750; group www-data)
* `~/source/.ikiwiki` (other than the gitweb config files randomly
  located in here)
* `~/source/.ikiwiki/gitweb*` (with suexec changes, gitweb runs as user)
* `~/source` as a whole
* `~/source.git` (when branchability is disabled)
* `~/customersite/`
* `~/apache/` (750; group www-data)

[[done]]
ikiwiki-hosting/doc/security/done.mdwn0000644000000000000000000000016612211430070015276 0ustar  Fixed security concerns:

[[!inline pages="security/* and !*/Discussion and link(security/done) and !security/done"]]
ikiwiki-hosting/doc/security/privilege_escalation.mdwn0000644000000000000000000002370412211430070020544 0ustar  (I originally reported this by private email, but Joey is fairly sure it's not
exploitable on branchable.com, so I'm summarizing discussion here. --[[smcv]])

By design, the hosted users aren't meant to be able to execute arbitrary code.
However, if they can (due to a bug), they can get arbitrary code executed as
`www-data`, at which point they can interfere with other users, steal
`httpauth` passwords (although those aren't supported on `branchable.com`),
and so on.

For this page I'll assume that the user trying to escalate privilege is
`b-example`.

> All issues raised here are [[done]] --[[Joey]] 

[[!toc ]]

# open issues

## apache.conf.tmpl

`apache.conf.tmpl` is, if not root-equivalent, then at least `www-data`-equivalent,
because it can set security-related Apache configuration. I've noticed that it's
not backed up, but it *is* restored from backups and `chown`'d to root, so restoring
an untrusted backup is currently dangerous.

> `apache.conf.tmpl` *is* backed up; it's committed to the setup branch.
> 
> Restoring from an untrusted backup is dangerous anyway, because for
> example `ikiwiki.setup` can be configured to make ikiwiki run arbitrary
> code. All the dangerous stuff should be confined to the setup branch,
> which is why pushing that branch is rejected. --[[Joey]]

>> Right, but my goal with this stuff is more or less: if bob is a user
>> with shell access and b-example is his website, giving bob permission
>> to become `b-example` via sudo doesn't let him escalate privileges
>> any further than that. --[[smcv]]

>>> Ok, so the risk with apache.conf.tmpl is that while it's owned by root,
>>> the site user can move it out of the way. This allows three attacks:
>>> 
>>> 1. find a somehow exploitable apache.conf.tmpl on the host that's owned
>>>    by root, and move it into place.
>>> 2. race `ikisite enable`, which test the file owner and then reads it,
>>>    so it thinks it's reading a file owned by root.
>>> 3. move it out of the way, put in a bad file, let `ikisite backup`, 
>>>    back it up, and then use some method to get the site restored;
>>>    restore will trust the file and make it owned by root.
>>>
>>> 1 is unlikely, 3 can be fixed in `ikisite backup` perhaps, 2 should
>>> be solvable, with some difficulty (I think that hard linking the file
>>> and checking the owner of the hard link, the reading the hard link,
>>> would work?) Still, keeping the file in $HOME is not looking very
>>> smart. --[[Joey]] 

Since only root can set this up anyway, I'd personally be inclined to treat
it as server-wide configuration that should be backed up as part of `/etc`,

> There will be frontends to configure common stuff in it later, and
> it is site-specific apache configuration, that needs to be transferred
> along with the site if it is, say, bundled and transferred to a different
> server.
> 
> What could be done is to use `apache-site.tmpl`, and add to it variables
> exposing and configuring stuff like htpasswd setup or whatever else
> is needed (we have some collectd monitoring snippets in one of our
> sites at branchable, for example). Then have
> ikiwiki.setup hold the configuration for those variables.
> --[[Joey]]

>> My feeling about moving the config to a site-wide template file(s)
>> that are then enabled by ikiwiki.setup settings is that it's great
>> for common things, but it loses a lot of easy flexability of being able
>> to go in and use the full power of apache to tweak sites, in an ad-hoc
>> way. That is flexability and power that I'm uncertian about sacrificing.
>> --[[Joey]] 

create `/etc/apache2/ikisite`, and put something like this in `apache-site.tmpl`:

    
        ServerName :80
        ...
        Include /etc/apache2/ikisite/.*.conf
    

    
        ServerName :80
        ...
        Include /etc/apache2/ikisite/src..*.conf
    

Then you can put whatever you want in those files. Note that Apache considers
it to be a syntax error if an `Include` directory doesn't exist, but is
perfectly happy if a glob fails to match any files.

If the `HTML::Template` functionality is useful in practice (is it?),
`ikisite` could perhaps automatically generate `foo.conf` from `foo.tmpl`.

> I haven't done this on my branch so far. --[[smcv]]

>> This is fine, except it adds a second thing that needs to be managed
>> and transferred along with ikisite backups to move sites. It's important
>> to me that ikisite sites be self-contained at the backup level. Now, the
>> backups could be modified to include the site's file from /etc in them,
>> and restore to restore them, I suppose.. --[[Joey]]

>> Ok, I have done something based on this but without the includes.
>> I did keep the `HTML::Template`
>> functionality, it's useful to have branchable sites whose apache config
>> tweaks follow them when branched. Generalized to a
>> `/etc/ikiwiki-hosting/config/$user/` directory for root-owned configs.
>> --[[Joey]] 

# resolved issues

## IkiWiki CGI

If the user can remove the setuid/setgid bits from their ikiwiki.cgi wrapper
(which they own), it'll execute as `www-data`. They could then get anything
executed with `www-data` privileges.

Possible fixes:

### Use Apache's `suexec`

Something like this:
.
(If you don't want to put the Debian dependencies one per line, leave out
that commit, but I find git diff isn't very useful for long dependency
lists otherwise...)

suexec will only work if the CGI script is under /var/www, owned by
`b-example`, not writeable by anyone else, and in a directory also meeting
those conditions; CGIs not meeting those conditions won't be executed at
all.

> Done in your branch, which I've merged. --[[Joey]] 

### Central setuid-root wrapper

Have a central setuid-root wrapper `/usr/lib/ikiwiki-hosting/ikiwiki.cgi`
which is only executable by www-data, and when run with
`IKISITE_USER=b-example` or something, switches user/group to `b-example`
and execs `/home/b-example/public_html/ikiwiki.cgi`. This is basically
reinventing suexec.

Joey pointed out that because ikiwiki plugins are allowed to add C code to
the `cgi_wrapper`, every site's C CGI wrapper can be different, so the
central setuid-root wrapper would have to be a wrapper for a wrapper.

## Gitweb CGI

`gitweb` will run arbitrary Perl code from `.ikiwiki/gitweb.conf.real`.
Possible fixes:

### Configuration elsewhere

Put the gitweb.conf somewhere else (`/var/lib/ikiwiki-hosting-web/gitweb`?)
and have root own it, so `www-data` can't be subverted.

### Use `suexec` again

Install a wrapper in each user's `suexec` directory, owned by the user
themselves, so that each user's gitweb runs with the privileges of that user,
via suexec. The wrapper can be as simple as this: the important thing for
suexec is that the user owns it.

    #!/bin/sh
    exec /usr/lib/cgi-bin/gitweb.cgi "$@"

As noted on [[homedir perms]] this has the nice side-effect that more
of the home directory can be private.

> I've done this on my `escalation` branch, which is the earlier `suexec`
> branch redone so that existing users will (mostly) keep working, with
> their old log/CGI directory (if you disable/enable sites it might still
> break, not sure). It'd be great if you could merge some or all of
> this. --[[smcv]]

>> And merged. --[[Joey]] 

## ikisite parsing `ikiwiki.setup`

ikisite loads `ikiwiki.setup` in "safe mode", so hopefully there's no
possibility of arbitrary code execution there.

> Specifically, safe mode prevents ikiwiki.setup from being a perl script.
> Of course, if any value is used in an insecure way by ikisite, it loses.
> --[[Joey]]

>> I haven't audited this thoroughly yet, but nothing jumps out at me... --[[smcv]]

## logs

 says:

> If the logs directory is writeable (by a non-root user), someone could
> replace a log file with a symlink to some other system file, and then
> root might overwrite that file with arbitrary data.

I don't know how plausible that is in practice, but on shared
hosting services I've used in the past, the base directory for my
website has been owned by root, with docroot, cgi-bin etc. subdirectories
owned and writeable by me, and a logs directory not owned by me.

If you want users to own their own home directories (which ikisite
does currently assume), an alternative way to do this would be to
put the logs in `/var/log/apache2/b-example/` or
`/var/log/apache2/example.branchable.com/` or whatever, and have
the home directory contain a logs/ symlink for convenience?

> I think it's reasonable to move the logs outside of home. Having a
> symlink would only encourage something to write through the symlink,
> breaking security again. The other issue with having them outside of home
> is /var needs to have space for them -- but this also means that
> something filling the logs doesn't necessarily full the home partition
> too. --[[Joey]] 

>> I've added that to my branch; I used
>> `/var/log/ikiwiki-hosting/b-example` owned by `root:b-example` so
>> the hosting user can (potentially) read their own logs later.
>> --[[smcv]]

>>> And merged. --[[Joey]] 

## users not owning their own $HOME?

One way to avoid various privilege escalations would be for the site
users' home directories to be owned by root and not writeable by the
user, with particular subdirectories writeable. Things that that
would break:

* the code in ikisite that replaces ikiwiki.setup
* ikiwiki.setup in general, possibly, depending how ikiwiki rewrites it
  (an easy workaround would be to put it in a subdirectory)
* any random program that assumes it can replace a dotfile by creating
  a temporary file and renaming over the original

Probably not worth it...

> I agree, this is not an approach I really want to pursue. --[[Joey]]

## `~/apache/`

I don't know what you put in here in practice, so I don't know whether
it can cause privilege escalation :-)

> Currently, only htpasswd files. Exploitability unlikely. --[[Joey]]
ikiwiki-hosting/doc/security/unsanitized_html.mdwn0000644000000000000000000000224412211430070017731 0ustar  Is it safe to allow users to put unsanitized html on their sites?

Reasons users might want to do this:

* To embed videos, etc into posts to a blog.
* To customize templates.
* To modify style sheets (not html per se, but broadly equivilant).

Note that allowing a user to git push automatically allows them to add
all sorts of files to their site. Unless something is done to lock down
regular git push, a-la-untrusted git push.

## The cookie problem

A subdomain can access cookies that belong to its parent domain. Both
reading cookies, and setting cookies. So foo.example.com could access
cookies for example.com. If example.com has something important on it,
such as a site administration interface, its session cookies could be
stolen this way.

A workaround is to make example.com hard redirect to a different domain and
only set cookies in that domain. Just redirecting to www.example.com should
do (TODO: check that browsers don't have some DWIM special case for `www`).
Or use example.net, or admin.example.com, etc. A subdomain *cannot* access
cookies belonging to sibling subdomains.

## TODO

This analysis is not complete; what about other means of attack beyond
cookies?
ikiwiki-hosting/doc/design/0000755000000000000000000000000012232570331013071 5ustar  ikiwiki-hosting/doc/design/backupformat.mdwn0000644000000000000000000000211512211430070016425 0ustar  Backups of a site are an uncompressed tar file. Uncompressed because most
of the files in it are compressed. All files in the tar file need to be
owned by root:root, and the backup file should be kept mode 400 as it
contains eg, passwords.

## tarball contents

* `$vcs.dump` - some kind of serialization of the vcs repository
  (for git, `git.dump` is a git-bundle)
* other files can be included in the tarball using the same
  filename as is used in the home directory. For example as
  `source/.ikiwiki/indexdb` and `source/.ikiwiki/userdb`.
* the contents of the `rootconfig` directory in the tarball
  are installed into `/etc/ikiwiki-hosting/config/$user`

This format allows the backup to be created by serializing the vcs
and then just tarring up selected files from the home directory of the
site, along with any files from `/etc/ikiwiki-hosting/config/$user`.

## rsyncability

git dumps are highly rsyncable, and since the tarball is uncompressed, as a
whole it is quite rsyncable too. Real world numbers: For a 15 mb backup,
rsync reported a speedup of 166, and needed to send only 68 kb.
ikiwiki-hosting/doc/design/apacheconfig.mdwn0000644000000000000000000000410312232570331016365 0ustar  A site's apache configuration is automatically generated in
`/etc/apache2/sites-available/ikisite-*`.

It can be useful to tweak apache settings for a single site. To allow this,
use `/etc/ikiwiki-hosting/config/$username/apache.conf.tmpl`

This is an apache2 config fragment, that, if present, is inserted into the
site's generated apache config file by `ikisite enable`. Remeber to run
`ikisite enable` if modifying it.

It is inserted inside the `` tag in the apache config file,
at the end. So it can override anything that comes before, add additional
settings, etc. It is actually a HTML::Template template, and can contain
the same variables as `apache-site.tmpl`. (HOSTNAME, HOME, etc.)

Example:

	/public_html>
		Options +Foo
		AuthName ""
		AuthType Basic
		AuthUserFile /apache/htpasswd
		Require valid-user
	

Note the use of "+" to add to the default Options, rather than overriding it.

Note that the advantage of using this, over just allowing a htaccess file,
is that the site's owner could modify a htaccess file, but can't modify
this file. This avoids needing to worry about security issues with htaccess
files. (Also, apache's documentation warns about performance implications
of enabling htaccess files.) Of course, if a htaccess file is really wanted
for a given site, this file can be used to enable it.

---

To enable SSL, just install the ssl cerificate to
`/etc/ikiwiki-hosting/config/$username/ssl.crt`
and the key to 
`/etc/ikiwiki-hosting/config/$username/ssl.key`.
(Be sure to only let root read this file!)
When both files exist, and it is able to verify that
the key file is valid and not password protected, `ikisite enable`
will generate an apache config that enables using ssl.

A `/etc/ikiwiki-hosting/config/$username/ssl.chain` can also be provided,
if the CA need to provide additional certificate chain material.

Note that it's up to you to configure apache to use SNI if using SSL
for multiple virtual hosts on a single IP. See

ikiwiki-hosting/doc/design/customersite.mdwn0000644000000000000000000000707612232566773016537 0ustar  When ikiwiki-hosting is used as a business, information about customers
needs to be stored. Rather than use a database, we use the customersite,
accessed by the `IkiWiki::Customer` module.

This is a regular site managed by ikiwiki-hosting like any other. Its
configuration is somewhat special though. When setting it up:

* Be sure to configure ikiwiki to `exclude:
  customer/*`, and turn off branchability, to avoid customer data leaking
  out! Also, disable recentchanges, that can leak data too.
* Configure it to accept a ssh key owned by the controlsite, so that
  site can push into it.
* Configure its `git_wrapper_background_command` to update
  the controlsite's checkout of the customersite, by having it run
  ". /etc/ikiwiki-hosting/ikiwiki-hosting.conf; ikisite-wrapper updatecustomersite $controlsite"

Inside the customersite, there is a `customer` directory, which holds
information about all the customers. Each customer has a subdirectory
there, which holds files for each piece of data tracked about a customer.

The name of a customer's subdirectory is obtained by taking the sha1sum of
their openid. If a customer has multiple openids, they can have multiple
subdirectories, symlinked together. (ikiwiki will not render the symlinks,
but that's ok; it's not rendering customer data at all if configured right.)

The individual files hold one single piece of data, or a unordered 
list (ie, a list of email addresses). Files holding a list have "_list"
in their filename.

This design is intended to allow changes to be made to multiple branches
of the customersite, and merged together with little chance of conflicts.
Git's union merge driver can be used to merge the list files. Other
fields may have custom merge drivers.

## locking

A file named `lock` in each customer's directory is used as a lock file
by anything that reads or writes a customer's data.

A file named `lock` in the top of the customersite is used as a lock file
for anything that needs to change arbitrary files anywhere in the
customersite. (For example, a git pull.) This lock should be taken first,
as a shared lock by things that do customer-level locking.

## fields

* `notes.mdwn` freeform notes about customer
* `currentplan` holds the name of the price plan the user has agreed to
* `email` holds the current contact email of the user
* `email_list` holds all known contact emails of the user
* `name` holds the user's name (preferably full name, but may be whatever
  is available from openid, or possibly derived from email address).
* `openid_list` holds the openid(s) of the user
* `startdate` when this customer's account was first created (represented
  as time from unix epoch)
* `balance` amount of money owed (in cents) (a negative balance indicates
  the customer has a credit)
* `previousbalance` what the balance was before the last billing or payment
   event (be sure to update this when changing the balance)
* `lastbilldate` when user was last billed (represented as time
  from unix epoch)
* `disable_billing` if set to a true value, bills will not be sent
  (used for test accounts)
* `lastreminderdate` when last billing reminder was sent
* `bill_startdate` start of billing period for last bill
* `bill_enddate` end of billing period for last bill
* `lastinvoiceid` id of the last invoice sent
* `lastplan` name of the price plan used in the last invoice sent

## non-customer data

In a charming example of scope creep, we're storing a bit of
non-customer-specific data in the customersite already:

* `invoice/last` - stores the last used invoice id.
* `invoice/YYY-MM/` - stores copies of invoices sent to customers
ikiwiki-hosting/doc/design/username.mdwn0000644000000000000000000000426112211430070015572 0ustar  Site subdomains are mapped to unique unix usernames.

Subdomain limits:

* 253 characters total (including top level domain)
* a-zA-Z0-9-

Unix username limits:

* 32 characters limit (some things support longer, but not everything)
* recommended to match: [a-z_][a-z0-9_-]*
* may be user-visible (ie, for ssh)

## format for short subdomains

This format will be used most of the time, and it makes it easy to see
what hostname a process is running for, etc.

First 2 chars: Avoid conflict with system usernames, and briefly indicate
top-level domain name. "b-"

Remaining 30 chars: subdomain, if it is short enough

## format with hash for longer subdomains

Any hash has potential for collisions. The chances of such a collision
occuring in relatively short, limited DNS subdomains are quite rare, and in
the unlikely case of one happening, the system should just print an error
and the user can pick a different subdomain. This means that the system has
to explicitly check for a hash collision at site creation time.

I think it follows that we do not have to choose a hash that is
crytographically strong.

md5_base64, with + replaced by _, and / by -

* 22 characters
* could spell a rude word, if unlucky
* other 10 characters could be "b5-" + <7 characters of subdomain>
  to avoid any overlap with other usernames, encode the hash type,
  and make the subdomain semi-discoverable
* small potential for collisions

md5_hex

* 32 characters
* no rude words
* small potential for overlap with (32 character only!) system usernames
* small potential for collisions

sha1_base64, with + replaced by _, and / by -

* 27 characters
* could spell a rude word, if unlucky
* other 5 characters could be "bsh1-" to avoid any overlap
  with other usernames, and encode the hash type
  (or "b1-" + <2 characters of subdomain>, but 2 chars is not
  a worthwhile amount)
* small(er?) potential for collisions

half of md5_hex

* 16 characters
* no rude words
* other 16 characters can be "b5-" + <12 characters of subdomain> + "-"
  to avoid any overlap with other usernames, encode the hash type,
  and make the subdomain semi-discoverable
* larger potential for collisions, as we have a 64 bit hash
* **what is currently used**
ikiwiki-hosting/doc/index.mdwn0000644000000000000000000000107512232566773013640 0ustar  ikiwiki-hosting is the [ikiwiki](http://ikiwiki.info) management
interface for [Branchable](http://branchable.com/).

Stuff here:

* [[TODO]]
* [[bugs]]
* [[security]]
* [[design]]
* [[howto]]

Commands:

* [[ikisite]]
* [[ikidns]]
* [[ikisite-wrapper]]
* [[ikiwiki-hosting-web-daily]]
* [[ikiwiki-hosting-web-backup]]
* [[ikisite-delete-unfinished-site]]

This wiki is built from the files in the doc directory of
the ikiwiki-hosting git repository. You can get the source code
to ikiwiki-hosting here:

	git clone git://ikiwiki-hosting.branchable.com/

License: [[AGPL]]
ikiwiki-hosting/doc/AGPL0000644000000000000000000010333012211430070012256 0ustar                      GNU AFFERO GENERAL PUBLIC LICENSE
                       Version 3, 19 November 2007

 Copyright (C) 2007 Free Software Foundation, Inc. 
 Everyone is permitted to copy and distribute verbatim copies
 of this license document, but changing it is not allowed.

                            Preamble

  The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.

  The licenses for most software and other practical works are designed
to take away your freedom to share and change the works.  By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.

  When we speak of free software, we are referring to freedom, not
price.  Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.

  Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.

  A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate.  Many developers of free software are heartened and
encouraged by the resulting cooperation.  However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.

  The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community.  It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server.  Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.

  An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals.  This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.

  The precise terms and conditions for copying, distribution and
modification follow.

                       TERMS AND CONDITIONS

  0. Definitions.

  "This License" refers to version 3 of the GNU Affero General Public License.

  "Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.

  "The Program" refers to any copyrightable work licensed under this
License.  Each licensee is addressed as "you".  "Licensees" and
"recipients" may be individuals or organizations.

  To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy.  The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.

  A "covered work" means either the unmodified Program or a work based
on the Program.

  To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy.  Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.

  To "convey" a work means any kind of propagation that enables other
parties to make or receive copies.  Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.

  An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License.  If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.

  1. Source Code.

  The "source code" for a work means the preferred form of the work
for making modifications to it.  "Object code" means any non-source
form of a work.

  A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.

  The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form.  A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.

  The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities.  However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work.  For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.

  The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.

  The Corresponding Source for a work in source code form is that
same work.

  2. Basic Permissions.

  All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met.  This License explicitly affirms your unlimited
permission to run the unmodified Program.  The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work.  This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.

  You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force.  You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright.  Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.

  Conveying under any other circumstances is permitted solely under
the conditions stated below.  Sublicensing is not allowed; section 10
makes it unnecessary.

  3. Protecting Users' Legal Rights From Anti-Circumvention Law.

  No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.

  When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.

  4. Conveying Verbatim Copies.

  You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.

  You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.

  5. Conveying Modified Source Versions.

  You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:

    a) The work must carry prominent notices stating that you modified
    it, and giving a relevant date.

    b) The work must carry prominent notices stating that it is
    released under this License and any conditions added under section
    7.  This requirement modifies the requirement in section 4 to
    "keep intact all notices".

    c) You must license the entire work, as a whole, under this
    License to anyone who comes into possession of a copy.  This
    License will therefore apply, along with any applicable section 7
    additional terms, to the whole of the work, and all its parts,
    regardless of how they are packaged.  This License gives no
    permission to license the work in any other way, but it does not
    invalidate such permission if you have separately received it.

    d) If the work has interactive user interfaces, each must display
    Appropriate Legal Notices; however, if the Program has interactive
    interfaces that do not display Appropriate Legal Notices, your
    work need not make them do so.

  A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit.  Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.

  6. Conveying Non-Source Forms.

  You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:

    a) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by the
    Corresponding Source fixed on a durable physical medium
    customarily used for software interchange.

    b) Convey the object code in, or embodied in, a physical product
    (including a physical distribution medium), accompanied by a
    written offer, valid for at least three years and valid for as
    long as you offer spare parts or customer support for that product
    model, to give anyone who possesses the object code either (1) a
    copy of the Corresponding Source for all the software in the
    product that is covered by this License, on a durable physical
    medium customarily used for software interchange, for a price no
    more than your reasonable cost of physically performing this
    conveying of source, or (2) access to copy the
    Corresponding Source from a network server at no charge.

    c) Convey individual copies of the object code with a copy of the
    written offer to provide the Corresponding Source.  This
    alternative is allowed only occasionally and noncommercially, and
    only if you received the object code with such an offer, in accord
    with subsection 6b.

    d) Convey the object code by offering access from a designated
    place (gratis or for a charge), and offer equivalent access to the
    Corresponding Source in the same way through the same place at no
    further charge.  You need not require recipients to copy the
    Corresponding Source along with the object code.  If the place to
    copy the object code is a network server, the Corresponding Source
    may be on a different server (operated by you or a third party)
    that supports equivalent copying facilities, provided you maintain
    clear directions next to the object code saying where to find the
    Corresponding Source.  Regardless of what server hosts the
    Corresponding Source, you remain obligated to ensure that it is
    available for as long as needed to satisfy these requirements.

    e) Convey the object code using peer-to-peer transmission, provided
    you inform other peers where the object code and Corresponding
    Source of the work are being offered to the general public at no
    charge under subsection 6d.

  A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.

  A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling.  In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage.  For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product.  A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.

  "Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source.  The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.

  If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information.  But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).

  The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed.  Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.

  Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.

  7. Additional Terms.

  "Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law.  If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.

  When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it.  (Additional permissions may be written to require their own
removal in certain cases when you modify the work.)  You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.

  Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:

    a) Disclaiming warranty or limiting liability differently from the
    terms of sections 15 and 16 of this License; or

    b) Requiring preservation of specified reasonable legal notices or
    author attributions in that material or in the Appropriate Legal
    Notices displayed by works containing it; or

    c) Prohibiting misrepresentation of the origin of that material, or
    requiring that modified versions of such material be marked in
    reasonable ways as different from the original version; or

    d) Limiting the use for publicity purposes of names of licensors or
    authors of the material; or

    e) Declining to grant rights under trademark law for use of some
    trade names, trademarks, or service marks; or

    f) Requiring indemnification of licensors and authors of that
    material by anyone who conveys the material (or modified versions of
    it) with contractual assumptions of liability to the recipient, for
    any liability that these contractual assumptions directly impose on
    those licensors and authors.

  All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10.  If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term.  If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.

  If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.

  Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.

  8. Termination.

  You may not propagate or modify a covered work except as expressly
provided under this License.  Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).

  However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.

  Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.

  Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License.  If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.

  9. Acceptance Not Required for Having Copies.

  You are not required to accept this License in order to receive or
run a copy of the Program.  Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance.  However,
nothing other than this License grants you permission to propagate or
modify any covered work.  These actions infringe copyright if you do
not accept this License.  Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.

  10. Automatic Licensing of Downstream Recipients.

  Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License.  You are not responsible
for enforcing compliance by third parties with this License.

  An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations.  If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

  You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License.  For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.

  11. Patents.

  A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based.  The
work thus licensed is called the contributor's "contributor version".

  A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version.  For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.

  Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.

  In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement).  To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.

  If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients.  "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.

  If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.

  A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License.  You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.

  Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.

  12. No Surrender of Others' Freedom.

  If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License.  If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all.  For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.

  13. Remote Network Interaction; Use with the GNU General Public License.

  Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software.  This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.

  Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work.  The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.

  14. Revised Versions of this License.

  The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time.  Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.

  Each version is given a distinguishing version number.  If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation.  If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.

  If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.

  Later license versions may give you additional or different
permissions.  However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.

  15. Disclaimer of Warranty.

  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.

  16. Limitation of Liability.

  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.

  17. Interpretation of Sections 15 and 16.

  If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.

                     END OF TERMS AND CONDITIONS

            How to Apply These Terms to Your New Programs

  If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.

  To do so, attach the following notices to the program.  It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.

    
    Copyright (C)   

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero 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 Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .

Also add information on how to contact you by electronic and paper mail.

  If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source.  For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code.  There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.

  You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
.
ikiwiki-hosting/doc/howto.mdwn0000644000000000000000000000134512232566773013671 0ustar  Shell recipes for various things.

## Running a command on all sites

Sometimes you have a ikisite command that needs to be run on every site.
For example, adding a new setup option. No problem:

	for s in $(ikisite list); do ikisite foo $s ; done

## Parking a site

A parked site has all remote access to its content disabled, and replaced
with a vague message saying it is unavailable. This can be useful if there
was a legal or payment problem, or to reserve a particular site name.

	sudo ikisite changesetup test.branchable.com --enable-plugin parked --rebuild

Optionally, include `--set parked_message="reason why it was parked"`

## Unparking a site

	sudo ikisite changesetup test.branchable.com --disable-plugin parked --rebuild

ikiwiki-hosting/doc/ikiwiki-hosting-web-backup.mdwn0000644000000000000000000000064512211430070017633 0ustar  # NAME

ikiwiki-hosting-web-daily - backup sites

# SYNOPSIS

ikiwiki-hosting-web-backup

# DESCRIPTION

This script is run periodically by cron, as a low-priority job.
It handles backing up sites.

# SEE ALSO

* [[ikisite]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/iki-git-shell.mdwn0000644000000000000000000000160112232570331015147 0ustar  # NAME

iki-git-shell - ikiwiki-hosting helper

# SYNOPSIS

iki-git-shell -c I I

# DESCRIPTION

This is a wrapper for git-shell, and the parameters are passed
on to that program. Used in a ssh authorized_keys file for
an ikiwiki-hosting site user, it adds several safety features
to incoming commits. These include the ability to park a
repo, preventing changes, and limiting access to only a site's source.git
repository and no other repositories.

In addition to running git-shell commands, a very few other commands can be
run by the remote user. "logview" will tail their site's apache access.log,
and "logdump" will dump out out.

# SEE ALSO

* [[ikisite]](1)
* [[git-shell]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/todo.mdwn0000644000000000000000000000021112211430070013436 0ustar  Open todo items (link fixed ones to [[done]]):

[[!inline pages="todo/* and !todo/*/* and !link(todo/done) and !todo/done"
archive=yes]]
ikiwiki-hosting/doc/ikisite-delete-unfinished-site.mdwn0000644000000000000000000000076112211430070020500 0ustar  # NAME

ikisite-delete-unfinished-site - ikiwiki-hosting helper

# SYNOPSIS

ikisite-delete-unfinished-site openid|--all [--dry-run]

# DESCRIPTION

This helper script is run by by cron daily,
and deletes sites that someone started to create, but never finished
setting up.

# SEE ALSO

* [[ikisite]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/ikisite-wrapper.mdwn0000644000000000000000000000263112211430070015620 0ustar  # NAME

ikisite-wrapper - suid wrapper for ikisite

# SYNOPSIS

ikisite-wrapper subcommand options

# DESCRIPTION

ikisite-wrapper is a wrapper around [[ikisite]]. It is designed to be safely
made suid root, though it is not currently suid by default.

A few ikisite subcommands can be run using the wrapper without any
authorisation at all. These include: create, branch, list, sitelookup,
checklock, updatecustomersite, and enabledns. So making the wrapper suid
allows any user to create a site.

Other ikisite subcommands can only be run using the wrapper by
users who specify a nonce in the IKISITE_NONCE environment variable. These
include: delete, changesetup, domains, and deletenonce.

A site's current nonces are stored in its `.ikisite-nonce` file. A nonce
can be generated by root or the site's user via using the createnonce
subcommand, but it's usually generated by passing --createnonce to the
create or branch subcommands. This allows anyone to create or branch a site
and then use the nonce to allow further configuration of it (and delete it
if something goes wrong).

Subcommands that can be called by the wrapper either without or with a nonce
should be sure to fully validate their inputs.

# SEE ALSO

* [[ikisite]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/iki-git-hook-update.mdwn0000644000000000000000000000115212211430070016251 0ustar  # NAME

iki-git-hook-update - ikiwiki-hosting git update hook

# SYNOPSIS

iki-git-hook-update

# DESCRIPTION

This program is installed by ikisite as the git update hook for ikiwiki
git repositories. It allows trusted sources to commit any change,
while limiting commits from untrusted sources to prevent them making arbitrary
new branches, or making unsafe changes to the site's setup branch.

# SEE ALSO

* [[ikisite]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/todo/0000755000000000000000000000000012232570331012565 5ustar  ikiwiki-hosting/doc/todo/monkeysphere_for_ssh_key_setup.mdwn0000644000000000000000000000431512211430070021773 0ustar  Branchable's ssh host keys are now published on Monkeysphere.


However, there is
a limitation. Monkeysphere will only see the key if the user connects to
`branchable.com`, not to `sitename.branchable.com`.

We'd really like to be able to tell monkeysphere that "*.branchable.com"
can use any of our ssh host keys. But I don't think it supports
wildcarding.

An alternative would be to do `monkeysphere-host add-servicename` for each
subdomain we set up. But when they're all on the same key like that 
a) the key will tend to get *big* and b) the key will have a list of
every one of our subdomains, which is not information we want to expose.

The other approach would be to run `monkeysphere-host import-key` for each
subdomain we set up, so each has a unique key. But that has problems also:
a) we'd have to keep a gpg key on our server to sign those keys, and it
would have to be put in the web of trust -- I don't like keeping gpg keys
on network servers. b) It might *still* allow getting a list of all
our subdomains, if you can get ahold of the entire worldwide gpg web of
trust.

The final option would be for us to recommend users use ssh with
`branchable.com`, not a subdomain. And, behind the scenes, redirect it to
the appropriate server for the user who is logging in.

---

Eventually, I want to support using monkeysphere for user's ssh key configuration.

This would allow the site owner to enter an email address, and have ssh keys
for that user's email automatically retrieved. To help the site owner make
sure the ssh key was signed by the right gpg key, it could display a partial
web of trust. (Or, if we knew the user's own gpg key, we could do a better
display).

Revocations of configured keys could also be scanned for, and the keys
disabled. --[[Joey]] 

> Great idea! Documentation to set something like this up is [[the Monkeysphere SSH admin guide|http://web.monkeysphere.info/getting-started-admin/]], see "Monkeysphere for user authentication". --[[anarcat]]

---

Another use for monkeysphere is in distributing the per-site ssh user keys
used for automatic git pushes. Should be easy enough to commit those keys
right into monkeysphere when they're generated.
ikiwiki-hosting/doc/todo/anon_git_access_by_external_domain.mdwn0000644000000000000000000000060512211430070022507 0ustar  Currently, git://externl.example.com/ doesn't work. For this to work,
ikisite needs to manage links in /var/lib/ikiwiki-hosting-web/git/ for
each configured external domain. --[[Joey]] 

> [[done]] .. note that the Branchable tab still shows the internal
> domain in git urls. It's generally ok to use, unless you have some existing
> domain that is moving to Branchable, etc. --[[Joey]] 
ikiwiki-hosting/doc/todo/readonly_git-http-backend.mdwn0000644000000000000000000000024412211430070020466 0ustar  Git cloning from http://source.site.example.com/ rather than using git://
is valuable to some users. 

Of course, the branchable setting would enable/disable this.
ikiwiki-hosting/doc/todo/cleanup_unowned_sites.mdwn0000644000000000000000000000057512211430070020050 0ustar  If a user starts to make a site, but never clicks the Finish button,
the site will be made in the background, and linger around.

Such sites can be detected because the admin is `http://none/`.

Something should handle cleaning these up. Especially since
the user may later try again with the same name.

[[done]] -- ikisite-delete-unfinished-site written, can be used in cron job
ikiwiki-hosting/doc/todo/git_remotes_configurator.mdwn0000644000000000000000000000373612211430070020560 0ustar  A web interface for git remotes configuration would be very handy.

* Should make it really easy to set up pushing to github.
* Should be possible to push a site to any other git repo too.
* A special case is pushing changes to a site over to a branch of that
  site.

## design

`ikisite` already handles backup/restore of `.ssh/id_*`, so ssh-keygen
can be used a generate a passwordless ssh key for a site when remotes are
first set up. Provide a way to download the ssh public key, to provide to
github, etc.

For each remote, the user only needs to configure:

* The git URI, in any of the forms git recognises, *except* file:/// and 
  a bare path to a local repo.
* Whether to make the remote a mirror. (`git push --mirror`; force-update
  the remote refs)
  Or find a sane default.
* Possibly, the remote branch to push origin/master to. (Or a prefix, so
  it pushes to foo/master, foo/setup, etc.)
  Being able to push origin/master to github/branchable might be nice,
  but this feature could be omitted for simplicity initially.

The pushing should be run by a post-update hook. Currently, the post-update
hook is the ikiwiki wrapper. So, the pushing could be implemented by
an ikiwiki plugin. It could use the `refresh` hook, which will always be
called when a change is made or received.

The plugin should daemonize, drop locks, wait a short while to avoid
contending with the main site build, and then push to configured remotes.

## gotchas

ssh host key checking needs to be dealt with somehow. By disabling
StrictHostKeyChecking in `~/.ssh/config`? Should be ok for
public/branchable repositories. The risk is that for a non-public
repository, an unsafe push could be done, exposing its contents.

ssh (or http) could stall asking for a password. The daemonization should
avoid this by ensuring that any controlling terminal is detached from.

May need to force push if pushing to an empty remote. (--mirror avoids
that problem)

> basic gitpush plugin now [[done]] --[[Joey]] 

[[!tag wishlist]]
ikiwiki-hosting/doc/todo/nginx_support.mdwn0000644000000000000000000000062512232570331016376 0ustar  It would be nice for ikiwiki-hosting to support other webservers. My first target would be nginx. Any pointers as to where to begin? (Note that ikiwiki's Nginx support is [suboptimal](http://ikiwiki.info/todo/fastcgi_or_modperl_installation_instructions/) and [buggy](http://ikiwiki.info/forum/nginx:_404_plugin_not_working/) at best, but it [works](http://ikiwiki.info/tips/dot_cgi/)...). -- [[anarcat]]
ikiwiki-hosting/doc/todo/external_hostnames.mdwn0000644000000000000000000000256512211430070017357 0ustar  Need to support domain names that the user has configured to CNAME to 
the site hostname (or ugh, has hardcoded our IP).

Per the [[design]], each site has a primary url, which may be
an external domain or one we assign. It may also have any number of
aliases.

## ikiwiki configuration

Set url, cgiurl to use the primary url. (do something about historyurl and
diffurl too, or will they point at a different domain?)

## apache configuration

Serve requests in the primary domain.

For alias domains, hard redirect to the page in the primary domain?
The other option would be to also serve pages under the alias domains,
which would work, but since ikiwiki uses some absolute urls (ie, the cgiurl),
the user would tend to bounce over to the primary domain eventually anyway.

Probably it's better for caching, link visited status, and even
security, for only one domain to really be used.

## data storage

(The primary url is best stored as the url field in ikiwiki's config.)

The list of aliases needs to be stored somewhere. This could be in a
separate [[design/database]], or it could be in ikiwiki's own config. 

If in ikiwiki's own config, it may or may not be editiable via websetup,
depending on whether we have granted a given user the ability to use
external domain names.

Currently, I am storing it using the ikiwikihosting plugin's urlalias setup file
setting.

[[done]] (mostly)
ikiwiki-hosting/doc/todo/done.mdwn0000644000000000000000000000015312211430070014370 0ustar  Fixed todo items:

[[!inline pages="todo/* and !todo/*/* and link(todo/done) and !todo/done" archive=yes]]
ikiwiki-hosting/doc/todo/disk_quotas.mdwn0000644000000000000000000000037312211430070015775 0ustar  A good first stage would be to use unix soft quota support to make it easy
to look up what sites are using how much space.

Second stage would be somehow limiting space that can be used.

(This is not a priority for me. --[[Joey]])

[[!tag wishlist]]
ikiwiki-hosting/doc/todo/do_not_strip_comments_from_ssh_keys.mdwn0000644000000000000000000000026412232570331023021 0ustar  It seems that comments are stripped from SSH keys when uploaded. This is unfortunate as it makes it harder to figure out which key is which. -- [[anarcat]]

> [[done]] --[[Joey]] 
ikiwiki-hosting/doc/todo/git_push_ssh_keys.mdwn0000644000000000000000000000344412232570331017213 0ustar  ssh authorized keys are currently not checked into git. It should be
possible to store them on the setup branch next to the ikiwiki setup file.
This would allow changing ssh keys in a git push. 

The existing code for `ikisite checksetup` 
that checks pushes of the setup branch to ensure that
they come from a trusted user, and contain only safe changes, would need to
be extended to also check ssh keys. Things to check:

* The keys are all well-formed.
* There's no ssh options tied to a key that could run commands, open ports, etc.
* There's at least 1 key if there was 1 before
  (so users don't lock themselves out accidentially).
* That the authorized keys file did not turn into a symlink
  or other special file, and was not removed, and has sane permissions.

Question: Is exposing the authorized keys of every Branchable site in their
setup branches desirable? Or is this something the site owner needs to opt
in to?

> I've always wondered about how secret we should generally consider public keys. If I make the parallel with my GPG keyring, then the answer is "not at all". Also, these days, my SSH keys *are* in my GPG keyring, so basically they are already public on the keyservers, although you need to look for them. This could even be considered a feature:
>
> > *my SSH key is also available on that git repository over there, if you want to double-check the one I sent in that email*
>
> ... could be a useful thing. However, I could understand how paranoid users may want to do things differently, so being able to opt out of that could be important for some users.
>
> If this makes things more complicated: don't bother, and deal with it when people complain. ;) -- [[anarcat]]

>> So, it turns out the public keys are already checked into the setup
>> branch. So only a validator is needed. --[[Joey]]
ikiwiki-hosting/doc/todo/ikiwiki_setup_should_work_on_user_machine.mdwn0000644000000000000000000000076612211430070024173 0ustar  A user should be able to clone their site, get ikiwiki.setup from the setup
branch, and run ikiwiki on it, w/o having ikiwiki-hosting installed.

Currently, this will fail because:

* The setup file uses a per-site home directory. How to deal with this?
  User can frob paths, of course.

* The setup file uses `add_plugins` to pull in some plugins that are
  not in stock ikiwiki. Maybe instead of listing those in the
  ikiwiki.setup, they could be automatically pulled in via a tweaked
  goodstuff?
ikiwiki-hosting/doc/todo/cia_integration.mdwn0000644000000000000000000000041012211430070016576 0ustar  Would be nice if changes to sites could be communicated to cia.vc.

This is similar to auto-pushing changes to
[[git_remotes|git_remotes_configurator]] except of course CIA's nasty
hook scripts have to be used, and some of their configuration exposed
to the user.
ikiwiki-hosting/doc/todo/split_ikisite_into_plugins.mdwn0000644000000000000000000000006712211430070021115 0ustar  The program is getting kinda long..

[[!tag wishlist]]
ikiwiki-hosting/doc/todo/better_error_handling.mdwn0000644000000000000000000000035212232570331020016 0ustar  There are a few places where error handling can be improved in ikiwiki, i have a patch in my git repo to help with that.

See the master branch of `git://src.anarc.at/ikiwiki-hosting`, commit 271f36ad2abbbd37fd3997d9d7419f60cbbc4dc8.
ikiwiki-hosting/doc/todo/ikisite-calendar-running.mdwn0000644000000000000000000000052412211430070020333 0ustar  Should be a way to trigger an ikisite-calendar run. 

The two cases seem to be a change to `archive_pagespec` or similar,
which should probably just automatically trigger a rebuild of all the
calendar pages; and pushing in past posts from someone else, in which case
a way needs to be present to generate calendar pages for the past years.
ikiwiki-hosting/doc/todo/resume_interrupted_site_creation.mdwn0000644000000000000000000000041412211430070022300 0ustar  If a user goes to create a site, and at the confirmation screen, their
browser crashes, or they get distracted, etc, the site is created, locked,
and they cannot resume the creation. (After 2 days, a cron job will delete
it.)

It should be possible to resume somehow.
ikiwiki-hosting/doc/todo/htaccess_support.mdwn0000644000000000000000000000136212211430070017037 0ustar  .htaccess files are currently ignored. Would be nice to allow
users to include them in their source, and use them.

On the ikiwiki side, that is just a matter of configuring `includes`
to make it not skip .htaccess files.

On the apache side, config has to include 'AllowOverride All'. Also
'Options SymLinksIfOwnerMatch' is needed to use the RewriteEngine from
htaccess files.

Apache's [htaccess documentation](http://httpd.apache.org/docs/1.3/howto/htaccess.html)
explains the overhead that searching for and using htaccess files entails.

We'd need to look at the security of it and make sure .htaccess files
cannot do anything bad.

A final reason not to do it, btw, is that it locks us into using apache.

[[done]] -- see [[design/apacheinclude]].
ikiwiki-hosting/doc/todo/directory.mdwn0000644000000000000000000000306412211430070015453 0ustar  Wanted: Directory of sites. Could be used for:

* internal monitoring of what sites are being made and used
* possibly, a public directory (modulo privacy concerns)

Data structure could be a directory with pages, created by ikisite when it
creates sites, by filling out a template. (And removed by ikisite when it
deletes sites.) Then the pages could be used to build a rss feed of sites,
or they could have aggregation directives, to make a planet of their
recentchanges feeds.

So, it needs to support multiple observing sites. At the simplest, it could
be configured thus:

	# List of sites that will be updated when sites are added/removed.
	# These sites should each have a template named observesite.tmpl, which
	# will be filled out to create pages under sites/existing/ and
	# sites/removed/
	observersites="internal.branchable.com www.branchable.com"

`ikisite observe hostname` can be called with either an existing, or a
non-existing hostname, and will update its pages for each of the observed
sites. It should only create the page for a hostname if it does not already
exist (to allow us to annotate hostnames with our comments, etc). When
removing a page, it should move it to removed/ (preserving its content).
It should also move any subpages associated with the site. That way we 
can add comments, screenshots, etc.

----

If we had a privacy flag, sites would need to be hidden if the flag
was set, and unhidden if it was cleared. Deferred for later. We'd
probably want to manually review sites in a public directory anyway,
and tag ones we want to publish.

[[done]]
ikiwiki-hosting/doc/todo/multiple_servers.mdwn0000644000000000000000000000300212211430070017043 0ustar  Eventually, we will want multiple web servers, each with their own set of
sites.

# Known multiple server issues

* `ikisite siteexits` only checks sites on the local server, would need
  to use dns or other means to check for sites elsewhere
* `ikisite sitelookup` also only checks local sites.
* If servers have different ssh host keys, then users would get scary
  prompts if a site moved to a different server. Could be avoided by using
  a single host key for all (but how secure is that?)
  (ssh's warning message about the IP changing is not scary)
  Alternate approach would be to proxy all ssh connections through a central
  server, which is IIRC what github does.
  (Monkeysphere is a way to distribute ssh keys to users, but only if they
  use it.)
* DNS ttl issues when moving a site to a different host.
* Hardcoded IPs in users' DNS prevent moving some sites around.
* `ikisite list` only lists local sites, and is used by controlpanel etc.

We may want to mirror some sites to multiple servers, too.

> One thing I am thinking of is that we could use CNAME records instead of A to point the domains to the relevant servers. Then it would be up to the admin to configure his DNS properly, and add the list of available servers to the configuration. This would fix the problem of hardcoded IPs, of different host keys (because the SSH hostname would simply change), and so on. I certainly already feel unconfortable with all those A records in my zones already: IP renumbering is then unreliable and flaky... -- [[anarcat]]
ikiwiki-hosting/doc/todo/anonymous_git_push.mdwn0000644000000000000000000000613512211430070017403 0ustar  [[done]].. needs some testing though



The main sticking point on this is that git-daemon runs as a single user,
who cannot write to all the sites. Seems some sort of suid wrapper is
called for.. Or is it?

How about this:

* Add a "ikiwiki-anon" user, with locked password etc. Run git-daemon
  as that user. (done)
* In branchable plugin, add a new "anonpush" config setting.
  Should default to false, and be forced to false when branchable=0.
  (done)
* Set `untrusted_committers` to ikiwiki-anon on all sites by default.
  (done)
* Set `git_wrappermode: 6755` on all sites. (ikiwiki-anon will run the
  wrapper when a commit is pushed; need to run it as the site user) (done)
* Make ~/source.git and all its contents be writable be ikiwiki-anon.
  Somehow. Note that normal unix permissions don't suffice. (done?)
* When anonpush is enabled, enable `git_test_receive_wrapper` -- 
  and when it's disabled, disable it, and delete the hook. (That
  hook slows down all commits some.. a C wrapper that
  just checks the uid of the committer would allow speeding it back up.)
  (done)
* When anonpush is enabled, **after** wrapper setup. 
  run: `git config daemon.receivepack = true`
  (and undo when disabled). (done)
* Set core.sharedRepository to true, in all bare repos. Needed to ensure
  dirs in bare repo are always made writable by group.
  (Already done, and spot checks indicate it's working; also tested
  that it works when the sourcedir is temporarily 700 and a commit comes
  in.)
* Update do=branchable page to say when anonpush is enabled. (done)

## permissions mess

All the above is reasonable except the bit about permissions. Problem is
that both ikiwiki-anon and the site user need to be able to write to the
git repo. But it's not enough to make it 02775 siteuser:ikiwiki-anon, because
when ikiwiki-anon makes new directories like objects/xx/, it will own
them, and as the site user is not in the ikiwiki-anon group, it will not
be able to write to those subdirs, leading to later failure.
Similarly, when the site user makes commits, it will make directories owned
by it, that ikiwiki-anon cannot write to.

The site user cannot be in the ikiwiki-anon group, because then they could
access and write to other sites' repositories.

Pre-creating all possible object subdirectories and making them 
02755 siteuser:ikiwiki-anon probalby won't work well; git-gc will
delete empty object subdirectories, and it would be easy to miss some
directory that git creates.

Use ACLs? Would require filesystem support (ie, that ext234 filesystems be
mounted with 'acl' option, which does not appear to be the default). 

The magic settings seem to be:

	chmod g+s source.git # already done by git init --bare --shared
	sudo setfacl -R -m d:g:ikiwiki-anon:rwX,d:g:$siteuser:rwX,g:ikiwiki-anon:rwX,g:$siteuser:rwX source.git

This allows ikiwiki-anon to read/write to all files/dirs in the repo,
and sets all directories to propigate the ACL to new files/dirs as they
are created. When ikiwiki-anon creates a file or directory, it will
be owned by ikiwiki-anon:siteuser, and siteuser can always write to it.
ikiwiki-hosting/doc/iki-ssh-unsafe.mdwn0000644000000000000000000000071112211430070015324 0ustar  # NAME

iki-ssh-unsafe - ikiwiki-hosting helper

# SYNOPSIS

iki-ssh-unsafe [ssh options]

# DESCRIPTION

This is a wrapper for ssh, and the parameters are passed
on to that program. Strict host key checking is disabled.

# SEE ALSO

* [[ikisite]](1)
* [[ssh]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/ikisite.mdwn0000644000000000000000000000166212211430070014145 0ustar  # NAME

ikisite - ikiwiki-hosting site helper

# SYNOPSIS

ikisite subcommand options

# DESCRIPTION

ikisite manages individual sites.

# FILES

`/etc/ikiwiki-hosting/ikiwiki-hosting.conf` is the config file read by
default.

`/etc/ikiwiki-hosting/autosetup/` holds the ikiwiki setup template files.

`/etc/ikiwiki-hosting/templates/` holds other templates (eg, for apache vhost
config files).

# EXAMPLES

Basic site management:

	ikisite create myhost.ikiwiki.net --type=blog --vcs=git --admin 'http://joey.kitenet.net/'

	ikisite backup myhost.ikiwiki.net mybackup

	ikisite delete myhost.ikiwiki.net

	ikisite sudo myhost.ikiwiki.net vim ikiwiki.setup

	ikisite ikiwikisetup myhost.ikiwiki.net

# SEE ALSO

* [[ikidns]](1)
* [[ikisite-wrapper]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/users/0000755000000000000000000000000012232570331012761 5ustar  ikiwiki-hosting/doc/users/anarcat.mdwn0000644000000000000000000000136212232570331015263 0ustar  See 

My todos
========

... or the ones I commented it, to be more precise.

[[!inline pages="todo/* and !todo/done and !link(todo/done) and
link(users/anarcat) and !todo/*/*" sort=mtime feeds=no actions=yes archive=yes show=0]]

Done
----

[[!inline pages="todo/* and !todo/done and link(todo/done) and
link(users/anarcat) and !todo/*/*" feeds=no actions=yes archive=yes show=0]]

My bugs
=======

... same.

[[!inline pages="bugs/* and !bugs/done and !link(bugs/done) and
link(users/anarcat) and !bugs/*/*" sort=mtime feeds=no actions=yes archive=yes show=0]]

Fixed
-----

[[!inline pages="bugs/* and !bugs/done and link(bugs/done) and
link(users/anarcat) and !bugs/*/*" feeds=no actions=yes archive=yes show=0]]
ikiwiki-hosting/doc/users/joey.mdwn0000644000000000000000000000003212211430070014601 0ustar  http://kitenet.net/~joey/
ikiwiki-hosting/doc/ikiwiki-hosting-web-daily.mdwn0000644000000000000000000000077712211430070017476 0ustar  # NAME

ikiwiki-hosting-web-daily - compact and maintain all sites

# SYNOPSIS

ikiwiki-hosting-web-daily

# DESCRIPTION

This script is run daily by cron, as a low-priority job.
It does various cleanup and bookkeeping tasks, including 
rotating sites logs, and updating their calendars.

# SEE ALSO

* [[ikisite]](1)

# AUTHOR

Joey Hess 

Warning: this page is automatically made into a man page via [mdwn2man](http://git.ikiwiki.info/?p=ikiwiki;a=blob;f=mdwn2man;hb=HEAD).  Edit with care
ikiwiki-hosting/doc/bugs.mdwn0000644000000000000000000000027112211430070013437 0ustar  Open bugs (link fixed ones to [[done]]):

[[!inline pages="bugs/* and !bugs/*/* and !link(bugs/done) and !bugs/done"
archive=yes rootpage="bugs" postformtext="Add a new bug titled:" ]]
ikiwiki-hosting/doc/security.mdwn0000644000000000000000000000022412211430070014344 0ustar  Open security concerns (link fixed ones to [[done]]):

[[!inline pages="security/* and !*/Discussion and !link(security/done) and !security/done"]]