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
  * SASL improvements
7
  * SASL improvements
8
    * The enabled mechanisms can now be configured (in the SASL DSL)
8
    * The enabled mechanisms can now be configured (in the SASL DSL)
9
    * Added support for EXTERNAL mechanism, disabled by default
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
  * (Internal) Minor version updates for Gradle, Kotlin and JUnit
11
  * (Internal) Minor version updates for Gradle, Kotlin and JUnit
11
 
12
 
12
 v0.6.0
13
 v0.6.0

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

20
             is ServerCapabilitiesFinished -> handleCapabilitiesFinished(client)
20
             is ServerCapabilitiesFinished -> handleCapabilitiesFinished(client)
21
             is ServerCapabilitiesAcknowledged -> handleCapabilitiesAcknowledged(client, event.capabilities)
21
             is ServerCapabilitiesAcknowledged -> handleCapabilitiesAcknowledged(client, event.capabilities)
22
             is AuthenticationMessage -> handleAuthenticationMessage(client, event.argument)
22
             is AuthenticationMessage -> handleAuthenticationMessage(client, event.argument)
23
+            is SaslMechanismNotAvailableError -> handleSaslMechanismChange(client, event.mechanisms)
23
             is SaslFinished -> handleSaslFinished(client)
24
             is SaslFinished -> handleSaslFinished(client)
24
         }
25
         }
25
         return emptyList()
26
         return emptyList()
