@@ -3,8 +3,14 @@
Loading
3 3
import cats.data.EitherT
4 4
import cats.implicits._
5 5
import com.google.inject.Inject
6 -
import io.toolsplus.atlassian.connect.play.api.events.{AppInstalledEvent, AppUninstalledEvent}
7 -
import io.toolsplus.atlassian.connect.play.api.models.{AtlassianHost, AtlassianHostUser}
6 +
import io.toolsplus.atlassian.connect.play.api.events.{
7 +
  AppInstalledEvent,
8 +
  AppUninstalledEvent
9 +
}
10 +
import io.toolsplus.atlassian.connect.play.api.models.{
11 +
  AtlassianHost,
12 +
  AtlassianHostUser
13 +
}
8 14
import io.toolsplus.atlassian.connect.play.api.repositories.AtlassianHostRepository
9 15
import io.toolsplus.atlassian.connect.play.events.EventBus
10 16
import io.toolsplus.atlassian.connect.play.models.Implicits._
@@ -65,8 +71,7 @@
Loading
65 71
      _ <- assertHostAuthorized(installedEvent, hostUser).toEitherT[Future]
66 72
      _ = logger.info(
67 73
        s"Saved installation for previously installed host ${newHost.baseUrl} (${newHost.clientKey})")
68 -
      host <- EitherT.right[LifecycleError](
69 -
        hostRepository.save(newHost))
74 +
      host <- EitherT.right[LifecycleError](hostRepository.save(newHost))
70 75
    } yield host
