123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231(* Ocsigen
* http://www.ocsigen.org
* Copyright (C) 2009 Boris Yakobowski
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)(* Display of a local file or directory. Currently used in staticmod
and eliom_predefmod *)letsection=Lwt_log.Section.make"ocsigen:local-file"exceptionFailed_403exceptionFailed_404exceptionNotReadableDirectory(* Policies for following symlinks *)typesymlink_policy=stat:Unix.LargeFile.stats->lstat:Unix.LargeFile.stats->boolletnever_follow_symlinks:symlink_policy=fun~stat:_~lstat:_->falseletfollow_symlinks_if_owner_match:symlink_policy=fun~stat~lstat->stat.Unix.LargeFile.st_uid=lstat.Unix.LargeFile.st_uid(* checks that [filename] can be followed depending on the predicate
[policy] which must receives as argument both the results
of calling [stat] and [lstat] on filenam.
If supplied, [stat] must be the result of calling [Unix.stat] on
[filename] *)letcheck_symlinks_auxfilename?(stat=Unix.LargeFile.statfilename)(policy:symlink_policy)=letlstat=Unix.LargeFile.lstatfilenameiniflstat.Unix.LargeFile.st_kind=Unix.S_LNKthenpolicy~stat~lstatelsetrue(* Check that there are no invalid symlinks in the directories leading to
[filename]. Paths upwards [no_check_for] are not checked. *)letreccheck_symlinks_parent_directories~filename~no_check_for(policy:symlink_policy)=iffilename="/"||filename="."||Somefilename=no_check_forthentrueelseletdirname=Filename.dirnamefilenameincheck_symlinks_auxdirnamepolicy&&check_symlinks_parent_directories~filename:dirname~no_check_forpolicy(* Check that [filename] can be reached according to the given
symlink policy *)letcheck_symlinks~no_check_for~filenamepolicy=letauxpolicy=iffilename="/"then(* The root cannot be a symlink, and this avoids some degenerate
cases later on *)trueelseletfilename=(* [filename] should start by at least a slash, as
[Filename.is_relative filename] should be false. Hence the length
should be at least 1 *)(* We remove an eventual trailing slash, in order to avoid a
needless recursion in check_symlinks_parent_directories, and so
that Unix.lstat returns the correct result (Unix.lstat "foo/" and
Unix.lstat "foo" return two different results...) *)letlen=String.lengthfilename-1iniffilename.[len]='/'thenString.subfilename0lenelsefilenameincheck_symlinks_auxfilenamepolicy&&check_symlinks_parent_directories~filename~no_check_forpolicyinmatchpolicywith|`Always->true|`No->auxnever_follow_symlinks|`Owner_match->auxfollow_symlinks_if_owner_matchletcheck_dotdot=letregexp=Ocsigen_lib.Netstring_pcre.regexp"(/\\.\\./)|(/\\.\\.$)"infun~filename->(* We always reject .. in filenames.
In URLs, .. have already been removed by the server,
but the filename may come from somewhere else than URLs ... *)tryignore(Ocsigen_lib.Netstring_pcre.search_forwardregexpfilename0);falsewithNot_found->trueletcan_sendfilenamerequest=letfilename=Ocsigen_lib.Url.split_pathfilename|>Ocsigen_lib.Url.norm_path|>Ocsigen_lib.Url.join_pathinLwt_log.ign_info_f~section"checking if file %s can be sent"filename;letmatchesarg=Ocsigen_lib.Netstring_pcre.string_match(Ocsigen_extensions.do_not_serve_to_regexparg)filename0<>Noneinifmatchesrequest.Ocsigen_extensions.do_not_serve_403then(Lwt_log.ign_info~section"this file is forbidden";raiseFailed_403)elseifmatchesrequest.Ocsigen_extensions.do_not_serve_404then(Lwt_log.ign_info~section"this file must be hidden";raiseFailed_404)(* Return type of a request for a local file. The string argument
represents the real file/directory to serve, eg. foo/index.html
instead of foo *)typeresolved=|RFileofstring|RDirofstring(* given [filename], we search for it in the local filesystem and
- we return ["filename/index.html"] if [filename] corresponds to
a directory, ["filename/index.html"] is valid, and ["index.html"]
is one possible index (trying all possible indexes in order)
- we raise [Failed_404] if [filename] corresponds to a directory,
no index exists and [list_dir_content] is false.
Warning: this behaviour is not the same as Apache's but it corresponds
to a missing service in Eliom (answers 404). This also allows to have
an Eliom service after a "forbidden" directory
- we raise [Failed_403] if [filename] is a symlink that must
not be followed
- raises [Failed_404] if [filename] does not exist, or is a special file
- otherwise returns [filename]
*)(* See also module Files in eliom.ml *)letresolve?no_check_for~request:({Ocsigen_extensions.request_config;_}asrequest)~filename()=(* We only accept absolute filenames in daemon mode,
as we do not really know what is the current directory *)letfilename=ifFilename.is_relativefilename&&Ocsigen_config.get_daemon()then"/"^filenameelsefilenameintryLwt_log.ign_info_f~section"Testing \"%s\"."filename;letstat=Unix.LargeFile.statfilenameinlet(filename,stat)=ifstat.Unix.LargeFile.st_kind=Unix.S_DIRtheniffilename.[String.lengthfilename-1]<>'/'thenbegin(* In this case, [filename] is a directory but this is not visible in
its name as there is no final slash. We signal this fact to
Ocsigen, which will then issue a 301 redirection to "filename/" *)Lwt_log.ign_info_f~section"LocalFiles: %s is a directory"filename;raise(Ocsigen_extensions.Ocsigen_is_dir(Ocsigen_extensions.new_url_of_directory_requestrequest))endelseletrecfind_index=function|[]->(* No suitable index, we try to list the directory *)ifrequest_config.Ocsigen_extensions.list_directory_contentthen(Lwt_log.ign_info~section"Displaying directory content";(filename,stat))else((* No suitable index *)Lwt_log.ign_info~section"No index and no listing";raiseNotReadableDirectory)|e::q->letindex=filename^einLwt_log.ign_info_f~section"Testing \"%s\" as possible index."index;try(index,Unix.LargeFile.statindex)with|Unix.Unix_error(Unix.ENOENT,_,_)->find_indexqinfind_indexrequest_config.Ocsigen_extensions.default_directory_indexelse(filename,stat)inifnot(check_dotdot~filename)then(Lwt_log.ign_info_f~section"Filenames cannot contain .. as in \"%s\"."filename;raiseFailed_403)elseifcheck_symlinks~filename~no_check_forrequest_config.Ocsigen_extensions.follow_symlinksthen(can_sendfilenamerequest_config;(* If the previous function did not fail, we are authorized to
send this file *)Lwt_log.ign_info_f~section"Returning \"%s\"."filename;ifstat.Unix.LargeFile.st_kind=Unix.S_REGthenRFilefilenameelseifstat.Unix.LargeFile.st_kind=Unix.S_DIRthenRDirfilenameelseraiseFailed_404)else((* [filename] is accessed through as symlink which we should not
follow according to the current policy *)Lwt_log.ign_info_f~section"Failed symlink check for \"%s\"."filename;raiseFailed_403)with(* We can get an EACCESS here, if are missing some rights on a directory *)|Unix.Unix_error(Unix.EACCES,_,_)->raiseFailed_403|Unix.Unix_error(Unix.ENOENT,_,_)->raiseFailed_404