Browse Source

Try to retry SASL if we get a RPL_SASLMECHS message

tags/v0.7.0
Chris Smith 5 years ago
parent
commit
890d63e2cd

+ 1
- 0
CHANGELOG View File

@@ -7,6 +7,7 @@ vNEXT (in development)
7 7
  * SASL improvements
8 8
    * The enabled mechanisms can now be configured (in the SASL DSL)
9 9
    * Added support for EXTERNAL mechanism, disabled by default
10
+   * Now attempts to renegotiate if the server doesn't recognise the SASL mechanism that was tried
10 11
  * (Internal) Minor version updates for Gradle, Kotlin and JUnit
11 12
 
12 13
 v0.6.0

+ 27
- 10
src/main/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandler.kt View File

@@ -20,6 +20,7 @@ internal class CapabilitiesHandler : EventHandler {
20 20
             is ServerCapabilitiesFinished -> handleCapabilitiesFinished(client)
21 21
             is ServerCapabilitiesAcknowledged -> handleCapabilitiesAcknowledged(client, event.capabilities)
22 22
             is AuthenticationMessage -> handleAuthenticationMessage(client, event.argument)
23
+            is SaslMechanismNotAvailableError -> handleSaslMechanismChange(client, event.mechanisms)
23 24
             is SaslFinished -> handleSaslFinished(client)
24 25
         }
25 26
         return emptyList()
