Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Evaluator Server Update #62

Merged
merged 6 commits into from
Sep 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions project/ProjectPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ object ProjectPlugin extends AutoPlugin {
lazy val roshttp = "2.2.4"
lazy val slf4jSimple = "1.7.28"
lazy val jwtCore = "4.0.0"
lazy val coursier = "2.0.0-RC3-2"
lazy val coursier = "2.0.0-RC3-4"
}

lazy val dockerSettings = Seq(
Expand Down Expand Up @@ -82,9 +82,11 @@ object ProjectPlugin extends AutoPlugin {
%%("http4s-circe", V.http4s),
%("config"),
%%("jwt-core", V.jwtCore),
%%("coursier", V.coursier),
%%("coursier-cache", V.coursier),
"io.get-coursier" %% "coursier-cats-interop" % V.coursier,
%%("scalatest", V.scalatest) % "test"
),
addSbtPlugin("io.get-coursier" % "sbt-coursier" % V.coursier)
)
)

lazy val buildInfoSettings = Seq(
Expand Down Expand Up @@ -136,21 +138,19 @@ object ProjectPlugin extends AutoPlugin {
organizationEmail = "hello@47deg.com"
),
orgLicenseSetting := ApacheLicense,
scalaVersion := "2.12.10",
scalaVersion := "2.11.11",
scalaOrganization := "org.scala-lang",
javacOptions ++= Seq("-encoding", "UTF-8", "-Xlint:-options"),
fork in Test := false,
parallelExecution in Test := false,
cancelable in Global := true,
headerLicense := Some(
HeaderLicense.Custom(
s"""|/*
| * scala-exercises - ${name.value}
| * Copyright (C) 2015-2019 47 Degrees, LLC. <http://www.47deg.com>
| */
|
|""".stripMargin
)),
headerMappings := headerMappings.value + (HeaderFileType.scala -> HeaderCommentStyle.CStyleBlockComment)
s"""|
| scala-exercises - ${name.value}
| Copyright (C) 2015-2019 47 Degrees, LLC. <http://www.47deg.com>
|
|""".stripMargin
))
) ++ shellPromptSettings
}
55 changes: 31 additions & 24 deletions server/src/main/scala/org/scalaexercises/evaluator/auth.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
/*
* scala-exercises - evaluator-server
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
*
* scala-exercises - evaluator-server
* Copyright (C) 2015-2019 47 Degrees, LLC. <http://www.47deg.com>
*
*/

package org.scalaexercises.evaluator

import org.http4s._, org.http4s.dsl._, org.http4s.server._
import cats.effect.Sync
import com.typesafe.config._
import org.http4s._
import org.http4s.syntax.kleisli.http4sKleisliResponseSyntax
import org.http4s.util._
import scala.util.{Failure, Success, Try}
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim, JwtHeader, JwtOptions}

import org.log4s.getLogger
import pdi.jwt.{Jwt, JwtAlgorithm}

import scalaz.concurrent.Task
import scala.util.{Failure, Success}

