diff options
author | Juan J. MartÃnez <jjm@usebox.net> | 2022-01-20 21:44:34 +0000 |
---|---|---|
committer | Juan J. MartÃnez <jjm@usebox.net> | 2022-01-20 21:44:34 +0000 |
commit | f90e7bad9f1df2d652b82048e9073096ddc8df18 (patch) | |
tree | c6f53976dfcb03cabf474a9c8528db44da762c2e /server/test/src/handlers | |
parent | 397bee70a0baa6ab7cc2115c3a5dd555b381a49d (diff) | |
parent | aee32d5f42560ad0ff5d47034da699b3e608d2b4 (diff) | |
download | spacebeans-f90e7bad9f1df2d652b82048e9073096ddc8df18.tar.gz spacebeans-f90e7bad9f1df2d652b82048e9073096ddc8df18.zip |
Merge branch 'protocol-refactor' into 'main'
Protocol refactor
See merge request reidrac/spacebeans!2
Diffstat (limited to 'server/test/src/handlers')
-rw-r--r-- | server/test/src/handlers/GeminiHandlerSpec.scala | 528 | ||||
-rw-r--r-- | server/test/src/handlers/ProtocolHandlerSpec.scala | 100 |
2 files changed, 628 insertions, 0 deletions
diff --git a/server/test/src/handlers/GeminiHandlerSpec.scala b/server/test/src/handlers/GeminiHandlerSpec.scala new file mode 100644 index 0000000..f3545a0 --- /dev/null +++ b/server/test/src/handlers/GeminiHandlerSpec.scala @@ -0,0 +1,528 @@ +package net.usebox.gemini.server.handlers + +import java.net.URI + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import net.usebox.gemini.server._ + +class GeminiHandlerSpec extends AnyFlatSpec with Matchers { + + val handler = new GeminiHandler(TestData.conf) + val handlerUserDir = new GeminiHandler(TestData.confUserDir) + val handlerCgi = new GeminiHandler(TestData.cgiConf) + + def handleWith( + geminiHandler: GeminiHandler, + req: String, + remoteAddr: String = "127.0.0.1" + ): Response = + geminiHandler.handle(req, URI.create(req), remoteAddr) + + behavior of "handle" + + it should "handle host case insensitive" in { + handleWith(handler, "gemini://LOCALHOST/") should be(a[Success]) + } + + it should "return proxy request refused when the vhost is not found" in { + handleWith(handler, "gemini://otherhost/") should be( + a[ProxyRequestRefused] + ) + } + + it should "return bad request when user info is present" in { + handleWith(handler, "gemini://user@localhost/") should be( + a[BadRequest] + ) + } + + it should "return bad request when the path is out of root dir" in { + handleWith(handler, "gemini://localhost/../../") should be( + a[BadRequest] + ) + } + + it should "redirect to normalize the URL" in { + handleWith(handler, "gemini://localhost/./") should be( + a[PermanentRedirect] + ) + } + + it should "return not found if the path doesn't exist" in { + handleWith( + handler, + "gemini://localhost/doesnotexist" + ) should be(a[NotFound]) + } + + it should "return not found if a dot file" in { + handleWith(handler, "gemini://localhost/.dotfile") should be( + a[NotFound] + ) + } + + it should "return success on reading file" in { + handleWith( + handler, + "gemini://localhost/index.gmi" + ) should matchPattern { + case Success(_, "text/gemini", Some(_), 25L) => + } + } + + it should "redirect and normalize request on a directory" in { + handleWith(handler, "gemini://localhost/dir") should be( + a[PermanentRedirect] + ) + } + + it should "return an existing index file when requesting a directory" in { + handleWith(handler, "gemini://localhost/") should matchPattern { + case Success(_, "text/gemini", Some(_), 25L) => + } + } + + it should "include gemini params for gemini MIME type" in { + handleWith( + new GeminiHandler( + TestData.conf.copy(virtualHosts = + List(TestData.conf.virtualHosts(0).copy(geminiParams = Some("test"))) + ) + ), + "gemini://localhost/index.gmi" + ) should matchPattern { + case Success(_, "text/gemini; test", Some(_), 25L) => + } + } + + behavior of "handler, directory listings" + + it should "return a directory listing if is enabled and no index" in { + handleWith(handler, "gemini://localhost/dir/") should be( + a[DirListing] + ) + } + + it should "return a directory listing, directory listing flags: vhost flag false, directories flag true" in { + handleWith( + new GeminiHandler( + ServiceConf.initConf( + TestData.conf.copy(virtualHosts = + List( + TestData.conf + .virtualHosts(0) + .copy( + directoryListing = false, + directories = List( + Directory( + "dir/", + directoryListing = Some(true), + allowCgi = None + ) + ) + ) + ) + ) + ) + ), + "gemini://localhost/dir/" + ) should be(a[DirListing]) + } + + it should "return not found with no index, directory listing flags: vhost flag true, directories flag false" in { + handleWith( + new GeminiHandler( + ServiceConf.initConf( + TestData.conf.copy(virtualHosts = + List( + TestData.conf + .virtualHosts(0) + .copy( + directoryListing = true, + directories = List( + Directory( + "dir/", + directoryListing = Some(false), + allowCgi = None + ) + ) + ) + ) + ) + ) + ), + "gemini://localhost/dir/" + ) should be(a[NotFound]) + } + + it should "not apply directory listing override to subdirectories" in { + handleWith( + new GeminiHandler( + ServiceConf.initConf( + TestData.conf.copy(virtualHosts = + List( + TestData.conf + .virtualHosts(0) + .copy( + directoryListing = false, + directories = List( + Directory( + "dir/", + directoryListing = Some(true), + allowCgi = None + ) + ) + ) + ) + ) + ) + ), + "gemini://localhost/dir/sub/" + ) should be( + a[NotFound] + ) + } + + it should "return not found if directory listing is not enabled and no index" in { + handleWith( + new GeminiHandler( + TestData.conf.copy(virtualHosts = + List(TestData.conf.virtualHosts(0).copy(directoryListing = false)) + ) + ), + "gemini://localhost/dir/" + ) should be(a[NotFound]) + } + + behavior of "handler, user directories" + + it should "return success on reading file" in { + handleWith( + handlerUserDir, + "gemini://localhost/~username/index.gmi" + ) should matchPattern { + case Success(_, "text/gemini", Some(_), 38L) => + } + } + + it should "return redirect accessing the user directory without ending slash" in { + handleWith(handlerUserDir, "gemini://localhost/~username") should be( + a[PermanentRedirect] + ) + } + + it should "return success accessing the user directory index" in { + handleWith( + handlerUserDir, + "gemini://localhost/~username/" + ) should matchPattern { + case Success(_, "text/gemini", Some(_), 38L) => + } + } + + it should "return bad request trying to exit the root directory" in { + handleWith(handlerUserDir, "gemini://localhost/~username/../../") should be( + a[BadRequest] + ) + } + + it should "return redirect to the virtual host root when leaving the user dir" in { + handleWith(handlerUserDir, "gemini://localhost/~username/../") should be( + a[PermanentRedirect] + ) + } + + it should "not translate root if used an invalid user pattern" in { + handleWith(handlerUserDir, "gemini://localhost/~username../") should be( + a[NotFound] + ) + + handleWith(handlerUserDir, "gemini://localhost/~0invalid/") should be( + a[NotFound] + ) + } + + it should "not translate root if no user directory path was provided" in { + handleWith( + new GeminiHandler( + TestData.conf.copy(virtualHosts = + List( + TestData.conf + .virtualHosts(0) + .copy(userDirectories = true) + ) + ) + ), + "gemini://localhost/~username/", + "127.0.0.1" + ) should be(a[NotFound]) + } + + it should "not execute a CGI if the target resource is not executable" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/file.txt" + ) should matchPattern { + case Success(_, "text/plain", Some(_), 5L) => + } + } + + it should "not execute a CGI if the target resource is a directory" in { + handleWith(handlerCgi, "gemini://localhost/dir/sub/") should be( + a[DirListing] + ) + } + + it should "not apply allow CGI to subdirectories" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/sub/cgi" + ) should matchPattern { + case Success(_, "text/plain", Some(_), 72) => + } + } + + it should "execute a CGI" in { + val cgi = handleWith(handlerCgi, "gemini://localhost/dir/cgi") + .asInstanceOf[Cgi] + + cgi.status should be(20) + cgi.meta should be("text/gemini") + cgi.body should include("GATEWAY_INTERFACE=CGI/1.1") + } + + it should "execute a CGI: empty parameters, host and port" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/cgi" + ) should matchPattern { + case Cgi( + _, + _, + "", + "", + "cgi", + TestData.host, + TestData.portStr, + _, + _ + ) => + } + } + + it should "execute a CGI: host is case-insensitive and the value in the conf is used" in { + handleWith( + handlerCgi, + "gemini://LOCALHOST/dir/cgi" + ) should matchPattern { + case Cgi( + _, + _, + "", + "", + "cgi", + TestData.host, + TestData.portStr, + _, + _ + ) => + } + } + + it should "execute a CGI: query string" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/cgi?query&string" + ) should matchPattern { + case Cgi( + _, + _, + "query&string", + "", + "cgi", + TestData.host, + TestData.portStr, + _, + _ + ) => + } + } + + it should "execute a CGI: path info" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/cgi/path/info" + ) should matchPattern { + case Cgi( + _, + _, + "", + "/path/info", + "cgi", + TestData.host, + TestData.portStr, + _, + _ + ) => + } + } + + it should "execute a CGI: query string and path info" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/cgi/path/info?query=string" + ) should matchPattern { + case Cgi( + _, + _, + "query=string", + "/path/info", + "cgi", + TestData.host, + TestData.portStr, + _, + _ + ) => + } + } + + it should "not execute an executable if allow CGI is off" in { + handleWith( + handler, + "gemini://localhost/dir/cgi" + ) should matchPattern { + case Success(_, "text/plain", Some(_), _) => + } + } + + it should "response with an error if the CGI exits with non 0" in { + val bad = + handleWith(handlerCgi, "gemini://localhost/dir/bad-cgi") + .asInstanceOf[Cgi] + + val meta = "Error executing CGI" + bad.status should be(50) + bad.meta should be(meta) + bad.body should include(meta) + } + + it should "return a response with an error if the CGI exits with non 0" in { + val bad = + handleWith(handlerCgi, "gemini://localhost/dir/bad-cgi") + .asInstanceOf[Cgi] + + val meta = "Error executing CGI" + bad.status should be(50) + bad.meta should be(meta) + bad.body should include(meta) + } + + it should "return a response with an error if the CGI response is invalid" in { + val bad = + handleWith(handlerCgi, "gemini://localhost/dir/bad-response") + .asInstanceOf[Cgi] + + val meta = "Invalid response from CGI" + bad.status should be(40) + bad.meta should be(meta) + bad.body should include(meta) + } + + it should "environment variables are optional" in { + handleWith( + handlerCgi, + "gemini://localhost/dir/cgi/" + ) should matchPattern { + case Cgi( + _, + _, + _, + _, + "cgi", + TestData.host, + TestData.portStr, + _, + m + ) if m == Map() => + } + } + + it should "pass environment variables to the CGI" in { + handleWith( + new GeminiHandler(TestData.cgiEnvConf), + "gemini://localhost/dir/cgi/" + ) should matchPattern { + case Cgi( + _, + _, + _, + _, + "cgi", + TestData.host, + TestData.portStr, + _, + m + ) if m == Map("env1" -> "value") => + } + } + + it should "execute a CGI with the environment variables" in { + val cgi = handleWith( + new GeminiHandler(TestData.cgiEnvConf), + "gemini://localhost/dir/cgi" + ).asInstanceOf[Cgi] + + cgi.status should be(20) + cgi.meta should be("text/gemini") + cgi.body should include("env1=value") + } + + it should "execute a CGI when it is the index document" in { + val cgi = handleWith( + new GeminiHandler(TestData.cgiIndexConf), + "gemini://localhost/dir/" + ).asInstanceOf[Cgi] + + cgi.status should be(20) + cgi.meta should be("text/gemini") + cgi.body should include("GATEWAY_INTERFACE=CGI/1.1") + } + + it should "execute a CGI when it is the index document (full name)" in { + val cgi = handleWith( + new GeminiHandler(TestData.cgiIndexConf), + "gemini://localhost/dir/cgi" + ).asInstanceOf[Cgi] + + cgi.status should be(20) + cgi.meta should be("text/gemini") + cgi.body should include("GATEWAY_INTERFACE=CGI/1.1") + } + + it should "execute a CGI when it is the index document (full name, path info)" in { + val cgi = handleWith( + new GeminiHandler(TestData.cgiIndexConf), + "gemini://localhost/dir/cgi/path/info" + ).asInstanceOf[Cgi] + + cgi.status should be(20) + cgi.meta should be("text/gemini") + cgi.body should include("GATEWAY_INTERFACE=CGI/1.1") + cgi.body should include("PATH_INFO=/path/info") + } + + it should "resolve CGI directories from more to less specific" in { + // issue: https://gitlab.com/reidrac/spacebeans/-/issues/2 + val cgi = handleWith( + new GeminiHandler(TestData.cgiPrefConf), + "gemini://localhost/dir/sub/cgiOk/path/info" + ).asInstanceOf[Cgi] + + cgi.status should be(20) + cgi.meta should be("text/gemini") + cgi.body should include("GATEWAY_INTERFACE=CGI/1.1") + cgi.body should include("PATH_INFO=/path/info") + } +} diff --git a/server/test/src/handlers/ProtocolHandlerSpec.scala b/server/test/src/handlers/ProtocolHandlerSpec.scala new file mode 100644 index 0000000..d35c9f2 --- /dev/null +++ b/server/test/src/handlers/ProtocolHandlerSpec.scala @@ -0,0 +1,100 @@ +package net.usebox.gemini.server.handlers + +import java.nio.file.FileSystems +import java.net.URI + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import net.usebox.gemini.server.{ServiceConf, Response} +import net.usebox.gemini.server.TestData + +class ProtocolHandlerSpec extends AnyFlatSpec with Matchers { + + def getPath(value: String) = FileSystems.getDefault().getPath(value) + + class TestHandler(conf: ServiceConf) extends ProtocolHandler(conf) { + def handle(req: String, uri: URI, remoteAddr: String): Response = ??? + } + + val handler = new TestHandler(TestData.conf) + + behavior of "guessMimeType using the internal resolver" + + it should "resolve a known MIME type" in { + handler + .guessMimeType( + getPath("file.html"), + None + ) shouldBe "text/html" + } + + it should "resolve de default MIME type for unknown types" in { + handler + .guessMimeType( + getPath("unknow"), + None + ) shouldBe TestData.conf.defaultMimeType + } + + it should "resolve gemini MIME type" in { + handler + .guessMimeType( + getPath("file.gmi"), + None + ) shouldBe "text/gemini" + handler + .guessMimeType( + getPath("file.gemini"), + None + ) shouldBe "text/gemini" + } + + it should "resolve gemini MIME type, including parameters" in { + handler + .guessMimeType( + getPath("file.gmi"), + Some("param") + ) shouldBe "text/gemini; param" + handler + .guessMimeType( + getPath("file.gemini"), + Some("param") + ) shouldBe "text/gemini; param" + } + + it should "gemini MIME type parameters are sanitized" in { + handler + .guessMimeType( + getPath("file.gmi"), + Some(" ; param") + ) shouldBe "text/gemini; param" + } + + behavior of "guessMimeType using the configured types" + + it should "resolve a known MIME type" in { + new TestHandler(TestData.conf.copy(mimeTypes = TestData.mimeTypes)) + .guessMimeType( + getPath("file.gmi"), + None + ) shouldBe "config" + } + + it should "include parameters for text/gemini MIME types" in { + new TestHandler( + TestData.conf.copy(mimeTypes = Some(Map("text/gemini" -> List(".gmi")))) + ).guessMimeType( + getPath("file.gmi"), + Some("param") + ) shouldBe "text/gemini; param" + } + + it should "resolve de default MIME type for unknown types" in { + new TestHandler(TestData.conf.copy(mimeTypes = TestData.mimeTypes)) + .guessMimeType( + getPath("unknow"), + None + ) shouldBe TestData.conf.defaultMimeType + } +} |