#!/usr/local/bin/perl # listen v0.1-perl # A standalone HTTP Wiki server. # Released without warranty and without restriction, except that the author(s) should # receive appropriate credit for any derivative products. # # Doesn't use a database, isn't multithreaded, isn't documented, is written in perl. # # Revision history: # 200503122337 Created 2005.03.11-03.12 by rjmccall@gmail.com # # 200503132359 Incorporated patch from jcreed@andrew.cmu.edu. # Added actions, cleaned up APIs, allow for a configuration file. # The configuration file is a series of key/value pairs, one per line (though # values may be arbitrarily complex. Here are the currently-supported commands: # Alias alias_path true_path # States that the resource 'alias_path' can actually be found at 'true_path'. # Edit.editbox.dimensions COLS x ROWS # Provides explicit dimensions for the edit box. # Title title string here # Titles the wiki. # Edit.links # Fetch.links # List.links # Provide a list of the automatic links to make available on each of these pages. # Supportered link generators are: # edit - edit the current resource # list - list the contents of the directory containing the current resource # main - jump to the root page # Configuration files are processed at the moment they are encountered on the command # line; they override earlier arguments and may be themselves overriden by later # arguments. use Fcntl ':flock'; use Socket; use strict; # default configuration details my $listen_port = 80; my $database_directory = "."; my $server_title = "listen"; my $main_page_title = "Main page"; my $edit_box_width = undef; my $edit_box_height = undef; my $directory_page = "Main"; # init: unit -> unit sub init { set_handler( "get", \&handle_get ); set_handler( "head", \&handle_get ); set_handler( "post", \&handle_post ); my $submit_action = create_action( "submit" ); set_content_handler( $submit_action, \&handle_content_submit ); set_resource_path_processor( $submit_action, \&get_true_path_to_resource ); my $edit_action = create_action( "edit" ); set_content_handler( $edit_action, \&handle_content_edit ); set_link_generator_names( $edit_action, "main", "list" ); set_resource_path_processor( $edit_action, \&get_true_path_to_resource ); my $list_action = create_action( "list" ); set_content_handler( $list_action, \&handle_content_list ); set_link_generator_names( $list_action, "main", "list" ); set_resource_path_processor( $list_action, \&get_true_path ); my $fetch_action = create_action( "fetch" ); set_content_handler( $fetch_action, handle_content_fetch_filtered( \&content_filter ) ); set_link_generator_names( $fetch_action, "edit", "main", "list" ); set_resource_path_processor( $fetch_action, \&get_true_path_to_resource ); set_default_action( $fetch_action ); } # create_html_response: http_request * string -> string # Takes a resource path and a body and produces an HTML message sub create_html_response { my ($http_request, $body) = @_; my $semantics = $http_request->{request_semantics}; my $action = $semantics->{action}; my $resource_path = $semantics->{resource_path}; my $buffer = "
'; foreach my $link_generator (@$link_generators) { $buffer .= ' | ' if $cnt++ > 0; $buffer .= $link_generator->( $resource_path ); } $buffer .= '
'; } $buffer .= ""; return $buffer; } my %link_generators = ( main=>\&go_to_main_page, list=>\&list_pages_here, edit=>\&edit_this_page ); # set_link_generator_names: action * (string...) -> unit # Note that this function silently ignores invalid producers sub set_link_generator_names { my $action = shift; my @link_generators = (); foreach my $linkname (@_) { my $link_generator = $link_generators{ $linkname }; if ($link_generator) { push @link_generators, $link_generator; } } set_link_generators( $action, \@link_generators ); } # go_to_main_page: string -> string sub go_to_main_page { return "" . $main_page_title . ""; } # list_pages_here: string -> string sub list_pages_here { my ($resource_path) = @_; return "List other pages here"; } # edit_this_page: string -> string sub edit_this_page { my ($resource_path) = @_; return "Edit this page"; } # should_list_resource: string * string -> boolean # (path, filename) sub should_list_resource { my ($path, $filename) = @_; return $filename !~ /^\./; } # create_local_path: string -> string sub create_local_path { my ($resource_path) = @_; return $database_directory . $resource_path; } # create_resource_list_path: string -> string sub create_resource_list_path { my ($resource_path) = @_; $resource_path =~ s@/[^/]*$@@; return $resource_path . '/list'; } # create_resource_edit_path: string -> string sub create_resource_edit_path { my ($resource_path) = @_; return $resource_path . '/edit'; } # create_resource_submit_path: string -> string sub create_resource_submit_path { my ($resource_path) = @_; return $resource_path . '/submit'; } # parse_resource_path: string -> string list ref sub parse_resource_path { my ($resource) = @_; my @elements = split /\//, $resource; my @path = (); foreach my $element (@elements) { next if $element eq "" or $element eq "."; if ($element eq "..") { pop @path; # silently fail if we're at the beginning } else { push @path, $element; } } foreach my $pathelement (@path) { $pathelement = decode_urlencoding($pathelement); $pathelement =~ s/[^\w ]//g; } return \@path; } # decode_urlencoding: string -> string sub decode_urlencoding { my ($value) = @_; $value =~ s/\+/ /g; $value =~ s/%([0-9A-Fa-f]{2})/chr hex $1/eg; return $value; } # parse_urlencoded_parameters: string -> (string, string) hash ref sub parse_urlencoded_parameters { my ($message) = @_; my %values; my @pairs = split /&/, $message; foreach my $pair (@pairs) { if ($pair =~ /^([^=]*)=(.*)$/) { my $key = $1; my $value = $2; $value = decode_urlencoding $value; $value =~ s/\+/ /g; $value =~ s/%([0-9A-Fa-f]{2})/chr hex $1/eg; $values{$key} = $value; } } return \%values; } # parse_uri: string -> unit sub parse_uri { my ($http_request) = @_; my $uri = $http_request->{uri}; my %request_semantics; my $resource_path_elements = []; my $parameters = {}; $uri =~ s!^[a-zA-Z]+://[^/]+!!; if ($uri =~ /^([^?]*)(?:\?(.*))?$/) { my $resource = $1; my $parameters_string = $2; $resource_path_elements = parse_resource_path $resource; $parameters = parse_urlencoded_parameters $parameters_string; } $request_semantics{resource_path_elements} = $resource_path_elements; $request_semantics{parameters} = $parameters; $http_request->{request_semantics} = \%request_semantics; classify_request_semantics( $http_request ); } { my %actions; my $default_action; # add_action_alias: string * action -> unit sub add_action_alias { my ($name, $action) = @_; $actions{$name} = $action; } # create_action: string -> action sub create_action { my ($name) = @_; my %action; $action{name} = $name; return $actions{$name} = \%action; } # get_action: string -> action option sub get_action { my ($action_name) = @_; return $actions{$action_name}; } # set_content_handler: action * (http_request -> unit) -> unit sub set_content_handler { my ($action, $handler) = @_; $action->{content_handler} = $handler; } # set_default_action: action -> unit sub set_default_action { my ($action) = @_; $default_action = $action; } # set_link_generators: action * ((string -> string) list ref) -> unit sub set_link_generators { my ($action, $link_generators) = @_; $action->{link_generators} = $link_generators; } # get_link_generators: action -> ((string -> string) list ref) sub get_link_generators { my ($action) = @_; return $action->{link_generators}; } # set_resource_path_processor: action * (string -> string) -> unit sub set_resource_path_processor { my ($action, $processor) = @_; $action->{pathfinder} = $processor; } # classify_request_semantics: http_request -> unit sub classify_request_semantics { my ($http_request) = @_; my $semantics = $http_request->{request_semantics}; my $path = $semantics->{resource_path_elements}; my $length = scalar @$path; my $action = $default_action; if ($length > 0) { my $action_candidate = lc $path->[$length - 1]; if (defined $actions{$action_candidate}) { $action = $actions{$action_candidate}; pop @$path; $length--; } } $semantics->{action} = $action; my $resource_path = '/' . (join '/', @$path); $resource_path = $action->{pathfinder}->( $resource_path ); $semantics->{resource_path} = $resource_path; } } # set_response_content_type: http_request * string -> unit sub set_response_content_type { my ($http_request, $type) = @_; set_response_field( $http_request, "Content-Type", $type ); } # set_response_field: http_request * string * string -> unit sub set_response_field { my ($http_request, $field, $type) = @_; $http_request->{response_headers}->{$field} = $type; } { my %path_aliases; # add_path_alias: string * string -> unit # (alias_path, true_path) sub add_path_alias { my ($alias_path, $true_path) = @_; $alias_path = "/$alias_path" if $alias_path !~ m@^/@; $true_path = "/$true_path" if $true_path !~ m@^/@; $path_aliases{$alias_path} = $true_path; } # get_true_path: string -> string sub get_true_path { my ($path) = @_; my $true_path = $path_aliases{$path}; return $true_path if defined $true_path; return $path; } # get_true_path_to_resource: string -> string sub get_true_path_to_resource { my ($path) = @_; $path = get_true_path( $path ); my $local_path = create_local_path( $path ); if (-d $local_path) { if ($path =~ /\/$/) { $path .= $directory_page; } else { $path .= '/' . $directory_page; } } return $path; } } # handle_content_request: http_request -> unit sub handle_content_request { my ($http_request) = @_; parse_uri $http_request; my $action = $http_request->{request_semantics}->{action}; my $length = $http_request->{request_headers}->{"Content-Length"}; if ($length > 0) { $http_request->{request_content} = recv_nbytes( $http_request, $length ); } my $handler = $action->{content_handler}; if (not $handler) { send_error( $http_request, 500, "Internal Server Error", "Couldn't find a content handler for the semantic action '" . $action->{name} . "'." ); } else { $handler->( $http_request ); } } # linkify: string -> string sub linkify { my ($text) = @_; $text =~ s/[^\w ]//g; if ($text eq "lb") { return "[" } if ($text eq "rb") { return "]" } my ($href) = $text; $href =~ s/ /+/g; "$text" } # content_filter: string -> string sub content_filter { my ($text) = @_; $text =~ s/\[\[(.*?)\]\]/linkify($1)/eg; $text } # handle_content_fetch: http_request -> unit *handle_content_fetch = handle_content_fetch_filtered( sub { $_[0] } ); # handle_content_fetch_filtered: (string -> string) -> (http_request -> unit) sub handle_content_fetch_filtered { my ($filter) = @_; return sub { my ($http_request) = @_; my $semantics = $http_request->{request_semantics}; my $resource_path = $semantics->{resource_path}; my $resource = read_semantic_resource( $resource_path ); if (not defined $resource) { my $editpath = create_resource_edit_path( $resource_path ); send_redirect( $http_request, $editpath ); } else { if (defined $filter) { $resource->{text} = $filter->($resource->{text}) } my $html_message = create_html_response( $http_request, $resource->{text} ); send_html_okay_response( $http_request, $html_message ); } } } # handle_content_list: http_request -> unit sub handle_content_list { my ($http_request) = @_; my $resource_path = $http_request->{request_semantics}->{resource_path}; if (not opendir DIR, create_local_path( $resource_path, "directory" )) { my $buffer = "Could not list the entries in the path '" . $resource_path . "': $!"; send_error( $http_request, 400, "Not a directory", $buffer ); } else { my @files = readdir DIR; close DIR; my $path = $resource_path; $path =~ s/\/*$//; my $cnt = 0; my $buffer; foreach my $filename (@files) { next if not should_list_resource( $resource_path, $filename ); $buffer .= '' . $filename . '" . $http_message . "
"; set_response_content_type( $http_request, "text/html" ); } send_response( $http_request, $http_error_code, $http_error_message, $buffer); } # send_redirect: http_request * string -> unit sub send_redirect { my ($http_request, $target) = @_; my $host = $http_request->{request_headers}->{Host}; if ($host) { set_response_field( $http_request, "Location", "http://$host$target" ); send_response( $http_request, 303, "Redirect", undef ); } else { my $buffer = "$line
" ); close Conn; next; } } my %http_request_headers; while (1) { my $line = recv_line $http_request; $line =~ s/\s+$//; last if not $line; $line =~ /^([^:]+):\s*(.*)$/; $http_request_headers{$1} = $2; } $http_request->{request_headers} = \%http_request_headers; handle_request $http_request; close Conn; }