Skip to content

Commit f087129

Browse files
committed
Add support for SSL
1 parent 18038e0 commit f087129

File tree

7 files changed

+172
-10
lines changed

7 files changed

+172
-10
lines changed

src/main/scala/com/redis/IO.scala

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.redis
22

33
import java.io._
44
import java.net.{InetSocketAddress, Socket, SocketTimeoutException}
5+
import javax.net.ssl.SSLContext
56

67
import com.redis.serialization.Parse.parseStringSafe
78

@@ -10,6 +11,8 @@ trait IO extends Log {
1011
val port: Int
1112
val timeout: Int
1213

14+
val sslContext: Option[SSLContext] = None
15+
1316
var socket: Socket = _
1417
var out: OutputStream = _
1518
var in: InputStream = _
@@ -31,6 +34,10 @@ trait IO extends Log {
3134
socket.setKeepAlive(true)
3235
socket.setTcpNoDelay(true)
3336

37+
sslContext.foreach { sc =>
38+
socket = sc.getSocketFactory().createSocket(socket, host, port, true)
39+
}
40+
3441
out = socket.getOutputStream
3542
in = new BufferedInputStream(socket.getInputStream)
3643
onConnect()

src/main/scala/com/redis/Pool.scala

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package com.redis
22

33
import java.util.concurrent.TimeUnit
4+
import javax.net.ssl.SSLContext
45

56
import org.apache.commons.pool2._
67
import org.apache.commons.pool2.impl._
78

8-
private [redis] class RedisClientFactory(val host: String, val port: Int, val database: Int = 0, val secret: Option[Any] = None, val timeout : Int = 0)
9+
private [redis] class RedisClientFactory(val host: String, val port: Int, val database: Int = 0, val secret: Option[Any] = None, val timeout : Int = 0, val sslContext: Option[SSLContext] = None)
910
extends PooledObjectFactory[RedisClient] {
1011

1112
// when we make an object it's already connected
1213
override def makeObject: PooledObject[RedisClient] = {
13-
new DefaultPooledObject[RedisClient](new RedisClient(host, port, database, secret, timeout))
14+
new DefaultPooledObject[RedisClient](new RedisClient(host, port, database, secret, timeout, sslContext))
1415
}
1516

1617
// quit & disconnect
@@ -40,7 +41,8 @@ class RedisClientPool(
4041
val secret: Option[Any] = None,
4142
val timeout: Int = 0,
4243
val maxConnections: Int = RedisClientPool.UNLIMITED_CONNECTIONS,
43-
val poolWaitTimeout: Long = 3000
44+
val poolWaitTimeout: Long = 3000,
45+
val sslContext: Option[SSLContext] = None
4446
) {
4547

4648
val objectPoolConfig = new GenericObjectPoolConfig[RedisClient]
@@ -52,7 +54,7 @@ class RedisClientPool(
5254

5355
val abandonedConfig = new AbandonedConfig
5456
abandonedConfig.setRemoveAbandonedTimeout(TimeUnit.MILLISECONDS.toSeconds(poolWaitTimeout).toInt)
55-
val pool = new GenericObjectPool(new RedisClientFactory(host, port, database, secret, timeout), objectPoolConfig,abandonedConfig)
57+
val pool = new GenericObjectPool(new RedisClientFactory(host, port, database, secret, timeout, sslContext), objectPoolConfig,abandonedConfig)
5658
override def toString: String = host + ":" + String.valueOf(port)
5759

5860
def withClient[T](body: RedisClient => T): T = {
@@ -67,4 +69,3 @@ class RedisClientPool(
6769
// close pool & free resources
6870
def close(): Unit = pool.close()
6971
}
70-

src/main/scala/com/redis/RedisClient.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.redis
22

33
import java.net.SocketException
4+
import javax.net.ssl.SSLContext
45

56
import com.redis.serialization.Format
67

@@ -93,7 +94,7 @@ trait RedisCommand extends Redis
9394

9495

9596
class RedisClient(override val host: String, override val port: Int,
96-
override val database: Int = 0, override val secret: Option[Any] = None, override val timeout : Int = 0)
97+
override val database: Int = 0, override val secret: Option[Any] = None, override val timeout : Int = 0, override val sslContext: Option[SSLContext] = None)
9798
extends RedisCommand with PubSub {
9899

99100
def this() = this("localhost", 6379)

src/test/resources/certs/cert.pem

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIE8jCCAtoCCQC2vIrA8jYWszANBgkqhkiG9w0BAQsFADA7MQswCQYDVQQGEwJV
3+
UzEVMBMGA1UECAwMUGVubnN5bHZhbmlhMRUwEwYDVQQHDAxQaGlsYWRlbHBoaWEw
4+
HhcNMjAwNDI5MTgxMDQzWhcNMjUwNDI5MTgxMDQzWjA7MQswCQYDVQQGEwJVUzEV
5+
MBMGA1UECAwMUGVubnN5bHZhbmlhMRUwEwYDVQQHDAxQaGlsYWRlbHBoaWEwggIi
6+
MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC1/41qBr3ZP3zyk462HLkcMwTx
7+
9M/p9dvCQiV9ug5MGdY1oRxmzn25X40neXZAE1y45iIab7PAp4yPlOsZq6Sdvt3i
8+
VhR+VdROpbM16r5pyvNFnBmg/ZpCmrYDiqPyqMIV72qh15Yjs3bwUrfCDE2CG33g
9+
6cxLxZSUE/CHetrDGSAsE8+uPtj/FFoshD+yNZVy13Jmbz5Qy5Dlgj66q/ZPqZIJ
10+
Xrd1S9/epgdoYyGT/TI1fFCT352ieLWTuCI81Xci6K4BkfjarxjKwjM7Pp6WdrU7
11+
skzqO/Vu/mIrCI6eL8fmJjkILRJl5G09XofioliUhsPWegERcxj5FKT1V0qzZGH7
12+
Ny9awtygdDy4bcoM8v55/co5ep7PEv7zcMcL0ScvoVtxqVL0gR6QcO2by35O/olp
13+
/8LqtLROEOoRSwbHQ///8yTL+S5Sap+hGM4VypNJRtT6yB5Uv/mjLXmHNnnvkoLS
14+
Ppv51cPWOsx8BS608KokKChBTpfpGCSQcPttFc08KHRmJMwZFG62Q8SspRPImNiG
15+
Gd5RynWjLJt9u7RihVxiVZ1T9cIbT6Vvq+f/EZQPVGk+QsZoiN4GZPX0wO/udAS/
16+
W4cxceAT9kZPMMFEbElZ5/oH5YIN9v42U5+5DNFpDNAmCV1UxUnvMMlWtoV6Jm7e
17+
rC0vW2zx0OgeEAroLQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQArE6FBCz1baxc4
18+
GfC0XGumcqqkbu3bjKoquWlOI0FteODwxxaLls4aCe2cL30jBdSeMmvi0z00Ek+7
19+
GIqfzosDVTNGL9oqBOUFEQY5x9Upkb6/DWBNjN24j1bVSGE1PczSoSvo594yXByZ
20+
r8jmlcdPJtY0vCvz+DqZxnqpP1vKI3pXX3LhNr/52m1BH8QMb0ZE29EeIShbHlCp
21+
/5GY9NL8qejsj/ShJFZs3PMcNjhdEJUTO6RNcEqzhZaSrPZVbEOYvLnxB8Zg5QIL
22+
gu/kGdXpGGfZIRMZIrDZwBoLbxZf39H68BhlTyi01MJ7KOGpxWDeKNNQO8kUeuaA
23+
o6QYjaCgLrKyuVVmw1GSsX4VGnsRhPpXi/CcJ9P7C2kFTptsa//rdZAvyKSni75V
24+
iiCAFJzPqSwfHAcGQodejC8kdJ6C2PRvOaU/kwa9Myf97peKkTEjY1MU2au1KFeS
25+
M5ZeAb8kuJA8WpJSiGA4gBRGGV2ByEZxUZ6coSu9+y0X2cJphBopilEh/B+jr0Ry
26+
Yc5GjG9hzWy/fHkEzQWJmneVTQfDHCKst5wtWpLdjhwE1+OA1uoZfOcoDwjQUqRc
27+
/qvWlE5Le8lu2VG4teR6jYUj9DPh49MOxS+fBeRlhzOvUnqh2hOI/EqwR/RJmHzB
28+
2BoTT2fcYroT9oMO7uPhMdTlp5z5BA==
29+
-----END CERTIFICATE-----

src/test/resources/certs/key.pem

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
51+
QKFtlTfZEWPcfRJwaMUGTqdInKAwk+g=
52+
-----END PRIVATE KEY-----
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.redis
2+
3+
import java.security.cert.X509Certificate
4+
import org.apache.http.ssl.{SSLContexts, TrustStrategy}
5+
6+
import com.redis.common.RedisDockerSSL
7+
import org.scalatest.{FunSpec, Matchers}
8+
9+
class SSLSpec extends FunSpec with Matchers with RedisDockerSSL {
10+
11+
// Our certificate on the test server is self-signed, which will be
12+
// rejected by the default SSLContext. This SSLContext is therefore
13+
// specifically configured to trust all certificates.
14+
private val sslContext = SSLContexts
15+
.custom()
16+
.loadTrustMaterial(null, new TrustStrategy() {
17+
def isTrusted(arg0: Array[X509Certificate], arg1: String) = true
18+
})
19+
.build()
20+
21+
describe("ssl connections") {
22+
it("should be established for a RedisClient with a valid SSLContext") {
23+
val secureClient: RedisClient = new RedisClient(
24+
redisContainerHost,
25+
redisContainerPort,
26+
sslContext = Some(sslContext)
27+
)
28+
secureClient.ping shouldEqual Some("PONG")
29+
secureClient.close()
30+
}
31+
32+
it("should be established for a RedisClientPool with a valid SSLContext") {
33+
val clients = new RedisClientPool(
34+
redisContainerHost,
35+
redisContainerPort,
36+
sslContext = Some(sslContext)
37+
)
38+
clients.withClient(_.ping) shouldEqual Some("PONG")
39+
clients.withClient { client =>
40+
client.disconnect
41+
}
42+
clients.close()
43+
}
44+
45+
it("should be rejected for a RedisClient with no SSLContext") {
46+
val insecureClient =
47+
new RedisClient(redisContainerHost, redisContainerPort) {
48+
// this disables retries
49+
override def disconnect = false
50+
}
51+
52+
assertThrows[RedisConnectionException] {
53+
insecureClient.ping
54+
}
55+
56+
insecureClient.close()
57+
}
58+
}
59+
}

src/test/scala/com/redis/common/RedisDocker.scala

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.redis.common
22

3+
import java.io.File
4+
35
import com.whisk.docker.impl.dockerjava.DockerKitDockerJava
46
import com.whisk.docker.scalatest.DockerTestKit
5-
import com.whisk.docker.{DockerContainer, DockerKit, DockerReadyChecker}
7+
import com.whisk.docker.{DockerContainer, DockerKit, DockerReadyChecker, VolumeMapping}
68
import org.apache.commons.lang.RandomStringUtils
79
import org.scalatest.Suite
810
import org.scalatest.concurrent.ScalaFutures
@@ -15,7 +17,7 @@ trait RedisDockerCluster extends RedisContainer {
1517
protected def redisContainerPort(container: DockerContainer): Int = container.getPorts().futureValue.apply(redisPort)
1618

1719
protected lazy val runningContainers: List[DockerContainer] = (0 until 4)
18-
.map(i => createContainer())
20+
.map(_ => createContainer())
1921
.toList
2022

2123
abstract override def dockerContainers: List[DockerContainer] =
@@ -35,6 +37,16 @@ trait RedisDocker extends RedisContainer {
3537

3638
}
3739

40+
trait RedisDockerSSL extends RedisDocker {
41+
that: Suite =>
42+
43+
private val certsPath = new File("src/test/resources/certs").getAbsolutePath
44+
45+
override protected def baseContainer(name: Option[String]) =
46+
DockerContainer("madflojo/redis-tls:latest", name = name)
47+
.withVolumes(Seq(VolumeMapping(certsPath, "/certs")))
48+
}
49+
3850
trait RedisContainer extends DockerKit with DockerTestKit with DockerKitDockerJava with ScalaFutures {
3951
that: Suite =>
4052

@@ -43,6 +55,8 @@ trait RedisContainer extends DockerKit with DockerTestKit with DockerKitDockerJa
4355
protected val redisContainerHost: String = "localhost"
4456
protected val redisPort: Int = 6379
4557

58+
protected def baseContainer(name: Option[String]) = DockerContainer("redis:latest", name=name)
59+
4660
protected def createContainer(name: Option[String] = Some(RandomStringUtils.randomAlphabetic(10)),
4761
ports: Map[Int, Int] = Map.empty): DockerContainer = {
4862
val containerPorts: Seq[(Int, Option[Int])] = if (ports.isEmpty) {
@@ -51,8 +65,7 @@ trait RedisContainer extends DockerKit with DockerTestKit with DockerKitDockerJa
5165
ports.mapValues(i => Some(i)).toSeq
5266
}
5367

54-
DockerContainer("redis:latest", name = name)
55-
.withPorts(containerPorts: _*)
68+
baseContainer(name).withPorts(containerPorts: _*)
5669
.withReadyChecker(DockerReadyChecker.LogLineContains("Ready to accept connections"))
5770
}
5871
}

0 commit comments

Comments
 (0)