71 76
72 77
  /** Install the given [[AtlassianHost]] for the first time.
@@ -112,8 +117,7 @@
Loading
112 117
        .toEitherT[Future]
113 118
      _ <- assertHostAuthorized(uninstalledEvent, hostUser).toEitherT[Future]
114 119
      maybeExistingHost <- EitherT
115 -
        .right[LifecycleError](
116 -
          existingHostByLifecycleEvent(uninstalledEvent))
120 +
        .right[LifecycleError](existingHostByLifecycleEvent(uninstalledEvent))
117 121
      result <- uninstall(uninstalledEvent, maybeExistingHost)
118 122
      _ = EventBus.publish(AppUninstalledEvent(hostUser.host))
119 123
    } yield result
@@ -129,7 +133,8 @@
Loading
129 133
      case None =>
130 134
        logger.error(
131 135
          s"Received authenticated uninstall request but no installation for host ${uninstalledEvent.baseUrl} has been found. Assume the add-on has been removed.")
132 -
        EitherT.left(Future.successful(MissingAtlassianHostError))
136 +
        EitherT[Future, LifecycleError, AtlassianHost](
137 +
          Future.successful(Left(MissingAtlassianHostError)))
133 138
    }
134 139
  }
135 140

@@ -1,13 +1,13 @@
Loading
1 1
package io.toolsplus.atlassian.connect.play.ws
2 2
3 -
import javax.inject.Inject
3 +
import java.net.URI
4 4
5 -
import com.netaporter.uri.Uri
5 +
import javax.inject.Inject
6 6
import io.toolsplus.atlassian.connect.play.api.models.AtlassianHost
7 7
import io.toolsplus.atlassian.connect.play.api.repositories.AtlassianHostRepository
8 -
import io.toolsplus.atlassian.connect.play.ws.UriImplicits._
9 8
10 9
import scala.concurrent.Future
10 +
import scala.util.{Failure, Success, Try}
11 11
12 12
/**
13 13
  * A helper class for resolving URLs relative to the base URL of an AtlassianHost.
@@ -15,21 +15,29 @@
Loading
15 15
class AtlassianHostUriResolver @Inject()(
16 16
    hostRepository: AtlassianHostRepository) {
17 17
18 -
  def hostFromRequestUrl(uri: Uri): Future[Option[AtlassianHost]] = {
18 +
  def hostFromRequestUrl(uri: URI): Future[Option[AtlassianHost]] = {
19 19
    if (uri.isAbsolute) {
20 -
      uri.baseUrl match {
20 +
      AtlassianHostUriResolver.baseUrl(uri) match {
21 21
        case Some(url) => hostRepository.findByBaseUrl(url)
22 -
        case None => Future.successful(None)
22 +
        case None      => Future.successful(None)
23 23
      }
24 24
    } else Future.successful(None)
25 25
  }
26 -
27 26
}
28 27
29 28
object AtlassianHostUriResolver {
30 29
31 -
  def isRequestToHost(requestUri: Uri, host: AtlassianHost) = {
32 -
    val hostBaseUri = Uri.parse(host.baseUrl)
33 -
    !hostBaseUri.toURI.relativize(requestUri.toURI).isAbsolute
30 +
  def baseUrl(uri: URI): Option[String] = {
31 +
    Try {
32 +
      new URI(uri.getScheme, uri.getAuthority, null, null, null).toString
33 +
    } match {
34 +
      case Success(url) => if (url.isEmpty) None else Some(url)
35 +
      case Failure(_)   => None
36 +
    }
37 +
  }
38 +
39 +
  def isRequestToHost(requestUri: URI, host: AtlassianHost): Boolean = {
40 +
    val hostBaseUri = URI.create(host.baseUrl)
41 +
    !hostBaseUri.relativize(requestUri).isAbsolute
34 42
  }
35 43
}

@@ -1,16 +1,12 @@
Loading
1 1
package io.toolsplus.atlassian.connect.play.ws.jwt
2 2
3 -
import cats.syntax.either._
4 -
import com.netaporter.uri.Uri
3 +
import java.net.URI
4 +
5 5
import io.toolsplus.atlassian.connect.play.api.models.AtlassianHost
6 6
import io.toolsplus.atlassian.connect.play.auth.jwt.JwtGenerator
7 7
import play.api.http.HeaderNames.{AUTHORIZATION, USER_AGENT}
8 8
import play.api.libs.ws.WSSignatureCalculator
9 -
import play.shaded.ahc.org.asynchttpclient.{
10 -
  Request,
11 -
  RequestBuilderBase,
12 -
  SignatureCalculator
13 -
}
9 +
import play.shaded.ahc.org.asynchttpclient.{Request, RequestBuilderBase, SignatureCalculator}
14 10
15 11
class JwtSignatureCalculator(host: AtlassianHost, jwtGenerator: JwtGenerator)
16 12
    extends WSSignatureCalculator
@@ -28,7 +24,7 @@
Loading
28 24
29 25
  private def generateJwt(request: Request, host: AtlassianHost) = {
30 26
    jwtGenerator.createJwtToken(request.getMethod,
31 -
                                Uri.parse(request.getUrl),
27 +
                                URI.create(request.getUrl),
32 28
                                host)
33 29
  }
34 30
}

@@ -1,16 +1,16 @@
Loading
1 1
package io.toolsplus.atlassian.connect.play.auth.jwt
2 2
3 +
import java.net.URI
3 4
import java.time.Duration
4 5
import java.time.temporal.ChronoUnit
5 6
6 7
import cats.syntax.either._
7 -
import com.netaporter.uri.Uri
8 8
import io.toolsplus.atlassian.connect.play.api.models.{AppProperties, AtlassianHost}
9 9
import io.toolsplus.atlassian.connect.play.auth.jwt.JwtGenerator._
10 10
import io.toolsplus.atlassian.connect.play.models.AtlassianConnectProperties
11 11
import io.toolsplus.atlassian.connect.play.ws.AtlassianHostUriResolver
12 -
import io.toolsplus.atlassian.jwt.{HttpRequestCanonicalizer, JwtBuilder}
13 12
import io.toolsplus.atlassian.jwt.api.Predef.RawJwt
13 +
import io.toolsplus.atlassian.jwt.{HttpRequestCanonicalizer, JwtBuilder}
14 14
import javax.inject.Inject
15 15
import play.api.Logger
16 16
@@ -25,7 +25,7 @@
Loading
25 25
  private val logger = Logger(classOf[JwtGenerator])
26 26
27 27
  def createJwtToken(httpMethod: String,
28 -
                     uri: Uri): Future[Either[JwtGeneratorError, RawJwt]] = {
28 +
                     uri: URI): Future[Either[JwtGeneratorError, RawJwt]] = {
29 29
    assertUriAbsolute(uri) match {
30 30
      case Left(e) => Future.successful(Left(e))
31 31
      case Right(_) =>
@@ -37,7 +37,7 @@
Loading
37 37
  }
38 38
39 39
  def createJwtToken(httpMethod: String,
40 -
                     uri: Uri,
40 +
                     uri: URI,
41 41
                     host: AtlassianHost): Either[JwtGeneratorError, RawJwt] =
42 42
    for {
43 43
      absoluteUri <- assertUriAbsolute(uri)
@@ -47,7 +47,7 @@
Loading
47 47
48 48
  private def internalCreateJwtToken(
49 49
      httpMethod: String,
50 -
      uri: Uri,
50 +
      uri: URI,
51 51
      host: AtlassianHost): Either[JwtGeneratorError, RawJwt] = {
52 52
    val canonicalHttpRequest =
53 53
      CanonicalUriHttpRequest(httpMethod, uri, host.baseUrl)
@@ -73,13 +73,13 @@
Loading
73 73
    if (secretKey.getBytes.length < (256 / 8)) Left(InvalidSecretKey)
74 74
    else Right(secretKey)
75 75
76 -
  private def assertUriAbsolute(uri: Uri): Either[JwtGeneratorError, Uri] = {
77 -
    if (uri.toURI.isAbsolute) Right(uri) else Left(RelativeUriError)
76 +
  private def assertUriAbsolute(uri: URI): Either[JwtGeneratorError, URI] = {
77 +
    if (uri.isAbsolute) Right(uri) else Left(RelativeUriError)
78 78
  }
79 79
80 80
  private def assertRequestToHost(
81 -
      uri: Uri,
82 -
      host: AtlassianHost): Either[JwtGeneratorError, Uri] = {
81 +
      uri: URI,
82 +
      host: AtlassianHost): Either[JwtGeneratorError, URI] = {
83 83
    if (AtlassianHostUriResolver.isRequestToHost(uri, host)) Right(uri)
84 84
    else Left(BaseUrlMismatchError)
85 85
  }
@@ -104,7 +104,7 @@
Loading
104 104
  final case class JwtSigningError(message: String, cause: Throwable)
105 105
      extends JwtGeneratorError
106 106
107 -
  final case class AtlassianHostNotFoundError(uri: Uri)
107 +
  final case class AtlassianHostNotFoundError(uri: URI)
108 108
      extends JwtGeneratorError {
109 109
    override val message: String =
110 110
      s"No Atlassian host found for the given URI $uri"

@@ -1,17 +1,11 @@
Loading
1 1
package io.toolsplus.atlassian.connect.play.actions
2 2
3 -
import javax.inject.Inject
4 -
5 -
import com.netaporter.uri.Uri
3 +
import io.lemonlabs.uri.Url
6 4
import io.toolsplus.atlassian.connect.play.api.models.AtlassianHostUser
7 -
import io.toolsplus.atlassian.connect.play.auth.jwt.{
8 -
  JwtAuthenticationError,
9 -
  JwtAuthenticationProvider,
10 -
  JwtCredentials,
11 -
  UnknownJwtIssuerError
12 -
}
5 +
import io.toolsplus.atlassian.connect.play.auth.jwt.{JwtAuthenticationError, JwtAuthenticationProvider, JwtCredentials, UnknownJwtIssuerError}
13 6
import io.toolsplus.atlassian.connect.play.controllers.routes
14 7
import io.toolsplus.atlassian.connect.play.models.AtlassianConnectProperties
8 +
import javax.inject.Inject
15 9
import play.api.Logger
16 10
import play.api.mvc.Results.Unauthorized
17 11
import play.api.mvc._
@@ -98,8 +92,8 @@
Loading
98 92
      routes.LifecycleController.uninstalled().absoluteURL()(request))
99 93
100 94
  private def isRequestToUrl[A](request: Request[A], url: String): Boolean = {
101 -
    val requestUri = Uri.parse(request.uri)
102 -
    val referenceUri = Uri.parse(url)
95 +
    val requestUri = Url.parse(request.uri)
96 +
    val referenceUri = Url.parse(url)
103 97
    requestUri.path == referenceUri.path && referenceUri.query.paramMap.toSet
104 98
      .subsetOf(requestUri.query.paramMap.toSet)
105 99
  }
@@ -113,7 +107,7 @@
Loading
113 107
    extends ActionBuilder[MaybeAtlassianHostUserRequest, AnyContent] {
114 108
  override def invokeBlock[A](
115 109
      request: Request[A],
116 -
      block: (MaybeAtlassianHostUserRequest[A]) => Future[Result]) = {
110 +
      block: MaybeAtlassianHostUserRequest[A] => Future[Result]): Future[Result] = {
117 111
    (jwtActionRefiner andThen atlassianHostUserActionRefiner)
118 112
      .invokeBlock(request, block)
119 113
  }

@@ -1,12 +1,12 @@
Loading
1 1
package io.toolsplus.atlassian.connect.play.ws
2 2
3 -
import javax.inject.Inject
3 +
import java.net.URI
4 4
5 -
import com.netaporter.uri.Uri
6 -
import UriImplicits._
5 +
import io.lemonlabs.uri.Url
7 6
import io.toolsplus.atlassian.connect.play.api.models.AtlassianHost
8 7
import io.toolsplus.atlassian.connect.play.auth.jwt.JwtGenerator
9 8
import io.toolsplus.atlassian.connect.play.ws.jwt.JwtSignatureCalculator
9 +
import javax.inject.Inject
10 10
import play.api.libs.ws.{WSClient, WSRequest, WSSignatureCalculator}
11 11
12 12
/**
@@ -35,18 +35,21 @@
Loading
35 35
36 36
  private def request(url: String, signatureCalculator: WSSignatureCalculator)(
37 37
      implicit host: AtlassianHost) = {
38 -
    val requestUri = Uri.parse(url)
38 +
    val requestUri = URI.create(url)
39 39
    val absoluteUrl =
40 40
      if (!requestUri.isAbsolute) absoluteRequestUrl(requestUri, host).toString
41 41
      else url
42 42
    ws.url(absoluteUrl).sign(signatureCalculator)
43 43
  }
44 44
45 -
  private def absoluteRequestUrl(requestUri: Uri, host: AtlassianHost) = {
46 -
    val baseUri = Uri.parse(host.baseUrl)
47 -
    val fullRelativeUri = Uri(
48 -
      pathParts = baseUri.pathParts ++ requestUri.pathParts)
49 -
    Uri(baseUri.toURI.resolve(fullRelativeUri.toURI))
45 +
  private def absoluteRequestUrl(requestUri: URI, host: AtlassianHost): URI = {
46 +
    val baseUrl = Url.parse(host.baseUrl)
47 +
    val requestUrl = Url.parse(requestUri.toString)
48 +
    URI.create(
49 +
      baseUrl
50 +
        .withPath(baseUrl.path.addParts(requestUrl.path.parts))
51 +
        .withQueryString(requestUrl.query)
52 +
        .withFragment(requestUrl.fragment)
53 +
        .toString)
50 54
  }
51 -
52 55
}

@@ -1,21 +1,34 @@
Loading
1 1
package io.toolsplus.atlassian.connect.play.auth.jwt
2 2
3 -
import com.netaporter.uri.Uri
3 +
import java.net.URI
4 +
5 +
import io.lemonlabs.uri.Url
4 6
import io.toolsplus.atlassian.jwt.api.CanonicalHttpRequest
5 7
6 8
case class CanonicalUriHttpRequest(httpMethod: String,
7 -
                                   requestUri: Uri,
9 +
                                   requestUri: URI,
8 10
                                   contextPath: String)
9 11
    extends CanonicalHttpRequest {
10 12
11 -
  override def method = httpMethod
13 +
  override def method: String = httpMethod
12 14
13 -
  override def relativePath = {
14 -
    val relPath = requestUri.path
15 -
      .replaceFirst(s"^${Uri.parse(contextPath)}", "")
16 -
      .replaceFirst("/$", "")
17 -
    if (relPath.isEmpty) "/" else Uri.parse(relPath).path
15 +
  /**
16 +
    * Removes the context path from the requestUri.
17 +
    *
18 +
    * For example, if the requestUri is /context/some/request/path and contextPath
19 +
    * is /context or https://xyz.atlassian.net/context then this method should return
20 +
    * /some/request/path.
21 +
    *
22 +
    * @return Relative path without the leading context path if it exists.
23 +
    */
24 +
  override def relativePath: String = {
25 +
    val contextPathToRemove = if ("/" == contextPath) "" else contextPath
26 +
    Option(requestUri.getPath)
27 +
      .filter(_.nonEmpty)
28 +
      .map(_.replaceFirst(s"^$contextPathToRemove", ""))
29 +
      .map(_.replaceFirst("/$", ""))
30 +
      .getOrElse("/")
18 31
  }
19 32
20 -
  override def parameterMap = requestUri.query.paramMap
33 +
  override def parameterMap: Map[String, Seq[String]] = Url.parse(requestUri.toString).query.paramMap
21 34
}
Files Coverage
modules 88.64%
Project Totals (19 files) 88.64%
1360.1
TRAVIS_JDK_VERSION=oraclejdk8
TRAVIS_OS_NAME=linux
1359.1
TRAVIS_JDK_VERSION=oraclejdk8
TRAVIS_OS_NAME=linux
1
coverage:
2
  status:
3
    project:
4
      default:
5
        threshold: 5%
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading