From dfd878753475a8c8100be15d740eaacc78ed76f9 Mon Sep 17 00:00:00 2001 From: "Juan J. Martinez" Date: Sun, 28 Feb 2021 15:16:46 +0000 Subject: User directories support --- server/src/net/usebox/gemini/server/Server.scala | 6 +- .../src/net/usebox/gemini/server/ServiceConf.scala | 30 +++++- .../resources/username/public_gemini/index.gmi | 4 + server/test/src/ServerSpec.scala | 103 +++++++++++++++++++-- spacebeans.conf.example | 7 ++ 5 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 server/test/resources/username/public_gemini/index.gmi diff --git a/server/src/net/usebox/gemini/server/Server.scala b/server/src/net/usebox/gemini/server/Server.scala index 2e9bb6b..d6bcb8a 100644 --- a/server/src/net/usebox/gemini/server/Server.scala +++ b/server/src/net/usebox/gemini/server/Server.scala @@ -100,10 +100,12 @@ case class Server(conf: ServiceConf) { case ("gemini", _, _, _) if uri.normalize() != uri => logger.debug("redirect to normalize uri") PermanentRedirect(req, uri.normalize().toString()) - case ("gemini", host, path, Some(vhost)) => + case ("gemini", host, rawPath, Some(vhost)) => + val (root, path) = vhost.getRoot(rawPath) + val resource = FileSystems .getDefault() - .getPath(vhost.root, path) + .getPath(root, path) .normalize() logger.debug(s"requesting: '$resource'") diff --git a/server/src/net/usebox/gemini/server/ServiceConf.scala b/server/src/net/usebox/gemini/server/ServiceConf.scala index e90d6a8..74ed548 100644 --- a/server/src/net/usebox/gemini/server/ServiceConf.scala +++ b/server/src/net/usebox/gemini/server/ServiceConf.scala @@ -20,10 +20,16 @@ case class VirtualHost( indexFile: String = "index.gmi", directoryListing: Boolean = true, geminiParams: Option[String] = None, - directories: List[Directory] = Nil + directories: List[Directory] = Nil, + userDirectories: Boolean = false, + userDirectoryPath: Option[String] = None ) object VirtualHost { + + val userTag = "{user}" + val userRe = raw"/~([a-z_][a-z0-9_-]*)(/{1}.*)?".r + implicit class VirtualHostOps(vhost: VirtualHost) { def getDirectoryListing(path: Path): Boolean = vhost.directories @@ -31,6 +37,18 @@ object VirtualHost { .fold(vhost.directoryListing)(loc => loc.directoryListing.getOrElse(vhost.directoryListing) ) + + def getRoot(path: String): (String, String) = + path match { + case userRe(user, null) + if vhost.userDirectories && vhost.userDirectoryPath.nonEmpty => + // username with no end slash, force redirect + (vhost.userDirectoryPath.get.replace(userTag, user), ".") + case userRe(user, userPath) + if vhost.userDirectories && vhost.userDirectoryPath.nonEmpty => + (vhost.userDirectoryPath.get.replace(userTag, user), userPath) + case _ => (vhost.root, path) + } } } @@ -55,9 +73,19 @@ object ServiceConf { implicit val virtualHostReader = deriveReader[VirtualHost] implicit val serviceConfReader = deriveReader[ServiceConf] + import VirtualHost.userTag + def load(confFile: String) = ConfigSource.file(confFile).load[ServiceConf].map { conf => conf.copy(virtualHosts = conf.virtualHosts.map { vhost => + if ( + vhost.userDirectories && !vhost.userDirectoryPath + .fold(false)(dir => dir.contains(userTag)) + ) + logger.warn( + s"In virtual host '${vhost.host}': user-directories is enabled but $userTag not found in user-directory-path" + ) + vhost.copy(directories = vhost.directories.map { dir => val path = FileSystems diff --git a/server/test/resources/username/public_gemini/index.gmi b/server/test/resources/username/public_gemini/index.gmi new file mode 100644 index 0000000..587d071 --- /dev/null +++ b/server/test/resources/username/public_gemini/index.gmi @@ -0,0 +1,4 @@ +# User directory support + +Some text. + diff --git a/server/test/src/ServerSpec.scala b/server/test/src/ServerSpec.scala index 3067fb8..05e1580 100644 --- a/server/test/src/ServerSpec.scala +++ b/server/test/src/ServerSpec.scala @@ -231,6 +231,25 @@ class ServerSpec extends AnyFlatSpec with Matchers { } } + it should "return proxy request refused for non gemini schemes" in { + Server(TestData.conf) + .handleReq("https://localhost/") should matchPattern { + case _: ProxyRequestRefused => + } + } + + it should "include gemini params for gemini MIME type" in { + Server( + TestData.conf.copy(virtualHosts = + List(TestData.conf.virtualHosts(0).copy(geminiParams = Some("test"))) + ) + ).handleReq("gemini://localhost/index.gmi") should matchPattern { + case Success(_, "text/gemini; test", Some(_), 25L) => + } + } + + behavior of "handleReq, directory listings" + it should "return a directory listing if is enabled and no index" in { Server(TestData.conf) .handleReq("gemini://localhost/dir/") should matchPattern { @@ -294,20 +313,75 @@ class ServerSpec extends AnyFlatSpec with Matchers { } } - it should "return proxy request refused for non gemini schemes" in { - Server(TestData.conf) - .handleReq("https://localhost/") should matchPattern { - case _: ProxyRequestRefused => + behavior of "handleReq, user directories" + + it should "return success on reading file" in { + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~username/index.gmi" + ) should matchPattern { + case Success(_, "text/gemini", Some(_), 38L) => } } - it should "include gemini params for gemini MIME type" in { + it should "return redirect accessing the user directory without ending slash" in { + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~username" + ) should matchPattern { + case _: PermanentRedirect => + } + } + + it should "return success accessing the user directory index" in { + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~username/" + ) should matchPattern { + case Success(_, "text/gemini", Some(_), 38L) => + } + } + + it should "return bad request trying to exit the root directory" in { + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~username/../../" + ) should matchPattern { + case _: BadRequest => + } + } + + it should "return redirect to the virtual host root when leaving the user dir" in { + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~username/../" + ) should matchPattern { + case _: PermanentRedirect => + } + } + + it should "not translate root if used an invalid user pattern" in { + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~username../" + ) should matchPattern { + case _: NotFound => + } + + Server(TestData.confUserDir).handleReq( + "gemini://localhost/~0invalid/" + ) should matchPattern { + case _: NotFound => + } + } + + it should "not translate root if no user directory path was provided" in { Server( TestData.conf.copy(virtualHosts = - List(TestData.conf.virtualHosts(0).copy(geminiParams = Some("test"))) + List( + TestData.conf + .virtualHosts(0) + .copy(userDirectories = true) + ) ) - ).handleReq("gemini://localhost/index.gmi") should matchPattern { - case Success(_, "text/gemini; test", Some(_), 25L) => + ).handleReq( + "gemini://localhost/~username/" + ) should matchPattern { + case _: NotFound => } } @@ -328,6 +402,19 @@ class ServerSpec extends AnyFlatSpec with Matchers { enabledCipherSuites = Nil ) + val confUserDir = conf.copy(virtualHosts = + List( + conf + .virtualHosts(0) + .copy( + userDirectories = true, + userDirectoryPath = Some( + getClass.getResource("/").getPath() + "{user}/public_gemini/" + ) + ) + ) + ) + val mimeTypes = Some( Map( "config" -> List(".gmi", ".gemini") diff --git a/spacebeans.conf.example b/spacebeans.conf.example index 8bd5949..69d278f 100644 --- a/spacebeans.conf.example +++ b/spacebeans.conf.example @@ -36,6 +36,13 @@ virtual-hosts = [ // { path = "relative/path/", directory-listing = true } // ] + // user directory support + // important: users are not checked, it only translates + // gemini://host/~user/ to use the user specific root path + // + // user-directories = false + // user-directory-path = "/home/{user}/public_gemini/" + // comment out to use an auto-generated self-signed certificate key-store { path = "/path/to/keystore.jks" -- cgit v1.2.3