diff --git a/http-server/src/main/scala/coursier/HttpServer.scala b/http-server/src/main/scala/coursier/HttpServer.scala index fbef7f8a9..1090c7ee8 100644 --- a/http-server/src/main/scala/coursier/HttpServer.scala +++ b/http-server/src/main/scala/coursier/HttpServer.scala @@ -8,13 +8,59 @@ import org.http4s._ import org.http4s.dsl._ import org.http4s.headers.{Authorization, `Content-Type`} import org.http4s.server.blaze.BlazeBuilder +import org.http4s.server.Server import caseapp._ import scala.collection.JavaConverters._ import scalaz.concurrent.Task +final case class AuthOptions( + @ExtraName("u") + @ValueDescription("user") + user: String, + @ExtraName("P") + @ValueDescription("password") + password: String, + @ExtraName("r") + @ValueDescription("realm") + realm: String +) { + def checks(): Unit = { + if (user.nonEmpty && password.isEmpty) + Console.err.println( + "Warning: authentication enabled but no password specified. " + + "Specify one with the --password or -P option." + ) + + if (password.nonEmpty && user.isEmpty) + Console.err.println( + "Warning: authentication enabled but no user specified. " + + "Specify one with the --user or -u option." + ) + + if ((user.nonEmpty || password.nonEmpty) && realm.isEmpty) + Console.err.println( + "Warning: authentication enabled but no realm specified. " + + "Specify one with the --realm or -r option." + ) + } +} + +final case class VerbosityOptions( + @ExtraName("v") + verbose: Int @@ Counter, + @ExtraName("q") + quiet: Boolean +) { + lazy val verbosityLevel = Tag.unwrap(verbose) - (if (quiet) 1 else 0) +} + final case class HttpServerOptions( + @Recurse + auth: AuthOptions, + @Recurse + verbosity: VerbosityOptions, @ExtraName("d") @ValueDescription("served directory") directory: String, @@ -31,188 +77,158 @@ final case class HttpServerOptions( @ExtraName("w") @HelpMessage("Accept write requests. Equivalent to -s -t") acceptWrite: Boolean, - @ExtraName("v") - verbose: Int @@ Counter, - @ExtraName("q") - quiet: Boolean, - @ExtraName("u") - @ValueDescription("user") - user: String, - @ExtraName("P") - @ValueDescription("password") - password: String, - @ExtraName("r") - @ValueDescription("realm") - realm: String, @ExtraName("l") @HelpMessage("Generate content listing pages for directories") listPages: Boolean ) object HttpServer extends CaseApp[HttpServerOptions] { - def run(options: HttpServerOptions, args: RemainingArgs): Unit = { - val baseDir = new File(if (options.directory.isEmpty) "." else options.directory) + def write(baseDir: File, path: Seq[String], req: Request): Boolean = { - val verbosityLevel = Tag.unwrap(options.verbose) - (if (options.quiet) 1 else 0) + val f = new File(baseDir, path.toList.mkString("/")) + f.getParentFile.mkdirs() - def write(path: Seq[String], req: Request): Boolean = { - - val f = new File(baseDir, path.toList.mkString("/")) - f.getParentFile.mkdirs() - - var os: FileOutputStream = null - var lock: FileLock = null - try { - os = new FileOutputStream(f) - lock = - try os.getChannel.tryLock() - catch { - case _: OverlappingFileLockException => - null - } - - if (lock == null) - false - else { - req.body.runLog.unsafePerformSync.foreach { b => - b.copyToStream(os) - } - - true + var os: FileOutputStream = null + var lock: FileLock = null + try { + os = new FileOutputStream(f) + lock = + try os.getChannel.tryLock() + catch { + case _: OverlappingFileLockException => + null } - } finally { - if (lock != null) - lock.release() - if (os != null) - os.close() + + if (lock == null) + false + else { + req.body.runLog.unsafePerformSync.foreach { b => + b.copyToStream(os) + } + + true } + } finally { + if (lock != null) + lock.release() + if (os != null) + os.close() + } + } + + def isDirectory(f: File): Task[Option[Boolean]] = + Task { + if (f.isDirectory) + Some(true) + else if (f.isFile) + Some(false) + else + None } - if (options.user.nonEmpty && options.password.isEmpty) - Console.err.println( - "Warning: authentication enabled but no password specified. " + - "Specify one with the --password or -P option." - ) - if (options.password.nonEmpty && options.user.isEmpty) - Console.err.println( - "Warning: authentication enabled but no user specified. " + - "Specify one with the --user or -u option." - ) - if ((options.user.nonEmpty || options.password.nonEmpty) && options.realm.isEmpty) - Console.err.println( - "Warning: authentication enabled but no realm specified. " + - "Specify one with the --realm or -r option." - ) - - val unauthorized = Unauthorized(Challenge("Basic", options.realm)) - - def authenticated(pf: PartialFunction[Request, Task[Response]]): HttpService = - authenticated0(HttpService(pf)) - - def authenticated0(service: HttpService): HttpService = - if (options.user.isEmpty && options.password.isEmpty) - service - else - HttpService { - case req => - def warn(msg: => String) = - if (verbosityLevel >= 1) - Console.err.println(s"${req.method.name} ${req.uri.path}: $msg") - - req.headers.get(Authorization) match { - case None => - warn("no authentication provided") - unauthorized - case Some(auth) => - auth.credentials match { - case basic: BasicCredentials => - if (basic.username == options.user && basic.password == options.password) - service.run(req) - else { - warn { - val msg = - if (basic.username == options.user) - "wrong password" - else - s"unrecognized user ${basic.username}" - - s"authentication failed ($msg)" - } - unauthorized - } - case _ => - warn("no basic credentials found") - unauthorized - } - } + def directoryListingPage(dir: File, title: String): Task[String] = + Task { + val entries = dir + .listFiles() + .flatMap { f => + def name = f.getName + if (f.isDirectory) + Seq(name + "/") + else if (f.isFile) + Seq(name) + else + Nil } - def putService = authenticated { + // meh escaping + // TODO Use to scalatags to generate that + s""" + | + |
+ |