@@ -6,11 +6,19 @@ import cats.data._
66import cats .effect ._
77import cats .implicits ._
88import java .io .File
9+ import java .nio .file .{LinkOption , Paths }
910import org .http4s .headers .Range .SubRange
1011import org .http4s .headers ._
12+ import org .http4s .server .middleware .TranslateUri
13+ import org .log4s .getLogger
1114import scala .concurrent .ExecutionContext
15+ import scala .util .control .NoStackTrace
16+ import scala .util .{Failure , Success , Try }
17+ import java .nio .file .NoSuchFileException
1218
1319object FileService {
20+ private [this ] val logger = getLogger
21+
1422 type PathCollector [F [_]] = (File , Config [F ], Request [F ]) => OptionT [F , Response [F ]]
1523
1624 /** [[org.http4s.server.staticcontent.FileService ]] configuration
@@ -43,14 +51,46 @@ object FileService {
4351 }
4452
4553 /** Make a new [[org.http4s.HttpRoutes ]] that serves static files. */
46- private [staticcontent] def apply [F [_]](config : Config [F ])(implicit F : Effect [F ]): HttpRoutes [F ] =
47- Kleisli {
48- case request if request.pathInfo.startsWith(config.pathPrefix) =>
49- getFile(s " ${config.systemPath}/ ${getSubPath(request.pathInfo, config.pathPrefix)}" )
50- .flatMap(f => config.pathCollector(f, config, request))
51- .semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
52- case _ => OptionT .none
54+ private [staticcontent] def apply [F [_]](config : Config [F ])(
55+ implicit F : Effect [F ]): HttpRoutes [F ] = {
56+ object BadTraversal extends Exception with NoStackTrace
57+ Try (Paths .get(config.systemPath).toRealPath()) match {
58+ case Success (rootPath) =>
59+ TranslateUri (config.pathPrefix)(Kleisli {
60+ case request =>
61+ request.pathInfo.split(" /" ) match {
62+ case Array (head, segments @ _* ) if head.isEmpty =>
63+ OptionT
64+ .liftF(F .catchNonFatal {
65+ segments.foldLeft(rootPath) {
66+ case (_, " " | " ." | " .." ) => throw BadTraversal
67+ case (path, segment) =>
68+ path.resolve(Uri .decode(segment, plusIsSpace = true ))
69+ }
70+ })
71+ .semiflatMap(path => F .delay(path.toRealPath(LinkOption .NOFOLLOW_LINKS )))
72+ .collect { case path if path.startsWith(rootPath) => path.toFile }
73+ .flatMap(f => config.pathCollector(f, config, request))
74+ .semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
75+ .recoverWith {
76+ case _ : NoSuchFileException => OptionT .none
77+ case BadTraversal => OptionT .some(Response (Status .BadRequest ))
78+ }
79+ case _ => OptionT .none
80+ }
81+ })
82+
83+ case Failure (_ : NoSuchFileException ) =>
84+ logger.error(
85+ s " Could not find root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will return none. " )
86+ Kleisli (_ => OptionT .none)
87+
88+ case Failure (e) =>
89+ logger.error(e)(
90+ s " Could not resolve root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will fail with a 500. " )
91+ Kleisli (_ => OptionT .pure(Response (Status .InternalServerError )))
5392 }
93+ }
5494
5595 private def filesOnly [F [_]](file : File , config : Config [F ], req : Request [F ])(
5696 implicit F : Sync [F ],
@@ -124,12 +164,4 @@ object FileService {
124164
125165 case _ => OptionT .none
126166 }
127-
128- // Attempts to sanitize the file location and retrieve the file. Returns None if the file doesn't exist.
129- private def getFile [F [_]](unsafePath : String )(implicit F : Sync [F ]): OptionT [F , File ] =
130- OptionT (F .delay {
131- val f = new File (Uri .removeDotSegments(unsafePath))
132- if (f.exists()) Some (f)
133- else None
134- })
135167}
0 commit comments