server.pl 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. #!/usr/bin/env perl
  2. # server.pl
  3. #
  4. # Basic HTTP web server without installing any extra modules*
  5. # Features:
  6. # - directory listing
  7. # - auto directory index (shows index.html/index.htm if exists)
  8. # - watch feature*
  9. # - mimetype handling etc.
  10. # * Requires Filesys::Notify::Simple if watch feature used, but otherwise
  11. # not needed. However, it is possible so that the user doesn't need to install
  12. # the module at all. See comment for instructions on embedding this module.
  13. # License: CC0 1.0 (https://creativecommons.org/publicdomain/zero/1.0/)
  14. # Usage:
  15. # - Have Perl installed
  16. # - Run "perl server.pl"
  17. # - Visit "http://localhost:7777" on a web browser
  18. # Run with -h for more options
  19. # Limitations:
  20. # - Does not handle POST (only GET for now)
  21. # Original (public domain):
  22. # https://renenyffenegger.ch/notes/development/languages/Perl/modules/IO/Socket/echo-server-client
  23. use warnings;
  24. use strict;
  25. use IO::Socket::INET;
  26. use Net::hostent; # for OO version of gethostbyaddr
  27. # To handle CLI parameters
  28. use Getopt::Long;
  29. use File::Basename;
  30. use Cwd qw( abs_path cwd );
  31. my $script_dir = abs_path(dirname($0));
  32. # Set $PWD/public as webroot
  33. my $webroot_dir = $script_dir . '/public';
  34. # Fallback to current directory in case $script_dir/public is not found
  35. unless ( -d $webroot_dir ) {
  36. $webroot_dir = cwd();
  37. }
  38. # The port to run the server in
  39. my $port_listen = 7777;
  40. # The dir to watch changes for
  41. my $watch_dir;
  42. # The command to run when file change is detected
  43. my $watch_command;
  44. # Check for file changes at this many seconds interval
  45. my $watch_time_interval = 5;
  46. $| = 1; # Autoflush
  47. # Shows up when --help or -h is passed
  48. sub help_text {
  49. print("usage: server.pl [-h] [-p PORT] [-d WEBROOT] [-w DIR] [-c COMMAND]
  50. [-t SECONDS]
  51. A simple HTTP web server in Perl.
  52. optional arguments:
  53. -h, --help show this help message and exit
  54. -p PORT, --port PORT
  55. port to listen for requests [default:7777]
  56. -d WEBROOT, --directory WEBROOT
  57. directory of the files to serve [default:public or \$PWD]
  58. -w DIR, --watch DIR
  59. watch DIR for file changes
  60. -c COMMAND, --watch COMMAND
  61. run command when file changed in DIR
  62. -t SECONDS, --time SECONDS
  63. check for file changes after each SECONDS seconds [default:5]
  64. examples:
  65. start server in 7777 port and serve files in public or \$PWD:
  66. \$ ./server.pl
  67. start server in 3344 port and serve files in www/static directory:
  68. \$ ./server.pl -p 3344 -d www/static
  69. start server in watch mode and run generate.pl when file changes:
  70. \$ ./server.pl -w src -c 'perl generate.pl'\n");
  71. exit;
  72. }
  73. # Process CLI parameters and update config values as necessary
  74. GetOptions ("p|port=i" => \$port_listen,
  75. "d|directory=s" => \$webroot_dir,
  76. "w|watch=s" => \$watch_dir,
  77. "c|command=s" => \$watch_command,
  78. "t|time=i" => \$watch_time_interval,
  79. "h|help" => \&help_text)
  80. or die("Error in command line arguments. Please review and try again. Run with -h for help.\n");
  81. # Watch
  82. if ( $watch_dir ) {
  83. unless ( $watch_command ) {
  84. die('A command is required when run with -w/--watch. Please set a command with -c param to run a command when files change. See -h for details.');
  85. }
  86. # To detect if threads are built into Perl installation
  87. use Config;
  88. $Config{useithreads} or
  89. die('Recompile Perl with threads to run this program or try without -w/--watch param.');
  90. # Detect if Filesys::Notify::Simple module is installed.
  91. use Module::Load::Conditional qw( can_load );
  92. unless ( can_load( modules => {'Filesys::Notify::Simple'} ) ) {
  93. # Or if you want to have the Filesys::Notify::Simple module saved under
  94. # a "./perl" dir in the same dir as this script, download the module
  95. # from https://metacpan.org/pod/Filesys::Notify::Simple
  96. # and have the files under "/Filesys-Notify-Simple-0.14/lib/" in the
  97. # archive saved to ./perl, so that there is a
  98. # ./perl/Filesys/Notify/Simple.pm file. The module is under the same
  99. # license as Perl. Please include the text under the "LICENSE" heading
  100. # on the URL above to somewhere on your project if you include the
  101. # module files. Replace "./perl" with something else in this script to
  102. # use another directory of your choice.
  103. if ( -d './perl' ) {
  104. use lib './perl';
  105. } else {
  106. die('Filesys::Notify::Simple module is not installed. Please install and try again or do not use the -w/--watch parameter.');
  107. }
  108. }
  109. use Filesys::Notify::Simple;
  110. $watch_dir = abs_path($watch_dir);
  111. my $watcher = Filesys::Notify::Simple->new( [ $watch_dir ] );
  112. # We'll run the watch on separate thread
  113. use threads;
  114. my $thr = threads->create( \&watch_sub );
  115. # Will check for changes every 5 seconds or $watch_time_interval seconds
  116. sub watch_sub {
  117. while ( 1 ) {
  118. print "> Checking for changes...\n";
  119. $watcher->wait( sub {
  120. for my $event ( @_ ) {
  121. if ( $event->{path} ) {
  122. print "> Change detected. Running command...\n";
  123. system($watch_command) == 0 or print "> Command has returned a non-zero exit status :(\n";
  124. }
  125. }
  126. } );
  127. sleep $watch_time_interval;
  128. }
  129. }
  130. print "> Watching '${watch_dir}' directory for changes...\n";
  131. }
  132. my $socket = IO::Socket::INET->new(
  133. LocalHost => '0.0.0.0',
  134. LocalPort => $port_listen,
  135. Proto => 'tcp',
  136. Listen => 5,
  137. Reuse => 1
  138. ) or die "Cannot create socket. The port $port_listen is probably already being used? Please pass -p PORT to set a different port or stop already running instances of this script.";
  139. print "> Server started\n";
  140. print "> Waiting for requests on http://localhost:${port_listen}\n";
  141. my $request_path;
  142. my $content='';
  143. my $host;
  144. while ( my $client = $socket->accept() ) {
  145. # Host related
  146. my $hostinfo = gethostbyaddr($client->peeraddr);
  147. $host = $client->peerhost();
  148. my $request;
  149. my $request_url = '';
  150. my $request_params = '';
  151. my $request_url_full;
  152. my $request_method;
  153. # Response related
  154. my $response_mimetype = 'text/plain';
  155. local $/ = Socket::CRLF;
  156. # Read request up to an empty line
  157. while ( <$client> ) {
  158. last unless /\S/;
  159. if (/(\S+) ([^\?]+)(\?.*)? HTTP\//) {
  160. $request_method = $1;
  161. $request_url = $2;
  162. $request_params = $3; # GET params. e.g. "?test=1"
  163. }
  164. }
  165. $request_url_full = $host . ':' . $port_listen . $request_url;
  166. $request_path = $webroot_dir . $request_url;
  167. if ( -d $request_path ) {
  168. if ( -f "${request_path}/index.html" ) {
  169. $request_path = "${request_path}/index.html";
  170. } elsif ( -f "${request_path}/index.htm" ) {
  171. $request_path = "${request_path}/index.htm";
  172. } else {
  173. opendir DIR, $request_path;
  174. my @dir = sort readdir(DIR);
  175. close DIR;
  176. # Indicate that we're outputting HTML for the page
  177. $response_mimetype = 'text/html';
  178. # Prepare the content for the file index
  179. $content = "<h1>${request_url_full}</h1>";
  180. $content .= "<ul>";
  181. foreach (@dir) {
  182. if ( -d $request_path . $_ ) {
  183. $content .= "<li><strong><a href=\"http://${request_url_full}/$_\">$_</a></strong></li>";
  184. } else {
  185. $content .= "<li><a href=\"http://${request_url_full}/$_\">$_</a></li>";
  186. }
  187. }
  188. $content .= "</ul>";
  189. }
  190. print "> Directory requested. Will serve index HTML instead if found or a directory file list.\n";
  191. }
  192. # File is there, so show its content.
  193. if ( -f $request_path ) {
  194. open my $CRF, '<', $request_path or die "Can't open cache file $!";
  195. $content = do { local $/; <$CRF> };
  196. close($CRF);
  197. # File does not exist and no directory index content is there to serve.
  198. # So show error.
  199. } elsif ( $content eq '' ) {
  200. print "> ${request_url_full} does not exist, so serving an error instead\n";
  201. $content = "ERROR: ${request_url_full} could not be found";
  202. }
  203. # Set mimetype
  204. if ( $request_path =~ /\.htm$/ or $request_path =~ /\.html$/ ) {
  205. $response_mimetype = 'text/html';
  206. } elsif ( $request_path =~ /\.js$/ ) {
  207. $response_mimetype = 'text/javascript';
  208. } elsif ( $request_path =~ /\.css$/ ) {
  209. $response_mimetype = 'text/css';
  210. } elsif ( $request_path =~ /\.png$/ ) {
  211. $response_mimetype = 'image/png';
  212. } elsif ( $request_path =~ /\.jpg$/ or $request_path =~ /\.jpeg$/ ) {
  213. $response_mimetype = 'image/jpeg';
  214. } elsif ( $request_path =~ /\.ico$/ ) {
  215. $response_mimetype = 'image/x-icon';
  216. } elsif ( $request_path =~ /\.gif$/ ) {
  217. $response_mimetype = 'image/gif';
  218. } elsif ( $request_path =~ /\.svg$/ ) {
  219. $response_mimetype = 'image/svg+xml';
  220. } elsif ( $request_path =~ /\.webp$/ ) {
  221. $response_mimetype = 'image/webp';
  222. } else {
  223. print "> Mimetype is not programmed in server for $request_url! Serving as ${response_mimetype}\n";
  224. }
  225. # Send header and content
  226. print $client "HTTP/1.0 200 OK", Socket::CRLF;
  227. print $client "Content-type: $response_mimetype", Socket::CRLF;
  228. print $client Socket::CRLF;
  229. $client->send( $content );
  230. # Close client and print a message on console
  231. close $client;
  232. print "> Request for ${request_url_full} has been answered\n";
  233. }