32
     private fun handleCapabilitiesFinished(client: IrcClient) {
33
     private fun handleCapabilitiesFinished(client: IrcClient) {
33
         // TODO: We probably need to split the outgoing REQ lines if there are lots of caps
34
         // TODO: We probably need to split the outgoing REQ lines if there are lots of caps
34
         // TODO: For caps with values we may need to decide which value to use/whether to enable them/etc
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
             if (advertisedCapabilities.keys.isEmpty()) {
37
             if (advertisedCapabilities.keys.isEmpty()) {
37
                 negotiationState = CapabilitiesNegotiationState.FINISHED
38
                 negotiationState = CapabilitiesNegotiationState.FINISHED
38
                 client.sendCapabilityEnd()
39
                 client.sendCapabilityEnd()
48
 
49
 
49
     private fun handleCapabilitiesAcknowledged(client: IrcClient, capabilities: Map<Capability, String>) {
50
     private fun handleCapabilitiesAcknowledged(client: IrcClient, capabilities: Map<Capability, String>) {
50
         // TODO: Check if everything we wanted is enabled
51
         // TODO: Check if everything we wanted is enabled
51
-        with (client.serverState.capabilities) {
52
+        with(client.serverState.capabilities) {
52
             log.info { "Acknowledged capabilities: ${capabilities.keys.map { it.name }.toList()}" }
53
             log.info { "Acknowledged capabilities: ${capabilities.keys.map { it.name }.toList()}" }
53
             enabledCapabilities.putAll(capabilities)
54
             enabledCapabilities.putAll(capabilities)
54
 
55
 
55
             if (client.serverState.sasl.mechanisms.isNotEmpty()) {
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
                 log.warning { "SASL is enabled but we couldn't negotiate a SASL mechanism with the server" }
62
                 log.warning { "SASL is enabled but we couldn't negotiate a SASL mechanism with the server" }
64
             }
63
             }
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
     private fun handleAuthenticationMessage(client: IrcClient, argument: String?) {
87
     private fun handleAuthenticationMessage(client: IrcClient, argument: String?) {
71
         if (argument?.length == 400) {
88
         if (argument?.length == 400) {
72
             client.serverState.sasl.saslBuffer += argument
89
             client.serverState.sasl.saslBuffer += argument
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
             saslBuffer = ""
102
             saslBuffer = ""
86
             mechanismState = null
103
             mechanismState = null
87
             currentMechanism = null
104
             currentMechanism = null

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

104
 class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)
104
 class SaslFinished(time: LocalDateTime, var success: Boolean) : IrcEvent(time)
105
 
105
 
106
 /** Raised when the server says our SASL mechanism isn't available, but gives us a list of others. */
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
 
2
 
3
 import com.dmdirc.ktirc.events.AuthenticationMessage
3
 import com.dmdirc.ktirc.events.AuthenticationMessage
4
 import com.dmdirc.ktirc.events.SaslFinished
4
 import com.dmdirc.ktirc.events.SaslFinished
5
+import com.dmdirc.ktirc.events.SaslMechanismNotAvailableError
5
 import com.dmdirc.ktirc.model.IrcMessage
6
 import com.dmdirc.ktirc.model.IrcMessage
6
 
7
 
7
 internal class AuthenticationProcessor : MessageProcessor {
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
     override fun process(message: IrcMessage) = when(message.command) {
12
     override fun process(message: IrcMessage) = when(message.command) {
12
         "AUTHENTICATE" -> listOf(AuthenticationMessage(message.time, message.authenticateArgument))
13
         "AUTHENTICATE" -> listOf(AuthenticationMessage(message.time, message.authenticateArgument))
13
         RPL_SASLSUCCESS -> listOf(SaslFinished(message.time, true))
14
         RPL_SASLSUCCESS -> listOf(SaslFinished(message.time, true))
14
         ERR_SASLFAIL -> listOf(SaslFinished(message.time, false))
15
         ERR_SASLFAIL -> listOf(SaslFinished(message.time, false))
16
+        RPL_SASLMECHS -> listOf(SaslMechanismNotAvailableError(message.time, message.mechanisms))
15
         else -> emptyList()
17
         else -> emptyList()
16
     }
18
     }
17
 
19
 
18
     private val IrcMessage.authenticateArgument: String?
20
     private val IrcMessage.authenticateArgument: String?
19
         get() = if (params.isEmpty() || params[0].size == 1 && String(params[0]) == "+") null else String(params[0])
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
 
14
 
15
 internal const val RPL_SASLSUCCESS = "903"
15
 internal const val RPL_SASLSUCCESS = "903"
16
 internal const val ERR_SASLFAIL = "904"
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
 
18
 
19
     var mechanismState: Any? = null
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
         return mechanisms
22
         return mechanisms
24
                 .filter { it.priority < currentMechanism?.priority ?: Int.MAX_VALUE }
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
                 .maxBy { it.priority }
25
                 .maxBy { it.priority }
27
     }
26
     }
28
 
27
 

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

134
         assertSame(saslMech1, serverState.sasl.currentMechanism)
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
     @Test
149
     @Test
138
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
150
     fun `updates negotiation state when capabilities acknowledged with shared mechanism`() {
139
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
151
         serverState.sasl.mechanisms.addAll(listOf(saslMech1, saslMech2, saslMech3))
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
 import com.dmdirc.ktirc.TestConstants
3
 import com.dmdirc.ktirc.TestConstants
4
 import com.dmdirc.ktirc.events.AuthenticationMessage
4
 import com.dmdirc.ktirc.events.AuthenticationMessage
5
 import com.dmdirc.ktirc.events.SaslFinished
5
 import com.dmdirc.ktirc.events.SaslFinished
6
+import com.dmdirc.ktirc.events.SaslMechanismNotAvailableError
6
 import com.dmdirc.ktirc.model.IrcMessage
7
 import com.dmdirc.ktirc.model.IrcMessage
7
 import com.dmdirc.ktirc.params
8
 import com.dmdirc.ktirc.params
8
 import com.dmdirc.ktirc.util.currentTimeProvider
9
 import com.dmdirc.ktirc.util.currentTimeProvider
69
         assertFalse(event.success)
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
         val state = SaslState(null)
30
         val state = SaslState(null)
31
         state.mechanisms.addAll(mechanisms)
31
         state.mechanisms.addAll(mechanisms)
32
 
32
 
33
-        assertEquals(mech3, state.getPreferredSaslMechanism(""))
33
+        assertEquals(mech3, state.getPreferredSaslMechanism(emptyList()))
34
     }
34
     }
35
 
35
 
36
     @Test
36
     @Test
39
         state.mechanisms.addAll(mechanisms)
39
         state.mechanisms.addAll(mechanisms)
40
         state.currentMechanism = mech3
40
         state.currentMechanism = mech3
41
 
41
 
42
-        assertEquals(mech2, state.getPreferredSaslMechanism(""))
42
+        assertEquals(mech2, state.getPreferredSaslMechanism(emptyList()))
43
     }
43
     }
44
 
44
 
45
     @Test
45
     @Test
48
         state.mechanisms.addAll(mechanisms)
48
         state.mechanisms.addAll(mechanisms)
49
         state.currentMechanism = mech1
49
         state.currentMechanism = mech1
50
 
50
 
51
-        assertNull(state.getPreferredSaslMechanism(""))
51
+        assertNull(state.getPreferredSaslMechanism(emptyList()))
52
     }
52
     }
53
 
53
 
54
     @Test
54
     @Test
56
         val state = SaslState(null)
56
         val state = SaslState(null)
57
         state.mechanisms.addAll(mechanisms)
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
     @Test
62
     @Test
64
         val state = SaslState(null)
64
         val state = SaslState(null)
65
         state.mechanisms.addAll(mechanisms)
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
     @Test
70
     @Test
72
         val state = SaslState(null)
72
         val state = SaslState(null)
73
         state.mechanisms.addAll(mechanisms)
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
     @Test
78
     @Test

Loading…
Cancel
Save