summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJuan J. Martinez <jjm@usebox.net>2021-02-28 15:16:46 +0000
committerJuan J. Martinez <jjm@usebox.net>2021-07-22 19:49:51 +0100
commitdfd878753475a8c8100be15d740eaacc78ed76f9 (patch)
treecd1dbeb75ba8f5f522465961cdac3ca27832581c
parent5777f650c69609fecd3e7696432bad256c778856 (diff)
downloadspacebeans-dfd878753475a8c8100be15d740eaacc78ed76f9.tar.gz
spacebeans-dfd878753475a8c8100be15d740eaacc78ed76f9.zip
User directories support
-rw-r--r--server/src/net/usebox/gemini/server/Server.scala6
-rw-r--r--server/src/net/usebox/gemini/server/ServiceConf.scala30
-rw-r--r--server/test/resources/username/public_gemini/index.gmi4
-rw-r--r--server/test/src/ServerSpec.scala103
-rw-r--r--spacebeans.conf.example7
5 files changed, 139 insertions, 11 deletions
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"