@@ -4,12 +4,22 @@ package staticcontent
44
55import cats .data ._
66import cats .effect ._
7+ import cats .implicits ._
78import java .io .File
9+ import java .nio .file .{LinkOption , Paths }
810import org .http4s .headers .Range .SubRange
911import org .http4s .headers ._
12+ import org .http4s .server .middleware .TranslateUri
13+ import org .http4s .util .UrlCodingUtils .urlDecode
14+ import org .log4s .getLogger
1015import scala .concurrent .ExecutionContext
16+ import scala .util .control .NoStackTrace
17+ import scala .util .{Failure , Success , Try }
18+ import java .nio .file .NoSuchFileException
1119
1220object FileService {
21+ private [this ] val logger = getLogger
22+
1323 type PathCollector [F [_]] = (File , Config [F ], Request [F ]) => OptionT [F , Response [F ]]
1424
1525 /** [[org.http4s.server.staticcontent.FileService ]] configuration
@@ -42,14 +52,46 @@ object FileService {
4252 }
4353
4454 /** Make a new [[org.http4s.HttpService ]] that serves static files. */
45- private [staticcontent] def apply [F [_]](config : Config [F ])(implicit F : Effect [F ]): HttpService [F ] =
46- Kleisli {
47- case request if request.pathInfo.startsWith(config.pathPrefix) =>
48- 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
55+ private [staticcontent] def apply [F [_]](config : Config [F ])(
56+ implicit F : Effect [F ]): HttpService [F ] = {
57+ object BadTraversal extends Exception with NoStackTrace
58+ Try (Paths .get(config.systemPath).toRealPath()) match {
59+ case Success (rootPath) =>
60+ TranslateUri (config.pathPrefix)(Kleisli {
61+ case request =>
62+ request.pathInfo.split(" /" ) match {
63+ case Array (head, segments @ _* ) if head.isEmpty =>
64+ OptionT
65+ .liftF(F .catchNonFatal {
66+ segments.foldLeft(rootPath) {
67+ case (_, " " | " ." | " .." ) => throw BadTraversal
68+ case (path, segment) =>
69+ path.resolve(urlDecode(segment, plusIsSpace = true ))
70+ }
71+ })
72+ .semiflatMap(path => F .delay(path.toRealPath(LinkOption .NOFOLLOW_LINKS )))
73+ .collect { case path if path.startsWith(rootPath) => path.toFile }
74+ .flatMap(f => config.pathCollector(f, config, request))
75+ .semiflatMap(config.cacheStrategy.cache(request.pathInfo, _))
76+ .recoverWith {
77+ case _ : NoSuchFileException => OptionT .none
78+ case BadTraversal => OptionT .some(Response (Status .BadRequest ))
79+ }
80+ case _ => OptionT .none
81+ }
82+ })
83+
84+ case Failure (_ : NoSuchFileException ) =>
85+ logger.error(
86+ s " Could not find root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will return none. " )
87+ Kleisli (_ => OptionT .none)
88+
89+ case Failure (e) =>
90+ logger.error(e)(
91+ s " Could not resolve root path from FileService config: systemPath = ${config.systemPath}, pathPrefix = ${config.pathPrefix}. All requests will fail with a 500. " )
92+ Kleisli (_ => OptionT .pure(Response (Status .InternalServerError )))
5293 }
94+ }
5395
5496 private def filesOnly [F [_]](file : File , config : Config [F ], req : Request [F ])(
5597 implicit F : Sync [F ]): OptionT [F , Response [F ]] =
@@ -95,12 +137,4 @@ object FileService {
95137
96138 case _ => OptionT .none
97139 }
98-
99- // Attempts to sanitize the file location and retrieve the file. Returns None if the file doesn't exist.
100- private def getFile [F [_]](unsafePath : String )(implicit F : Sync [F ]): OptionT [F , File ] =
101- OptionT (F .delay {
102- val f = new File (PathNormalizer .removeDotSegments(unsafePath))
103- if (f.exists()) Some (f)
104- else None
105- })
106140}
0 commit comments