object auth {

Expand All @@ -37,15 +39,14 @@ object auth {

type HeaderT = `X-Scala-Eval-Api-Token`

def name: CaseInsensitiveString = "x-scala-eval-api-token".ci
def name: CaseInsensitiveString = CaseInsensitiveString("x-scala-eval-api-token")

override def parse(s: String): ParseResult[`X-Scala-Eval-Api-Token`] =
ParseResult.success(`X-Scala-Eval-Api-Token`(s))

def matchHeader(header: Header): Option[HeaderT] = {
def matchHeader(header: Header): Option[HeaderT] =
if (header.name == name) Some(`X-Scala-Eval-Api-Token`(header.value))
else None
}

}

Expand All @@ -55,20 +56,26 @@ object auth {
writer.append(token)
}

def apply(service: HttpService): HttpService = Service.lift { req =>
req.headers.get(`X-Scala-Eval-Api-Token`) match {
case Some(header) =>
Jwt.decodeRaw(header.value, secretKey, Seq(JwtAlgorithm.HS256)) match {
case Success(tokenIdentity) =>
logger.info(s"Auth success with identity : $tokenIdentity")
service(req)
case Failure(ex) =>
logger.warn(s"Auth failed : $ex")
Task.now(Response(Status.Unauthorized))
def apply[F[_]: Sync](service: HttpApp[F]): HttpApp[F] =
HttpRoutes
.of[F] {
case req if req.headers.nonEmpty => {
req.headers.get(`X-Scala-Eval-Api-Token`) match {
case Some(header) =>
Jwt.decodeRaw(header.token, secretKey, Seq(JwtAlgorithm.HS256)) match {
case Success(tokenIdentity) => {
logger.info(s"Auth success with identity : $tokenIdentity")
service(req)
}
case Failure(ex) => {
logger.warn(s"Auth failed : $ex")
Sync[F].pure(Response(Status.Unauthorized))
}
}
}
}
case None => Task.now(Response(Status.Unauthorized))
}

}
case _ => Sync[F].pure(Response(Status.Unauthorized))
}
.orNotFound

}
37 changes: 10 additions & 27 deletions server/src/main/scala/org/scalaexercises/evaluator/codecs.scala
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
/*
* scala-exercises - evaluator-server
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
*
* scala-exercises - evaluator-server
* Copyright (C) 2015-2019 47 Degrees, LLC. <http://www.47deg.com>
*
*/

package org.scalaexercises.evaluator

import org.http4s._, org.http4s.dsl._
import io.circe.{Decoder, Encoder, Json, Printer}
import org.http4s.headers.`Content-Type`
import io.circe.jawn.CirceSupportParser.facade
import cats.effect.Sync
import io.circe.{Decoder, Encoder}
import org.http4s._
import org.http4s.circe._

/** Provides Json serialization codecs for the http4s services */
trait Http4sCodecInstances {

implicit val jsonDecoder: EntityDecoder[Json] = jawn.jawnDecoder(facade)
implicit def entityDecoderOf[F[_]: Sync, A: Decoder]: EntityDecoder[F, A] = jsonOf[F, A]

implicit def jsonDecoderOf[A](implicit decoder: Decoder[A]): EntityDecoder[A] =
jsonDecoder.flatMapR { json =>
decoder
.decodeJson(json)
.fold(
failure =>
DecodeResult.failure(
InvalidMessageBodyFailure(s"Could not decode JSON: $json", Some(failure))),
DecodeResult.success(_)
)
}

implicit val jsonEntityEncoder: EntityEncoder[Json] = EntityEncoder[String]
.contramap[Json] { json =>
Printer.noSpaces.pretty(json)
}
.withContentType(`Content-Type`(MediaType.`application/json`))

implicit def jsonEncoderOf[A](implicit encoder: Encoder[A]): EntityEncoder[A] =
jsonEntityEncoder.contramap[A](encoder.apply)
implicit def entityEncoderOf[F[_]: Sync, A: Encoder]: EntityEncoder[F, A] = jsonEncoderOf[F, A]

}

Expand Down
95 changes: 54 additions & 41 deletions server/src/main/scala/org/scalaexercises/evaluator/evaluation.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* scala-exercises - evaluator-server
* Copyright (C) 2015-2016 47 Degrees, LLC. <http://www.47deg.com>
*
* scala-exercises - evaluator-server
* Copyright (C) 2015-2019 47 Degrees, LLC. <http://www.47deg.com>
*
*/

package org.scalaexercises.evaluator
Expand All @@ -9,11 +11,13 @@ import java.io.{ByteArrayOutputStream, File}
import java.math.BigInteger
import java.net.URLClassLoader
import java.security.MessageDigest
import java.util.concurrent.TimeoutException
import java.util.jar.JarFile

import cats.effect.{Concurrent, ConcurrentEffect, ContextShift, Timer}
import cats.implicits._
import coursier._
import monix.execution.Scheduler
import coursier.cache.{ArtifactError, FileCache}
import coursier.util.Sync
import org.scalaexercises.evaluator.Eval.CompilerException

import scala.concurrent.duration._
Expand All @@ -24,45 +28,47 @@ import scala.tools.nsc.reporters._
import scala.tools.nsc.{Global, Settings}
import scala.util.Try
import scala.util.control.NonFatal
import scalaz.Scalaz._
import scalaz._
import scalaz.concurrent.Task

class Evaluator(timeout: FiniteDuration = 20.seconds)(
implicit S: Scheduler
) {
class Evaluator[F[_]: Sync](timeout: FiniteDuration = 20.seconds)(
implicit CS: ContextShift[F],
F: ConcurrentEffect[F],
T: Timer[F]) {
type Remote = String

private[this] def convert(errors: (Position, String, String)): (String, List[CompilationInfo]) = {
val (pos, msg, severity) = errors
(severity, CompilationInfo(msg, Some(RangePosition(pos.start, pos.point, pos.end))) :: Nil)
}

def remoteToRepository(remote: Remote): Repository =
MavenRepository(remote)
def remoteToRepository(remote: Remote): Repository = MavenRepository(remote)

def dependencyToModule(dependency: Dependency): coursier.Dependency =
coursier.Dependency(
Module(dependency.groupId, dependency.artifactId),
coursier.Dependency.of(
Module(Organization(dependency.groupId), ModuleName(dependency.artifactId)),
dependency.version
)

def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): Task[Resolution] = {
val resolution = Resolution(dependencies.map(dependencyToModule).toSet)
val repositories: Seq[Repository] = Cache.ivy2Local +: remotes.map(remoteToRepository)
val fetch = Fetch.from(repositories, Cache.fetch())
resolution.process.run(fetch)
val cache: FileCache[F] = FileCache[F].noCredentials

def resolveArtifacts(remotes: Seq[Remote], dependencies: Seq[Dependency]): F[Resolution] = {
Resolve[F](cache)
.addDependencies(dependencies.map(dependencyToModule): _*)
.addRepositories(remotes.map(remoteToRepository): _*)
.addRepositories(coursier.LocalRepositories.ivy2Local)
.io
}

def fetchArtifacts(
remotes: Seq[Remote],
dependencies: Seq[Dependency]): Task[coursier.FileError \/ List[File]] =
dependencies: Seq[Dependency]): F[Either[ArtifactError, List[File]]] =
for {
resolution <- resolveArtifacts(remotes, dependencies)
artifacts <- Task.gatherUnordered(
resolution.artifacts.map(Cache.file(_).run)
)
} yield artifacts.sequenceU
resolution <- resolveArtifacts(remotes, dependencies)
gatheredArtifacts <- resolution.artifacts().toList.traverse(cache.file(_).run)
artifacts = gatheredArtifacts.foldRight(Right(Nil): Either[ArtifactError, List[File]]) {
case (Right(file), acc) => acc.map(file :: _)
case (Left(ae), _) => Left(ae)
}
} yield artifacts

def createEval(jars: Seq[File]) = {
new Eval(jars = jars.toList) {
Expand Down Expand Up @@ -124,22 +130,29 @@ class Evaluator(timeout: FiniteDuration = 20.seconds)(
code: String,
remotes: Seq[Remote] = Nil,
dependencies: Seq[Dependency] = Nil
): Task[EvalResult[T]] = {
): F[EvalResult[T]] = {
for {
allJars <- fetchArtifacts(remotes, dependencies)
result <- allJars match {
case \/-(jars) =>
Task({
evaluate(code, jars)
}).timed(timeout)
.handle({
case err: TimeoutException => Timeout[T](timeout)
})
case -\/(fileError) =>
Task.now(UnresolvedDependency(fileError.describe))
case Right(jars) =>
val fallback: EvalResult[T] = Timeout[T](timeout)
val success: F[EvalResult[T]] =
timeoutTo(F.delay { evaluate(code, jars) }, timeout, fallback)
success
case Left(fileError) =>
val failure: F[EvalResult[T]] = F.pure(UnresolvedDependency[T](fileError.describe))
failure
}
} yield result
}

def timeoutTo[A](fa: F[A], after: FiniteDuration, fallback: A): F[A] = {

Concurrent[F].race(fa, T.sleep(after)).flatMap {
case Left(a) => a.pure[F]
case Right(_) => fallback.pure[F]
}
}
}

/**
Expand Down Expand Up @@ -170,7 +183,7 @@ private class StringCompiler(
val settings = StringCompiler.this.settings
val messages = new scala.collection.mutable.ListBuffer[List[String]]

def display(pos: Position, message: String, severity: Severity) {
def display(pos: Position, message: String, severity: Severity) = {
severity.count += 1
val severityName = severity match {
case ERROR => "error: "
Expand All @@ -193,19 +206,19 @@ private class StringCompiler(
})
}

def displayPrompt {
def displayPrompt = {
// no.
}

override def reset {
override def reset = {
super.reset
messages.clear()
}
}

val global = new Global(settings, reporter)

def reset() {
def reset() = {
targetDir match {
case None => {
output.asInstanceOf[VirtualDirectory].clear()
Expand Down Expand Up @@ -239,7 +252,7 @@ private class StringCompiler(
/**
* Compile scala code. It can be found using the above class loader.
*/
def apply(code: String) {
def apply(code: String) = {
// if you're looking for the performance hit, it's 1/2 this line...
val compiler = new global.Run
val sourceFiles = List(new BatchSourceFile("(inline)", code))
Expand Down Expand Up @@ -366,7 +379,7 @@ class Eval(target: Option[File] = None, jars: List[File] = Nil) {
* Check if code is Eval-able.
* @throws CompilerException if not Eval-able.
*/
def check(code: String) {
def check(code: String) = {
val id = uniqueId(code)
val className = "Evaluator__" + id
val wrappedCode = wrapCodeInClass(className, code)
Expand Down
Loading