Skip to main content

arzmq/
security.rs

1//! 0MQ security mechanisms
2//!
3//! Ssupports [`Null`] and [`Plain`] out of the box. `Curve` and `GSSAPI` can be enabled via
4//! feature flags.
5//!
6//! [`Null`]: SecurityMechanism::Null
7//! [`Plain`]: SecurityMechanism::Plain
8
9use derive_more::Display;
10#[cfg(zmq_has = "gssapi")]
11pub use gssapi::GssApiNametype;
12
13use crate::{
14    ZmqError, ZmqResult, sealed,
15    socket::{Socket, SocketOption},
16    zmq_sys_crate,
17};
18
19#[derive(Default, Debug, Display, PartialEq, Eq, Clone, Hash)]
20#[cfg_attr(feature = "builder", derive(serde::Deserialize, serde::Serialize))]
21#[repr(i32)]
22#[non_exhaustive]
23/// # 0MQ security mechanisms
24///
25/// A 0MQ socket can select a security mechanism. Both peers must use the same security mechanism.
26pub enum SecurityMechanism {
27    #[default]
28    /// Null security
29    Null,
30    #[display("Plain {{ username = {username}, password = {password} }}")]
31    /// Plain-textauthentication using username and password
32    Plain { username: String, password: String },
33    #[cfg(zmq_has = "curve")]
34    #[display("CurveClient {{ ... }}")]
35    /// Elliptic curve client authentication and encryption
36    CurveClient {
37        server_key: Vec<u8>,
38        public_key: Vec<u8>,
39        secret_key: Vec<u8>,
40    },
41    #[cfg(zmq_has = "curve")]
42    #[display("CurveServer {{ ... }}")]
43    /// Elliptic curve server authentication and encryption
44    CurveServer { secret_key: Vec<u8> },
45    #[cfg(zmq_has = "gssapi")]
46    #[display("GssApiClient {{ ... }}")]
47    /// GSSAPI client authentication and encryption
48    GssApiClient { service_principal: String },
49    #[cfg(zmq_has = "gssapi")]
50    #[display("GssApiServer {{ ... }}")]
51    /// GSSAPI server authentication and encryption
52    GssApiServer,
53}
54
55impl SecurityMechanism {
56    /// Applies the security mechanism to the provided socket
57    pub fn apply<T: sealed::SocketType>(&self, socket: &Socket<T>) -> ZmqResult<()> {
58        match self {
59            SecurityMechanism::Null => socket.set_sockopt_bool(SocketOption::PlainServer, false)?,
60            SecurityMechanism::Plain { username, password } => {
61                socket.set_sockopt_bool(SocketOption::PlainServer, true)?;
62                socket.set_sockopt_string(SocketOption::PlainUsername, username)?;
63                socket.set_sockopt_string(SocketOption::PlainPassword, password)?;
64            }
65            #[cfg(zmq_has = "curve")]
66            SecurityMechanism::CurveServer { secret_key } => {
67                socket.set_sockopt_bool(SocketOption::CurveServer, true)?;
68                socket.set_sockopt_bytes(SocketOption::CurveSecretKey, secret_key)?;
69            }
70            #[cfg(zmq_has = "curve")]
71            SecurityMechanism::CurveClient {
72                server_key,
73                public_key,
74                secret_key,
75            } => {
76                socket.set_sockopt_bytes(SocketOption::CurveServerKey, server_key)?;
77                socket.set_sockopt_bytes(SocketOption::CurvePublicKey, public_key)?;
78                socket.set_sockopt_bytes(SocketOption::CurveSecretKey, secret_key)?;
79            }
80            #[cfg(zmq_has = "gssapi")]
81            SecurityMechanism::GssApiClient { service_principal } => {
82                socket
83                    .set_sockopt_string(SocketOption::GssApiServicePrincipal, service_principal)?;
84            }
85            #[cfg(zmq_has = "gssapi")]
86            SecurityMechanism::GssApiServer => {
87                socket.set_sockopt_bool(SocketOption::GssApiServer, true)?;
88            }
89        }
90        Ok(())
91    }
92}
93
94impl<T: sealed::SocketType> TryFrom<&Socket<T>> for SecurityMechanism {
95    type Error = ZmqError;
96
97    fn try_from(socket: &Socket<T>) -> Result<Self, Self::Error> {
98        match socket.get_sockopt_int::<i32>(SocketOption::Mechanism)? {
99            value if value == zmq_sys_crate::ZMQ_NULL as i32 => Ok(Self::Null),
100            value if value == zmq_sys_crate::ZMQ_PLAIN as i32 => {
101                let username = socket.get_sockopt_string(SocketOption::PlainUsername)?;
102                let password = socket.get_sockopt_string(SocketOption::PlainPassword)?;
103                Ok(Self::Plain { username, password })
104            }
105            #[cfg(zmq_has = "curve")]
106            value if value == zmq_sys_crate::ZMQ_CURVE as i32 => {
107                let secret_key = socket.get_sockopt_curve(SocketOption::CurveSecretKey)?;
108                if socket.get_sockopt_bool(SocketOption::CurveServer)? {
109                    Ok(Self::CurveServer { secret_key })
110                } else {
111                    let server_key = socket.get_sockopt_curve(SocketOption::CurveServerKey)?;
112                    let public_key = socket.get_sockopt_curve(SocketOption::CurvePublicKey)?;
113                    Ok(Self::CurveClient {
114                        server_key,
115                        public_key,
116                        secret_key,
117                    })
118                }
119            }
120            #[cfg(zmq_has = "gssapi")]
121            value if value == zmq_sys_crate::ZMQ_GSSAPI as i32 => {
122                if socket.get_sockopt_bool(SocketOption::GssApiServer)? {
123                    Ok(Self::GssApiServer)
124                } else {
125                    let service_principal =
126                        socket.get_sockopt_string(SocketOption::GssApiServicePrincipal)?;
127                    Ok(Self::GssApiClient { service_principal })
128                }
129            }
130            _ => Err(ZmqError::Unsupported),
131        }
132    }
133}
134
135#[cfg(test)]
136mod security_mechanism_tests {
137    use super::SecurityMechanism;
138    #[cfg(zmq_has = "curve")]
139    use super::curve::curve_keypair;
140    use crate::{
141        prelude::{Context, DealerSocket, SocketOption, ZmqResult},
142        zmq_sys_crate,
143    };
144
145    #[test]
146    fn apply_null_security() -> ZmqResult<()> {
147        let context = Context::new()?;
148
149        let socket = DealerSocket::from_context(&context)?;
150
151        SecurityMechanism::Null.apply(&socket)?;
152
153        assert_eq!(
154            socket.get_sockopt_int::<i32>(SocketOption::Mechanism)?,
155            zmq_sys_crate::ZMQ_NULL as i32
156        );
157
158        Ok(())
159    }
160
161    #[test]
162    fn apply_plain_security() -> ZmqResult<()> {
163        let context = Context::new()?;
164
165        let socket = DealerSocket::from_context(&context)?;
166        let security = SecurityMechanism::Plain {
167            username: "username".to_string(),
168            password: "password".to_string(),
169        };
170
171        security.apply(&socket)?;
172
173        assert_eq!(
174            socket.get_sockopt_int::<i32>(SocketOption::Mechanism)?,
175            zmq_sys_crate::ZMQ_PLAIN as i32
176        );
177        assert_eq!(
178            socket.get_sockopt_string(SocketOption::PlainUsername)?,
179            "username"
180        );
181        assert_eq!(
182            socket.get_sockopt_string(SocketOption::PlainPassword)?,
183            "password"
184        );
185
186        Ok(())
187    }
188
189    #[cfg(zmq_has = "curve")]
190    #[test]
191    fn apply_curve_server_security() -> ZmqResult<()> {
192        let (_, secret_key) = curve_keypair()?;
193
194        let context = Context::new()?;
195
196        let socket = DealerSocket::from_context(&context)?;
197        let security = SecurityMechanism::CurveServer {
198            secret_key: secret_key.clone(),
199        };
200        security.apply(&socket)?;
201
202        assert_eq!(
203            socket.get_sockopt_int::<i32>(SocketOption::Mechanism)?,
204            zmq_sys_crate::ZMQ_CURVE as i32
205        );
206        assert!(socket.get_sockopt_bool(SocketOption::CurveServer)?);
207        assert_eq!(
208            socket.get_sockopt_curve(SocketOption::CurveSecretKey)?,
209            secret_key
210        );
211
212        Ok(())
213    }
214
215    #[cfg(zmq_has = "curve")]
216    #[test]
217    fn apply_curve_client_security() -> ZmqResult<()> {
218        let (_, server_key) = curve_keypair()?;
219        let (public_key, secret_key) = curve_keypair()?;
220
221        let context = Context::new()?;
222
223        let socket = DealerSocket::from_context(&context)?;
224        let security = SecurityMechanism::CurveClient {
225            server_key: server_key.clone(),
226            public_key: public_key.clone(),
227            secret_key: secret_key.clone(),
228        };
229        security.apply(&socket)?;
230
231        assert_eq!(
232            socket.get_sockopt_int::<i32>(SocketOption::Mechanism)?,
233            zmq_sys_crate::ZMQ_CURVE as i32
234        );
235        assert!(!socket.get_sockopt_bool(SocketOption::CurveServer)?);
236        assert_eq!(
237            socket.get_sockopt_curve(SocketOption::CurveServerKey)?,
238            server_key
239        );
240        assert_eq!(
241            socket.get_sockopt_curve(SocketOption::CurvePublicKey)?,
242            public_key
243        );
244        assert_eq!(
245            socket.get_sockopt_curve(SocketOption::CurveSecretKey)?,
246            secret_key
247        );
248
249        Ok(())
250    }
251
252    #[cfg(zmq_has = "gssapi")]
253    #[test]
254    fn apply_gssapi_server_security() -> ZmqResult<()> {
255        let context = Context::new()?;
256
257        let socket = DealerSocket::from_context(&context)?;
258        let security = SecurityMechanism::GssApiServer;
259        security.apply(&socket)?;
260
261        assert!(socket.get_sockopt_bool(SocketOption::GssApiServer)?);
262
263        Ok(())
264    }
265
266    #[cfg(zmq_has = "gssapi")]
267    #[test]
268    fn apply_gssapi_client_security() -> ZmqResult<()> {
269        let context = Context::new()?;
270
271        let socket = DealerSocket::from_context(&context)?;
272        let security = SecurityMechanism::GssApiClient {
273            service_principal: "service_principal".to_string(),
274        };
275        security.apply(&socket)?;
276
277        assert_eq!(
278            socket.get_sockopt_string(SocketOption::GssApiServicePrincipal)?,
279            "service_principal"
280        );
281
282        Ok(())
283    }
284
285    #[test]
286    fn try_from_socket_with_no_security() -> ZmqResult<()> {
287        let context = Context::new()?;
288
289        let socket = DealerSocket::from_context(&context)?;
290
291        assert_eq!(
292            SecurityMechanism::try_from(&socket)?,
293            SecurityMechanism::Null
294        );
295
296        Ok(())
297    }
298
299    #[test]
300    fn try_from_socket_with_plain_security() -> ZmqResult<()> {
301        let context = Context::new()?;
302
303        let socket = DealerSocket::from_context(&context)?;
304        socket.set_sockopt_string(SocketOption::PlainUsername, "username")?;
305        socket.set_sockopt_string(SocketOption::PlainPassword, "password")?;
306
307        assert_eq!(
308            SecurityMechanism::try_from(&socket)?,
309            SecurityMechanism::Plain {
310                username: "username".to_string(),
311                password: "password".to_string(),
312            }
313        );
314
315        Ok(())
316    }
317
318    #[cfg(zmq_has = "curve")]
319    #[test]
320    fn try_from_socket_with_curve_security() -> ZmqResult<()> {
321        let (_, secret_key) = curve_keypair()?;
322
323        let context = Context::new()?;
324
325        let socket = DealerSocket::from_context(&context)?;
326
327        socket.set_sockopt_bytes(SocketOption::CurveSecretKey, secret_key.clone())?;
328        socket.set_sockopt_bool(SocketOption::CurveServer, true)?;
329        assert_eq!(
330            SecurityMechanism::try_from(&socket)?,
331            SecurityMechanism::CurveServer {
332                secret_key: secret_key.clone(),
333            }
334        );
335
336        Ok(())
337    }
338
339    #[cfg(zmq_has = "curve")]
340    #[test]
341    fn try_from_socket_with_curve_client_security() -> ZmqResult<()> {
342        let (_, server_key) = curve_keypair()?;
343        let (public_key, secret_key) = curve_keypair()?;
344
345        let context = Context::new()?;
346
347        let socket = DealerSocket::from_context(&context)?;
348        socket.set_sockopt_bool(SocketOption::CurveServer, false)?;
349        socket.set_sockopt_bytes(SocketOption::CurveServerKey, server_key.clone())?;
350        socket.set_sockopt_bytes(SocketOption::CurvePublicKey, public_key.clone())?;
351        socket.set_sockopt_bytes(SocketOption::CurveSecretKey, secret_key.clone())?;
352        assert_eq!(
353            SecurityMechanism::try_from(&socket)?,
354            SecurityMechanism::CurveClient {
355                server_key: server_key.clone(),
356                public_key: public_key.clone(),
357                secret_key: secret_key.clone(),
358            }
359        );
360
361        Ok(())
362    }
363
364    #[cfg(zmq_has = "gssapi")]
365    #[test]
366    fn try_from_socket_with_gssapi_security() -> ZmqResult<()> {
367        let context = Context::new()?;
368
369        let socket = DealerSocket::from_context(&context)?;
370        socket.set_sockopt_string(SocketOption::GssApiServicePrincipal, "service_principal")?;
371        socket.set_sockopt_bool(SocketOption::GssApiServer, true)?;
372        assert_eq!(
373            SecurityMechanism::try_from(&socket)?,
374            SecurityMechanism::GssApiServer
375        );
376
377        Ok(())
378    }
379
380    #[cfg(zmq_has = "gssapi")]
381    #[test]
382    fn try_from_socket_with_gssapi_client_security() -> ZmqResult<()> {
383        let context = Context::new()?;
384
385        let socket = DealerSocket::from_context(&context)?;
386        socket.set_sockopt_string(SocketOption::GssApiServicePrincipal, "service_principal")?;
387        socket.set_sockopt_bool(SocketOption::GssApiServer, false)?;
388        assert_eq!(
389            SecurityMechanism::try_from(&socket)?,
390            SecurityMechanism::GssApiClient {
391                service_principal: "service_principal".to_string()
392            }
393        );
394
395        Ok(())
396    }
397}
398
399/// # `Curve` related convenience functions
400#[cfg(zmq_has = "curve")]
401pub mod curve {
402    use alloc::ffi::CString;
403    use core::ffi::c_char;
404    #[cfg(nightly)]
405    use core::hint::cold_path;
406
407    use derive_more::Display;
408    use thiserror::Error;
409
410    use crate::{
411        prelude::{ZmqError, ZmqResult},
412        zmq_sys_crate,
413    };
414
415    #[derive(Debug, PartialEq, Eq, Clone, Hash, Error, Display)]
416    /// Error that can occur while encoding Z85
417    pub enum EncodeError {
418        /// The input string slice’s length was not a multiple of 4.
419        BadLength,
420        /// The underlying `zmq_z85_encode()` function returned an error.
421        EncodingFailed,
422        /// Converting the returned string failed.
423        Utf8Error,
424    }
425
426    /// # encode a binary key as Z85 printable text
427    ///
428    /// The [`encode()`] function shall encode the binary block specified by 'data' into a string.
429    /// The size of the binary block must be divisible by 4. A 32-byte CURVE key is encoded as 40 ASCII
430    /// characters plus a null terminator.
431    ///
432    /// [`encode()`]: #method.encode
433    pub fn encode<T>(data: T) -> Result<String, EncodeError>
434    where
435        T: AsRef<[u8]>,
436    {
437        let input = data.as_ref();
438        let input_len = input.len();
439        if input_len % 4 != 0 {
440            return Err(EncodeError::BadLength);
441        }
442
443        let len = input_len * 5 / 4 + 1;
444        let mut dest = vec![0u8; len];
445
446        if unsafe {
447            zmq_sys_crate::zmq_z85_encode(
448                dest.as_mut_ptr() as *mut c_char,
449                input.as_ptr(),
450                input.len(),
451            )
452        }
453        .is_null()
454        {
455            #[cfg(nightly)]
456            cold_path();
457            return Err(EncodeError::EncodingFailed);
458        }
459
460        dest.truncate(len - 1);
461        String::from_utf8(dest).map_err(|_| EncodeError::Utf8Error)
462    }
463
464    #[cfg(test)]
465    mod z85_encode_tests {
466        use super::{EncodeError, encode};
467
468        #[test]
469        fn z85_encode_for_empty_input() -> Result<(), EncodeError> {
470            let encoded_string = encode(vec![])?;
471            assert_eq!(encoded_string, "");
472            Ok(())
473        }
474
475        #[test]
476        fn z85_encode_for_invalid_input_length() {
477            let result = encode(b"a");
478            assert!(result.is_err_and(|err| err == EncodeError::BadLength));
479        }
480
481        #[test]
482        fn z85_encode_for_valid_input() -> Result<(), EncodeError> {
483            let encoded_string = encode(b"Hello World!")?;
484            assert_eq!(encoded_string, "nm=QNzY&b1A+]nf");
485
486            Ok(())
487        }
488    }
489
490    #[derive(Debug, PartialEq, Eq, Clone, Hash, Error, Display)]
491    /// Error that can occur while decoding Z85.
492    pub enum DecodeError {
493        /// The input string slice’s length was not a multiple of 5.
494        InvalidLength,
495        /// The underlying `zmq_z85_decode()` function returned an error.
496        DecodingFailed,
497    }
498
499    /// # decode a binary key from Z85 printable text
500    ///
501    /// The [`decode()`] function shall decode 'string'. The length of 'string' shall be divisible
502    /// by 5.
503    ///
504    /// [`decode()`]: #method.decode
505    pub fn decode<T>(string: T) -> Result<Vec<u8>, DecodeError>
506    where
507        T: AsRef<str>,
508    {
509        let input = string.as_ref();
510        let input_len = input.len();
511        if input_len == 0 {
512            return Ok(vec![]);
513        }
514
515        if input_len % 5 != 0 {
516            return Err(DecodeError::InvalidLength);
517        }
518
519        let dest_len = input_len * 4 / 5;
520        let mut dest = vec![0; dest_len];
521
522        let c_str = CString::new(input).map_err(|_| DecodeError::DecodingFailed)?;
523
524        if unsafe { zmq_sys_crate::zmq_z85_decode(dest.as_mut_ptr(), c_str.into_raw()) }.is_null() {
525            #[cfg(nightly)]
526            cold_path();
527            return Err(DecodeError::DecodingFailed);
528        }
529
530        Ok(dest)
531    }
532
533    #[cfg(test)]
534    mod z85_decode_tests {
535        use super::{DecodeError, decode};
536
537        #[test]
538        fn z85_decode_z85_encoded_string() -> Result<(), DecodeError> {
539            let encoded_string = "nm=QNzY&b1A+]nf";
540            let decoded_string = decode(encoded_string)?;
541
542            assert_eq!(decoded_string, b"Hello World!");
543
544            Ok(())
545        }
546
547        #[test]
548        fn z85_decode_for_empty_input() -> Result<(), DecodeError> {
549            let encoded_string = "";
550            let decoded_string = decode(encoded_string)?;
551
552            assert_eq!(decoded_string, vec![]);
553
554            Ok(())
555        }
556
557        #[test]
558        fn z85_decode_for_invalid_input_length() {
559            let encoded_string = "a";
560            let result = decode(encoded_string);
561
562            assert!(result.is_err_and(|err| err == DecodeError::InvalidLength));
563        }
564    }
565
566    /// # generate a new CURVE keypair
567    ///
568    /// The [`curve_keypair()`] function returns a newly generated random keypair consisting of a
569    /// public key and a secret key. The keys are encoded using [`z85_encode()`].
570    ///
571    /// [`curve_keypair()`]: curve_keypair
572    /// [`z85_encode()`]: encode
573    pub fn curve_keypair() -> ZmqResult<(Vec<u8>, Vec<u8>)> {
574        let mut public_key: [u8; 41] = [0; 41];
575        let mut secret_key: [u8; 41] = [0; 41];
576
577        if unsafe {
578            zmq_sys_crate::zmq_curve_keypair(
579                public_key.as_mut_ptr() as *mut c_char,
580                secret_key.as_mut_ptr() as *mut c_char,
581            )
582        } == -1
583        {
584            #[cfg(nightly)]
585            cold_path();
586            match unsafe { zmq_sys_crate::zmq_errno() } {
587                errno @ zmq_sys_crate::errno::ENOTSUP => return Err(ZmqError::from(errno)),
588                _ => unreachable!(),
589            }
590        }
591
592        Ok((public_key.to_vec(), secret_key.to_vec()))
593    }
594
595    /// # derive the public key from a private key
596    ///
597    /// The [`curve_public()`] function shall derive the public key from a private key. The keys are
598    /// encoded using [`z85_encode()`].
599    ///
600    /// [`curve_public()`]: curve_public
601    /// [`z85_encode()`]: encode
602    pub fn curve_public<T>(mut secret_key: T) -> ZmqResult<Vec<u8>>
603    where
604        T: AsMut<[u8]>,
605    {
606        let mut public_key: [u8; 41] = [0; 41];
607        let secret_key_array = secret_key.as_mut();
608
609        if unsafe {
610            zmq_sys_crate::zmq_curve_public(
611                public_key.as_mut_ptr() as *mut c_char,
612                secret_key_array.as_ptr() as *const c_char,
613            )
614        } == -1
615        {
616            #[cfg(nightly)]
617            cold_path();
618            match unsafe { zmq_sys_crate::zmq_errno() } {
619                errno @ zmq_sys_crate::errno::ENOTSUP => return Err(ZmqError::from(errno)),
620                _ => unreachable!(),
621            }
622        }
623
624        Ok(public_key.to_vec())
625    }
626
627    #[cfg(test)]
628    mod curve_keypair_tests {
629        use super::{curve_keypair, curve_public};
630        use crate::prelude::ZmqResult;
631
632        #[test]
633        fn curve_keypair_generate_curve_keypair() -> ZmqResult<()> {
634            let (public_key, secret_key) = curve_keypair()?;
635
636            let pub_key = curve_public(secret_key)?;
637
638            assert_eq!(public_key, pub_key);
639
640            Ok(())
641        }
642    }
643}
644
645#[cfg(zmq_has = "gssapi")]
646mod gssapi {
647    use derive_more::Display;
648
649    use crate::{prelude::ZmqError, zmq_sys_crate};
650
651    #[derive(Debug, Display, PartialEq, Eq, Clone, Hash)]
652    #[repr(i32)]
653    /// # name types for GSSAPI
654    pub enum GssApiNametype {
655        /// the name is interpreted as a host based name
656        NtHostbased,
657        /// the name is interpreted as a local user name
658        NtUsername,
659        /// the name is interpreted as an unparsed principal name string (valid only with the krb5
660        /// GSSAPI mechanism).
661        NtKrb5Principal,
662    }
663
664    impl TryFrom<i32> for GssApiNametype {
665        type Error = ZmqError;
666
667        fn try_from(value: i32) -> Result<Self, Self::Error> {
668            match value {
669                _ if value == zmq_sys_crate::ZMQ_GSSAPI_NT_HOSTBASED as i32 => {
670                    Ok(Self::NtHostbased)
671                }
672                _ if value == zmq_sys_crate::ZMQ_GSSAPI_NT_USER_NAME as i32 => Ok(Self::NtUsername),
673                _ if value == zmq_sys_crate::ZMQ_GSSAPI_NT_KRB5_PRINCIPAL as i32 => {
674                    Ok(Self::NtKrb5Principal)
675                }
676                _ => Err(ZmqError::Unsupported),
677            }
678        }
679    }
680
681    #[cfg(test)]
682    mod gss_api_nametype_tests {
683        use rstest::*;
684
685        use super::GssApiNametype;
686        use crate::{
687            prelude::{ZmqError, ZmqResult},
688            zmq_sys_crate,
689        };
690
691        #[rstest]
692        #[case(zmq_sys_crate::ZMQ_GSSAPI_NT_HOSTBASED as i32, Ok(GssApiNametype::NtHostbased))]
693        #[case(zmq_sys_crate::ZMQ_GSSAPI_NT_USER_NAME as i32, Ok(GssApiNametype::NtUsername))]
694        #[case(zmq_sys_crate::ZMQ_GSSAPI_NT_KRB5_PRINCIPAL as i32, Ok(GssApiNametype::NtKrb5Principal)
695        )]
696        #[case(666, Err(ZmqError::Unsupported))]
697        fn nametype_try_from(#[case] value: i32, #[case] expected: ZmqResult<GssApiNametype>) {
698            assert_eq!(expected, GssApiNametype::try_from(value));
699        }
700    }
701}