@@ -6,10 +6,18 @@ import cats.data.{Kleisli, NonEmptyList, OptionT}
66import cats .effect .{Blocker , ContextShift , Sync }
77import cats .implicits ._
88import java .io .File
9+ import java .nio .file .NoSuchFileException
10+ import java .nio .file .{LinkOption , Paths }
911import org .http4s .headers .Range .SubRange
1012import org .http4s .headers ._
13+ import org .http4s .server .middleware .TranslateUri
14+ import org .log4s .getLogger
15+ import scala .util .control .NoStackTrace
16+ import scala .util .{Failure , Success , Try }
1117
1218object FileService {
19+ private [this ] val logger = getLogger
20+
1321 type PathCollector [F [_]] = (File , Config [F ], Request [F ]) => OptionT [F , Response [F ]]
1422
1523 /** [[org.http4s.server.staticcontent.FileService ]] configuration
@@ -42,14 +50,45 @@ object FileService {
4250 }
4351
4452 /** Make a new [[org.http4s.HttpRoutes ]] that serves static files. */
45- private [staticcontent] def apply [F [_]](config : Config [F ])(implicit F : Sync [F ]): HttpRoutes [F ] =
46- Kleisli {
47- case request if request.pathInfo.startsWith(config.pathPrefix) =>
48- OptionT (getFile(s " ${config.systemPath}/ ${getSubPath(request.pathInfo, config.pathPrefix)}" ))
49- .flatMap(f => config.pathCollector(f, config, request))
50- .semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
51- case _ => OptionT .none
53+ private [staticcontent] def apply [F [_]](config : Config [F ])(implicit F : Sync [F ]): HttpRoutes [F ] = {
54+ object BadTraversal extends Exception with NoStackTrace
55+ Try (Paths .get(config.systemPath).toRealPath()) match {
56+ case Success (rootPath) =>
57+ TranslateUri (config.pathPrefix)(Kleisli {
58+ case request =>
59+ request.pathInfo.split(" /" ) match {
60+ case Array (head, segments @ _* ) if head.isEmpty =>
61+ OptionT
62+ .liftF(F .catchNonFatal {
63+ segments.foldLeft(rootPath) {
64+ case (_, " " | " ." | " .." ) => throw BadTraversal
65+ case (path, segment) =>
66+ path.resolve(Uri .decode(segment, plusIsSpace = true ))
67+ }
68+ })
69+ .semiflatMap(path => F .delay(path.toRealPath(LinkOption .NOFOLLOW_LINKS )))
70+ .collect { case path if path.startsWith(rootPath) => path.toFile }
71+ .flatMap(f => config.pathCollector(f, config, request))
72+ .semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
73+ .recoverWith {
74+ case _ : NoSuchFileException => OptionT .none
75+ case BadTraversal => OptionT .some(Response (Status .BadRequest ))
76+ }
77+ case _ => OptionT .none
78+ }
79+ })
80+
81+ case Failure (_ : NoSuchFileException ) =>
82+ logger.error(
83+ s " Could not find root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will return none. " )
84+ Kleisli (_ => OptionT .none)
85+
86+ case Failure (e) =>
87+ logger.error(e)(
88+ s " Could not resolve root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will fail with a 500. " )
89+ Kleisli (_ => OptionT .pure(Response (Status .InternalServerError )))
5290 }
91+ }
5392
5493 private def filesOnly [F [_]](file : File , config : Config [F ], req : Request [F ])(
5594 implicit F : Sync [F ],
@@ -115,12 +154,4 @@ object FileService {
115154 }
116155 case _ => F .pure(None )
117156 }
118-
119- // Attempts to sanitize the file location and retrieve the file. Returns None if the file doesn't exist.
120- private def getFile [F [_]](unsafePath : String )(implicit F : Sync [F ]): F [Option [File ]] =
121- F .delay {
122- val f = new File (Uri .removeDotSegments(unsafePath))
123- if (f.exists()) Some (f)
124- else None
125- }
126157}
0 commit comments