implement tokenfile authentication

This commit is contained in:
Eugene Yokota
2017-09-21 23:05:48 -04:00
parent 8a8215cf1b
commit 348a077797
19 changed files with 322 additions and 33 deletions
@@ -0,0 +1,26 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait ServerAuthenticationFormats { self: sjsonnew.BasicJsonProtocol =>
implicit lazy val ServerAuthenticationFormat: JsonFormat[sbt.ServerAuthentication] = new JsonFormat[sbt.ServerAuthentication] {
override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.ServerAuthentication = {
jsOpt match {
case Some(js) =>
unbuilder.readString(js) match {
case "Token" => sbt.ServerAuthentication.Token
}
case None =>
deserializationError("Expected JsString but found None")
}
}
override def write[J](obj: sbt.ServerAuthentication, builder: Builder[J]): Unit = {
val str = obj match {
case sbt.ServerAuthentication.Token => "Token"
}
builder.writeString(str)
}
}
}
@@ -0,0 +1,12 @@
/**
* This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt
sealed abstract class ServerAuthentication extends Serializable
object ServerAuthentication {
case object Token extends ServerAuthentication
}
@@ -12,3 +12,7 @@ type Exec {
type CommandSource {
channelName: String!
}
enum ServerAuthentication {
Token
}
@@ -17,6 +17,15 @@ object BasicKeys {
val watch = AttributeKey[Watched]("watch", "Continuous execution configuration.", 1000)
val serverPort =
AttributeKey[Int]("server-port", "The port number used by server command.", 10000)
val serverHost =
AttributeKey[String]("serverHost", "The host used by server command.", 10000)
val serverAuthentication =
AttributeKey[Set[ServerAuthentication]]("serverAuthentication",
"Method of authenticating server command.",
10000)
private[sbt] val interactive = AttributeKey[Boolean](
"interactive",
"True if commands are currently being entered from an interactive environment.",
@@ -7,19 +7,22 @@ package server
import java.io.File
import java.net.{ SocketTimeoutException, InetAddress, ServerSocket, Socket }
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicLong }
import java.nio.file.attribute.{ UserPrincipal, AclEntry, AclEntryPermission, AclEntryType }
import scala.concurrent.{ Future, Promise }
import scala.util.{ Try, Success, Failure }
import scala.util.{ Try, Success, Failure, Random }
import sbt.internal.util.ErrorHandling
import sbt.internal.protocol.PortFile
import sbt.internal.protocol.{ PortFile, TokenFile }
import sbt.util.Logger
import sbt.io.IO
import sbt.io.syntax._
import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter }
import sbt.internal.protocol.codec._
private[sbt] sealed trait ServerInstance {
def shutdown(): Unit
def ready: Future[Unit]
def authenticate(challenge: String): Boolean
}
private[sbt] object Server {
@@ -31,14 +34,16 @@ private[sbt] object Server {
def start(host: String,
port: Int,
onIncomingSocket: Socket => Unit,
onIncomingSocket: (Socket, ServerInstance) => Unit,
auth: Set[ServerAuthentication],
portfile: File,
tokenfile: File,
log: Logger): ServerInstance =
new ServerInstance {
new ServerInstance { self =>
val running = new AtomicBoolean(false)
val p: Promise[Unit] = Promise[Unit]()
val ready: Future[Unit] = p.future
val token = new AtomicLong(Random.nextLong)
val serverThread = new Thread("sbt-socket-server") {
override def run(): Unit = {
@@ -57,7 +62,7 @@ private[sbt] object Server {
while (running.get()) {
try {
val socket = serverSocket.accept()
onIncomingSocket(socket)
onIncomingSocket(socket, self)
} catch {
case _: SocketTimeoutException => // its ok
}
@@ -67,6 +72,15 @@ private[sbt] object Server {
}
serverThread.start()
override def authenticate(challenge: String): Boolean = {
try {
val l = challenge.toLong
token.compareAndSet(l, Random.nextLong)
} catch {
case _: NumberFormatException => false
}
}
override def shutdown(): Unit = {
log.info("shutting down server")
if (portfile.exists) {
@@ -78,10 +92,51 @@ private[sbt] object Server {
running.set(false)
}
def writeTokenfile(): Unit = {
import JsonProtocol._
val uri = s"tcp://$host:$port"
val t = TokenFile(uri, token.get.toString)
val jsonToken = Converter.toJson(t).get
if (tokenfile.exists) {
IO.delete(tokenfile)
}
IO.touch(tokenfile)
ownerOnly(tokenfile)
IO.write(tokenfile, CompactPrinter(jsonToken), IO.utf8, true)
}
/** Set the persmission of the file such that the only the owner can read/write it. */
def ownerOnly(file: File): Unit = {
def acl(owner: UserPrincipal) = {
val builder = AclEntry.newBuilder
builder.setPrincipal(owner)
builder.setPermissions(AclEntryPermission.values(): _*)
builder.setType(AclEntryType.ALLOW)
builder.build
}
file match {
case _ if IO.isPosix =>
IO.chmod("rw-------", file)
case _ if IO.hasAclFileAttributeView =>
val view = file.aclFileAttributeView
view.setAcl(java.util.Collections.singletonList(acl(view.getOwner)))
case _ => ()
}
}
// This file exists through the lifetime of the server.
def writePortfile(): Unit = {
import JsonProtocol._
val p = PortFile(s"tcp://$host:$port", None)
val uri = s"tcp://$host:$port"
val tokenRef =
if (auth(ServerAuthentication.Token)) {
writeTokenfile()
Some(tokenfile.toURI.toString)
} else None
val p = PortFile(uri, tokenRef)
val json = Converter.toJson(p).get
IO.write(portfile, CompactPrinter(json))
}