@@ -32,7 +33,7 @@ internal class CapabilitiesHandler : EventHandler {
32 33
     private fun handleCapabilitiesFinished(client: IrcClient) {
33 34
         // TODO: We probably need to split the outgoing REQ lines if there are lots of caps
34 35
         // TODO: For caps with values we may need to decide which value to use/whether to enable them/etc
35
-        with (client.serverState.capabilities) {
36
+        with(client.serverState.capabilities) {
36 37
             if (advertisedCapabilities.keys.isEmpty()) {
37 38
                 negotiationState = CapabilitiesNegotiationState.FINISHED
38 39
                 client.sendCapabilityEnd()
@@ -48,17 +49,15 @@ internal class CapabilitiesHandler : EventHandler {
48 49
 
49 50
     private fun handleCapabilitiesAcknowledged(client: IrcClient, capabilities: Map<Capability, String>) {
50 51
         // TODO: Check if everything we wanted is enabled
51
-        with (client.serverState.capabilities) {
52
+        with(client.serverState.capabilities) {
52 53
             log.info { "Acknowledged capabilities: ${capabilities.keys.map { it.name }.toList()}" }
53 54
             enabledCapabilities.putAll(capabilities)
54 55
 
55 56
             if (client.serverState.sasl.mechanisms.isNotEmpty()) {
56
-                client.serverState.sasl.getPreferredSaslMechanism(enabledCapabilities[Capability.SaslAuthentication])?.let { mechanism ->
57
-                    log.info { "Attempting SASL authentication using ${mechanism.ircName}" }
58
-                    client.serverState.sasl.currentMechanism = mechanism
59
-                    negotiationState = CapabilitiesNegotiationState.AUTHENTICATING
60
-                    client.sendAuthenticationMessage(mechanism.ircName)
61
-                    return
57
+                enabledCapabilities[Capability.SaslAuthentication]?.let { serverCaps ->
58
+                    if (startSaslAuth(client, serverCaps.split(','))) {
59
+                        return
60
+                    }
62 61
                 }
63 62
                 log.warning { "SASL is enabled but we couldn't negotiate a SASL mechanism with the server" }
64 63
             }
@@ -67,6 +66,24 @@ internal class CapabilitiesHandler : EventHandler {
67 66
         }
68 67
     }
69 68
 
69
+    private fun handleSaslMechanismChange(client: IrcClient, mechanisms: Collection<String>) {
70
+        if (!startSaslAuth(client, mechanisms)) {
71
+            log.warning { "SASL is enabled but we couldn't negotiate a SASL mechanism with the server" }
72
+            client.endNegotiation()
73
+        }
74
+    }
75
+
76
+    private fun startSaslAuth(client: IrcClient, serverMechanisms: Collection<String>) =
77
+            with(client.serverState) {
78
+                sasl.getPreferredSaslMechanism(serverMechanisms)?.let { mechanism ->
79
+                    log.info { "Attempting SASL authentication using ${mechanism.ircName}" }
80
+                    sasl.currentMechanism = mechanism
81
+                    capabilities.negotiationState = CapabilitiesNegotiationState.AUTHENTICATING
82
+                    client.sendAuthenticationMessage(mechanism.ircName)
83
+                    true
84
+                } ?: false
85
+            }
86
+
70 87
     private fun handleAuthenticationMessage(client: IrcClient, argument: String?) {
71 88
         if (argument?.length == 400) {
72 89
             client.serverState.sasl.saslBuffer += argument
@@ -80,8 +97,8 @@ internal class CapabilitiesHandler : EventHandler {
80 97
         }
81 98
     }
82 99
 
83
-    private fun handleSaslFinished(client: IrcClient) = with (client) {
84
-        with (serverState.sasl) {
100
+    private fun handleSaslFinished(client: IrcClient) = with(client) {
101
+        with(serverState.sasl) {
85 102
             saslBuffer = ""
86 103
             mechanismState = null
87 104
             currentMechanism = null

+ 1
- 1
src/main/kotlin/com/dmdirc/ktirc/events/Events.kt View File

@@ -104,4 +104,4 @@ class AuthenticationMessage(time: LocalDateTime, val argument: String?) : IrcEve
104 104
 class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)
105 105
 
106 106
 /** Raised when the server says our SASL mechanism isn't available, but gives us a list of others. */
107
-class SaslMechanismNotAvailableError(time: LocalDateTime, var mechanisms: Array<String>) : IrcEvent(time)
107
+class SaslMechanismNotAvailableError(time: LocalDateTime, var mechanisms: Collection<String>) : IrcEvent(time)

+ 6
- 1
src/main/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessor.kt View File

@@ -2,20 +2,25 @@ package com.dmdirc.ktirc.messages
2 2
 
3 3
 import com.dmdirc.ktirc.events.AuthenticationMessage
4 4
 import com.dmdirc.ktirc.events.SaslFinished
5
+import com.dmdirc.ktirc.events.SaslMechanismNotAvailableError
5 6
 import com.dmdirc.ktirc.model.IrcMessage
6 7
 
7 8
 internal class AuthenticationProcessor : MessageProcessor {
8 9
 
9
-    override val commands = arrayOf("AUTHENTICATE", RPL_SASLSUCCESS, ERR_SASLFAIL)
10
+    override val commands = arrayOf("AUTHENTICATE", RPL_SASLSUCCESS, ERR_SASLFAIL, RPL_SASLMECHS)
10 11
 
11 12
     override fun process(message: IrcMessage) = when(message.command) {
12 13
         "AUTHENTICATE" -> listOf(AuthenticationMessage(message.time, message.authenticateArgument))
13 14
         RPL_SASLSUCCESS -> listOf(SaslFinished(message.time, true))
14 15
         ERR_SASLFAIL -> listOf(SaslFinished(message.time, false))
16
+        RPL_SASLMECHS -> listOf(SaslMechanismNotAvailableError(message.time, message.mechanisms))
15 17
         else -> emptyList()
16 18
     }
17 19
 
18 20
     private val IrcMessage.authenticateArgument: String?
19 21
         get() = if (params.isEmpty() || params[0].size == 1 && String(params[0]) == "+") null else String(params[0])
20 22
 
23
+    private val IrcMessage.mechanisms: List<String>
24
+        get() = if (params.size < 2) emptyList() else String(params[1]).split(',')
25
+
21 26
 }

+ 1
- 0
src/main/kotlin/com/dmdirc/ktirc/messages/NumericConstants.kt View File

@@ -14,3 +14,4 @@ internal const val ERR_NOMOTD = "422"
14 14
 
15 15
 internal const val RPL_SASLSUCCESS = "903"
16 16
 internal const val ERR_SASLFAIL = "904"
17
+internal const val RPL_SASLMECHS = "908"

+ 2
- 3
src/main/kotlin/com/dmdirc/ktirc/model/SaslState.kt View File

@@ -18,11 +18,10 @@ internal class SaslState(config: SaslConfig?) {
18 18
 
19 19
     var mechanismState: Any? = null
20 20
 
21
-    fun getPreferredSaslMechanism(serverMechanisms: String?): SaslMechanism? {
22
-        val serverSupported = serverMechanisms?.split(',') ?: return null
21
+    fun getPreferredSaslMechanism(serverMechanisms: Collection<String>): SaslMechanism? {
23 22
         return mechanisms
24 23
                 .filter { it.priority < currentMechanism?.priority ?: Int.MAX_VALUE }
25
-                .filter { serverMechanisms.isEmpty() || it.ircName in serverSupported }
24
+                .filter { serverMechanisms.isEmpty() || it.ircName in serverMechanisms }
26 25
                 .maxBy { it.priority }
27 26
     }
28 27
 

+ 36
- 0
src/test/kotlin/com/dmdirc/ktirc/events/CapabilitiesHandlerTest.kt View File

@@ -134,6 +134,18 @@ internal class CapabilitiesHandlerTest {
134 134
         assertSame(saslMech1, serverState.sasl.currentMechanism)
135 135
     }
136 136
 
137
+
138
+    @Test
139
+    fun `sends authenticate when capabilities acknowledged with shared mechanism`() {
140
+        serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
141
+        handler.processEvent(ircClient, ServerCapabilitiesAcknowledged(TestConstants.time, hashMapOf(
142
+                Capability.SaslAuthentication to "mech1,fake2",
143
+                Capability.HostsInNamesReply to "123"
144
+        )))
145
+
146
+        verify(ircClient).send("AUTHENTICATE mech1")
147
+    }
148
+
137 149
     @Test
138 150
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
139 151
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
@@ -265,4 +277,28 @@ internal class CapabilitiesHandlerTest {
265 277
         }
266 278
     }
267 279
 
280
+    @Test
281
+    fun `sends a new authenticate request when sasl mechanism rejected and new one is acceptable`() {
282
+        serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
283
+        handler.processEvent(ircClient, SaslMechanismNotAvailableError(TestConstants.time, listOf("mech1", "fake2")))
284
+
285
+        verify(ircClient).send("AUTHENTICATE mech1")
286
+    }
287
+
288
+    @Test
289
+    fun `sends cap end when sasl mechanism rejected and no new one is acceptable`() {
290
+        serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
291
+        handler.processEvent(ircClient, SaslMechanismNotAvailableError(TestConstants.time, listOf("fake1", "fake2")))
292
+
293
+        verify(ircClient).send("CAP END")
294
+    }
295
+
296
+    @Test
297
+    fun `sets negotiation state when sasl mechanism rejected and no new one is acceptable`() {
298
+        serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
299
+        handler.processEvent(ircClient, SaslMechanismNotAvailableError(TestConstants.time, listOf("fake1", "fake2")))
300
+
301
+        assertEquals(CapabilitiesNegotiationState.FINISHED, serverState.capabilities.negotiationState)
302
+    }
303
+
268 304
 }

+ 21
- 0
src/test/kotlin/com/dmdirc/ktirc/messages/AuthenticationProcessorTest.kt View File

@@ -3,6 +3,7 @@ package com.dmdirc.ktirc.messages
3 3
 import com.dmdirc.ktirc.TestConstants
4 4
 import com.dmdirc.ktirc.events.AuthenticationMessage
5 5
 import com.dmdirc.ktirc.events.SaslFinished
6
+import com.dmdirc.ktirc.events.SaslMechanismNotAvailableError
6 7
 import com.dmdirc.ktirc.model.IrcMessage
7 8
 import com.dmdirc.ktirc.params
8 9
 import com.dmdirc.ktirc.util.currentTimeProvider
@@ -69,4 +70,24 @@ internal class AuthenticationProcessorTest {
69 70
         assertFalse(event.success)
70 71
     }
71 72
 
73
+    @Test
74
+    fun `raises not available error on mechs numeric`() {
75
+        val events = processor.process(IrcMessage(emptyMap(), ":the.gibson".toByteArray(), "908", params("*", "PLAIN,EXTERNAL,MAGIC", "are supported by this server")))
76
+
77
+        assertEquals(1, events.size)
78
+        val event = events[0] as SaslMechanismNotAvailableError
79
+        assertEquals(TestConstants.time, event.time)
80
+        assertEquals(listOf("PLAIN", "EXTERNAL", "MAGIC"), event.mechanisms)
81
+    }
82
+
83
+    @Test
84
+    fun `raises empty not available error on malformed mechs numeric`() {
85
+        val events = processor.process(IrcMessage(emptyMap(), ":the.gibson".toByteArray(), "908", params("*")))
86
+
87
+        assertEquals(1, events.size)
88
+        val event = events[0] as SaslMechanismNotAvailableError
89
+        assertEquals(TestConstants.time, event.time)
90
+        assertTrue(event.mechanisms.isEmpty())
91
+    }
92
+
72 93
 }

+ 6
- 6
src/test/kotlin/com/dmdirc/ktirc/model/SaslStateTest.kt View File

@@ -30,7 +30,7 @@ internal class SaslStateTest {
30 30
         val state = SaslState(null)
31 31
         state.mechanisms.addAll(mechanisms)
32 32
 
33
-        assertEquals(mech3, state.getPreferredSaslMechanism(""))
33
+        assertEquals(mech3, state.getPreferredSaslMechanism(emptyList()))
34 34
     }
35 35
 
36 36
     @Test
@@ -39,7 +39,7 @@ internal class SaslStateTest {
39 39
         state.mechanisms.addAll(mechanisms)
40 40
         state.currentMechanism = mech3
41 41
 
42
-        assertEquals(mech2, state.getPreferredSaslMechanism(""))
42
+        assertEquals(mech2, state.getPreferredSaslMechanism(emptyList()))
43 43
     }
44 44
 
45 45
     @Test
@@ -48,7 +48,7 @@ internal class SaslStateTest {
48 48
         state.mechanisms.addAll(mechanisms)
49 49
         state.currentMechanism = mech1
50 50
 
51
-        assertNull(state.getPreferredSaslMechanism(""))
51
+        assertNull(state.getPreferredSaslMechanism(emptyList()))
52 52
     }
53 53
 
54 54
     @Test
@@ -56,7 +56,7 @@ internal class SaslStateTest {
56 56
         val state = SaslState(null)
57 57
         state.mechanisms.addAll(mechanisms)
58 58
 
59
-        assertEquals(mech3, state.getPreferredSaslMechanism("mech1,mech3,mech2"))
59
+        assertEquals(mech3, state.getPreferredSaslMechanism(listOf("mech1", "mech3", "mech2")))
60 60
     }
61 61
 
62 62
     @Test
@@ -64,7 +64,7 @@ internal class SaslStateTest {
64 64
         val state = SaslState(null)
65 65
         state.mechanisms.addAll(mechanisms)
66 66
 
67
-        assertEquals(mech2, state.getPreferredSaslMechanism("mech2,mech1,other"))
67
+        assertEquals(mech2, state.getPreferredSaslMechanism(listOf("mech2", "mech1", "other")))
68 68
     }
69 69
 
70 70
     @Test
@@ -72,7 +72,7 @@ internal class SaslStateTest {
72 72
         val state = SaslState(null)
73 73
         state.mechanisms.addAll(mechanisms)
74 74
 
75
-        assertNull(state.getPreferredSaslMechanism("foo,bar,baz"))
75
+        assertNull(state.getPreferredSaslMechanism(listOf("foo", "bar", "baz")))
76 76
     }
77 77
 
78 78
     @Test

Loading…
Cancel
Save