diff --git a/xmpp-parsers/Cargo.toml b/xmpp-parsers/Cargo.toml new file mode 100644 index 0000000..c2e8fd3 --- /dev/null +++ b/xmpp-parsers/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "xmpp-parsers-gst-meet" +version = "0.18.2" +authors = [ + "Emmanuel Gil Peyrot ", + "Maxime “pep” Buquet ", +] +description = "Collection of parsers and serialisers for XMPP extensions" +homepage = "https://gitlab.com/xmpp-rs/xmpp-rs" +repository = "https://gitlab.com/xmpp-rs/xmpp-rs" +keywords = ["xmpp", "jabber", "xml"] +categories = ["parsing", "network-programming"] +license = "MPL-2.0" +edition = "2018" + +[dependencies] +minidom = { package = "minidom-gst-meet", version = "0.13" } +jid = { package = "jid-gst-meet", version = "0.9", features = ["minidom"] } +base64 = "0.13" +digest = "0.9" +sha-1 = "0.9" +sha2 = "0.9" +sha3 = "0.9" +blake2 = "0.9" +chrono = { version = "0.4.5", default-features = false, features = ["std"] } + +[features] +# Build xmpp-parsers to make components instead of clients. +component = [] +# Disable validation of unknown attributes. +disable-validation = [] +serde = ["jid/serde"] + +[package.metadata.docs.rs] +rustdoc-args = [ "--sort-modules-by-appearance", "-Zunstable-options" ] diff --git a/xmpp-parsers/ChangeLog b/xmpp-parsers/ChangeLog new file mode 100644 index 0000000..0329f69 --- /dev/null +++ b/xmpp-parsers/ChangeLog @@ -0,0 +1,365 @@ +Version 0.18.0: +2021-01-13 Emmanuel Gil Peyrot + * Bugfixes: + - Bump minidom to 0.13, as 0.12.1 got yanked. + +Version 0.18.0: +2021-01-13 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Jingle Raw UDP Transport Method (XEP-0177). + - Jingle RTP Header Extensions Negotiation (XEP-0294). + - Jingle Grouping Framework (XEP-0338). + - Mediated Information eXchange (MIX) (XEP-0369). + * Improvements: + - Everything is now PartialEq! + - Add "serde" feature to enable "jid/serde". + - Implement more of XEP-0060. + - Bump XEP-0167 to version 1.2.0, adding rtcp-mux. + - Bump XEP-0176 to version 1.1, fixing interoperability with other + clients. + - Bump XEP-0402 to version 1.1.1, bumping its namespace and adding + support for extension data. + - Bump all dependencies to their latest version. + - Some more helper constructors. + - Make public some stuff that should have been public from the very + beginning. + * Bugfixes: + - Jingle::set_reason() does what it says now (copy/paste error). + - Bookmarks’ names are now optional like they should. + +Version 0.17.0: +2020-02-15 Emmanuel Gil Peyrot , Maxime “pep” Buquet , Paul Fariello + * Improvements: + - Add serialization tests where possible + - Use minidom's NSChoice API for Jingle parser + - Remove NamespaceAwareCompare. Move to minidom + * Breaking changes: + - Prevent generate_serializer macro from adding another layer of Node. + Fixes some serializers. + - ecaps2: Use the Error type instead of () + +Version 0.16.0: +2019-10-15 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Client Certificate Management for SASL EXTERNAL (XEP-0257) + - JID Prep (XEP-0328) + - Client State Indication (XEP-0352) + - OpenPGP for XMPP (XEP-0373) + - Bookmarks 2 (This Time it's Serious) (XEP-0402) + - Anonymous unique occupant identifiers for MUCs (XEP-0421) + - Source-Specific Media Attributes in Jingle (XEP-0339) + - Jingle RTP Feedback Negotiation (XEP-0293) + * Breaking changes: + - Presence constructors now take Into and assume Some. + * Improvements: + - CI: refactor, add caching + - Update jid-rs to 0.8 + +Version 0.15.0: +2019-09-06 Emmanuel Gil Peyrot + * New parsers/serialisers: + - XHTML-IM (XEP-0071) + - User Tune (XEP-0118) + - Bits of Binary (XEP-0231) + - Message Carbons (XEP-0280) + * Breaking changes: + - Stop reexporting TryFrom and TryInto, they are available in + std::convert nowadays. + - Bind has been split into BindQuery and BindResponse. + * Improvements: + - New DOAP file for a machine-readable description of the features. + - Add various parser and formatter helpers on Hash. + +Version 0.14.0: +2019-07-13 Emmanuel Gil Peyrot , Maxime “pep” Buquet + * New parsers/serialisers: + - Entity Time (XEP-0202). + * Improvements: + - Microblog NS (XEP-0227). + - Update jid-rs dependency with jid split change (Jid, FullJid, + BareJid) and reexport them. + - Fix rustdoc options in Cargo.toml for docs.rs + * Breaking changes: + - Presence's show attribute is now Option and Show::None is no + more. + +Version 0.13.1: +2019-04-12 Emmanuel Gil Peyrot + * Bugfixes: + - Fix invalid serialisation of priority in presence. + - Bump image size to u16 from u8, as per XEP-0084 version 1.1.2. + * Improvements: + - Drop try_from dependency, as std::convert::TryFrom got + stabilised. + +Version 0.13.0: +2019-03-20 Emmanuel Gil Peyrot + * New parsers/serialisers: + - User Avatar (XEP-0084). + - Contact Addresses for XMPP Services (XEP-0157). + - Jingle RTP Sessions (XEP-0167). + - Jingle ICE-UDP Transport Method (XEP-0176). + - Use of DTLS-SRTP in Jingle Sessions (XEP-0320). + * Breaking changes: + - Make 'id' required on iq, as per RFC6120 §8.1.3. + - Refactor PubSub to have more type-safety. + - Treat FORM_TYPE as a special case in data forms, to avoid + duplicating it into a field. + - Add forgotten i18n to Jingle text element. + * Improvements: + - Add various helpers for hash representations. + - Add helpers constructors for multiple extensions (disco, caps, + pubsub, stanza_error). + - Use Into in more constructors. + - Internal change on attribute declaration in macros. + - Reexport missing try_from::TryInto. + +Version 0.12.2: +2019-01-16 Emmanuel Gil Peyrot + * Improvements: + - Reexport missing util::error::Error and try_from::TryFrom. + +Version 0.12.1: +2019-01-16 Emmanuel Gil Peyrot + * Improvements: + - Reexport missing JidParseError from the jid crate. + +Version 0.12.0: +2019-01-16 Emmanuel Gil Peyrot + * Breaking changes: + - Update dependencies. + - Switch to git, upstream is now available at + https://gitlab.com/xmpp-rs/xmpp-parsers + - Switch to Edition 2018, this removes support for rustc + versions older than 1.31. + - Implement support for XEP-0030 2.5rc3, relaxing the ordering + of children in disco#info. + * Improvements: + - Test for struct size, to keep them known and avoid bloat. + - Add various constructors to make the API easier to use. + - Reexport Jid from the jid crate, to avoid any weird issue on + using different incompatible versions of the same crate. + - Add forgotten 'ask' attribute on roster item (thanks O01eg!). + - Use cargo-fmt on the codebase, to lower the barrier of entry. + - Add a disable-validation feature, disabling many checks + xmpp-parsers is doing. This should be used for software + which want to let invalid XMPP pass through instead of being + rejected as invalid (thanks Astro-!). + +Version 0.11.1: +2018-09-20 Emmanuel Gil Peyrot + * Improvements: + - Document all of the modules. + +Version 0.11.0: +2018-08-03 Emmanuel Gil Peyrot + * Breaking changes: + - Split Software Version (XEP-0092) into a query and response + elements. + - Split RSM (XEP-0059) into a query and response elements. + - Fix type safety and spec issues in RSM and MAM (XEP-0313). + - Remove item@node and EmptyItems from PubSub events + (XEP-0060). + * Improvements: + - Document many additional modules. + - Add the SASL nonza, as well as the SCRAM-SHA-256 + and the two -PLUS mechanisms. + +Version 0.10.0: +2018-07-31 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Added , SASL and bind (RFC6120) parsers. + - Added a WebSocket (RFC7395) implementation. + - Added a Jabber Component (XEP-0114). + - Added support for User Nickname (XEP-0172). + - Added support for Stream Management (XEP-0198). + - Added support for Bookmarks (XEP-0048). + - Publish-Subscribe (XEP-0060) now supports requests in + addition to events. + * Breaking changes: + - Switch from std::error to failure to report better errors. + - Bump to minidom 0.9.1, and reexport minidom::Element. + * Improvements: + - Add getters for the best body and subject in message, to make + it easier to determine which one the user wants based on + their language preferences. + - Add constructors and setters for most Jingle elements, to + ease their creation. + - Add constructors for hash, MUC item, iq and more. + - Use more macros to simplify and factorise the code. + - Use traits to define iq payloads. + - Document more modules. + +Version 0.9.0: +2017-10-31 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Blocking Command (XEP-0191) has been added. + - Date and Time Profiles (XEP-0082) has been added, replacing + ad-hoc use of chrono in various places. + - User Mood (XEP-0107) has been added. + * Breaking changes: + - Fix subscription="none" not being the default. + - Add more type safety to pubsub#event. + - Reuse Jingle’s ContentId type in JingleFT. + - Import the disposition attribute values in Jingle. + * Improvements: + - Refactor a good part of the code using macros. + - Simplify the parsing code wherever it makes sense. + - Check for children ordering in disco#info result. + - Finish implementation of , and + in JingleFT. + - Correctly serialise , and test it. + +Version 0.8.0: +2017-08-27 Emmanuel Gil Peyrot + * New parsers/serialisers: + - iq:version (XEP-0092) has been added. + - Finally implement extension serialisation in disco. + * Breaking changes: + - Wrap even more elements into their own type, in jingle, + jingle_ft, roster, message. + - Split loose enums into multiple structs where it makes sense, + such as for IBB, StanzaId, Receipts. + - Split disco query and answer elements into their own struct, + to enforce more guarantees on both. + * Improvements: + - Use Vec::into_iter() more to avoid references and clones. + - Make data_forms propagate a media_element error. + - Document more of disco, roster, chatstates. + - Use the minidom feature of jid, for IntoAttributeValue. + - Add a component feature, changing the default namespace to + jabber:component:accept. + - Add support for indicating ranged transfers in jingle_ft. + +Version 0.7.1: +2017-07-24 Emmanuel Gil Peyrot + * Hotfixes: + - Stub out blake2 support, since the blake2 crate broke its API + between their 0.6.0 and 0.6.1 releases… + +Version 0.7.0: +2017-07-23 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Jingle Message Initialisation (XEP-0353) was added. + - The disco#items query (XEP-0030) is now supported, in + addition to the existing disco#info one. + * Breaking changes: + - Replaced many type aliases with proper wrapping structs. + - Split Disco into a query and a result part, since they have + very different constraints. + - Split IqPayload in three to avoid parsing queries as results + for example. + * Improvements: + - Use TryFrom from the try_from crate, thus removing the + dependency on nightly! + - Always implement From instead of Into, the latter is + generated anyway. + - Add helpers to construct your Presence stanza. + +Version 0.6.0: +2017-06-27 Emmanuel Gil Peyrot + * New parsers/serialisers: + - In-Band Registration (XEP-0077) was added. + - Multi-User Chat (XEP-0045) got expanded a lot, thanks pep.! + * Breaking changes: + - Added wrappers for Strings used as identifiers, to add type + safety. + - Use chrono’s DateTime for JingleFT’s date element. + - Use Jid for JingleS5B’s jid attribute. + * Improvements: + - Use more macros for common tasks. + - Add a constructor for Message and Presence. + - Implement std::fmt::Display and std::error::Error on our + error type. + - Fix DataForms serialisation. + - Fix roster group serialisation. + - Update libraries, notably chrono whose version 0.3.1 got + yanked. + +Version 0.5.0: +2017-06-11 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Implementation of the roster management protocol defined in + RFC 6121 §2. + - Implementation of PubSub events (except collections). + - Early implementation of MUC. + * Breaking changes: + - Rename presence enums to make them easier to use. + * Improvements: + - Make hashes comparable and hashable. + - Make data forms embeddable easily into minidom + Element::builder. + +Version 0.4.0: +2017-05-28 Emmanuel Gil Peyrot + * Incompatible changes: + - Receipts now make the id optional, as per the specification. + - Hashes now expose their raw binary value, instead of staying + base64-encoded. + - Parse dates (XEP-0082) in delayed delivery (XEP-0203) and + last user interaction (XEP-0319), using the chrono crate. + * Improvements: + - Removal of most of the remaining clones, the only ones left + are due to minidom not exposing a draining iterator over the + children. + - Finish to parse all of the attributes using get_attr!(). + - More attribute checks. + - Split more parsers into one parser per element. + - Rely on minidom 0.4.3 to serialise more standard types + automatically. + - Implement forgotten serialisation for data forms (XEP-0004). + - Implement legacy capabilities (XEP-0115) for compatibility + with older software. + +Version 0.3.0: +2017-05-23 Emmanuel Gil Peyrot + * Big changes: + - All parsers and serialisers now consume their argument, this + makes the API way more efficient, but you will have to clone + before passing your structs in it if you want to keep them. + - Payloads of stanzas are not parsed automatically anymore, to + let applications which want to forward them as-is do so more + easily. Parsing now always succeeds on unknown payloads, it + just puts them into an Unknown value containing the existing + minidom Element. + * New parsers/serialisers: + - Last User Interaction in Presence, XEP-0319. + * Improved parsers/serialisers: + - Message now supports subject, bodies and threads as per + RFC 6121 §5.2. + - Replace most attribute reads with a nice macro. + - Use enums for more enum-like things, for example Algo in + Hash, or FieldType in DataForm. + - Wire up stanza-id and origin-id to MessagePayload. + - Wire up MAM elements to message and iq payloads. + - Changes in the RSM API. + - Add support for more data forms elements, but still not the + complete set. + - Thanks to minidom 0.3.1, check for explicitly disallowed + extra attributes in some elements. + * Crate updates: + - minidom 0.4.1 + +Version 0.2.0: +2017-05-06 Emmanuel Gil Peyrot + * New parsers/serialisers: + - Stanza error, as per RFC 6120 §8.3. + - Jingle SOCKS5 Transport, XEP-0260. + * Incompatible changes: + - Parsers and serialisers now all implement TryFrom + and Into, instead of the old parse_* and serialise_* + functions. + - Presence has got an overhaul, it now hosts show, statuses and + priority in its struct. The status module has also been + dropped. + - Message now supports multiple bodies, each in a different + language. The body module has also been dropped. + - Iq now gets a proper StanzaError when the type is error. + - Fix bogus Jingle payload, which was requiring both + description and transport. + * Crate updates: + - minidom 0.3.0 + +Version 0.1.0: +2017-04-29 Emmanuel Gil Peyrot + * Implement many extensions. diff --git a/xmpp-parsers/LICENSE b/xmpp-parsers/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/xmpp-parsers/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/xmpp-parsers/doap.xml b/xmpp-parsers/doap.xml new file mode 100644 index 0000000..31d6e13 --- /dev/null +++ b/xmpp-parsers/doap.xml @@ -0,0 +1,684 @@ + + + + + xmpp-parsers + + 2017-04-18 + + Collection of parsers and serialisers for XMPP extensions + Collection de parseurs et de sérialiseurs pour extensions XMPP + + TODO + TODO + + + + + + + + + + + + + + + + Rust + + + + + + Link Mauve + + aaa4dac2b31c1be4ee8f8e2ab986d34fb261974f + + + + + pep. + + 99bcf9784288e323b0d2dea9c9ac7a2ede98395a + + + + + + + + + + + + + + + + + partial + 2.9 + 0.1.0 + + + + + + complete + 2.5rc3 + 0.1.0 + + + + + + complete + 1.32.0 + 0.5.0 + + + + + + complete + 2.0 + 0.1.0 + + + + + + complete + 1.1 + 0.10.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + partial + 1.15.8 + 0.5.0 + + + + + + complete + 1.2 + 0.1.0 + there is no specific module for this, the feature is all in the XEP-0004 module + + + + + + complete + 1.5.4 + 0.15.0 + + + + + + complete + 2.4 + 0.6.0 + + + + + + complete + 1.1 + 0.9.0 + + + + + + complete + 1.1.2 + 0.13.0 + + + + + + complete + 2.1 + 0.1.0 + + + + + + complete + 1.1 + 0.8.0 + + + + + + complete + 1.2.1 + 0.9.0 + + + + + + complete + 1.6 + 0.10.0 + + + + + + complete + 1.5.1 + 0.4.0 + + + + + + complete + 1.2 + 0.15.0 + + + + + + complete + 1.0.1 + 0.13.0 + + + + + + complete + 1.1.2 + 0.1.0 + + + + + + complete + 1.2.0 + 0.13.0 + + + + + + complete + 1.1 + 0.10.0 + + + + + + complete + 1.1 + 0.13.0 + + + + + + complete + 1.1 + NEXT + + + + + + complete + 1.4.0 + 0.1.0 + + + + + + complete + 1.3 + 0.9.0 + + + + + + complete + 1.6 + 0.10.0 + + + + + + complete + 2.0.1 + 0.1.0 + + + + + + complete + 2.0 + 0.14.0 + + + + + + complete + 2.0 + 0.1.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + complete + 1.0 + 0.15.0 + + + + + + complete + 0.19.1 + 0.1.0 + + + + + + complete + 0.3 + 0.16.0 + + + + + + complete + 1.0.3 + 0.2.0 + + + + + + complete + 1.0 + 0.1.0 + + + + + + partial + 0.6.3 + 0.14.0 + only the namespace is included for now + + + + + + complete + 0.13.0 + 0.15.0 + + + + + + partial + 1.0.1 + 0.16.0 + Only supported in payload-type, and only for rtcp-fb. + + + + + + partial + 1.0 + NEXT + Parameters aren’t yet implemented. + + + + + + complete + 1.0 + 0.1.0 + + + + + + complete + 0.6.0 + 0.1.0 + + + + + + complete + 1.1.0 + 0.1.0 + + + + + + complete + 0.7.5 + 0.1.0 + + + + + + complete + 1.0.2 + 0.3.0 + + + + + + complete + 0.3.1 + 0.13.0 + + + + + + complete + 0.1 + 0.16.0 + + + + + + complete + 1.0.0 + NEXT + + + + + + complete + 0.3 + 0.16.0 + + + + + + complete + 0.3.0 + 0.16.0 + + + + + + complete + 0.3 + 0.7.0 + + + + + + complete + 0.6.0 + 0.1.0 + + + + + + complete + 0.14.3 + NEXT + + + + + + partial + 0.4.0 + 0.16.0 + + + + + + complete + 0.2.0 + 0.1.0 + + + + + + complete + 0.3.0 + 0.1.0 + + + + + + complete + 1.1.1 + 0.16.0 + + + + + + complete + 0.1.0 + 0.16.0 + + + + + + complete + 0.2.0 + 0.1.0 + + + + + + 0.15.0 + 2019-09-06 + + + + + + 0.14.0 + 2019-07-13 + + + + + + 0.13.1 + 2019-04-12 + + + + + + 0.13.0 + 2019-03-20 + + + + + + 0.12.2 + 2019-01-16 + + + + + + 0.12.1 + 2019-01-16 + + + + + + 0.12.0 + 2019-01-16 + + + + + + 0.11.1 + 2018-09-20 + + + + + + 0.11.0 + 2018-08-02 + + + + + + 0.10.0 + 2018-07-31 + + + + + + 0.9.0 + 2017-12-27 + + + + + + 0.8.0 + 2017-11-30 + + + + + + 0.7.1 + 2017-11-30 + + + + + + 0.7.0 + 2017-11-30 + + + + + + 0.6.0 + 2017-11-30 + + + + + + 0.5.0 + 2017-11-30 + + + + + + 0.4.0 + 2017-11-30 + + + + + + 0.3.0 + 2017-11-30 + + + + + + 0.2.0 + 2017-11-30 + + + + + + 0.1.0 + 2017-11-30 + + + + + diff --git a/xmpp-parsers/examples/generate-caps.rs b/xmpp-parsers/examples/generate-caps.rs new file mode 100644 index 0000000..a36963a --- /dev/null +++ b/xmpp-parsers/examples/generate-caps.rs @@ -0,0 +1,67 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use std::convert::TryFrom; +use std::env; +use std::io::{self, Read}; +use xmpp_parsers::{ + caps::{compute_disco as compute_disco_caps, hash_caps, Caps}, + disco::DiscoInfoResult, + ecaps2::{compute_disco as compute_disco_ecaps2, hash_ecaps2, ECaps2}, + hashes::Algo, + Element, Error, +}; + +fn get_caps(disco: &DiscoInfoResult, node: String) -> Result { + let data = compute_disco_caps(&disco); + let hash = hash_caps(&data, Algo::Sha_1)?; + Ok(Caps::new(node, hash)) +} + +fn get_ecaps2(disco: &DiscoInfoResult) -> Result { + let data = compute_disco_ecaps2(&disco)?; + let hash_sha256 = hash_ecaps2(&data, Algo::Sha_256)?; + let hash_sha3_256 = hash_ecaps2(&data, Algo::Sha3_256)?; + let hash_blake2b_256 = hash_ecaps2(&data, Algo::Blake2b_256)?; + Ok(ECaps2::new(vec![ + hash_sha256, + hash_sha3_256, + hash_blake2b_256, + ])) +} + +fn main() -> Result<(), Box> { + let args: Vec<_> = env::args().collect(); + if args.len() != 2 { + println!("Usage: {} ", args[0]); + std::process::exit(1); + } + let node = args[1].clone(); + + eprintln!("Reading a disco#info payload from stdin..."); + + // Read from stdin. + let stdin = io::stdin(); + let mut data = String::new(); + let mut handle = stdin.lock(); + handle.read_to_string(&mut data)?; + + // Parse the payload into a DiscoInfoResult. + let elem: Element = data.parse()?; + let disco = DiscoInfoResult::try_from(elem)?; + + // Compute both kinds of caps. + let caps = get_caps(&disco, node)?; + let ecaps2 = get_ecaps2(&disco)?; + + // Print them. + let caps_elem = Element::from(caps); + let ecaps2_elem = Element::from(ecaps2); + println!("{}", String::from(&caps_elem)); + println!("{}", String::from(&ecaps2_elem)); + + Ok(()) +} diff --git a/xmpp-parsers/src/attention.rs b/xmpp-parsers/src/attention.rs new file mode 100644 index 0000000..83a871c --- /dev/null +++ b/xmpp-parsers/src/attention.rs @@ -0,0 +1,72 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; + +generate_empty_element!( + /// Requests the attention of the recipient. + Attention, + "attention", + ATTENTION +); + +impl MessagePayload for Attention {} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(Attention, 0); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Attention::try_from(elem).unwrap(); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Attention::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in attention element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Attention::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in attention element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "".parse().unwrap(); + let attention = Attention; + let elem2: Element = attention.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/avatar.rs b/xmpp-parsers/src/avatar.rs new file mode 100644 index 0000000..c475db8 --- /dev/null +++ b/xmpp-parsers/src/avatar.rs @@ -0,0 +1,128 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::hashes::Sha1HexAttribute; +use crate::pubsub::PubSubPayload; +use crate::util::helpers::WhitespaceAwareBase64; + +generate_element!( + /// Communicates information about an avatar. + Metadata, "metadata", AVATAR_METADATA, + children: [ + /// List of information elements describing this avatar. + infos: Vec = ("info", AVATAR_METADATA) => Info + ] +); + +impl PubSubPayload for Metadata {} + +generate_element!( + /// Communicates avatar metadata. + Info, "info", AVATAR_METADATA, + attributes: [ + /// The size of the image data in bytes. + bytes: Required = "bytes", + + /// The width of the image in pixels. + width: Option = "width", + + /// The height of the image in pixels. + height: Option = "height", + + /// The SHA-1 hash of the image data for the specified content-type. + id: Required = "id", + + /// The IANA-registered content type of the image data. + type_: Required = "type", + + /// The http: or https: URL at which the image data file is hosted. + url: Option = "url", + ] +); + +generate_element!( + /// The actual avatar data. + Data, "data", AVATAR_DATA, + text: ( + /// Vector of bytes representing the avatar’s image. + data: WhitespaceAwareBase64> + ) +); + +impl PubSubPayload for Data {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::hashes::Algo; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Metadata, 12); + assert_size!(Info, 64); + assert_size!(Data, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Metadata, 24); + assert_size!(Info, 120); + assert_size!(Data, 24); + } + + #[test] + fn test_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + let metadata = Metadata::try_from(elem).unwrap(); + assert_eq!(metadata.infos.len(), 1); + let info = &metadata.infos[0]; + assert_eq!(info.bytes, 12345); + assert_eq!(info.width, Some(64)); + assert_eq!(info.height, Some(64)); + assert_eq!(info.id.algo, Algo::Sha_1); + assert_eq!(info.type_, "image/png"); + assert_eq!(info.url, None); + assert_eq!( + info.id.hash, + [ + 17, 31, 75, 60, 80, 215, 176, 223, 114, 157, 41, 155, 198, 248, 233, 239, 144, 102, + 151, 31 + ] + ); + + let elem: Element = "AAAA" + .parse() + .unwrap(); + let data = Data::try_from(elem).unwrap(); + assert_eq!(data.data, b"\0\0\0"); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Data::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in data element.") + } +} diff --git a/xmpp-parsers/src/bind.rs b/xmpp-parsers/src/bind.rs new file mode 100644 index 0000000..e6d4cf9 --- /dev/null +++ b/xmpp-parsers/src/bind.rs @@ -0,0 +1,203 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::{FullJid, Jid}; +use std::convert::TryFrom; +use std::str::FromStr; + +/// The request for resource binding, which is the process by which a client +/// can obtain a full JID and start exchanging on the XMPP network. +/// +/// See https://xmpp.org/rfcs/rfc6120.html#bind +#[derive(Debug, Clone, PartialEq)] +pub struct BindQuery { + /// Requests this resource, the server may associate another one though. + /// + /// If this is None, we request no particular resource, and a random one + /// will be affected by the server. + resource: Option, +} + +impl BindQuery { + /// Creates a resource binding request. + pub fn new(resource: Option) -> BindQuery { + BindQuery { resource } + } +} + +impl IqSetPayload for BindQuery {} + +impl TryFrom for BindQuery { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "bind", BIND); + check_no_attributes!(elem, "bind"); + + let mut resource = None; + for child in elem.children() { + if resource.is_some() { + return Err(Error::ParseError("Bind can only have one child.")); + } + if child.is("resource", ns::BIND) { + check_no_attributes!(child, "resource"); + check_no_children!(child, "resource"); + resource = Some(child.text()); + } else { + return Err(Error::ParseError("Unknown element in bind request.")); + } + } + + Ok(BindQuery { resource }) + } +} + +impl From for Element { + fn from(bind: BindQuery) -> Element { + Element::builder("bind", ns::BIND) + .append_all( + bind.resource + .map(|resource| Element::builder("resource", ns::BIND).append(resource)), + ) + .build() + } +} + +/// The response for resource binding, containing the client’s full JID. +/// +/// See https://xmpp.org/rfcs/rfc6120.html#bind +#[derive(Debug, Clone, PartialEq)] +pub struct BindResponse { + /// The full JID returned by the server for this client. + jid: FullJid, +} + +impl IqResultPayload for BindResponse {} + +impl From for FullJid { + fn from(bind: BindResponse) -> FullJid { + bind.jid + } +} + +impl From for Jid { + fn from(bind: BindResponse) -> Jid { + Jid::Full(bind.jid) + } +} + +impl TryFrom for BindResponse { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "bind", BIND); + check_no_attributes!(elem, "bind"); + + let mut jid = None; + for child in elem.children() { + if jid.is_some() { + return Err(Error::ParseError("Bind can only have one child.")); + } + if child.is("jid", ns::BIND) { + check_no_attributes!(child, "jid"); + check_no_children!(child, "jid"); + jid = Some(FullJid::from_str(&child.text())?); + } else { + return Err(Error::ParseError("Unknown element in bind response.")); + } + } + + Ok(BindResponse { + jid: match jid { + None => { + return Err(Error::ParseError( + "Bind response must contain a jid element.", + )) + } + Some(jid) => jid, + }, + }) + } +} + +impl From for Element { + fn from(bind: BindResponse) -> Element { + Element::builder("bind", ns::BIND) + .append(Element::builder("jid", ns::BIND).append(bind.jid)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(BindQuery, 12); + assert_size!(BindResponse, 36); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(BindQuery, 24); + assert_size!(BindResponse, 72); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let bind = BindQuery::try_from(elem).unwrap(); + assert_eq!(bind.resource, None); + + let elem: Element = + "Hello™" + .parse() + .unwrap(); + let bind = BindQuery::try_from(elem).unwrap(); + // FIXME: “™” should be resourceprep’d into “TM” here… + //assert_eq!(bind.resource.unwrap(), "HelloTM"); + assert_eq!(bind.resource.unwrap(), "Hello™"); + + let elem: Element = "coucou@linkmauve.fr/HelloTM" + .parse() + .unwrap(); + let bind = BindResponse::try_from(elem).unwrap(); + assert_eq!(bind.jid, FullJid::new("coucou", "linkmauve.fr", "HelloTM")); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_resource() { + let elem: Element = "resource" + .parse() + .unwrap(); + let error = BindQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in resource element."); + + let elem: Element = "resource" + .parse() + .unwrap(); + let error = BindQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in resource element."); + } +} diff --git a/xmpp-parsers/src/blocking.rs b/xmpp-parsers/src/blocking.rs new file mode 100644 index 0000000..246a715 --- /dev/null +++ b/xmpp-parsers/src/blocking.rs @@ -0,0 +1,221 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::convert::TryFrom; + +generate_empty_element!( + /// The element requesting the blocklist, the result iq will contain a + /// [BlocklistResult]. + BlocklistRequest, + "blocklist", + BLOCKING +); + +impl IqGetPayload for BlocklistRequest {} + +macro_rules! generate_blocking_element { + ($(#[$meta:meta])* $elem:ident, $name:tt) => ( + $(#[$meta])* + #[derive(Debug, Clone)] + pub struct $elem { + /// List of JIDs affected by this command. + pub items: Vec, + } + + impl TryFrom for $elem { + type Error = Error; + + fn try_from(elem: Element) -> Result<$elem, Error> { + check_self!(elem, $name, BLOCKING); + check_no_attributes!(elem, $name); + let mut items = vec!(); + for child in elem.children() { + check_self!(child, "item", BLOCKING); + check_no_unknown_attributes!(child, "item", ["jid"]); + check_no_children!(child, "item"); + items.push(get_attr!(child, "jid", Required)); + } + Ok($elem { items }) + } + } + + impl From<$elem> for Element { + fn from(elem: $elem) -> Element { + Element::builder($name, ns::BLOCKING) + .append_all(elem.items.into_iter().map(|jid| { + Element::builder("item", ns::BLOCKING) + .attr("jid", jid) + })) + .build() + } + } + ); +} + +generate_blocking_element!( + /// The element containing the current blocklist, as a reply from + /// [BlocklistRequest]. + BlocklistResult, + "blocklist" +); + +impl IqResultPayload for BlocklistResult {} + +// TODO: Prevent zero elements from being allowed. +generate_blocking_element!( + /// A query to block one or more JIDs. + Block, + "block" +); + +impl IqSetPayload for Block {} + +generate_blocking_element!( + /// A query to unblock one or more JIDs, or all of them. + /// + /// Warning: not putting any JID there means clearing out the blocklist. + Unblock, + "unblock" +); + +impl IqSetPayload for Unblock {} + +generate_empty_element!( + /// The application-specific error condition when a message is blocked. + Blocked, + "blocked", + BLOCKING_ERRORS +); + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(BlocklistRequest, 0); + assert_size!(BlocklistResult, 12); + assert_size!(Block, 12); + assert_size!(Unblock, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(BlocklistRequest, 0); + assert_size!(BlocklistResult, 24); + assert_size!(Block, 24); + assert_size!(Unblock, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let request_elem = elem.clone(); + BlocklistRequest::try_from(request_elem).unwrap(); + + let result_elem = elem.clone(); + let result = BlocklistResult::try_from(result_elem).unwrap(); + assert!(result.items.is_empty()); + + let elem: Element = "".parse().unwrap(); + let block = Block::try_from(elem).unwrap(); + assert!(block.items.is_empty()); + + let elem: Element = "".parse().unwrap(); + let unblock = Unblock::try_from(elem).unwrap(); + assert!(unblock.items.is_empty()); + } + + #[test] + fn test_items() { + let elem: Element = "".parse().unwrap(); + let two_items = vec![ + Jid::Bare(BareJid { + node: Some(String::from("coucou")), + domain: String::from("coucou"), + }), + Jid::Bare(BareJid { + node: None, + domain: String::from("domain"), + }), + ]; + + let result_elem = elem.clone(); + let result = BlocklistResult::try_from(result_elem).unwrap(); + assert_eq!(result.items, two_items); + + let elem: Element = "".parse().unwrap(); + let block = Block::try_from(elem).unwrap(); + assert_eq!(block.items, two_items); + + let elem: Element = "".parse().unwrap(); + let unblock = Unblock::try_from(elem).unwrap(); + assert_eq!(unblock.items, two_items); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let request_elem = elem.clone(); + let error = BlocklistRequest::try_from(request_elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in blocklist element."); + + let result_elem = elem.clone(); + let error = BlocklistResult::try_from(result_elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in blocklist element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Block::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in block element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Unblock::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in unblock element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_non_empty_blocklist_request() { + let elem: Element = "".parse().unwrap(); + let error = BlocklistRequest::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in blocklist element."); + } +} diff --git a/xmpp-parsers/src/bob.rs b/xmpp-parsers/src/bob.rs new file mode 100644 index 0000000..101a749 --- /dev/null +++ b/xmpp-parsers/src/bob.rs @@ -0,0 +1,182 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::hashes::{Algo, Hash}; +use crate::util::error::Error; +use crate::util::helpers::Base64; +use minidom::IntoAttributeValue; +use std::str::FromStr; + +/// A Content-ID, as defined in RFC2111. +/// +/// The text value SHOULD be of the form algo+hash@bob.xmpp.org, this struct +/// enforces that format. +#[derive(Clone, Debug, PartialEq)] +pub struct ContentId { + hash: Hash, +} + +impl FromStr for ContentId { + type Err = Error; + + fn from_str(s: &str) -> Result { + let temp: Vec<_> = s.splitn(2, '@').collect(); + let temp: Vec<_> = match temp[..] { + [lhs, rhs] => { + if rhs != "bob.xmpp.org" { + return Err(Error::ParseError("Wrong domain for cid URI.")); + } + lhs.splitn(2, '+').collect() + } + _ => return Err(Error::ParseError("Missing @ in cid URI.")), + }; + let (algo, hex) = match temp[..] { + [lhs, rhs] => { + let algo = match lhs { + "sha1" => Algo::Sha_1, + "sha256" => Algo::Sha_256, + _ => unimplemented!(), + }; + (algo, rhs) + } + _ => return Err(Error::ParseError("Missing + in cid URI.")), + }; + let hash = Hash::from_hex(algo, hex)?; + Ok(ContentId { hash }) + } +} + +impl IntoAttributeValue for ContentId { + fn into_attribute_value(self) -> Option { + let algo = match self.hash.algo { + Algo::Sha_1 => "sha1", + Algo::Sha_256 => "sha256", + _ => unimplemented!(), + }; + Some(format!("{}+{}@bob.xmpp.org", algo, self.hash.to_hex())) + } +} + +generate_element!( + /// Request for an uncached cid file. + Data, "data", BOB, + attributes: [ + /// The cid in question. + cid: Required = "cid", + + /// How long to cache it (in seconds). + max_age: Option = "max-age", + + /// The MIME type of the data being transmitted. + /// + /// See the [IANA MIME Media Types Registry][1] for a list of + /// registered types, but unregistered or yet-to-be-registered are + /// accepted too. + /// + /// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml + type_: Option = "type" + ], + text: ( + /// The actual data. + data: Base64> + ) +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ContentId, 28); + assert_size!(Data, 60); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ContentId, 56); + assert_size!(Data, 120); + } + + #[test] + fn test_simple() { + let cid: ContentId = "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org" + .parse() + .unwrap(); + assert_eq!(cid.hash.algo, Algo::Sha_1); + assert_eq!( + cid.hash.hash, + b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42" + ); + assert_eq!( + cid.into_attribute_value().unwrap(), + "sha1+8f35fef110ffc5df08d579a50083ff9308fb6242@bob.xmpp.org" + ); + + let elem: Element = "".parse().unwrap(); + let data = Data::try_from(elem).unwrap(); + assert_eq!(data.cid.hash.algo, Algo::Sha_1); + assert_eq!( + data.cid.hash.hash, + b"\x8f\x35\xfe\xf1\x10\xff\xc5\xdf\x08\xd5\x79\xa5\x00\x83\xff\x93\x08\xfb\x62\x42" + ); + assert!(data.max_age.is_none()); + assert!(data.type_.is_none()); + assert!(data.data.is_empty()); + } + + #[test] + fn invalid_cid() { + let error = "Hello world!".parse::().unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Missing @ in cid URI."); + + let error = "Hello world@bob.xmpp.org".parse::().unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Missing + in cid URI."); + + let error = "sha1+1234@coucou.linkmauve.fr" + .parse::() + .unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Wrong domain for cid URI."); + + let error = "sha1+invalid@bob.xmpp.org" + .parse::() + .unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(message.to_string(), "invalid digit found in string"); + } + + #[test] + fn unknown_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Data::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in data element."); + } +} diff --git a/xmpp-parsers/src/bookmarks.rs b/xmpp-parsers/src/bookmarks.rs new file mode 100644 index 0000000..6276d2b --- /dev/null +++ b/xmpp-parsers/src/bookmarks.rs @@ -0,0 +1,121 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use jid::BareJid; + +generate_attribute!( + /// Whether a conference bookmark should be joined automatically. + Autojoin, + "autojoin", + bool +); + +generate_element!( + /// A conference bookmark. + Conference, "conference", BOOKMARKS, + attributes: [ + /// Whether a conference bookmark should be joined automatically. + autojoin: Default = "autojoin", + + /// The JID of the conference. + jid: Required = "jid", + + /// A user-defined name for this conference. + name: Option = "name", + ], + children: [ + /// The nick the user will use to join this conference. + nick: Option = ("nick", BOOKMARKS) => String, + + /// The password required to join this conference. + password: Option = ("password", BOOKMARKS) => String + ] +); + +generate_element!( + /// An URL bookmark. + Url, "url", BOOKMARKS, + attributes: [ + /// A user-defined name for this URL. + name: Option = "name", + + /// The URL of this bookmark. + url: Required = "url", + ] +); + +generate_element!( + /// Container element for multiple bookmarks. + #[derive(Default)] + Storage, "storage", BOOKMARKS, + children: [ + /// Conferences the user has expressed an interest in. + conferences: Vec = ("conference", BOOKMARKS) => Conference, + + /// URLs the user is interested in. + urls: Vec = ("url", BOOKMARKS) => Url + ] +); + +impl Storage { + /// Create an empty bookmarks storage. + pub fn new() -> Storage { + Storage::default() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Conference, 64); + assert_size!(Url, 24); + assert_size!(Storage, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Conference, 128); + assert_size!(Url, 48); + assert_size!(Storage, 48); + } + + #[test] + fn empty() { + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let storage = Storage::try_from(elem).unwrap(); + assert_eq!(storage.conferences.len(), 0); + assert_eq!(storage.urls.len(), 0); + + let elem2 = Element::from(Storage::new()); + assert_eq!(elem1, elem2); + } + + #[test] + fn complete() { + let elem: Element = "Coucousecret".parse().unwrap(); + let storage = Storage::try_from(elem).unwrap(); + assert_eq!(storage.urls.len(), 1); + assert_eq!(storage.urls[0].clone().name.unwrap(), "Example"); + assert_eq!(storage.urls[0].url, "https://example.org/"); + assert_eq!(storage.conferences.len(), 1); + assert_eq!(storage.conferences[0].autojoin, Autojoin::True); + assert_eq!( + storage.conferences[0].jid, + BareJid::new("test-muc", "muc.localhost") + ); + assert_eq!(storage.conferences[0].clone().name.unwrap(), "Test MUC"); + assert_eq!(storage.conferences[0].clone().nick.unwrap(), "Coucou"); + assert_eq!(storage.conferences[0].clone().password.unwrap(), "secret"); + } +} diff --git a/xmpp-parsers/src/bookmarks2.rs b/xmpp-parsers/src/bookmarks2.rs new file mode 100644 index 0000000..bbccd40 --- /dev/null +++ b/xmpp-parsers/src/bookmarks2.rs @@ -0,0 +1,201 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use std::convert::TryFrom; + +generate_attribute!( + /// Whether a conference bookmark should be joined automatically. + Autojoin, + "autojoin", + bool +); + +/// A conference bookmark. +#[derive(Debug, Clone)] +pub struct Conference { + /// Whether a conference bookmark should be joined automatically. + pub autojoin: Autojoin, + + /// A user-defined name for this conference. + pub name: Option, + + /// The nick the user will use to join this conference. + pub nick: Option, + + /// The password required to join this conference. + pub password: Option, + + /// Extensions elements. + pub extensions: Option>, +} + +impl Conference { + /// Create a new conference. + pub fn new() -> Conference { + Conference { + autojoin: Autojoin::False, + name: None, + nick: None, + password: None, + extensions: None, + } + } +} + +impl TryFrom for Conference { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "conference", BOOKMARKS2, "Conference"); + check_no_unknown_attributes!(root, "Conference", ["autojoin", "name"]); + + let mut conference = Conference { + autojoin: get_attr!(root, "autojoin", Default), + name: get_attr!(root, "name", Option), + nick: None, + password: None, + extensions: None, + }; + + for child in root.children().cloned() { + if child.is("extensions", ns::BOOKMARKS2) { + if conference.extensions.is_some() { + return Err(Error::ParseError( + "Conference must not have more than one extensions element.", + )); + } + conference.extensions = Some(child.children().cloned().collect()); + } else if child.is("nick", ns::BOOKMARKS2) { + if conference.nick.is_some() { + return Err(Error::ParseError( + "Conference must not have more than one nick.", + )); + } + check_no_children!(child, "nick"); + check_no_attributes!(child, "nick"); + conference.nick = Some(child.text()); + } else if child.is("password", ns::BOOKMARKS2) { + if conference.password.is_some() { + return Err(Error::ParseError( + "Conference must not have more than one password.", + )); + } + check_no_children!(child, "password"); + check_no_attributes!(child, "password"); + conference.password = Some(child.text()); + } + } + + Ok(conference) + } +} + +impl From for Element { + fn from(conference: Conference) -> Element { + Element::builder("conference", ns::BOOKMARKS2) + .attr("autojoin", conference.autojoin) + .attr("name", conference.name) + .append_all( + conference + .nick + .map(|nick| Element::builder("nick", ns::BOOKMARKS2).append(nick)), + ) + .append_all( + conference + .password + .map(|password| Element::builder("password", ns::BOOKMARKS2).append(password)), + ) + .append_all(match conference.extensions { + Some(extensions) => extensions, + None => vec![], + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::pubsub::pubsub::Item as PubSubItem; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Conference, 52); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Conference, 104); + } + + #[test] + fn simple() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let conference = Conference::try_from(elem).unwrap(); + assert_eq!(conference.autojoin, Autojoin::False); + assert_eq!(conference.name, None); + assert_eq!(conference.nick, None); + assert_eq!(conference.password, None); + + let elem2 = Element::from(Conference::new()); + assert_eq!(elem1, elem2); + } + + #[test] + fn complete() { + let elem: Element = "Coucousecret".parse().unwrap(); + let conference = Conference::try_from(elem).unwrap(); + assert_eq!(conference.autojoin, Autojoin::True); + assert_eq!(conference.name, Some(String::from("Test MUC"))); + assert_eq!(conference.clone().nick.unwrap(), "Coucou"); + assert_eq!(conference.clone().password.unwrap(), "secret"); + assert_eq!(conference.clone().extensions.unwrap().len(), 1); + assert!(conference.clone().extensions.unwrap()[0].is("test", "urn:xmpp:unknown")); + } + + #[test] + fn wrapped() { + let elem: Element = "Coucousecret".parse().unwrap(); + let item = PubSubItem::try_from(elem).unwrap(); + let payload = item.payload.clone().unwrap(); + println!("FOO: payload: {:?}", payload); + // let conference = Conference::try_from(payload).unwrap(); + let conference = Conference::try_from(payload); + println!("FOO: conference: {:?}", conference); + /* + assert_eq!(conference.autojoin, Autojoin::True); + assert_eq!(conference.name, Some(String::from("Test MUC"))); + assert_eq!(conference.clone().nick.unwrap(), "Coucou"); + assert_eq!(conference.clone().password.unwrap(), "secret"); + + let elem: Element = "Coucousecret".parse().unwrap(); + let mut items = match PubSubEvent::try_from(elem) { + Ok(PubSubEvent::PublishedItems { node, items }) => { + assert_eq!(&node.0, ns::BOOKMARKS2); + items + } + _ => panic!(), + }; + assert_eq!(items.len(), 1); + let item = items.pop().unwrap(); + let payload = item.payload.clone().unwrap(); + let conference = Conference::try_from(payload).unwrap(); + assert_eq!(conference.autojoin, Autojoin::True); + assert_eq!(conference.name, Some(String::from("Test MUC"))); + assert_eq!(conference.clone().nick.unwrap(), "Coucou"); + assert_eq!(conference.clone().password.unwrap(), "secret"); + */ + } +} diff --git a/xmpp-parsers/src/caps.rs b/xmpp-parsers/src/caps.rs new file mode 100644 index 0000000..24f129a --- /dev/null +++ b/xmpp-parsers/src/caps.rs @@ -0,0 +1,342 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; +use crate::hashes::{Algo, Hash}; +use crate::ns; +use crate::presence::PresencePayload; +use crate::util::error::Error; +use crate::Element; +use blake2::VarBlake2b; +use digest::{Digest, Update, VariableOutput}; +use sha1::Sha1; +use sha2::{Sha256, Sha512}; +use sha3::{Sha3_256, Sha3_512}; +use std::convert::TryFrom; + +/// Represents a capability hash for a given client. +#[derive(Debug, Clone)] +pub struct Caps { + /// Deprecated list of additional feature bundles. + pub ext: Option, + + /// A URI identifying an XMPP application. + pub node: String, + + /// The hash of that application’s + /// [disco#info](../disco/struct.DiscoInfoResult.html). + /// + /// Warning: This protocol is insecure, you may want to switch to + /// [ecaps2](../ecaps2/index.html) instead, see [this + /// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). + pub hash: Hash, +} + +impl PresencePayload for Caps {} + +impl TryFrom for Caps { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "c", CAPS, "caps"); + check_no_children!(elem, "caps"); + check_no_unknown_attributes!(elem, "caps", ["hash", "ver", "ext", "node"]); + let ver: String = get_attr!(elem, "ver", Required); + let hash = Hash { + algo: get_attr!(elem, "hash", Required), + hash: base64::decode(&ver)?, + }; + Ok(Caps { + ext: get_attr!(elem, "ext", Option), + node: get_attr!(elem, "node", Required), + hash, + }) + } +} + +impl From for Element { + fn from(caps: Caps) -> Element { + Element::builder("c", ns::CAPS) + .attr("ext", caps.ext) + .attr("hash", caps.hash.algo) + .attr("node", caps.node) + .attr("ver", base64::encode(&caps.hash.hash)) + .build() + } +} + +impl Caps { + /// Create a Caps element from its node and hash. + pub fn new>(node: N, hash: Hash) -> Caps { + Caps { + ext: None, + node: node.into(), + hash, + } + } +} + +fn compute_item(field: &str) -> Vec { + let mut bytes = field.as_bytes().to_vec(); + bytes.push(b'<'); + bytes +} + +fn compute_items Vec>(things: &[T], encode: F) -> Vec { + let mut string: Vec = vec![]; + let mut accumulator: Vec> = vec![]; + for thing in things { + let bytes = encode(thing); + accumulator.push(bytes); + } + // This works using the expected i;octet collation. + accumulator.sort(); + for mut bytes in accumulator { + string.append(&mut bytes); + } + string +} + +fn compute_features(features: &[Feature]) -> Vec { + compute_items(features, |feature| compute_item(&feature.var)) +} + +fn compute_identities(identities: &[Identity]) -> Vec { + compute_items(identities, |identity| { + let lang = identity.lang.clone().unwrap_or_default(); + let name = identity.name.clone().unwrap_or_default(); + let string = format!("{}/{}/{}/{}", identity.category, identity.type_, lang, name); + let bytes = string.as_bytes(); + let mut vec = Vec::with_capacity(bytes.len()); + vec.extend_from_slice(bytes); + vec.push(b'<'); + vec + }) +} + +fn compute_extensions(extensions: &[DataForm]) -> Vec { + compute_items(extensions, |extension| { + let mut bytes = vec![]; + // TODO: maybe handle the error case? + if let Some(ref form_type) = extension.form_type { + bytes.extend_from_slice(form_type.as_bytes()); + } + bytes.push(b'<'); + for field in extension.fields.clone() { + if field.var == "FORM_TYPE" { + continue; + } + bytes.append(&mut compute_item(&field.var)); + bytes.append(&mut compute_items(&field.values, |value| { + compute_item(value) + })); + } + bytes + }) +} + +/// Applies the caps algorithm on the provided disco#info result, to generate +/// the hash input. +/// +/// Warning: This protocol is insecure, you may want to switch to +/// [ecaps2](../ecaps2/index.html) instead, see [this +/// email](https://mail.jabber.org/pipermail/security/2009-July/000812.html). +pub fn compute_disco(disco: &DiscoInfoResult) -> Vec { + let identities_string = compute_identities(&disco.identities); + let features_string = compute_features(&disco.features); + let extensions_string = compute_extensions(&disco.extensions); + + let mut final_string = vec![]; + final_string.extend(identities_string); + final_string.extend(features_string); + final_string.extend(extensions_string); + final_string +} + +fn get_hash_vec(hash: &[u8]) -> Vec { + hash.to_vec() +} + +/// Hashes the result of [compute_disco()] with one of the supported [hash +/// algorithms](../hashes/enum.Algo.html). +pub fn hash_caps(data: &[u8], algo: Algo) -> Result { + Ok(Hash { + hash: match algo { + Algo::Sha_1 => { + let hash = Sha1::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha_256 => { + let hash = Sha256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha_512 => { + let hash = Sha512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_256 => { + let hash = Sha3_256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_512 => { + let hash = Sha3_512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Blake2b_256 => { + let mut hasher = VarBlake2b::new(32).unwrap(); + hasher.update(data); + let mut vec = Vec::with_capacity(32); + hasher.finalize_variable(|slice| vec.extend_from_slice(slice)); + vec + } + Algo::Blake2b_512 => { + let mut hasher = VarBlake2b::new(64).unwrap(); + hasher.update(data); + let mut vec = Vec::with_capacity(64); + hasher.finalize_variable(|slice| vec.extend_from_slice(slice)); + vec + } + Algo::Unknown(algo) => return Err(format!("Unknown algorithm: {}.", algo)), + }, + algo, + }) +} + +/// Helper function to create the query for the disco#info corresponding to a +/// caps hash. +pub fn query_caps(caps: Caps) -> DiscoInfoQuery { + DiscoInfoQuery { + node: Some(format!("{}#{}", caps.node, base64::encode(&caps.hash.hash))), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::caps; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Caps, 52); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Caps, 104); + } + + #[test] + fn test_parse() { + let elem: Element = "".parse().unwrap(); + let caps = Caps::try_from(elem).unwrap(); + assert_eq!(caps.node, String::from("coucou")); + assert_eq!(caps.hash.algo, Algo::Sha_256); + assert_eq!( + caps.hash.hash, + base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap() + ); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_child() { + let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=".parse().unwrap(); + let error = Caps::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in caps element."); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let caps = caps::compute_disco(&disco); + assert_eq!(caps.len(), 50); + } + + #[test] + fn test_xep_5_2() { + let elem: Element = r#" + + + + + + + +"# + .parse() + .unwrap(); + + let data = b"client/pc//Exodus 0.9.1 + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + ipv4 + ipv6 + + + Mac + + + 10.5.1 + + + Psi + + + 0.11 + + + +"# + .parse() + .unwrap(); + let expected = b"client/pc/el/\xce\xa8 0.11 = ("forwarded", FORWARD) => Forwarded + ] +); + +impl MessagePayload for Received {} + +generate_element!( + /// Wrapper for a message sent from another resource. + Sent, "sent", CARBONS, + + children: [ + /// Wrapper for the enclosed message. + forwarded: Required = ("forwarded", FORWARD) => Forwarded + ] +); + +impl MessagePayload for Sent {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Enable, 0); + assert_size!(Disable, 0); + assert_size!(Private, 0); + assert_size!(Received, 212); + assert_size!(Sent, 212); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Enable, 0); + assert_size!(Disable, 0); + assert_size!(Private, 0); + assert_size!(Received, 408); + assert_size!(Sent, 408); + } + + #[test] + fn empty_elements() { + let elem: Element = "".parse().unwrap(); + Enable::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Disable::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Private::try_from(elem).unwrap(); + } + + #[test] + fn forwarded_elements() { + let elem: Element = " + + + +" + .parse() + .unwrap(); + let received = Received::try_from(elem).unwrap(); + assert!(received.forwarded.stanza.is_some()); + + let elem: Element = " + + + +" + .parse() + .unwrap(); + let sent = Sent::try_from(elem).unwrap(); + assert!(sent.forwarded.stanza.is_some()); + } + + #[test] + fn test_serialize_received() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + let forwarded = Forwarded::try_from(elem).unwrap(); + + let received = Received { + forwarded: forwarded, + }; + + let serialized: Element = received.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_sent() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + let forwarded = Forwarded::try_from(elem).unwrap(); + + let sent = Sent { + forwarded: forwarded, + }; + + let serialized: Element = sent.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/cert_management.rs b/xmpp-parsers/src/cert_management.rs new file mode 100644 index 0000000..d93ebb3 --- /dev/null +++ b/xmpp-parsers/src/cert_management.rs @@ -0,0 +1,297 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::util::helpers::Base64; + +generate_elem_id!( + /// The name of a certificate. + Name, + "name", + SASL_CERT +); + +generate_element!( + /// An X.509 certificate. + Cert, "x509cert", SASL_CERT, + text: ( + /// The BER X.509 data. + data: Base64> + ) +); + +generate_element!( + /// For the client to upload an X.509 certificate. + Append, "append", SASL_CERT, + children: [ + /// The name of this certificate. + name: Required = ("name", SASL_CERT) => Name, + + /// The X.509 certificate to set. + cert: Required = ("x509cert", SASL_CERT) => Cert, + + /// This client is forbidden from managing certificates. + no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool + ] +); + +impl IqSetPayload for Append {} + +generate_empty_element!( + /// Client requests the current list of X.509 certificates. + ListCertsQuery, + "items", + SASL_CERT +); + +impl IqGetPayload for ListCertsQuery {} + +generate_elem_id!( + /// One resource currently using a certificate. + Resource, + "resource", + SASL_CERT +); + +generate_element!( + /// A list of resources currently using this certificate. + Users, "users", SASL_CERT, + children: [ + /// Resources currently using this certificate. + resources: Vec = ("resource", SASL_CERT) => Resource + ] +); + +generate_element!( + /// An X.509 certificate being set for this user. + Item, "item", SASL_CERT, + children: [ + /// The name of this certificate. + name: Required = ("name", SASL_CERT) => Name, + + /// The X.509 certificate to set. + cert: Required = ("x509cert", SASL_CERT) => Cert, + + /// This client is forbidden from managing certificates. + no_cert_management: Present<_> = ("no-cert-management", SASL_CERT) => bool, + + /// List of resources currently using this certificate. + users: Option = ("users", SASL_CERT) => Users + ] +); + +generate_element!( + /// Server answers with the current list of X.509 certificates. + ListCertsResponse, "items", SASL_CERT, + children: [ + /// List of certificates. + items: Vec = ("item", SASL_CERT) => Item + ] +); + +impl IqResultPayload for ListCertsResponse {} + +generate_element!( + /// Client disables an X.509 certificate. + Disable, "disable", SASL_CERT, + children: [ + /// Name of the certificate to disable. + name: Required = ("name", SASL_CERT) => Name + ] +); + +impl IqSetPayload for Disable {} + +generate_element!( + /// Client revokes an X.509 certificate. + Revoke, "revoke", SASL_CERT, + children: [ + /// Name of the certificate to revoke. + name: Required = ("name", SASL_CERT) => Name + ] +); + +impl IqSetPayload for Revoke {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use crate::Element; + use std::convert::TryFrom; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Append, 28); + assert_size!(Disable, 12); + assert_size!(Revoke, 12); + assert_size!(ListCertsQuery, 0); + assert_size!(ListCertsResponse, 12); + assert_size!(Item, 40); + assert_size!(Resource, 12); + assert_size!(Users, 12); + assert_size!(Cert, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Append, 56); + assert_size!(Disable, 24); + assert_size!(Revoke, 24); + assert_size!(ListCertsQuery, 0); + assert_size!(ListCertsResponse, 24); + assert_size!(Item, 80); + assert_size!(Resource, 24); + assert_size!(Users, 24); + assert_size!(Cert, 24); + } + + #[test] + fn simple() { + let elem: Element = "Mobile ClientAAAA".parse().unwrap(); + let append = Append::try_from(elem).unwrap(); + assert_eq!(append.name.0, "Mobile Client"); + assert_eq!(append.cert.data, b"\0\0\0"); + + let elem: Element = + "Mobile Client" + .parse() + .unwrap(); + let disable = Disable::try_from(elem).unwrap(); + assert_eq!(disable.name.0, "Mobile Client"); + + let elem: Element = + "Mobile Client" + .parse() + .unwrap(); + let revoke = Revoke::try_from(elem).unwrap(); + assert_eq!(revoke.name.0, "Mobile Client"); + } + + #[test] + fn list() { + let elem: Element = r#" + + + Mobile Client + AAAA + + Phone + + + + Laptop + BBBB + + "# + .parse() + .unwrap(); + let mut list = ListCertsResponse::try_from(elem).unwrap(); + assert_eq!(list.items.len(), 2); + + let item = list.items.pop().unwrap(); + assert_eq!(item.name.0, "Laptop"); + assert_eq!(item.cert.data, [4, 16, 65]); + assert!(item.users.is_none()); + + let item = list.items.pop().unwrap(); + assert_eq!(item.name.0, "Mobile Client"); + assert_eq!(item.cert.data, b"\0\0\0"); + assert_eq!(item.users.unwrap().resources.len(), 1); + } + + #[test] + fn test_serialise() { + let append = Append { + name: Name::from_str("Mobile Client").unwrap(), + cert: Cert { + data: b"\0\0\0".to_vec(), + }, + no_cert_management: false, + }; + let elem: Element = append.into(); + assert!(elem.is("append", ns::SASL_CERT)); + + let disable = Disable { + name: Name::from_str("Mobile Client").unwrap(), + }; + let elem: Element = disable.into(); + assert!(elem.is("disable", ns::SASL_CERT)); + let elem = elem.children().cloned().collect::>().pop().unwrap(); + assert!(elem.is("name", ns::SASL_CERT)); + assert_eq!(elem.text(), "Mobile Client"); + } + + #[test] + fn test_serialize_item() { + let reference: Element = "Mobile ClientAAAA" + .parse() + .unwrap(); + + let item = Item { + name: Name::from_str("Mobile Client").unwrap(), + cert: Cert { + data: b"\0\0\0".to_vec(), + }, + no_cert_management: false, + users: None, + }; + + let serialized: Element = item.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_append() { + let reference: Element = "Mobile ClientAAAA" + .parse() + .unwrap(); + + let append = Append { + name: Name::from_str("Mobile Client").unwrap(), + cert: Cert { + data: b"\0\0\0".to_vec(), + }, + no_cert_management: false, + }; + + let serialized: Element = append.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_disable() { + let reference: Element = + "Mobile Client" + .parse() + .unwrap(); + + let disable = Disable { + name: Name::from_str("Mobile Client").unwrap(), + }; + + let serialized: Element = disable.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_revoke() { + let reference: Element = + "Mobile Client" + .parse() + .unwrap(); + + let revoke = Revoke { + name: Name::from_str("Mobile Client").unwrap(), + }; + + let serialized: Element = revoke.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/chatstates.rs b/xmpp-parsers/src/chatstates.rs new file mode 100644 index 0000000..5e98112 --- /dev/null +++ b/xmpp-parsers/src/chatstates.rs @@ -0,0 +1,100 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; + +generate_element_enum!( + /// Enum representing chatstate elements part of the + /// `http://jabber.org/protocol/chatstates` namespace. + ChatState, "chatstate", CHATSTATES, { + /// `` + Active => "active", + + /// `` + Composing => "composing", + + /// `` + Gone => "gone", + + /// `` + Inactive => "inactive", + + /// `` + Paused => "paused", + } +); + +impl MessagePayload for ChatState {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(ChatState, 1); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + ChatState::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ChatState::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a chatstate element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ChatState::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in chatstate element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ChatState::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in chatstate element."); + } + + #[test] + fn test_serialise() { + let chatstate = ChatState::Active; + let elem: Element = chatstate.into(); + assert!(elem.is("active", ns::CHATSTATES)); + } +} diff --git a/xmpp-parsers/src/component.rs b/xmpp-parsers/src/component.rs new file mode 100644 index 0000000..b8120ff --- /dev/null +++ b/xmpp-parsers/src/component.rs @@ -0,0 +1,87 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::util::helpers::PlainText; +use digest::Digest; +use sha1::Sha1; + +generate_element!( + /// The main authentication mechanism for components. + #[derive(Default)] + Handshake, "handshake", COMPONENT, + text: ( + /// If Some, contains the hex-encoded SHA-1 of the concatenation of the + /// stream id and the password, and is used to authenticate against the + /// server. + /// + /// If None, it is the successful reply from the server, the stream is now + /// fully established and both sides can now exchange stanzas. + data: PlainText> + ) +); + +impl Handshake { + /// Creates a successful reply from a server. + pub fn new() -> Handshake { + Handshake::default() + } + + /// Creates an authentication request from the component. + pub fn from_password_and_stream_id(password: &str, stream_id: &str) -> Handshake { + let input = String::from(stream_id) + password; + let hash = Sha1::digest(input.as_bytes()); + let content = format!("{:x}", hash); + Handshake { + data: Some(content), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Handshake, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Handshake, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let handshake = Handshake::try_from(elem).unwrap(); + assert_eq!(handshake.data, None); + + let elem: Element = "Coucou" + .parse() + .unwrap(); + let handshake = Handshake::try_from(elem).unwrap(); + assert_eq!(handshake.data, Some(String::from("Coucou"))); + } + + #[test] + fn test_constructors() { + let handshake = Handshake::new(); + assert_eq!(handshake.data, None); + + let handshake = Handshake::from_password_and_stream_id("123456", "sid"); + assert_eq!( + handshake.data, + Some(String::from("9accec263ab84a43c6037ccf7cd48cb1d3f6df8e")) + ); + } +} diff --git a/xmpp-parsers/src/csi.rs b/xmpp-parsers/src/csi.rs new file mode 100644 index 0000000..bfd44f8 --- /dev/null +++ b/xmpp-parsers/src/csi.rs @@ -0,0 +1,65 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +generate_empty_element!( + /// Stream:feature sent by the server to advertise it supports CSI. + Feature, + "csi", + CSI +); + +generate_empty_element!( + /// Client indicates it is inactive. + Inactive, + "inactive", + CSI +); + +generate_empty_element!( + /// Client indicates it is active again. + Active, + "active", + CSI +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(Feature, 0); + assert_size!(Inactive, 0); + assert_size!(Active, 0); + } + + #[test] + fn parsing() { + let elem: Element = "".parse().unwrap(); + Feature::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Inactive::try_from(elem).unwrap(); + + let elem: Element = "".parse().unwrap(); + Active::try_from(elem).unwrap(); + } + + #[test] + fn serialising() { + let elem: Element = Feature.into(); + assert!(elem.is("csi", ns::CSI)); + + let elem: Element = Inactive.into(); + assert!(elem.is("inactive", ns::CSI)); + + let elem: Element = Active.into(); + assert!(elem.is("active", ns::CSI)); + } +} diff --git a/xmpp-parsers/src/data_forms.rs b/xmpp-parsers/src/data_forms.rs new file mode 100644 index 0000000..596a317 --- /dev/null +++ b/xmpp-parsers/src/data_forms.rs @@ -0,0 +1,385 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::media_element::MediaElement; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use std::convert::TryFrom; + +generate_element!( + /// Represents one of the possible values for a list- field. + Option_, "option", DATA_FORMS, + attributes: [ + /// The optional label to be displayed to the user for this option. + label: Option = "label" + ], + children: [ + /// The value returned to the server when selecting this option. + value: Required = ("value", DATA_FORMS) => String + ] +); + +generate_attribute!( + /// The type of a [field](struct.Field.html) element. + FieldType, "type", { + /// This field can only take the values "0" or "false" for a false + /// value, and "1" or "true" for a true value. + Boolean => "boolean", + + /// This field describes data, it must not be sent back to the + /// requester. + Fixed => "fixed", + + /// This field is hidden, it should not be displayed to the user but + /// should be sent back to the requester. + Hidden => "hidden", + + /// This field accepts one or more [JIDs](../../jid/struct.Jid.html). + /// A client may want to let the user autocomplete them based on their + /// contacts list for instance. + JidMulti => "jid-multi", + + /// This field accepts one [JID](../../jid/struct.Jid.html). A client + /// may want to let the user autocomplete it based on their contacts + /// list for instance. + JidSingle => "jid-single", + + /// This field accepts one or more values from the list provided as + /// [options](struct.Option_.html). + ListMulti => "list-multi", + + /// This field accepts one value from the list provided as + /// [options](struct.Option_.html). + ListSingle => "list-single", + + /// This field accepts one or more free form text lines. + TextMulti => "text-multi", + + /// This field accepts one free form password, a client should hide it + /// in its user interface. + TextPrivate => "text-private", + + /// This field accepts one free form text line. + TextSingle => "text-single", + }, Default = TextSingle +); + +/// Represents a field in a [data form](struct.DataForm.html). +#[derive(Debug, Clone, PartialEq)] +pub struct Field { + /// The unique identifier for this field, in the form. + pub var: String, + + /// The type of this field. + pub type_: FieldType, + + /// The label to be possibly displayed to the user for this field. + pub label: Option, + + /// The form will be rejected if this field isn’t present. + pub required: bool, + + /// A list of allowed values. + pub options: Vec, + + /// The values provided for this field. + pub values: Vec, + + /// A list of media related to this field. + pub media: Vec, +} + +impl Field { + fn is_list(&self) -> bool { + self.type_ == FieldType::ListSingle || self.type_ == FieldType::ListMulti + } +} + +impl TryFrom for Field { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "field", DATA_FORMS); + check_no_unknown_attributes!(elem, "field", ["label", "type", "var"]); + let mut field = Field { + var: get_attr!(elem, "var", Required), + type_: get_attr!(elem, "type", Default), + label: get_attr!(elem, "label", Option), + required: false, + options: vec![], + values: vec![], + media: vec![], + }; + for element in elem.children() { + if element.is("value", ns::DATA_FORMS) { + check_no_children!(element, "value"); + check_no_attributes!(element, "value"); + field.values.push(element.text()); + } else if element.is("required", ns::DATA_FORMS) { + if field.required { + return Err(Error::ParseError("More than one required element.")); + } + check_no_children!(element, "required"); + check_no_attributes!(element, "required"); + field.required = true; + } else if element.is("option", ns::DATA_FORMS) { + if !field.is_list() { + return Err(Error::ParseError("Option element found in non-list field.")); + } + let option = Option_::try_from(element.clone())?; + field.options.push(option); + } else if element.is("media", ns::MEDIA_ELEMENT) { + let media_element = MediaElement::try_from(element.clone())?; + field.media.push(media_element); + } else { + return Err(Error::ParseError( + "Field child isn’t a value, option or media element.", + )); + } + } + Ok(field) + } +} + +impl From for Element { + fn from(field: Field) -> Element { + Element::builder("field", ns::DATA_FORMS) + .attr("var", field.var) + .attr("type", field.type_) + .attr("label", field.label) + .append_all(if field.required { + Some(Element::builder("required", ns::DATA_FORMS)) + } else { + None + }) + .append_all(field.options.iter().cloned().map(Element::from)) + .append_all( + field + .values + .into_iter() + .map(|value| Element::builder("value", ns::DATA_FORMS).append(value)), + ) + .append_all(field.media.iter().cloned().map(Element::from)) + .build() + } +} + +generate_attribute!( + /// Represents the type of a [data form](struct.DataForm.html). + DataFormType, "type", { + /// This is a cancel request for a prior type="form" data form. + Cancel => "cancel", + + /// This is a request for the recipient to fill this form and send it + /// back as type="submit". + Form => "form", + + /// This is a result form, which contains what the requester asked for. + Result_ => "result", + + /// This is a complete response to a form received before. + Submit => "submit", + } +); + +/// This is a form to be sent to another entity for filling. +#[derive(Debug, Clone, PartialEq)] +pub struct DataForm { + /// The type of this form, telling the other party which action to execute. + pub type_: DataFormType, + + /// An easy accessor for the FORM_TYPE of this form, see + /// [XEP-0068](https://xmpp.org/extensions/xep-0068.html) for more + /// information. + pub form_type: Option, + + /// The title of this form. + pub title: Option, + + /// The instructions given with this form. + pub instructions: Option, + + /// A list of fields comprising this form. + pub fields: Vec, +} + +impl TryFrom for DataForm { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "x", DATA_FORMS); + check_no_unknown_attributes!(elem, "x", ["type"]); + let type_ = get_attr!(elem, "type", Required); + let mut form = DataForm { + type_, + form_type: None, + title: None, + instructions: None, + fields: vec![], + }; + for child in elem.children() { + if child.is("title", ns::DATA_FORMS) { + if form.title.is_some() { + return Err(Error::ParseError("More than one title in form element.")); + } + check_no_children!(child, "title"); + check_no_attributes!(child, "title"); + form.title = Some(child.text()); + } else if child.is("instructions", ns::DATA_FORMS) { + if form.instructions.is_some() { + return Err(Error::ParseError( + "More than one instructions in form element.", + )); + } + check_no_children!(child, "instructions"); + check_no_attributes!(child, "instructions"); + form.instructions = Some(child.text()); + } else if child.is("field", ns::DATA_FORMS) { + let field = Field::try_from(child.clone())?; + if field.var == "FORM_TYPE" { + let mut field = field; + if form.form_type.is_some() { + return Err(Error::ParseError("More than one FORM_TYPE in a data form.")); + } + if field.type_ != FieldType::Hidden { + return Err(Error::ParseError("Invalid field type for FORM_TYPE.")); + } + if field.values.len() != 1 { + return Err(Error::ParseError("Wrong number of values in FORM_TYPE.")); + } + form.form_type = field.values.pop(); + } else { + form.fields.push(field); + } + } else { + return Err(Error::ParseError("Unknown child in data form element.")); + } + } + Ok(form) + } +} + +impl From for Element { + fn from(form: DataForm) -> Element { + Element::builder("x", ns::DATA_FORMS) + .attr("type", form.type_) + .append_all( + form.title + .map(|title| Element::builder("title", ns::DATA_FORMS).append(title)), + ) + .append_all( + form.instructions + .map(|text| Element::builder("instructions", ns::DATA_FORMS).append(text)), + ) + .append_all(form.form_type.map(|form_type| { + Element::builder("field", ns::DATA_FORMS) + .attr("var", "FORM_TYPE") + .attr("type", "hidden") + .append(Element::builder("value", ns::DATA_FORMS).append(form_type)) + })) + .append_all(form.fields.iter().cloned().map(Element::from)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Option_, 24); + assert_size!(FieldType, 1); + assert_size!(Field, 64); + assert_size!(DataFormType, 1); + assert_size!(DataForm, 52); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Option_, 48); + assert_size!(FieldType, 1); + assert_size!(Field, 128); + assert_size!(DataFormType, 1); + assert_size!(DataForm, 104); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let form = DataForm::try_from(elem).unwrap(); + assert_eq!(form.type_, DataFormType::Result_); + assert!(form.form_type.is_none()); + assert!(form.fields.is_empty()); + } + + #[test] + fn test_invalid() { + let elem: Element = "".parse().unwrap(); + let error = DataForm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + let elem: Element = "".parse().unwrap(); + let error = DataForm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'type' attribute."); + } + + #[test] + fn test_wrong_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = DataForm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in data form element."); + } + + #[test] + fn option() { + let elem: Element = + "" + .parse() + .unwrap(); + let option = Option_::try_from(elem).unwrap(); + assert_eq!(&option.label.unwrap(), "Coucou !"); + assert_eq!(&option.value, "coucou"); + + let elem: Element = "".parse().unwrap(); + let error = Option_::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Element option must not have more than one value child." + ); + } +} diff --git a/xmpp-parsers/src/date.rs b/xmpp-parsers/src/date.rs new file mode 100644 index 0000000..ee3080b --- /dev/null +++ b/xmpp-parsers/src/date.rs @@ -0,0 +1,137 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::util::error::Error; +use chrono::{DateTime as ChronoDateTime, FixedOffset}; +use minidom::{IntoAttributeValue, Node}; +use std::str::FromStr; + +/// Implements the DateTime profile of XEP-0082, which represents a +/// non-recurring moment in time, with an accuracy of seconds or fraction of +/// seconds, and includes a timezone. +#[derive(Debug, Clone, PartialEq)] +pub struct DateTime(pub ChronoDateTime); + +impl DateTime { + /// Retrieves the associated timezone. + pub fn timezone(&self) -> FixedOffset { + self.0.timezone() + } + + /// Returns a new `DateTime` with a different timezone. + pub fn with_timezone(&self, tz: FixedOffset) -> DateTime { + DateTime(self.0.with_timezone(&tz)) + } + + /// Formats this `DateTime` with the specified format string. + pub fn format(&self, fmt: &str) -> String { + format!("{}", self.0.format(fmt)) + } +} + +impl FromStr for DateTime { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?)) + } +} + +impl IntoAttributeValue for DateTime { + fn into_attribute_value(self) -> Option { + Some(self.0.to_rfc3339()) + } +} + +impl Into for DateTime { + fn into(self) -> Node { + Node::Text(self.0.to_rfc3339()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::{Datelike, Timelike}; + + // DateTime’s size doesn’t depend on the architecture. + #[test] + fn test_size() { + assert_size!(DateTime, 16); + } + + #[test] + fn test_simple() { + let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap(); + assert_eq!(date.0.year(), 2002); + assert_eq!(date.0.month(), 9); + assert_eq!(date.0.day(), 10); + assert_eq!(date.0.hour(), 23); + assert_eq!(date.0.minute(), 08); + assert_eq!(date.0.second(), 25); + assert_eq!(date.0.nanosecond(), 0); + assert_eq!(date.0.timezone(), FixedOffset::east(0)); + } + + #[test] + fn test_invalid_date() { + // There is no thirteenth month. + let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input is out of range"); + + // Timezone ≥24:00 aren’t allowed. + let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input is out of range"); + + // Timezone without the : separator aren’t allowed. + let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input contains invalid characters"); + + // No seconds, error message could be improved. + let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input contains invalid characters"); + + // TODO: maybe we’ll want to support this one, as per XEP-0082 §4. + let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input contains invalid characters"); + + // No timezone. + let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "premature end of input"); + } + + #[test] + fn test_serialise() { + let date = + DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap()); + let attr = date.into_attribute_value(); + assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00"))); + } +} diff --git a/xmpp-parsers/src/delay.rs b/xmpp-parsers/src/delay.rs new file mode 100644 index 0000000..897b98c --- /dev/null +++ b/xmpp-parsers/src/delay.rs @@ -0,0 +1,119 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::date::DateTime; +use crate::message::MessagePayload; +use crate::presence::PresencePayload; +use crate::util::helpers::PlainText; +use jid::Jid; + +generate_element!( + /// Notes when and by whom a message got stored for later delivery. + Delay, "delay", DELAY, + attributes: [ + /// The entity which delayed this message. + from: Option = "from", + + /// The time at which this message got stored. + stamp: Required = "stamp" + ], + text: ( + /// The optional reason this message got delayed. + data: PlainText> + ) +); + +impl MessagePayload for Delay {} +impl PresencePayload for Delay {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use jid::BareJid; + use std::convert::TryFrom; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Delay, 68); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Delay, 120); + } + + #[test] + fn test_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let delay = Delay::try_from(elem).unwrap(); + assert_eq!(delay.from.unwrap(), BareJid::domain("capulet.com")); + assert_eq!( + delay.stamp, + DateTime::from_str("2002-09-10T23:08:25Z").unwrap() + ); + assert_eq!(delay.data, None); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Delay::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a delay element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Delay::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in delay element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let delay = Delay { + from: None, + stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(), + data: None, + }; + let elem2 = delay.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_serialise_data() { + let elem: Element = "Reason".parse().unwrap(); + let delay = Delay { + from: Some(Jid::Bare(BareJid::new("juliet", "example.org"))), + stamp: DateTime::from_str("2002-09-10T23:08:25Z").unwrap(), + data: Some(String::from("Reason")), + }; + let elem2 = delay.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/disco.rs b/xmpp-parsers/src/disco.rs new file mode 100644 index 0000000..1bdb7a0 --- /dev/null +++ b/xmpp-parsers/src/disco.rs @@ -0,0 +1,452 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::{DataForm, DataFormType}; +use crate::iq::{IqGetPayload, IqResultPayload}; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::convert::TryFrom; + +generate_element!( +/// Structure representing a `` element. +/// +/// It should only be used in an ``, as it can only represent +/// the request, and not a result. +DiscoInfoQuery, "query", DISCO_INFO, +attributes: [ + /// Node on which we are doing the discovery. + node: Option = "node", +]); + +impl IqGetPayload for DiscoInfoQuery {} + +generate_element!( +#[derive(Eq, Hash)] +/// Structure representing a `` element. +Feature, "feature", DISCO_INFO, +attributes: [ + /// Namespace of the feature we want to represent. + var: Required = "var", +]); + +impl Feature { + /// Create a new `` with the according `@var`. + pub fn new>(var: S) -> Feature { + Feature { var: var.into() } + } +} + +generate_element!( + /// Structure representing an `` element. + Identity, "identity", DISCO_INFO, + attributes: [ + /// Category of this identity. + // TODO: use an enum here. + category: RequiredNonEmpty = "category", + + /// Type of this identity. + // TODO: use an enum here. + type_: RequiredNonEmpty = "type", + + /// Lang of the name of this identity. + lang: Option = "xml:lang", + + /// Name of this identity. + name: Option = "name", + ] +); + +impl Identity { + /// Create a new ``. + pub fn new(category: C, type_: T, lang: L, name: N) -> Identity + where + C: Into, + T: Into, + L: Into, + N: Into, + { + Identity { + category: category.into(), + type_: type_.into(), + lang: Some(lang.into()), + name: Some(name.into()), + } + } + + /// Create a new `` without a name. + pub fn new_anonymous(category: C, type_: T) -> Identity + where + C: Into, + T: Into, + { + Identity { + category: category.into(), + type_: type_.into(), + lang: None, + name: None, + } + } +} + +/// Structure representing a `` element. +/// +/// It should only be used in an ``, as it can only +/// represent the result, and not a request. +#[derive(Debug, Clone)] +pub struct DiscoInfoResult { + /// Node on which we have done this discovery. + pub node: Option, + + /// List of identities exposed by this entity. + pub identities: Vec, + + /// List of features supported by this entity. + pub features: Vec, + + /// List of extensions reported by this entity. + pub extensions: Vec, +} + +impl IqResultPayload for DiscoInfoResult {} + +impl TryFrom for DiscoInfoResult { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "query", DISCO_INFO, "disco#info result"); + check_no_unknown_attributes!(elem, "disco#info result", ["node"]); + + let mut result = DiscoInfoResult { + node: get_attr!(elem, "node", Option), + identities: vec![], + features: vec![], + extensions: vec![], + }; + + for child in elem.children() { + if child.is("identity", ns::DISCO_INFO) { + let identity = Identity::try_from(child.clone())?; + result.identities.push(identity); + } else if child.is("feature", ns::DISCO_INFO) { + let feature = Feature::try_from(child.clone())?; + result.features.push(feature); + } else if child.is("x", ns::DATA_FORMS) { + let data_form = DataForm::try_from(child.clone())?; + if data_form.type_ != DataFormType::Result_ { + return Err(Error::ParseError( + "Data form must have a 'result' type in disco#info.", + )); + } + if data_form.form_type.is_none() { + return Err(Error::ParseError("Data form found without a FORM_TYPE.")); + } + result.extensions.push(data_form); + } else { + return Err(Error::ParseError("Unknown element in disco#info.")); + } + } + + if result.identities.is_empty() { + return Err(Error::ParseError( + "There must be at least one identity in disco#info.", + )); + } + if result.features.is_empty() { + return Err(Error::ParseError( + "There must be at least one feature in disco#info.", + )); + } + if !result.features.contains(&Feature { + var: ns::DISCO_INFO.to_owned(), + }) { + return Err(Error::ParseError( + "disco#info feature not present in disco#info.", + )); + } + + Ok(result) + } +} + +impl From for Element { + fn from(disco: DiscoInfoResult) -> Element { + Element::builder("query", ns::DISCO_INFO) + .attr("node", disco.node) + .append_all(disco.identities.into_iter()) + .append_all(disco.features.into_iter()) + .append_all(disco.extensions.iter().cloned().map(Element::from)) + .build() + } +} + +generate_element!( +/// Structure representing a `` element. +/// +/// It should only be used in an ``, as it can only represent +/// the request, and not a result. +DiscoItemsQuery, "query", DISCO_ITEMS, +attributes: [ + /// Node on which we are doing the discovery. + node: Option = "node", +]); + +impl IqGetPayload for DiscoItemsQuery {} + +generate_element!( +/// Structure representing an `` element. +Item, "item", DISCO_ITEMS, +attributes: [ + /// JID of the entity pointed by this item. + jid: Required = "jid", + /// Node of the entity pointed by this item. + node: Option = "node", + /// Name of the entity pointed by this item. + name: Option = "name", +]); + +generate_element!( + /// Structure representing a `` element. + /// + /// It should only be used in an ``, as it can only + /// represent the result, and not a request. + DiscoItemsResult, "query", DISCO_ITEMS, + attributes: [ + /// Node on which we have done this discovery. + node: Option = "node" + ], + children: [ + /// List of items pointed by this entity. + items: Vec = ("item", DISCO_ITEMS) => Item + ] +); + +impl IqResultPayload for DiscoItemsResult {} + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Identity, 48); + assert_size!(Feature, 12); + assert_size!(DiscoInfoQuery, 12); + assert_size!(DiscoInfoResult, 48); + + assert_size!(Item, 64); + assert_size!(DiscoItemsQuery, 12); + assert_size!(DiscoItemsResult, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Identity, 96); + assert_size!(Feature, 24); + assert_size!(DiscoInfoQuery, 24); + assert_size!(DiscoInfoResult, 96); + + assert_size!(Item, 128); + assert_size!(DiscoItemsQuery, 24); + assert_size!(DiscoItemsResult, 48); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert!(query.node.is_none()); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert!(query.extensions.is_empty()); + } + + #[test] + fn test_identity_after_feature() { + let elem: Element = "".parse().unwrap(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert!(query.extensions.is_empty()); + } + + #[test] + fn test_feature_after_dataform() { + let elem: Element = "coucou".parse().unwrap(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert_eq!(query.extensions.len(), 1); + } + + #[test] + fn test_extension() { + let elem: Element = "example".parse().unwrap(); + let elem1 = elem.clone(); + let query = DiscoInfoResult::try_from(elem).unwrap(); + assert!(query.node.is_none()); + assert_eq!(query.identities.len(), 1); + assert_eq!(query.features.len(), 1); + assert_eq!(query.extensions.len(), 1); + assert_eq!(query.extensions[0].form_type, Some(String::from("example"))); + + let elem2 = query.into(); + assert_eq!(elem1, elem2); + } + + #[test] + fn test_invalid() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown element in disco#info."); + } + + #[test] + fn test_invalid_identity() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'category' missing."); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'category' must not be empty."); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' must not be empty."); + } + + #[test] + fn test_invalid_feature() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'var' missing."); + } + + #[test] + fn test_invalid_result() { + let elem: Element = "" + .parse() + .unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "There must be at least one identity in disco#info." + ); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "There must be at least one feature in disco#info."); + + let elem: Element = "".parse().unwrap(); + let error = DiscoInfoResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "disco#info feature not present in disco#info."); + } + + #[test] + fn test_simple_items() { + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsQuery::try_from(elem).unwrap(); + assert!(query.node.is_none()); + + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsQuery::try_from(elem).unwrap(); + assert_eq!(query.node, Some(String::from("coucou"))); + } + + #[test] + fn test_simple_items_result() { + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsResult::try_from(elem).unwrap(); + assert!(query.node.is_none()); + assert!(query.items.is_empty()); + + let elem: Element = "" + .parse() + .unwrap(); + let query = DiscoItemsResult::try_from(elem).unwrap(); + assert_eq!(query.node, Some(String::from("coucou"))); + assert!(query.items.is_empty()); + } + + #[test] + fn test_answers_items_result() { + let elem: Element = "".parse().unwrap(); + let query = DiscoItemsResult::try_from(elem).unwrap(); + let elem2 = Element::from(query); + let query = DiscoItemsResult::try_from(elem2).unwrap(); + assert_eq!(query.items.len(), 2); + assert_eq!(query.items[0].jid, BareJid::domain("component")); + assert_eq!(query.items[0].node, None); + assert_eq!(query.items[0].name, None); + assert_eq!(query.items[1].jid, BareJid::domain("component2")); + assert_eq!(query.items[1].node, Some(String::from("test"))); + assert_eq!(query.items[1].name, Some(String::from("A component"))); + } +} diff --git a/xmpp-parsers/src/ecaps2.rs b/xmpp-parsers/src/ecaps2.rs new file mode 100644 index 0000000..4ca1f2a --- /dev/null +++ b/xmpp-parsers/src/ecaps2.rs @@ -0,0 +1,481 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::disco::{DiscoInfoQuery, DiscoInfoResult, Feature, Identity}; +use crate::hashes::{Algo, Hash}; +use crate::ns; +use crate::presence::PresencePayload; +use crate::util::error::Error; +use blake2::VarBlake2b; +use digest::{Digest, Update, VariableOutput}; +use sha2::{Sha256, Sha512}; +use sha3::{Sha3_256, Sha3_512}; + +generate_element!( + /// Represents a set of capability hashes, all of them must correspond to + /// the same input [disco#info](../disco/struct.DiscoInfoResult.html), + /// using different [algorithms](../hashes/enum.Algo.html). + ECaps2, "c", ECAPS2, + children: [ + /// Hashes of the [disco#info](../disco/struct.DiscoInfoResult.html). + hashes: Vec = ("hash", HASHES) => Hash + ] +); + +impl PresencePayload for ECaps2 {} + +impl ECaps2 { + /// Create an ECaps2 element from a list of hashes. + pub fn new(hashes: Vec) -> ECaps2 { + ECaps2 { hashes } + } +} + +fn compute_item(field: &str) -> Vec { + let mut bytes = field.as_bytes().to_vec(); + bytes.push(0x1f); + bytes +} + +fn compute_items Vec>(things: &[T], separator: u8, encode: F) -> Vec { + let mut string: Vec = vec![]; + let mut accumulator: Vec> = vec![]; + for thing in things { + let bytes = encode(thing); + accumulator.push(bytes); + } + // This works using the expected i;octet collation. + accumulator.sort(); + for mut bytes in accumulator { + string.append(&mut bytes); + } + string.push(separator); + string +} + +fn compute_features(features: &[Feature]) -> Vec { + compute_items(features, 0x1c, |feature| compute_item(&feature.var)) +} + +fn compute_identities(identities: &[Identity]) -> Vec { + compute_items(identities, 0x1c, |identity| { + let mut bytes = compute_item(&identity.category); + bytes.append(&mut compute_item(&identity.type_)); + bytes.append(&mut compute_item( + &identity.lang.clone().unwrap_or_default(), + )); + bytes.append(&mut compute_item( + &identity.name.clone().unwrap_or_default(), + )); + bytes.push(0x1e); + bytes + }) +} + +fn compute_extensions(extensions: &[DataForm]) -> Result, Error> { + for extension in extensions { + if extension.form_type.is_none() { + return Err(Error::ParseError("Missing FORM_TYPE in extension.")); + } + } + Ok(compute_items(extensions, 0x1c, |extension| { + let mut bytes = compute_item("FORM_TYPE"); + bytes.append(&mut compute_item( + if let Some(ref form_type) = extension.form_type { + form_type + } else { + unreachable!() + }, + )); + bytes.push(0x1e); + bytes.append(&mut compute_items(&extension.fields, 0x1d, |field| { + let mut bytes = compute_item(&field.var); + bytes.append(&mut compute_items(&field.values, 0x1e, |value| { + compute_item(value) + })); + bytes + })); + bytes + })) +} + +/// Applies the [algorithm from +/// XEP-0390](https://xmpp.org/extensions/xep-0390.html#algorithm-input) on a +/// [disco#info query element](../disco/struct.DiscoInfoResult.html). +pub fn compute_disco(disco: &DiscoInfoResult) -> Result, Error> { + let features_string = compute_features(&disco.features); + let identities_string = compute_identities(&disco.identities); + let extensions_string = compute_extensions(&disco.extensions)?; + + let mut final_string = vec![]; + final_string.extend(features_string); + final_string.extend(identities_string); + final_string.extend(extensions_string); + Ok(final_string) +} + +fn get_hash_vec(hash: &[u8]) -> Vec { + let mut vec = Vec::with_capacity(hash.len()); + vec.extend_from_slice(hash); + vec +} + +/// Hashes the result of [compute_disco()] with one of the supported [hash +/// algorithms](../hashes/enum.Algo.html). +pub fn hash_ecaps2(data: &[u8], algo: Algo) -> Result { + Ok(Hash { + hash: match algo { + Algo::Sha_256 => { + let hash = Sha256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha_512 => { + let hash = Sha512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_256 => { + let hash = Sha3_256::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Sha3_512 => { + let hash = Sha3_512::digest(data); + get_hash_vec(hash.as_slice()) + } + Algo::Blake2b_256 => { + let mut hasher = VarBlake2b::new(32).unwrap(); + hasher.update(data); + let mut vec = Vec::with_capacity(32); + hasher.finalize_variable(|slice| vec.extend_from_slice(slice)); + vec + } + Algo::Blake2b_512 => { + let mut hasher = VarBlake2b::new(64).unwrap(); + hasher.update(data); + let mut vec = Vec::with_capacity(64); + hasher.finalize_variable(|slice| vec.extend_from_slice(slice)); + vec + } + Algo::Sha_1 => return Err(Error::ParseError("Disabled algorithm sha-1: unsafe.")), + Algo::Unknown(_algo) => return Err(Error::ParseError("Unknown algorithm in ecaps2.")), + }, + algo, + }) +} + +/// Helper function to create the query for the disco#info corresponding to an +/// ecaps2 hash. +pub fn query_ecaps2(hash: Hash) -> DiscoInfoQuery { + DiscoInfoQuery { + node: Some(format!( + "{}#{}.{}", + ns::ECAPS2, + String::from(hash.algo), + base64::encode(&hash.hash) + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ECaps2, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ECaps2, 24); + } + + #[test] + fn test_parse() { + let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=".parse().unwrap(); + let ecaps2 = ECaps2::try_from(elem).unwrap(); + assert_eq!(ecaps2.hashes.len(), 2); + assert_eq!(ecaps2.hashes[0].algo, Algo::Sha_256); + assert_eq!( + ecaps2.hashes[0].hash, + base64::decode("K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=").unwrap() + ); + assert_eq!(ecaps2.hashes[1].algo, Algo::Sha3_256); + assert_eq!( + ecaps2.hashes[1].hash, + base64::decode("+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=").unwrap() + ); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "K1Njy3HZBThlo4moOD5gBGhn0U0oK7/CbfLlIUDi6o4=+sDTQqBmX6iG/X3zjt06fjZMBBqL/723knFIyRf0sg8=".parse().unwrap(); + let error = ECaps2::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in c element."); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let ecaps2 = compute_disco(&disco).unwrap(); + assert_eq!(ecaps2.len(), 54); + } + + #[test] + fn test_xep_ex1() { + let elem: Element = r#" + + + + + + + + + + + + + + + + + + + + +"# + .parse() + .unwrap(); + let expected = vec![ + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, + 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, + 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, + 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, + 110, 102, 111, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, + 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, + 105, 116, 101, 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, + 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, + 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, + 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, 114, 120, 31, 104, 116, 116, + 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, + 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, + 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, + 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, 101, 45, 116, 114, 97, 110, + 115, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, + 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, + 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, + 98, 98, 101, 114, 58, 105, 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, + 58, 105, 113, 58, 118, 101, 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, + 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, + 103, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, + 115, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, + 108, 105, 101, 110, 116, 31, 109, 111, 98, 105, 108, 101, 31, 31, 66, 111, 109, 98, + 117, 115, 77, 111, 100, 31, 30, 28, 28, + ]; + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let ecaps2 = compute_disco(&disco).unwrap(); + assert_eq!(ecaps2.len(), 0x1d9); + assert_eq!(ecaps2, expected); + + let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap(); + assert_eq!( + sha_256.hash, + base64::decode("kzBZbkqJ3ADrj7v08reD1qcWUwNGHaidNUgD7nHpiw8=").unwrap() + ); + let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap(); + assert_eq!( + sha3_256.hash, + base64::decode("79mdYAfU9rEdTOcWDO7UEAt6E56SUzk/g6TnqUeuD9Q=").unwrap() + ); + } + + #[test] + fn test_xep_ex2() { + let elem: Element = r#" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + urn:xmpp:dataforms:softwareinfo + + + Tkabber + + + 0.11.1-svn-20111216-mod (Tcl/Tk 8.6b2) + + + Windows + + + XP + + + +"# + .parse() + .unwrap(); + let expected = vec![ + 103, 97, 109, 101, 115, 58, 98, 111, 97, 114, 100, 31, 104, 116, 116, 112, 58, 47, 47, + 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, + 108, 47, 97, 99, 116, 105, 118, 105, 116, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, + 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, + 97, 99, 116, 105, 118, 105, 116, 121, 43, 110, 111, 116, 105, 102, 121, 31, 104, 116, + 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, + 116, 111, 99, 111, 108, 47, 98, 121, 116, 101, 115, 116, 114, 101, 97, 109, 115, 31, + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 99, 104, 97, 116, 115, 116, 97, 116, 101, 115, + 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, + 112, 114, 111, 116, 111, 99, 111, 108, 47, 99, 111, 109, 109, 97, 110, 100, 115, 31, + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 110, 102, 111, + 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, + 112, 114, 111, 116, 111, 99, 111, 108, 47, 100, 105, 115, 99, 111, 35, 105, 116, 101, + 109, 115, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 101, 118, 105, 108, 31, 104, 116, + 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, + 116, 111, 99, 111, 108, 47, 102, 101, 97, 116, 117, 114, 101, 45, 110, 101, 103, 31, + 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, + 114, 111, 116, 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 31, 104, 116, 116, + 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, + 111, 99, 111, 108, 47, 103, 101, 111, 108, 111, 99, 43, 110, 111, 116, 105, 102, 121, + 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, + 112, 114, 111, 116, 111, 99, 111, 108, 47, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, + 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, + 108, 47, 105, 113, 105, 98, 98, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, + 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, + 111, 100, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 109, 111, 111, 100, 43, 110, 111, + 116, 105, 102, 121, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, + 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 114, 111, 115, 116, 101, + 114, 120, 31, 104, 116, 116, 112, 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, + 103, 47, 112, 114, 111, 116, 111, 99, 111, 108, 47, 115, 105, 31, 104, 116, 116, 112, + 58, 47, 47, 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, + 99, 111, 108, 47, 115, 105, 47, 112, 114, 111, 102, 105, 108, 101, 47, 102, 105, 108, + 101, 45, 116, 114, 97, 110, 115, 102, 101, 114, 31, 104, 116, 116, 112, 58, 47, 47, + 106, 97, 98, 98, 101, 114, 46, 111, 114, 103, 47, 112, 114, 111, 116, 111, 99, 111, + 108, 47, 116, 117, 110, 101, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, + 102, 97, 99, 101, 98, 111, 111, 107, 46, 99, 111, 109, 47, 120, 109, 112, 112, 47, 109, + 101, 115, 115, 97, 103, 101, 115, 31, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, + 46, 120, 109, 112, 112, 46, 111, 114, 103, 47, 101, 120, 116, 101, 110, 115, 105, 111, + 110, 115, 47, 120, 101, 112, 45, 48, 48, 56, 52, 46, 104, 116, 109, 108, 35, 110, 115, + 45, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, 105, 102, 121, 31, 106, 97, + 98, 98, 101, 114, 58, 105, 113, 58, 97, 118, 97, 116, 97, 114, 31, 106, 97, 98, 98, + 101, 114, 58, 105, 113, 58, 98, 114, 111, 119, 115, 101, 31, 106, 97, 98, 98, 101, 114, + 58, 105, 113, 58, 100, 116, 99, 112, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, + 102, 105, 108, 101, 120, 102, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, + 58, 105, 98, 98, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 105, 110, 98, 97, + 110, 100, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 106, 105, 100, 108, 105, + 110, 107, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 108, 97, 115, 116, 31, 106, + 97, 98, 98, 101, 114, 58, 105, 113, 58, 111, 111, 98, 31, 106, 97, 98, 98, 101, 114, + 58, 105, 113, 58, 112, 114, 105, 118, 97, 99, 121, 31, 106, 97, 98, 98, 101, 114, 58, + 105, 113, 58, 114, 111, 115, 116, 101, 114, 31, 106, 97, 98, 98, 101, 114, 58, 105, + 113, 58, 116, 105, 109, 101, 31, 106, 97, 98, 98, 101, 114, 58, 105, 113, 58, 118, 101, + 114, 115, 105, 111, 110, 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 100, 97, 116, 97, + 31, 106, 97, 98, 98, 101, 114, 58, 120, 58, 101, 118, 101, 110, 116, 31, 106, 97, 98, + 98, 101, 114, 58, 120, 58, 111, 111, 98, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, + 97, 118, 97, 116, 97, 114, 58, 109, 101, 116, 97, 100, 97, 116, 97, 43, 110, 111, 116, + 105, 102, 121, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 112, 105, 110, 103, 31, + 117, 114, 110, 58, 120, 109, 112, 112, 58, 114, 101, 99, 101, 105, 112, 116, 115, 31, + 117, 114, 110, 58, 120, 109, 112, 112, 58, 116, 105, 109, 101, 31, 28, 99, 108, 105, + 101, 110, 116, 31, 112, 99, 31, 101, 110, 31, 84, 107, 97, 98, 98, 101, 114, 31, 30, + 99, 108, 105, 101, 110, 116, 31, 112, 99, 31, 114, 117, 31, 208, 162, 208, 186, 208, + 176, 208, 177, 208, 177, 208, 181, 209, 128, 31, 30, 28, 70, 79, 82, 77, 95, 84, 89, + 80, 69, 31, 117, 114, 110, 58, 120, 109, 112, 112, 58, 100, 97, 116, 97, 102, 111, 114, + 109, 115, 58, 115, 111, 102, 116, 119, 97, 114, 101, 105, 110, 102, 111, 31, 30, 111, + 115, 31, 87, 105, 110, 100, 111, 119, 115, 31, 30, 111, 115, 95, 118, 101, 114, 115, + 105, 111, 110, 31, 88, 80, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 31, 84, 107, + 97, 98, 98, 101, 114, 31, 30, 115, 111, 102, 116, 119, 97, 114, 101, 95, 118, 101, 114, + 115, 105, 111, 110, 31, 48, 46, 49, 49, 46, 49, 45, 115, 118, 110, 45, 50, 48, 49, 49, + 49, 50, 49, 54, 45, 109, 111, 100, 32, 40, 84, 99, 108, 47, 84, 107, 32, 56, 46, 54, + 98, 50, 41, 31, 30, 29, 28, + ]; + let disco = DiscoInfoResult::try_from(elem).unwrap(); + let ecaps2 = compute_disco(&disco).unwrap(); + assert_eq!(ecaps2.len(), 0x543); + assert_eq!(ecaps2, expected); + + let sha_256 = hash_ecaps2(&ecaps2, Algo::Sha_256).unwrap(); + assert_eq!( + sha_256.hash, + base64::decode("u79ZroNJbdSWhdSp311mddz44oHHPsEBntQ5b1jqBSY=").unwrap() + ); + let sha3_256 = hash_ecaps2(&ecaps2, Algo::Sha3_256).unwrap(); + assert_eq!( + sha3_256.hash, + base64::decode("XpUJzLAc93258sMECZ3FJpebkzuyNXDzRNwQog8eycg=").unwrap() + ); + } + + #[test] + fn test_blake2b_512() { + let hash = hash_ecaps2("abc".as_bytes(), Algo::Blake2b_512).unwrap(); + let known_hash: Vec = vec![ + 0xBA, 0x80, 0xA5, 0x3F, 0x98, 0x1C, 0x4D, 0x0D, 0x6A, 0x27, 0x97, 0xB6, 0x9F, 0x12, + 0xF6, 0xE9, 0x4C, 0x21, 0x2F, 0x14, 0x68, 0x5A, 0xC4, 0xB7, 0x4B, 0x12, 0xBB, 0x6F, + 0xDB, 0xFF, 0xA2, 0xD1, 0x7D, 0x87, 0xC5, 0x39, 0x2A, 0xAB, 0x79, 0x2D, 0xC2, 0x52, + 0xD5, 0xDE, 0x45, 0x33, 0xCC, 0x95, 0x18, 0xD3, 0x8A, 0xA8, 0xDB, 0xF1, 0x92, 0x5A, + 0xB9, 0x23, 0x86, 0xED, 0xD4, 0x00, 0x99, 0x23, + ]; + assert_eq!(hash.hash, known_hash); + } +} diff --git a/xmpp-parsers/src/eme.rs b/xmpp-parsers/src/eme.rs new file mode 100644 index 0000000..1014318 --- /dev/null +++ b/xmpp-parsers/src/eme.rs @@ -0,0 +1,96 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; + +generate_element!( + /// Structure representing an `` element. + ExplicitMessageEncryption, "encryption", EME, + attributes: [ + /// Namespace of the encryption scheme used. + namespace: Required = "namespace", + + /// User-friendly name for the encryption scheme, should be `None` for OTR, + /// legacy OpenPGP and OX. + name: Option = "name", + ] +); + +impl MessagePayload for ExplicitMessageEncryption {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ExplicitMessageEncryption, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ExplicitMessageEncryption, 48); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let encryption = ExplicitMessageEncryption::try_from(elem).unwrap(); + assert_eq!(encryption.namespace, "urn:xmpp:otr:0"); + assert_eq!(encryption.name, None); + + let elem: Element = "".parse().unwrap(); + let encryption = ExplicitMessageEncryption::try_from(elem).unwrap(); + assert_eq!(encryption.namespace, "some.unknown.mechanism"); + assert_eq!(encryption.name, Some(String::from("SuperMechanism"))); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ExplicitMessageEncryption::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a encryption element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = ExplicitMessageEncryption::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in encryption element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let eme = ExplicitMessageEncryption { + namespace: String::from("coucou"), + name: None, + }; + let elem2 = eme.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/forwarding.rs b/xmpp-parsers/src/forwarding.rs new file mode 100644 index 0000000..c59e9d1 --- /dev/null +++ b/xmpp-parsers/src/forwarding.rs @@ -0,0 +1,100 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::delay::Delay; +use crate::message::Message; + +generate_element!( + /// Contains a forwarded stanza, either standalone or part of another + /// extension (such as carbons). + Forwarded, "forwarded", FORWARD, + children: [ + /// When the stanza originally got sent. + delay: Option = ("delay", DELAY) => Delay, + + // XXX: really? Option? + /// The stanza being forwarded. + stanza: Option = ("message", DEFAULT_NS) => Message + + // TODO: also handle the two other stanza possibilities. + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Forwarded, 212); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Forwarded, 408); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Forwarded::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Forwarded::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in forwarded element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "".parse().unwrap(); + let forwarded = Forwarded { + delay: None, + stanza: None, + }; + let elem2 = forwarded.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_serialize_with_delay_and_stanza() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + let message = Message::try_from(elem).unwrap(); + + let elem: Element = + "" + .parse() + .unwrap(); + let delay = Delay::try_from(elem).unwrap(); + + let forwarded = Forwarded { + delay: Some(delay), + stanza: Some(message), + }; + + let serialized: Element = forwarded.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/hashes.rs b/xmpp-parsers/src/hashes.rs new file mode 100644 index 0000000..704af52 --- /dev/null +++ b/xmpp-parsers/src/hashes.rs @@ -0,0 +1,274 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::util::error::Error; +use crate::util::helpers::Base64; +use minidom::IntoAttributeValue; +use std::num::ParseIntError; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; + +/// List of the algorithms we support, or Unknown. +#[allow(non_camel_case_types)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Algo { + /// The Secure Hash Algorithm 1, with known vulnerabilities, do not use it. + /// + /// See https://tools.ietf.org/html/rfc3174 + Sha_1, + + /// The Secure Hash Algorithm 2, in its 256-bit version. + /// + /// See https://tools.ietf.org/html/rfc6234 + Sha_256, + + /// The Secure Hash Algorithm 2, in its 512-bit version. + /// + /// See https://tools.ietf.org/html/rfc6234 + Sha_512, + + /// The Secure Hash Algorithm 3, based on Keccak, in its 256-bit version. + /// + /// See https://keccak.team/files/Keccak-submission-3.pdf + Sha3_256, + + /// The Secure Hash Algorithm 3, based on Keccak, in its 512-bit version. + /// + /// See https://keccak.team/files/Keccak-submission-3.pdf + Sha3_512, + + /// The BLAKE2 hash algorithm, for a 256-bit output. + /// + /// See https://tools.ietf.org/html/rfc7693 + Blake2b_256, + + /// The BLAKE2 hash algorithm, for a 512-bit output. + /// + /// See https://tools.ietf.org/html/rfc7693 + Blake2b_512, + + /// An unknown hash not in this list, you can probably reject it. + Unknown(String), +} + +impl FromStr for Algo { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "" => return Err(Error::ParseError("'algo' argument can’t be empty.")), + + "sha-1" => Algo::Sha_1, + "sha-256" => Algo::Sha_256, + "sha-512" => Algo::Sha_512, + "sha3-256" => Algo::Sha3_256, + "sha3-512" => Algo::Sha3_512, + "blake2b-256" => Algo::Blake2b_256, + "blake2b-512" => Algo::Blake2b_512, + value => Algo::Unknown(value.to_owned()), + }) + } +} + +impl From for String { + fn from(algo: Algo) -> String { + String::from(match algo { + Algo::Sha_1 => "sha-1", + Algo::Sha_256 => "sha-256", + Algo::Sha_512 => "sha-512", + Algo::Sha3_256 => "sha3-256", + Algo::Sha3_512 => "sha3-512", + Algo::Blake2b_256 => "blake2b-256", + Algo::Blake2b_512 => "blake2b-512", + Algo::Unknown(text) => return text, + }) + } +} + +impl IntoAttributeValue for Algo { + fn into_attribute_value(self) -> Option { + Some(String::from(self)) + } +} + +generate_element!( + /// This element represents a hash of some data, defined by the hash + /// algorithm used and the computed value. + Hash, "hash", HASHES, + attributes: [ + /// The algorithm used to create this hash. + algo: Required = "algo" + ], + text: ( + /// The hash value, as a vector of bytes. + hash: Base64> + ) +); + +impl Hash { + /// Creates a [Hash] element with the given algo and data. + pub fn new(algo: Algo, hash: Vec) -> Hash { + Hash { algo, hash } + } + + /// Like [new](#method.new) but takes base64-encoded data before decoding + /// it. + pub fn from_base64(algo: Algo, hash: &str) -> Result { + Ok(Hash::new(algo, base64::decode(hash)?)) + } + + /// Like [new](#method.new) but takes hex-encoded data before decoding it. + pub fn from_hex(algo: Algo, hex: &str) -> Result { + let mut bytes = vec![]; + for i in 0..hex.len() / 2 { + let byte = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16)?; + bytes.push(byte); + } + Ok(Hash::new(algo, bytes)) + } + + /// Like [new](#method.new) but takes hex-encoded data before decoding it. + pub fn from_colon_separated_hex(algo: Algo, hex: &str) -> Result { + let mut bytes = vec![]; + for i in 0..(1 + hex.len()) / 3 { + let byte = u8::from_str_radix(&hex[3 * i..3 * i + 2], 16)?; + if 3 * i + 2 < hex.len() { + assert_eq!(&hex[3 * i + 2..3 * i + 3], ":"); + } + bytes.push(byte); + } + Ok(Hash::new(algo, bytes)) + } + + /// Formats this hash into base64. + pub fn to_base64(&self) -> String { + base64::encode(&self.hash[..]) + } + + /// Formats this hash into hexadecimal. + pub fn to_hex(&self) -> String { + self.hash + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join("") + } + + /// Formats this hash into colon-separated hexadecimal. + pub fn to_colon_separated_hex(&self) -> String { + self.hash + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join(":") + } +} + +/// Helper for parsing and serialising a SHA-1 attribute. +#[derive(Debug, Clone, PartialEq)] +pub struct Sha1HexAttribute(Hash); + +impl FromStr for Sha1HexAttribute { + type Err = ParseIntError; + + fn from_str(hex: &str) -> Result { + let hash = Hash::from_hex(Algo::Sha_1, hex)?; + Ok(Sha1HexAttribute(hash)) + } +} + +impl IntoAttributeValue for Sha1HexAttribute { + fn into_attribute_value(self) -> Option { + Some(self.to_hex()) + } +} + +impl DerefMut for Sha1HexAttribute { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Deref for Sha1HexAttribute { + type Target = Hash; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Algo, 16); + assert_size!(Hash, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Algo, 32); + assert_size!(Hash, 56); + } + + #[test] + fn test_simple() { + let elem: Element = "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=".parse().unwrap(); + let hash = Hash::try_from(elem).unwrap(); + assert_eq!(hash.algo, Algo::Sha_256); + assert_eq!( + hash.hash, + base64::decode("2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=").unwrap() + ); + } + + #[test] + fn value_serialisation() { + let elem: Element = "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=".parse().unwrap(); + let hash = Hash::try_from(elem).unwrap(); + assert_eq!( + hash.to_base64(), + "2XarmwTlNxDAMkvymloX3S5+VbylNrJt/l5QyPa+YoU=" + ); + assert_eq!( + hash.to_hex(), + "d976ab9b04e53710c0324bf29a5a17dd2e7e55bca536b26dfe5e50c8f6be6285" + ); + assert_eq!(hash.to_colon_separated_hex(), "d9:76:ab:9b:04:e5:37:10:c0:32:4b:f2:9a:5a:17:dd:2e:7e:55:bc:a5:36:b2:6d:fe:5e:50:c8:f6:be:62:85"); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Hash::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a hash element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Hash::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in hash element."); + } +} diff --git a/xmpp-parsers/src/ibb.rs b/xmpp-parsers/src/ibb.rs new file mode 100644 index 0000000..3e4ab57 --- /dev/null +++ b/xmpp-parsers/src/ibb.rs @@ -0,0 +1,171 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::IqSetPayload; +use crate::util::helpers::Base64; + +generate_id!( + /// An identifier matching a stream. + StreamId +); + +generate_attribute!( +/// Which stanza type to use to exchange data. +Stanza, "stanza", { + /// `` gives a feedback on whether the chunk has been received or not, + /// which is useful in the case the recipient might not receive them in a + /// timely manner, or to do your own throttling based on the results. + Iq => "iq", + + /// `` can be faster, since it doesn’t require any feedback, but in + /// practice it will be throttled by the servers on the way. + Message => "message", +}, Default = Iq); + +generate_element!( +/// Starts an In-Band Bytestream session with the given parameters. +Open, "open", IBB, +attributes: [ + /// Maximum size in bytes for each chunk. + block_size: Required = "block-size", + + /// The identifier to be used to create a stream. + sid: Required = "sid", + + /// Which stanza type to use to exchange data. + stanza: Default = "stanza", +]); + +impl IqSetPayload for Open {} + +generate_element!( +/// Exchange a chunk of data in an open stream. +Data, "data", IBB, + attributes: [ + /// Sequence number of this chunk, must wraparound after 65535. + seq: Required = "seq", + + /// The identifier of the stream on which data is being exchanged. + sid: Required = "sid" + ], + text: ( + /// Vector of bytes to be exchanged. + data: Base64> + ) +); + +impl IqSetPayload for Data {} + +generate_element!( +/// Close an open stream. +Close, "close", IBB, +attributes: [ + /// The identifier of the stream to be closed. + sid: Required = "sid", +]); + +impl IqSetPayload for Close {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(StreamId, 12); + assert_size!(Stanza, 1); + assert_size!(Open, 16); + assert_size!(Data, 28); + assert_size!(Close, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(StreamId, 24); + assert_size!(Stanza, 1); + assert_size!(Open, 32); + assert_size!(Data, 56); + assert_size!(Close, 24); + } + + #[test] + fn test_simple() { + let sid = StreamId(String::from("coucou")); + + let elem: Element = + "" + .parse() + .unwrap(); + let open = Open::try_from(elem).unwrap(); + assert_eq!(open.block_size, 3); + assert_eq!(open.sid, sid); + assert_eq!(open.stanza, Stanza::Iq); + + let elem: Element = + "AAAA" + .parse() + .unwrap(); + let data = Data::try_from(elem).unwrap(); + assert_eq!(data.seq, 0); + assert_eq!(data.sid, sid); + assert_eq!(data.data, vec!(0, 0, 0)); + + let elem: Element = "" + .parse() + .unwrap(); + let close = Close::try_from(elem).unwrap(); + assert_eq!(close.sid, sid); + } + + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'block-size' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(message.to_string(), "invalid digit found in string"); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(error) => error, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'sid' missing."); + } + + #[test] + fn test_invalid_stanza() { + let elem: Element = "".parse().unwrap(); + let error = Open::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'stanza' attribute."); + } +} diff --git a/xmpp-parsers/src/ibr.rs b/xmpp-parsers/src/ibr.rs new file mode 100644 index 0000000..b0a2f85 --- /dev/null +++ b/xmpp-parsers/src/ibr.rs @@ -0,0 +1,195 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use std::collections::HashMap; +use std::convert::TryFrom; + +/// Query for registering against a service. +#[derive(Debug, Clone)] +pub struct Query { + /// Deprecated fixed list of possible fields to fill before the user can + /// register. + pub fields: HashMap, + + /// Whether this account is already registered. + pub registered: bool, + + /// Whether to remove this account. + pub remove: bool, + + /// A data form the user must fill before being allowed to register. + pub form: Option, + // Not yet implemented. + //pub oob: Option, +} + +impl IqGetPayload for Query {} +impl IqSetPayload for Query {} +impl IqResultPayload for Query {} + +impl TryFrom for Query { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "query", REGISTER, "IBR query"); + let mut query = Query { + registered: false, + fields: HashMap::new(), + remove: false, + form: None, + }; + for child in elem.children() { + let namespace = child.ns(); + if namespace == ns::REGISTER { + let name = child.name(); + let fields = vec![ + "address", + "city", + "date", + "email", + "first", + "instructions", + "key", + "last", + "misc", + "name", + "nick", + "password", + "phone", + "state", + "text", + "url", + "username", + "zip", + ]; + if fields.binary_search(&name).is_ok() { + query.fields.insert(name.to_owned(), child.text()); + } else if name == "registered" { + query.registered = true; + } else if name == "remove" { + query.remove = true; + } else { + return Err(Error::ParseError("Wrong field in ibr element.")); + } + } else if child.is("x", ns::DATA_FORMS) { + query.form = Some(DataForm::try_from(child.clone())?); + } else { + return Err(Error::ParseError("Unknown child in ibr element.")); + } + } + Ok(query) + } +} + +impl From for Element { + fn from(query: Query) -> Element { + Element::builder("query", ns::REGISTER) + .append_all(if query.registered { + Some(Element::builder("registered", ns::REGISTER)) + } else { + None + }) + .append_all( + query + .fields + .into_iter() + .map(|(name, value)| Element::builder(name, ns::REGISTER).append(value)), + ) + .append_all(if query.remove { + Some(Element::builder("remove", ns::REGISTER)) + } else { + None + }) + .append_all(query.form.map(Element::from)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Query, 88); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Query, 160); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_ex2() { + let elem: Element = r#" + + + Choose a username and password for use with this service. + Please also provide your email address. + + + + + +"# + .parse() + .unwrap(); + let query = Query::try_from(elem).unwrap(); + assert_eq!(query.registered, false); + assert_eq!(query.fields["instructions"], "\n Choose a username and password for use with this service.\n Please also provide your email address.\n "); + assert_eq!(query.fields["username"], ""); + assert_eq!(query.fields["password"], ""); + assert_eq!(query.fields["email"], ""); + assert_eq!(query.fields.contains_key("name"), false); + + // FIXME: HashMap doesn’t keep the order right. + //let elem2 = query.into(); + //assert_eq!(elem, elem2); + } + + #[test] + fn test_ex9() { + let elem: Element = "Use the enclosed form to register. If your Jabber client does not support Data Forms, visit http://www.shakespeare.lit/contests.phpContest RegistrationPlease provide the following information to sign up for our special contests!jabber:iq:register" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let query = Query::try_from(elem).unwrap(); + assert_eq!(query.registered, false); + assert!(!query.fields["instructions"].is_empty()); + let form = query.form.clone().unwrap(); + assert!(!form.instructions.unwrap().is_empty()); + let elem2 = query.into(); + assert_eq!(elem1, elem2); + } + + #[test] + fn test_ex10() { + let elem: Element = "jabber:iq:registerJulietCapuletjuliet@capulet.comF" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let query = Query::try_from(elem).unwrap(); + assert_eq!(query.registered, false); + for _ in &query.fields { + panic!(); + } + let elem2 = query.into(); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/idle.rs b/xmpp-parsers/src/idle.rs new file mode 100644 index 0000000..a575b32 --- /dev/null +++ b/xmpp-parsers/src/idle.rs @@ -0,0 +1,146 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::date::DateTime; +use crate::presence::PresencePayload; + +generate_element!( + /// Represents the last time the user interacted with their system. + Idle, "idle", IDLE, + attributes: [ + /// The time at which the user stopped interacting. + since: Required = "since", + ] +); + +impl PresencePayload for Idle {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + use std::str::FromStr; + + #[test] + fn test_size() { + assert_size!(Idle, 16); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + Idle::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in idle element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "".parse().unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'since' missing."); + } + + #[test] + fn test_invalid_date() { + // There is no thirteenth month. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input is out of range"); + + // Timezone ≥24:00 aren’t allowed. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input is out of range"); + + // Timezone without the : separator aren’t allowed. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input contains invalid characters"); + + // No seconds, error message could be improved. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input contains invalid characters"); + + // TODO: maybe we’ll want to support this one, as per XEP-0082 §4. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "input contains invalid characters"); + + // No timezone. + let elem: Element = "" + .parse() + .unwrap(); + let error = Idle::try_from(elem).unwrap_err(); + let message = match error { + Error::ChronoParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message.to_string(), "premature end of input"); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let idle = Idle { + since: DateTime::from_str("2017-05-21T20:19:55+01:00").unwrap(), + }; + let elem2 = idle.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/iq.rs b/xmpp-parsers/src/iq.rs new file mode 100644 index 0000000..b9c022f --- /dev/null +++ b/xmpp-parsers/src/iq.rs @@ -0,0 +1,461 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::stanza_error::StanzaError; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use minidom::IntoAttributeValue; +use std::convert::TryFrom; + +/// Should be implemented on every known payload of an ``. +pub trait IqGetPayload: TryFrom + Into {} + +/// Should be implemented on every known payload of an ``. +pub trait IqSetPayload: TryFrom + Into {} + +/// Should be implemented on every known payload of an ``. +pub trait IqResultPayload: TryFrom + Into {} + +/// Represents one of the four possible iq types. +#[derive(Debug, Clone)] +pub enum IqType { + /// This is a request for accessing some data. + Get(Element), + + /// This is a request for modifying some data. + Set(Element), + + /// This is a result containing some data. + Result(Option), + + /// A get or set request failed. + Error(StanzaError), +} + +impl<'a> IntoAttributeValue for &'a IqType { + fn into_attribute_value(self) -> Option { + Some( + match *self { + IqType::Get(_) => "get", + IqType::Set(_) => "set", + IqType::Result(_) => "result", + IqType::Error(_) => "error", + } + .to_owned(), + ) + } +} + +/// The main structure representing the `` stanza. +#[derive(Debug, Clone)] +pub struct Iq { + /// The JID emitting this stanza. + pub from: Option, + + /// The recipient of this stanza. + pub to: Option, + + /// The @id attribute of this stanza, which is required in order to match a + /// request with its result/error. + pub id: String, + + /// The payload content of this stanza. + pub payload: IqType, +} + +impl Iq { + /// Creates an `` stanza containing a get request. + pub fn from_get>(id: S, payload: impl IqGetPayload) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Get(payload.into()), + } + } + + /// Creates an `` stanza containing a set request. + pub fn from_set>(id: S, payload: impl IqSetPayload) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Set(payload.into()), + } + } + + /// Creates an empty `` stanza. + pub fn empty_result>(to: Jid, id: S) -> Iq { + Iq { + from: None, + to: Some(to), + id: id.into(), + payload: IqType::Result(None), + } + } + + /// Creates an `` stanza containing a result. + pub fn from_result>(id: S, payload: Option) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Result(payload.map(Into::into)), + } + } + + /// Creates an `` stanza containing an error. + pub fn from_error>(id: S, payload: StanzaError) -> Iq { + Iq { + from: None, + to: None, + id: id.into(), + payload: IqType::Error(payload), + } + } + + /// Sets the recipient of this stanza. + pub fn with_to(mut self, to: Jid) -> Iq { + self.to = Some(to); + self + } + + /// Sets the emitter of this stanza. + pub fn with_from(mut self, from: Jid) -> Iq { + self.from = Some(from); + self + } + + /// Sets the id of this stanza, in order to later match its response. + pub fn with_id(mut self, id: String) -> Iq { + self.id = id; + self + } +} + +impl TryFrom for Iq { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "iq", DEFAULT_NS); + let from = get_attr!(root, "from", Option); + let to = get_attr!(root, "to", Option); + let id = get_attr!(root, "id", Required); + let type_: String = get_attr!(root, "type", Required); + + let mut payload = None; + let mut error_payload = None; + for elem in root.children() { + if payload.is_some() { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + if type_ == "error" { + if elem.is("error", ns::DEFAULT_NS) { + if error_payload.is_some() { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + error_payload = Some(StanzaError::try_from(elem.clone())?); + } else if root.children().count() != 2 { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else { + payload = Some(elem.clone()); + } + } + + let type_ = if type_ == "get" { + if let Some(payload) = payload { + IqType::Get(payload) + } else { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else if type_ == "set" { + if let Some(payload) = payload { + IqType::Set(payload) + } else { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else if type_ == "result" { + if let Some(payload) = payload { + IqType::Result(Some(payload)) + } else { + IqType::Result(None) + } + } else if type_ == "error" { + if let Some(payload) = error_payload { + IqType::Error(payload) + } else { + return Err(Error::ParseError("Wrong number of children in iq element.")); + } + } else { + return Err(Error::ParseError("Unknown iq type.")); + }; + + Ok(Iq { + from, + to, + id, + payload: type_, + }) + } +} + +impl From for Element { + fn from(iq: Iq) -> Element { + let mut stanza = Element::builder("iq", ns::DEFAULT_NS) + .attr("from", iq.from) + .attr("to", iq.to) + .attr("id", iq.id) + .attr("type", &iq.payload) + .build(); + let elem = match iq.payload { + IqType::Get(elem) | IqType::Set(elem) | IqType::Result(Some(elem)) => elem, + IqType::Error(error) => error.into(), + IqType::Result(None) => return stanza, + }; + stanza.append_child(elem); + stanza + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::disco::DiscoInfoQuery; + use crate::stanza_error::{DefinedCondition, ErrorType}; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(IqType, 136); + assert_size!(Iq, 228); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(IqType, 272); + assert_size!(Iq, 456); + } + + #[test] + fn test_require_type() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = Iq::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = Iq::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + } + + #[test] + fn test_get() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let query: Element = "".parse().unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "foo"); + assert!(match iq.payload { + IqType::Get(element) => element == query, + _ => false, + }); + } + + #[test] + fn test_set() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let vcard: Element = "".parse().unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "vcard"); + assert!(match iq.payload { + IqType::Set(element) => element == vcard, + _ => false, + }); + } + + #[test] + fn test_result_empty() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "res"); + assert!(match iq.payload { + IqType::Result(None) => true, + _ => false, + }); + } + + #[test] + fn test_result() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let query: Element = "" + .parse() + .unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(&iq.id, "res"); + assert!(match iq.payload { + IqType::Result(Some(element)) => element == query, + _ => false, + }); + } + + #[test] + fn test_error() { + #[cfg(not(feature = "component"))] + let elem: Element = " + + + + + " + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = " + + + + + " + .parse() + .unwrap(); + let iq = Iq::try_from(elem).unwrap(); + assert_eq!(iq.from, None); + assert_eq!(iq.to, None); + assert_eq!(iq.id, "err1"); + match iq.payload { + IqType::Error(error) => { + assert_eq!(error.type_, ErrorType::Cancel); + assert_eq!(error.by, None); + assert_eq!( + error.defined_condition, + DefinedCondition::ServiceUnavailable + ); + assert_eq!(error.texts.len(), 0); + assert_eq!(error.other, None); + } + _ => panic!(), + } + } + + #[test] + fn test_children_invalid() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = Iq::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Wrong number of children in iq element."); + } + + #[test] + fn test_serialise() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let iq2 = Iq { + from: None, + to: None, + id: String::from("res"), + payload: IqType::Result(None), + }; + let elem2 = iq2.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_disco() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let iq = Iq::try_from(elem).unwrap(); + let disco_info = match iq.payload { + IqType::Get(payload) => DiscoInfoQuery::try_from(payload).unwrap(), + _ => panic!(), + }; + assert!(disco_info.node.is_none()); + } +} diff --git a/xmpp-parsers/src/jid_prep.rs b/xmpp-parsers/src/jid_prep.rs new file mode 100644 index 0000000..a78cc0b --- /dev/null +++ b/xmpp-parsers/src/jid_prep.rs @@ -0,0 +1,78 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqGetPayload, IqResultPayload}; +use crate::util::helpers::{JidCodec, Text}; +use jid::Jid; + +generate_element!( + /// Request from a client to stringprep/PRECIS a string into a JID. + JidPrepQuery, "jid", JID_PREP, + text: ( + /// The potential JID. + data: Text + ) +); + +impl IqGetPayload for JidPrepQuery {} + +impl JidPrepQuery { + /// Create a new JID Prep query. + pub fn new>(jid: J) -> JidPrepQuery { + JidPrepQuery { data: jid.into() } + } +} + +generate_element!( + /// Response from the server with the stringprep’d/PRECIS’d JID. + JidPrepResponse, "jid", JID_PREP, + text: ( + /// The JID. + jid: JidCodec + ) +); + +impl IqResultPayload for JidPrepResponse {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use jid::FullJid; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(JidPrepQuery, 12); + assert_size!(JidPrepResponse, 40); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(JidPrepQuery, 24); + assert_size!(JidPrepResponse, 80); + } + + #[test] + fn simple() { + let elem: Element = "ROMeo@montague.lit/orchard" + .parse() + .unwrap(); + let query = JidPrepQuery::try_from(elem).unwrap(); + assert_eq!(query.data, "ROMeo@montague.lit/orchard"); + + let elem: Element = "romeo@montague.lit/orchard" + .parse() + .unwrap(); + let response = JidPrepResponse::try_from(elem).unwrap(); + assert_eq!( + response.jid, + FullJid::new("romeo", "montague.lit", "orchard") + ); + } +} diff --git a/xmpp-parsers/src/jingle.rs b/xmpp-parsers/src/jingle.rs new file mode 100644 index 0000000..d15f6e8 --- /dev/null +++ b/xmpp-parsers/src/jingle.rs @@ -0,0 +1,911 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::IqSetPayload; +use crate::jingle_grouping::Group; +use crate::jingle_ibb::Transport as IbbTransport; +use crate::jingle_ice_udp::Transport as IceUdpTransport; +use crate::jingle_rtp::Description as RtpDescription; +use crate::jingle_s5b::Transport as Socks5Transport; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; + +generate_attribute!( + /// The action attribute. + Action, "action", { + /// Accept a content-add action received from another party. + ContentAccept => "content-accept", + + /// Add one or more new content definitions to the session. + ContentAdd => "content-add", + + /// Change the directionality of media sending. + ContentModify => "content-modify", + + /// Reject a content-add action received from another party. + ContentReject => "content-reject", + + /// Remove one or more content definitions from the session. + ContentRemove => "content-remove", + + /// Exchange information about parameters for an application type. + DescriptionInfo => "description-info", + + /// Exchange information about security preconditions. + SecurityInfo => "security-info", + + /// Definitively accept a session negotiation. + SessionAccept => "session-accept", + + /// Send session-level information, such as a ping or a ringing message. + SessionInfo => "session-info", + + /// Request negotiation of a new Jingle session. + SessionInitiate => "session-initiate", + + /// End an existing session. + SessionTerminate => "session-terminate", + + /// Accept a transport-replace action received from another party. + TransportAccept => "transport-accept", + + /// Exchange transport candidates. + TransportInfo => "transport-info", + + /// Reject a transport-replace action received from another party. + TransportReject => "transport-reject", + + /// Redefine a transport method or replace it with a different method. + TransportReplace => "transport-replace", + + /// --- Non-standard messages used by Jitsi Meet: + + /// Add a source to existing content. + SourceAdd => "source-add", + } +); + +generate_attribute!( + /// Which party originally generated the content type. + Creator, "creator", { + /// This content was created by the initiator of this session. + Initiator => "initiator", + + /// This content was created by the responder of this session. + Responder => "responder", + } +); + +generate_attribute!( + /// Which parties in the session will be generating content. + Senders, "senders", { + /// Both parties can send for this content. + Both => "both", + + /// Only the initiator can send for this content. + Initiator => "initiator", + + /// No one can send for this content. + None => "none", + + /// Only the responder can send for this content. + Responder => "responder", + } +); + +generate_attribute!( + /// How the content definition is to be interpreted by the recipient. The + /// meaning of this attribute matches the "Content-Disposition" header as + /// defined in RFC 2183 and applied to SIP by RFC 3261. + /// + /// Possible values are defined here: + /// https://www.iana.org/assignments/cont-disp/cont-disp.xhtml + Disposition, "disposition", { + /// Displayed automatically. + Inline => "inline", + + /// User controlled display. + Attachment => "attachment", + + /// Process as form response. + FormData => "form-data", + + /// Tunneled content to be processed silently. + Signal => "signal", + + /// The body is a custom ring tone to alert the user. + Alert => "alert", + + /// The body is displayed as an icon to the user. + Icon => "icon", + + /// The body should be displayed to the user. + Render => "render", + + /// The body contains a list of URIs that indicates the recipients of + /// the request. + RecipientListHistory => "recipient-list-history", + + /// The body describes a communications session, for example, an + /// RFC2327 SDP body. + Session => "session", + + /// Authenticated Identity Body. + Aib => "aib", + + /// The body describes an early communications session, for example, + /// and [RFC2327] SDP body. + EarlySession => "early-session", + + /// The body includes a list of URIs to which URI-list services are to + /// be applied. + RecipientList => "recipient-list", + + /// The payload of the message carrying this Content-Disposition header + /// field value is an Instant Message Disposition Notification as + /// requested in the corresponding Instant Message. + Notification => "notification", + + /// The body needs to be handled according to a reference to the body + /// that is located in the same SIP message as the body. + ByReference => "by-reference", + + /// The body contains information associated with an Info Package. + InfoPackage => "info-package", + + /// The body describes either metadata about the RS or the reason for + /// the metadata snapshot request as determined by the MIME value + /// indicated in the Content-Type. + RecordingSession => "recording-session", + }, Default = Session +); + +generate_id!( + /// An unique identifier in a session, referencing a + /// [struct.Content.html](Content element). + ContentId +); + +/// Enum wrapping all of the various supported descriptions of a Content. +#[derive(Debug, Clone, PartialEq)] +pub enum Description { + /// Jingle RTP Sessions (XEP-0167) description. + Rtp(RtpDescription), + + /// To be used for any description that isn’t known at compile-time. + Unknown(Element), +} + +impl TryFrom for Description { + type Error = Error; + + fn try_from(elem: Element) -> Result { + Ok(if elem.is("description", ns::JINGLE_RTP) { + Description::Rtp(RtpDescription::try_from(elem)?) + } else { + Description::Unknown(elem) + }) + } +} + +impl From for Description { + fn from(desc: RtpDescription) -> Description { + Description::Rtp(desc) + } +} + +impl From for Element { + fn from(desc: Description) -> Element { + match desc { + Description::Rtp(desc) => desc.into(), + Description::Unknown(elem) => elem, + } + } +} + +/// Enum wrapping all of the various supported transports of a Content. +#[derive(Debug, Clone, PartialEq)] +pub enum Transport { + /// Jingle ICE-UDP Bytestreams (XEP-0176) transport. + IceUdp(IceUdpTransport), + + /// Jingle In-Band Bytestreams (XEP-0261) transport. + Ibb(IbbTransport), + + /// Jingle SOCKS5 Bytestreams (XEP-0260) transport. + Socks5(Socks5Transport), + + /// To be used for any transport that isn’t known at compile-time. + Unknown(Element), +} + +impl TryFrom for Transport { + type Error = Error; + + fn try_from(elem: Element) -> Result { + Ok(if elem.is("transport", ns::JINGLE_ICE_UDP) { + Transport::IceUdp(IceUdpTransport::try_from(elem)?) + } else if elem.is("transport", ns::JINGLE_IBB) { + Transport::Ibb(IbbTransport::try_from(elem)?) + } else if elem.is("transport", ns::JINGLE_S5B) { + Transport::Socks5(Socks5Transport::try_from(elem)?) + } else { + Transport::Unknown(elem) + }) + } +} + +impl From for Transport { + fn from(transport: IceUdpTransport) -> Transport { + Transport::IceUdp(transport) + } +} + +impl From for Transport { + fn from(transport: IbbTransport) -> Transport { + Transport::Ibb(transport) + } +} + +impl From for Transport { + fn from(transport: Socks5Transport) -> Transport { + Transport::Socks5(transport) + } +} + +impl From for Element { + fn from(transport: Transport) -> Element { + match transport { + Transport::IceUdp(transport) => transport.into(), + Transport::Ibb(transport) => transport.into(), + Transport::Socks5(transport) => transport.into(), + Transport::Unknown(elem) => elem, + } + } +} + +generate_element!( + /// Describes a session’s content, there can be multiple content in one + /// session. + Content, "content", JINGLE, + attributes: [ + /// Who created this content. + creator: Option = "creator", + + /// How the content definition is to be interpreted by the recipient. + disposition: Default = "disposition", + + /// A per-session unique identifier for this content. + name: Required = "name", + + /// Who can send data for this content. + senders: Option = "senders", + ], + children: [ + /// What to send. + description: Option = ("description", *) => Description, + + /// How to send it. + transport: Option = ("transport", *) => Transport, + + /// With which security. + security: Option = ("security", JINGLE) => Element + ] +); + +impl Content { + /// Create a new content. + pub fn new(creator: Creator, name: ContentId) -> Content { + Content { + creator: Some(creator), + name, + disposition: Disposition::Session, + senders: Some(Senders::Both), + description: None, + transport: None, + security: None, + } + } + + /// Set how the content is to be interpreted by the recipient. + pub fn with_disposition(mut self, disposition: Disposition) -> Content { + self.disposition = disposition; + self + } + + /// Specify who can send data for this content. + pub fn with_senders(mut self, senders: Senders) -> Content { + self.senders = Some(senders); + self + } + + /// Set the description of this content. + pub fn with_description>(mut self, description: D) -> Content { + self.description = Some(description.into()); + self + } + + /// Set the transport of this content. + pub fn with_transport>(mut self, transport: T) -> Content { + self.transport = Some(transport.into()); + self + } + + /// Set the security of this content. + pub fn with_security(mut self, security: Element) -> Content { + self.security = Some(security); + self + } +} + +/// Lists the possible reasons to be included in a Jingle iq. +#[derive(Debug, Clone, PartialEq)] +pub enum Reason { + /// The party prefers to use an existing session with the peer rather than + /// initiate a new session; the Jingle session ID of the alternative + /// session SHOULD be provided as the XML character data of the + /// child. + AlternativeSession, //(String), + + /// The party is busy and cannot accept a session. + Busy, + + /// The initiator wishes to formally cancel the session initiation request. + Cancel, + + /// The action is related to connectivity problems. + ConnectivityError, + + /// The party wishes to formally decline the session. + Decline, + + /// The session length has exceeded a pre-defined time limit (e.g., a + /// meeting hosted at a conference service). + Expired, + + /// The party has been unable to initialize processing related to the + /// application type. + FailedApplication, + + /// The party has been unable to establish connectivity for the transport + /// method. + FailedTransport, + + /// The action is related to a non-specific application error. + GeneralError, + + /// The entity is going offline or is no longer available. + Gone, + + /// The party supports the offered application type but does not support + /// the offered or negotiated parameters. + IncompatibleParameters, + + /// The action is related to media processing problems. + MediaError, + + /// The action is related to a violation of local security policies. + SecurityError, + + /// The action is generated during the normal course of state management + /// and does not reflect any error. + Success, + + /// A request has not been answered so the sender is timing out the + /// request. + Timeout, + + /// The party supports none of the offered application types. + UnsupportedApplications, + + /// The party supports none of the offered transport methods. + UnsupportedTransports, +} + +impl FromStr for Reason { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "alternative-session" => Reason::AlternativeSession, + "busy" => Reason::Busy, + "cancel" => Reason::Cancel, + "connectivity-error" => Reason::ConnectivityError, + "decline" => Reason::Decline, + "expired" => Reason::Expired, + "failed-application" => Reason::FailedApplication, + "failed-transport" => Reason::FailedTransport, + "general-error" => Reason::GeneralError, + "gone" => Reason::Gone, + "incompatible-parameters" => Reason::IncompatibleParameters, + "media-error" => Reason::MediaError, + "security-error" => Reason::SecurityError, + "success" => Reason::Success, + "timeout" => Reason::Timeout, + "unsupported-applications" => Reason::UnsupportedApplications, + "unsupported-transports" => Reason::UnsupportedTransports, + + _ => return Err(Error::ParseError("Unknown reason.")), + }) + } +} + +impl From for Element { + fn from(reason: Reason) -> Element { + Element::builder( + match reason { + Reason::AlternativeSession => "alternative-session", + Reason::Busy => "busy", + Reason::Cancel => "cancel", + Reason::ConnectivityError => "connectivity-error", + Reason::Decline => "decline", + Reason::Expired => "expired", + Reason::FailedApplication => "failed-application", + Reason::FailedTransport => "failed-transport", + Reason::GeneralError => "general-error", + Reason::Gone => "gone", + Reason::IncompatibleParameters => "incompatible-parameters", + Reason::MediaError => "media-error", + Reason::SecurityError => "security-error", + Reason::Success => "success", + Reason::Timeout => "timeout", + Reason::UnsupportedApplications => "unsupported-applications", + Reason::UnsupportedTransports => "unsupported-transports", + }, + ns::JINGLE, + ) + .build() + } +} + +type Lang = String; + +/// Informs the recipient of something. +#[derive(Debug, Clone, PartialEq)] +pub struct ReasonElement { + /// The list of possible reasons to be included in a Jingle iq. + pub reason: Reason, + + /// A human-readable description of this reason. + pub texts: BTreeMap, +} + +impl fmt::Display for ReasonElement { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}", Element::from(self.reason.clone()).name())?; + if let Some(text) = self.texts.get("en") { + write!(fmt, ": {}", text)?; + } else if let Some(text) = self.texts.get("") { + write!(fmt, ": {}", text)?; + } + Ok(()) + } +} + +impl TryFrom for ReasonElement { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "reason", JINGLE); + check_no_attributes!(elem, "reason"); + let mut reason = None; + let mut texts = BTreeMap::new(); + for child in elem.children() { + if child.is("text", ns::JINGLE) { + check_no_children!(child, "text"); + check_no_unknown_attributes!(child, "text", ["xml:lang"]); + let lang = get_attr!(elem, "xml:lang", Default); + if texts.insert(lang, child.text()).is_some() { + return Err(Error::ParseError( + "Text element present twice for the same xml:lang.", + )); + } + } else if child.has_ns(ns::JINGLE) { + if reason.is_some() { + return Err(Error::ParseError( + "Reason must not have more than one reason.", + )); + } + check_no_children!(child, "reason"); + check_no_attributes!(child, "reason"); + reason = Some(child.name().parse()?); + } else { + return Err(Error::ParseError("Reason contains a foreign element.")); + } + } + let reason = reason.ok_or(Error::ParseError("Reason doesn’t contain a valid reason."))?; + Ok(ReasonElement { reason, texts }) + } +} + +impl From for Element { + fn from(reason: ReasonElement) -> Element { + Element::builder("reason", ns::JINGLE) + .append(Element::from(reason.reason)) + .append_all(reason.texts.into_iter().map(|(lang, text)| { + Element::builder("text", ns::JINGLE) + .attr("xml:lang", lang) + .append(text) + })) + .build() + } +} + +generate_id!( + /// Unique identifier for a session between two JIDs. + SessionId +); + +/// The main Jingle container, to be included in an iq stanza. +#[derive(Debug, Clone, PartialEq)] +pub struct Jingle { + /// The action to execute on both ends. + pub action: Action, + + /// Who the initiator is. + pub initiator: Option, + + /// Who the responder is. + pub responder: Option, + + /// Unique session identifier between two entities. + pub sid: SessionId, + + /// A list of contents to be negotiated in this session. + pub contents: Vec, + + /// An optional reason. + pub reason: Option, + + /// An optional grouping. + pub group: Option, + + /// Payloads to be included. + pub other: Vec, +} + +impl IqSetPayload for Jingle {} + +impl Jingle { + /// Create a new Jingle element. + pub fn new(action: Action, sid: SessionId) -> Jingle { + Jingle { + action, + sid, + initiator: None, + responder: None, + contents: Vec::new(), + reason: None, + group: None, + other: Vec::new(), + } + } + + /// Set the initiator’s JID. + pub fn with_initiator(mut self, initiator: Jid) -> Jingle { + self.initiator = Some(initiator); + self + } + + /// Set the responder’s JID. + pub fn with_responder(mut self, responder: Jid) -> Jingle { + self.responder = Some(responder); + self + } + + /// Add a content to this Jingle container. + pub fn add_content(mut self, content: Content) -> Jingle { + self.contents.push(content); + self + } + + /// Set the reason in this Jingle container. + pub fn set_reason(mut self, reason: ReasonElement) -> Jingle { + self.reason = Some(reason); + self + } + + /// Set the grouping in this Jingle container. + pub fn set_group(mut self, group: Group) -> Jingle { + self.group = Some(group); + self + } +} + +impl TryFrom for Jingle { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "jingle", JINGLE, "Jingle"); + check_no_unknown_attributes!(root, "Jingle", ["action", "initiator", "responder", "sid"]); + + let mut jingle = Jingle { + action: get_attr!(root, "action", Required), + initiator: get_attr!(root, "initiator", Option), + responder: get_attr!(root, "responder", Option), + sid: get_attr!(root, "sid", Required), + contents: vec![], + reason: None, + group: None, + other: vec![], + }; + + for child in root.children().cloned() { + if child.is("content", ns::JINGLE) { + let content = Content::try_from(child)?; + jingle.contents.push(content); + } else if child.is("reason", ns::JINGLE) { + if jingle.reason.is_some() { + return Err(Error::ParseError( + "Jingle must not have more than one reason.", + )); + } + let reason = ReasonElement::try_from(child)?; + jingle.reason = Some(reason); + } else if child.is("group", ns::JINGLE_GROUPING) { + if jingle.group.is_some() { + return Err(Error::ParseError( + "Jingle must not have more than one grouping.", + )); + } + let group = Group::try_from(child)?; + jingle.group = Some(group); + } else { + jingle.other.push(child); + } + } + + Ok(jingle) + } +} + +impl From for Element { + fn from(jingle: Jingle) -> Element { + Element::builder("jingle", ns::JINGLE) + .attr("action", jingle.action) + .attr("initiator", jingle.initiator) + .attr("responder", jingle.responder) + .attr("sid", jingle.sid) + .append_all(jingle.contents) + .append_all(jingle.reason.map(Element::from)) + .append_all(jingle.group.map(Element::from)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Action, 1); + assert_size!(Creator, 1); + assert_size!(Senders, 1); + assert_size!(Disposition, 1); + assert_size!(ContentId, 12); + assert_size!(Content, 252); + assert_size!(Reason, 1); + assert_size!(ReasonElement, 16); + assert_size!(SessionId, 12); + assert_size!(Jingle, 152); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Action, 1); + assert_size!(Creator, 1); + assert_size!(Senders, 1); + assert_size!(Disposition, 1); + assert_size!(ContentId, 24); + assert_size!(Content, 504); + assert_size!(Reason, 1); + assert_size!(ReasonElement, 32); + assert_size!(SessionId, 24); + assert_size!(Jingle, 304); + } + + #[test] + fn test_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.action, Action::SessionInitiate); + assert_eq!(jingle.sid, SessionId(String::from("coucou"))); + } + + #[test] + fn test_invalid_jingle() { + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'action' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'sid' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'action' attribute."); + } + + #[test] + fn test_content() { + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.contents[0].creator, Creator::Initiator); + assert_eq!(jingle.contents[0].name, ContentId(String::from("coucou"))); + assert_eq!(jingle.contents[0].senders, Senders::Both); + assert_eq!(jingle.contents[0].disposition, Disposition::Session); + + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.contents[0].senders, Senders::Both); + + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + assert_eq!(jingle.contents[0].disposition, Disposition::EarlySession); + } + + #[test] + fn test_invalid_content() { + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'creator' missing."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'name' missing."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'creator' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'senders' attribute."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'senders' attribute."); + } + + #[test] + fn test_reason() { + let elem: Element = "".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + let reason = jingle.reason.unwrap(); + assert_eq!(reason.reason, Reason::Success); + assert_eq!(reason.texts, BTreeMap::new()); + + let elem: Element = "coucou".parse().unwrap(); + let jingle = Jingle::try_from(elem).unwrap(); + let reason = jingle.reason.unwrap(); + assert_eq!(reason.reason, Reason::Success); + assert_eq!(reason.texts.get(""), Some(&String::from("coucou"))); + } + + #[test] + fn test_invalid_reason() { + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Reason doesn’t contain a valid reason."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown reason."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Reason contains a foreign element."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Jingle must not have more than one reason."); + + let elem: Element = "".parse().unwrap(); + let error = Jingle::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Text element present twice for the same xml:lang."); + } + + #[test] + fn test_serialize_jingle() { + let reference: Element = "" + .parse() + .unwrap(); + + let jingle = Jingle { + action: Action::SessionInitiate, + initiator: None, + responder: None, + sid: SessionId(String::from("a73sjjvkla37jfea")), + contents: vec![Content { + creator: Creator::Initiator, + disposition: Disposition::default(), + name: ContentId(String::from("this-is-a-stub")), + senders: Senders::default(), + description: Some(Description::Unknown( + Element::builder("description", "urn:xmpp:jingle:apps:stub:0").build(), + )), + transport: Some(Transport::Unknown( + Element::builder("transport", "urn:xmpp:jingle:transports:stub:0").build(), + )), + security: None, + }], + reason: None, + group: None, + other: vec![], + }; + let serialized: Element = jingle.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/jingle_dtls_srtp.rs b/xmpp-parsers/src/jingle_dtls_srtp.rs new file mode 100644 index 0000000..76dd92e --- /dev/null +++ b/xmpp-parsers/src/jingle_dtls_srtp.rs @@ -0,0 +1,112 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::hashes::{Algo, Hash}; +use crate::util::error::Error; +use crate::util::helpers::ColonSeparatedHex; + +generate_attribute!( + /// Indicates which of the end points should initiate the TCP connection establishment. + Setup, "setup", { + /// The endpoint will initiate an outgoing connection. + Active => "active", + + /// The endpoint will accept an incoming connection. + Passive => "passive", + + /// The endpoint is willing to accept an incoming connection or to initiate an outgoing + /// connection. + Actpass => "actpass", + + /* + /// The endpoint does not want the connection to be established for the time being. + /// + /// Note that this value isn’t used, as per the XEP. + Holdconn => "holdconn", + */ + } +); + +// TODO: use a hashes::Hash instead of two different fields here. +generate_element!( + /// Fingerprint of the key used for a DTLS handshake. + Fingerprint, "fingerprint", JINGLE_DTLS, + attributes: [ + /// The hash algorithm used for this fingerprint. + hash: Required = "hash", + + /// Indicates which of the end points should initiate the TCP connection establishment. + setup: Option = "setup", + + /// Indicates whether DTLS is mandatory + required: Option = "required" + ], + text: ( + /// Hash value of this fingerprint. + value: ColonSeparatedHex> + ) +); + +impl Fingerprint { + /// Create a new Fingerprint from a Setup and a Hash. + pub fn from_hash(setup: Setup, hash: Hash) -> Fingerprint { + Fingerprint { + hash: hash.algo, + setup: Some(setup), + value: hash.hash, + required: None, + } + } + + /// Create a new Fingerprint from a Setup and parsing the hash. + pub fn from_colon_separated_hex( + setup: Setup, + algo: &str, + hash: &str, + ) -> Result { + let algo = algo.parse()?; + let hash = Hash::from_colon_separated_hex(algo, hash)?; + Ok(Fingerprint::from_hash(setup, hash)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Setup, 1); + assert_size!(Fingerprint, 32); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Setup, 1); + assert_size!(Fingerprint, 64); + } + + #[test] + fn test_ex1() { + let elem: Element = "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" + .parse() + .unwrap(); + let fingerprint = Fingerprint::try_from(elem).unwrap(); + assert_eq!(fingerprint.setup, Some(Setup::Actpass)); + assert_eq!(fingerprint.hash, Algo::Sha_256); + assert_eq!( + fingerprint.value, + [ + 2, 26, 204, 84, 39, 171, 235, 156, 83, 63, 62, 75, 101, 46, 125, 70, 63, 84, 66, + 205, 84, 241, 122, 3, 162, 125, 249, 176, 127, 70, 25, 178 + ] + ); + } +} diff --git a/xmpp-parsers/src/jingle_ft.rs b/xmpp-parsers/src/jingle_ft.rs new file mode 100644 index 0000000..d44b7e1 --- /dev/null +++ b/xmpp-parsers/src/jingle_ft.rs @@ -0,0 +1,620 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::date::DateTime; +use crate::hashes::Hash; +use crate::jingle::{ContentId, Creator}; +use crate::ns; +use crate::util::error::Error; +use minidom::{Element, Node}; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::str::FromStr; + +generate_element!( + /// Represents a range in a file. + #[derive(Default)] + Range, "range", JINGLE_FT, + attributes: [ + /// The offset in bytes from the beginning of the file. + offset: Default = "offset", + + /// The length in bytes of the range, or None to be the entire + /// remaining of the file. + length: Option = "length" + ], + children: [ + /// List of hashes for this range. + hashes: Vec = ("hash", HASHES) => Hash + ] +); + +impl Range { + /// Creates a new range. + pub fn new() -> Range { + Default::default() + } +} + +type Lang = String; + +generate_id!( + /// Wrapper for a file description. + Desc +); + +/// Represents a file to be transferred. +#[derive(Debug, Clone, Default)] +pub struct File { + /// The date of last modification of this file. + pub date: Option, + + /// The MIME type of this file. + pub media_type: Option, + + /// The name of this file. + pub name: Option, + + /// The description of this file, possibly localised. + pub descs: BTreeMap, + + /// The size of this file, in bytes. + pub size: Option, + + /// Used to request only a part of this file. + pub range: Option, + + /// A list of hashes matching this entire file. + pub hashes: Vec, +} + +impl File { + /// Creates a new file descriptor. + pub fn new() -> File { + File::default() + } + + /// Sets the date of last modification on this file. + pub fn with_date(mut self, date: DateTime) -> File { + self.date = Some(date); + self + } + + /// Sets the date of last modification on this file from an ISO-8601 + /// string. + pub fn with_date_str(mut self, date: &str) -> Result { + self.date = Some(DateTime::from_str(date)?); + Ok(self) + } + + /// Sets the MIME type of this file. + pub fn with_media_type(mut self, media_type: String) -> File { + self.media_type = Some(media_type); + self + } + + /// Sets the name of this file. + pub fn with_name(mut self, name: String) -> File { + self.name = Some(name); + self + } + + /// Sets a description for this file. + pub fn add_desc(mut self, lang: &str, desc: Desc) -> File { + self.descs.insert(Lang::from(lang), desc); + self + } + + /// Sets the file size of this file, in bytes. + pub fn with_size(mut self, size: u64) -> File { + self.size = Some(size); + self + } + + /// Request only a range of this file. + pub fn with_range(mut self, range: Range) -> File { + self.range = Some(range); + self + } + + /// Add a hash on this file. + pub fn add_hash(mut self, hash: Hash) -> File { + self.hashes.push(hash); + self + } +} + +impl TryFrom for File { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "file", JINGLE_FT); + check_no_attributes!(elem, "file"); + + let mut file = File { + date: None, + media_type: None, + name: None, + descs: BTreeMap::new(), + size: None, + range: None, + hashes: vec![], + }; + + for child in elem.children() { + if child.is("date", ns::JINGLE_FT) { + if file.date.is_some() { + return Err(Error::ParseError("File must not have more than one date.")); + } + file.date = Some(child.text().parse()?); + } else if child.is("media-type", ns::JINGLE_FT) { + if file.media_type.is_some() { + return Err(Error::ParseError( + "File must not have more than one media-type.", + )); + } + file.media_type = Some(child.text()); + } else if child.is("name", ns::JINGLE_FT) { + if file.name.is_some() { + return Err(Error::ParseError("File must not have more than one name.")); + } + file.name = Some(child.text()); + } else if child.is("desc", ns::JINGLE_FT) { + let lang = get_attr!(child, "xml:lang", Default); + let desc = Desc(child.text()); + if file.descs.insert(lang, desc).is_some() { + return Err(Error::ParseError( + "Desc element present twice for the same xml:lang.", + )); + } + } else if child.is("size", ns::JINGLE_FT) { + if file.size.is_some() { + return Err(Error::ParseError("File must not have more than one size.")); + } + file.size = Some(child.text().parse()?); + } else if child.is("range", ns::JINGLE_FT) { + if file.range.is_some() { + return Err(Error::ParseError("File must not have more than one range.")); + } + file.range = Some(Range::try_from(child.clone())?); + } else if child.is("hash", ns::HASHES) { + file.hashes.push(Hash::try_from(child.clone())?); + } else { + return Err(Error::ParseError("Unknown element in JingleFT file.")); + } + } + + Ok(file) + } +} + +impl From for Element { + fn from(file: File) -> Element { + Element::builder("file", ns::JINGLE_FT) + .append_all( + file.date + .map(|date| Element::builder("date", ns::JINGLE_FT).append(date)), + ) + .append_all( + file.media_type.map(|media_type| { + Element::builder("media-type", ns::JINGLE_FT).append(media_type) + }), + ) + .append_all( + file.name + .map(|name| Element::builder("name", ns::JINGLE_FT).append(name)), + ) + .append_all(file.descs.into_iter().map(|(lang, desc)| { + Element::builder("desc", ns::JINGLE_FT) + .attr("xml:lang", lang) + .append(desc.0) + })) + .append_all( + file.size.map(|size| { + Element::builder("size", ns::JINGLE_FT).append(format!("{}", size)) + }), + ) + .append_all(file.range) + .append_all(file.hashes) + .build() + } +} + +/// A wrapper element for a file. +#[derive(Debug, Clone)] +pub struct Description { + /// The actual file descriptor. + pub file: File, +} + +impl TryFrom for Description { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "description", JINGLE_FT, "JingleFT description"); + check_no_attributes!(elem, "JingleFT description"); + let mut file = None; + for child in elem.children() { + if file.is_some() { + return Err(Error::ParseError( + "JingleFT description element must have exactly one child.", + )); + } + file = Some(File::try_from(child.clone())?); + } + if file.is_none() { + return Err(Error::ParseError( + "JingleFT description element must have exactly one child.", + )); + } + Ok(Description { + file: file.unwrap(), + }) + } +} + +impl From for Element { + fn from(description: Description) -> Element { + Element::builder("description", ns::JINGLE_FT) + .append(Node::Element(description.file.into())) + .build() + } +} + +/// A checksum for checking that the file has been transferred correctly. +#[derive(Debug, Clone)] +pub struct Checksum { + /// The identifier of the file transfer content. + pub name: ContentId, + + /// The creator of this file transfer. + pub creator: Creator, + + /// The file being checksummed. + pub file: File, +} + +impl TryFrom for Checksum { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "checksum", JINGLE_FT); + check_no_unknown_attributes!(elem, "checksum", ["name", "creator"]); + let mut file = None; + for child in elem.children() { + if file.is_some() { + return Err(Error::ParseError( + "JingleFT checksum element must have exactly one child.", + )); + } + file = Some(File::try_from(child.clone())?); + } + if file.is_none() { + return Err(Error::ParseError( + "JingleFT checksum element must have exactly one child.", + )); + } + Ok(Checksum { + name: get_attr!(elem, "name", Required), + creator: get_attr!(elem, "creator", Required), + file: file.unwrap(), + }) + } +} + +impl From for Element { + fn from(checksum: Checksum) -> Element { + Element::builder("checksum", ns::JINGLE_FT) + .attr("name", checksum.name) + .attr("creator", checksum.creator) + .append(Node::Element(checksum.file.into())) + .build() + } +} + +generate_element!( + /// A notice that the file transfer has been completed. + Received, "received", JINGLE_FT, + attributes: [ + /// The content identifier of this Jingle session. + name: Required = "name", + + /// The creator of this file transfer. + creator: Required = "creator", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::hashes::Algo; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Range, 40); + assert_size!(File, 128); + assert_size!(Description, 128); + assert_size!(Checksum, 144); + assert_size!(Received, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Range, 48); + assert_size!(File, 184); + assert_size!(Description, 184); + assert_size!(Checksum, 216); + assert_size!(Received, 32); + } + + #[test] + fn test_description() { + let elem: Element = r#" + + + text/plain + test.txt + 2015-07-26T21:46:00+01:00 + 6144 + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!(desc.file.media_type, Some(String::from("text/plain"))); + assert_eq!(desc.file.name, Some(String::from("test.txt"))); + assert_eq!(desc.file.descs, BTreeMap::new()); + assert_eq!( + desc.file.date, + DateTime::from_str("2015-07-26T21:46:00+01:00").ok() + ); + assert_eq!(desc.file.size, Some(6144u64)); + assert_eq!(desc.file.range, None); + assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1); + assert_eq!( + desc.file.hashes[0].hash, + base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap() + ); + } + + #[test] + fn test_request() { + let elem: Element = r#" + + + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!(desc.file.media_type, None); + assert_eq!(desc.file.name, None); + assert_eq!(desc.file.descs, BTreeMap::new()); + assert_eq!(desc.file.date, None); + assert_eq!(desc.file.size, None); + assert_eq!(desc.file.range, None); + assert_eq!(desc.file.hashes[0].algo, Algo::Sha_1); + assert_eq!( + desc.file.hashes[0].hash, + base64::decode("w0mcJylzCn+AfvuGdqkty2+KP48=").unwrap() + ); + } + + #[test] + fn test_descs() { + let elem: Element = r#" + + + text/plain + Fichier secret ! + Secret file! + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!( + desc.file.descs.keys().cloned().collect::>(), + ["en", "fr"] + ); + assert_eq!(desc.file.descs["en"], Desc(String::from("Secret file!"))); + assert_eq!( + desc.file.descs["fr"], + Desc(String::from("Fichier secret !")) + ); + + let elem: Element = r#" + + + text/plain + Fichier secret ! + Secret file! + w0mcJylzCn+AfvuGdqkty2+KP48= + + +"# + .parse() + .unwrap(); + let error = Description::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Desc element present twice for the same xml:lang."); + } + + #[test] + fn test_received() { + let elem: Element = "".parse().unwrap(); + let received = Received::try_from(elem).unwrap(); + assert_eq!(received.name, ContentId(String::from("coucou"))); + assert_eq!(received.creator, Creator::Initiator); + let elem2 = Element::from(received.clone()); + let received2 = Received::try_from(elem2).unwrap(); + assert_eq!(received2.name, ContentId(String::from("coucou"))); + assert_eq!(received2.creator, Creator::Initiator); + + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in received element."); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'name' missing."); + + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'creator' attribute."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_received() { + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in received element."); + } + + #[test] + fn test_checksum() { + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let hash = vec![ + 195, 73, 156, 39, 41, 115, 10, 127, 128, 126, 251, 134, 118, 169, 45, 203, 111, 138, + 63, 143, + ]; + let checksum = Checksum::try_from(elem).unwrap(); + assert_eq!(checksum.name, ContentId(String::from("coucou"))); + assert_eq!(checksum.creator, Creator::Initiator); + assert_eq!( + checksum.file.hashes, + vec!(Hash { + algo: Algo::Sha_1, + hash: hash.clone() + }) + ); + let elem2 = Element::from(checksum); + let checksum2 = Checksum::try_from(elem2).unwrap(); + assert_eq!(checksum2.name, ContentId(String::from("coucou"))); + assert_eq!(checksum2.creator, Creator::Initiator); + assert_eq!( + checksum2.file.hashes, + vec!(Hash { + algo: Algo::Sha_1, + hash: hash.clone() + }) + ); + + let elem: Element = "".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a file element."); + + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'name' missing."); + + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'creator' attribute."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_checksum() { + let elem: Element = "w0mcJylzCn+AfvuGdqkty2+KP48=".parse().unwrap(); + let error = Checksum::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in checksum element."); + } + + #[test] + fn test_range() { + let elem: Element = "" + .parse() + .unwrap(); + let range = Range::try_from(elem).unwrap(); + assert_eq!(range.offset, 0); + assert_eq!(range.length, None); + assert_eq!(range.hashes, vec!()); + + let elem: Element = "kHp5RSzW/h7Gm1etSf90Mr5PC/k=".parse().unwrap(); + let hashes = vec![Hash { + algo: Algo::Sha_1, + hash: vec![ + 144, 122, 121, 69, 44, 214, 254, 30, 198, 155, 87, 173, 73, 255, 116, 50, 190, 79, + 11, 249, + ], + }]; + let range = Range::try_from(elem).unwrap(); + assert_eq!(range.offset, 2048); + assert_eq!(range.length, Some(1024)); + assert_eq!(range.hashes, hashes); + let elem2 = Element::from(range); + let range2 = Range::try_from(elem2).unwrap(); + assert_eq!(range2.offset, 2048); + assert_eq!(range2.length, Some(1024)); + assert_eq!(range2.hashes, hashes); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_range() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Range::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in range element."); + } +} diff --git a/xmpp-parsers/src/jingle_grouping.rs b/xmpp-parsers/src/jingle_grouping.rs new file mode 100644 index 0000000..01c882d --- /dev/null +++ b/xmpp-parsers/src/jingle_grouping.rs @@ -0,0 +1,90 @@ +// Copyright (c) 2020 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::jingle::ContentId; + +generate_attribute!( + /// The semantics of the grouping. + Semantics, "semantics", { + /// Lip synchronsation. + Ls => "LS", + + /// Bundle. + Bundle => "BUNDLE", + } +); + +generate_element!( + /// Describes a content that should be grouped with other ones. + Content, "content", JINGLE_GROUPING, + attributes: [ + /// The name of the matching [`Content`](crate::jingle::Content). + name: Required = "name", + ] +); + +impl Content { + /// Creates a new element. + pub fn new(name: &str) -> Content { + Content { + name: ContentId(name.to_string()), + } + } +} + +generate_element!( + /// A semantic group of contents. + Group, "group", JINGLE_GROUPING, + attributes: [ + /// Semantics of the grouping. + semantics: Required = "semantics", + ], + children: [ + /// List of contents that should be grouped with each other. + contents: Vec = ("content", JINGLE_GROUPING) => Content + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Semantics, 1); + assert_size!(Content, 12); + assert_size!(Group, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Semantics, 1); + assert_size!(Content, 24); + assert_size!(Group, 32); + } + + #[test] + fn parse_group() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let group = Group::try_from(elem).unwrap(); + assert_eq!(group.semantics, Semantics::Bundle); + assert_eq!(group.contents.len(), 2); + assert_eq!( + group.contents, + &[Content::new("voice"), Content::new("webcam")] + ); + } +} diff --git a/xmpp-parsers/src/jingle_ibb.rs b/xmpp-parsers/src/jingle_ibb.rs new file mode 100644 index 0000000..3018842 --- /dev/null +++ b/xmpp-parsers/src/jingle_ibb.rs @@ -0,0 +1,113 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ibb::{Stanza, StreamId}; + +generate_element!( +/// Describes an [In-Band Bytestream](https://xmpp.org/extensions/xep-0047.html) +/// Jingle transport, see also the [IBB module](../ibb.rs). +Transport, "transport", JINGLE_IBB, +attributes: [ + /// Maximum size in bytes for each chunk. + block_size: Required = "block-size", + + /// The identifier to be used to create a stream. + sid: Required = "sid", + + /// Which stanza type to use to exchange data. + stanza: Default = "stanza", +]); + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Transport, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Transport, 32); + } + + #[test] + fn test_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.block_size, 3); + assert_eq!(transport.sid, StreamId(String::from("coucou"))); + assert_eq!(transport.stanza, Stanza::Iq); + } + + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'block-size' missing."); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!( + message.to_string(), + "number too large to fit in target type" + ); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(message.to_string(), "invalid digit found in string"); + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'sid' missing."); + } + + #[test] + fn test_invalid_stanza() { + let elem: Element = "".parse().unwrap(); + let error = Transport::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'stanza' attribute."); + } +} diff --git a/xmpp-parsers/src/jingle_ice_udp.rs b/xmpp-parsers/src/jingle_ice_udp.rs new file mode 100644 index 0000000..2f89d97 --- /dev/null +++ b/xmpp-parsers/src/jingle_ice_udp.rs @@ -0,0 +1,243 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::jingle_dtls_srtp::Fingerprint; +use std::net::IpAddr; + +generate_empty_element!( + /// Specifies the ability to multiplex RTP Data and Control Packets on a single port as + /// described in RFC 5761. + RtcpMux, + "rtcp-mux", + JINGLE_ICE_UDP +); + +generate_element!( + /// Wrapper element for an ICE-UDP transport. + Transport, "transport", JINGLE_ICE_UDP, + attributes: [ + /// A Password as defined in ICE-CORE. + pwd: Option = "pwd", + + /// A User Fragment as defined in ICE-CORE. + ufrag: Option = "ufrag", + ], + children: [ + /// List of candidates for this ICE-UDP session. + candidates: Vec = ("candidate", JINGLE_ICE_UDP) => Candidate, + + /// Fingerprint of the key used for the DTLS handshake. + fingerprint: Option = ("fingerprint", JINGLE_DTLS) => Fingerprint, + + /// Indicates that RTCP can be muxed + rtcp_mux: Option = ("rtcp-mux", JINGLE_ICE_UDP) => RtcpMux, + + /// Details of the Colibri WebSocket + web_socket: Option = ("web-socket", JITSI_COLIBRI) => WebSocket + ] +); + +impl Transport { + /// Create a new ICE-UDP transport. + pub fn new() -> Transport { + Transport { + pwd: None, + ufrag: None, + candidates: Vec::new(), + fingerprint: None, + rtcp_mux: None, + web_socket: None, + } + } + + /// Add a candidate to this transport. + pub fn add_candidate(mut self, candidate: Candidate) -> Self { + self.candidates.push(candidate); + self + } + + /// Set the DTLS-SRTP fingerprint of this transport. + pub fn with_fingerprint(mut self, fingerprint: Fingerprint) -> Self { + self.fingerprint = Some(fingerprint); + self + } +} + +generate_element!( + /// Colibri WebSocket details + WebSocket, "web-socket", JITSI_COLIBRI, + attributes: [ + /// The WebSocket URL + url: Required = "url", + ] +); + +generate_attribute!( + /// A Candidate Type as defined in ICE-CORE. + Type, "type", { + /// Host candidate. + Host => "host", + + /// Peer reflexive candidate. + Prflx => "prflx", + + /// Relayed candidate. + Relay => "relay", + + /// Server reflexive candidate. + Srflx => "srflx", + } +); + +generate_element!( + /// A candidate for an ICE-UDP session. + Candidate, "candidate", JINGLE_ICE_UDP, + attributes: [ + /// A Component ID as defined in ICE-CORE. + component: Required = "component", + + /// A Foundation as defined in ICE-CORE. + foundation: Required = "foundation", + + /// An index, starting at 0, that enables the parties to keep track of updates to the + /// candidate throughout the life of the session. + generation: Required = "generation", + + /// A unique identifier for the candidate. + id: Required = "id", + + /// The Internet Protocol (IP) address for the candidate transport mechanism; this can be + /// either an IPv4 address or an IPv6 address. + ip: Required = "ip", + + /// The port at the candidate IP address. + port: Required = "port", + + /// A Priority as defined in ICE-CORE. + priority: Required = "priority", + + /// The protocol to be used. The only value defined by this specification is "udp". + protocol: Required = "protocol", + + /// A related address as defined in ICE-CORE. + rel_addr: Option = "rel-addr", + + /// A related port as defined in ICE-CORE. + rel_port: Option = "rel-port", + + /// An index, starting at 0, referencing which network this candidate is on for a given + /// peer. + network: Option = "network", + + /// A Candidate Type as defined in ICE-CORE. + type_: Required = "type", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::hashes::Algo; + use crate::jingle_dtls_srtp::Setup; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Transport, 68); + assert_size!(Type, 1); + assert_size!(Candidate, 92); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Transport, 136); + assert_size!(Type, 1); + assert_size!(Candidate, 128); + } + + #[test] + fn test_gajim() { + let elem: Element = " + + + + + + + + + + + + + + + + + + + +" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.pwd.unwrap(), "wakMJ8Ydd5rqnPaFerws5o"); + assert_eq!(transport.ufrag.unwrap(), "aeXX"); + } + + #[test] + fn test_jitsi_meet() { + let elem: Element = " + + 97:F2:B5:BE:DB:A6:00:B1:3E:40:B2:41:3C:0D:FC:E0:BD:B2:A0:E8 + + + +" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.pwd.unwrap(), "7lk9uul39gckit6t02oavv2r9j"); + assert_eq!(transport.ufrag.unwrap(), "2acq51d4p07v2m"); + + let fingerprint = transport.fingerprint.unwrap(); + assert_eq!(fingerprint.hash, Algo::Sha_1); + assert_eq!(fingerprint.setup, Setup::Actpass); + assert_eq!( + fingerprint.value, + [ + 151, 242, 181, 190, 219, 166, 0, 177, 62, 64, 178, 65, 60, 13, 252, 224, 189, 178, + 160, 232 + ] + ); + } + + #[test] + fn test_serialize_transport() { + let reference: Element = + "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" + .parse() + .unwrap(); + + let elem: Element = "02:1A:CC:54:27:AB:EB:9C:53:3F:3E:4B:65:2E:7D:46:3F:54:42:CD:54:F1:7A:03:A2:7D:F9:B0:7F:46:19:B2" + .parse() + .unwrap(); + let fingerprint = Fingerprint::try_from(elem).unwrap(); + + let transport = Transport { + pwd: None, + ufrag: None, + candidates: vec![], + fingerprint: Some(fingerprint), + }; + + let serialized: Element = transport.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/jingle_message.rs b/xmpp-parsers/src/jingle_message.rs new file mode 100644 index 0000000..bfc8abb --- /dev/null +++ b/xmpp-parsers/src/jingle_message.rs @@ -0,0 +1,143 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::jingle::SessionId; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use std::convert::TryFrom; + +/// Defines a protocol for broadcasting Jingle requests to all of the clients +/// of a user. +#[derive(Debug, Clone)] +pub enum JingleMI { + /// Indicates we want to start a Jingle session. + Propose { + /// The generated session identifier, must be unique between two users. + sid: SessionId, + + /// The application description of the proposed session. + // TODO: Use a more specialised type here. + description: Element, + }, + + /// Cancels a previously proposed session. + Retract(SessionId), + + /// Accepts a session proposed by the other party. + Accept(SessionId), + + /// Proceed with a previously proposed session. + Proceed(SessionId), + + /// Rejects a session proposed by the other party. + Reject(SessionId), +} + +fn get_sid(elem: Element) -> Result { + check_no_unknown_attributes!(elem, "Jingle message", ["id"]); + Ok(SessionId(get_attr!(elem, "id", Required))) +} + +fn check_empty_and_get_sid(elem: Element) -> Result { + check_no_children!(elem, "Jingle message"); + get_sid(elem) +} + +impl TryFrom for JingleMI { + type Error = Error; + + fn try_from(elem: Element) -> Result { + if !elem.has_ns(ns::JINGLE_MESSAGE) { + return Err(Error::ParseError("This is not a Jingle message element.")); + } + Ok(match elem.name() { + "propose" => { + let mut description = None; + for child in elem.children() { + if child.name() != "description" { + return Err(Error::ParseError("Unknown child in propose element.")); + } + if description.is_some() { + return Err(Error::ParseError("Too many children in propose element.")); + } + description = Some(child.clone()); + } + JingleMI::Propose { + sid: get_sid(elem)?, + description: description.ok_or(Error::ParseError( + "Propose element doesn’t contain a description.", + ))?, + } + } + "retract" => JingleMI::Retract(check_empty_and_get_sid(elem)?), + "accept" => JingleMI::Accept(check_empty_and_get_sid(elem)?), + "proceed" => JingleMI::Proceed(check_empty_and_get_sid(elem)?), + "reject" => JingleMI::Reject(check_empty_and_get_sid(elem)?), + _ => return Err(Error::ParseError("This is not a Jingle message element.")), + }) + } +} + +impl From for Element { + fn from(jingle_mi: JingleMI) -> Element { + match jingle_mi { + JingleMI::Propose { sid, description } => { + Element::builder("propose", ns::JINGLE_MESSAGE) + .attr("id", sid) + .append(description) + } + JingleMI::Retract(sid) => { + Element::builder("retract", ns::JINGLE_MESSAGE).attr("id", sid) + } + JingleMI::Accept(sid) => Element::builder("accept", ns::JINGLE_MESSAGE).attr("id", sid), + JingleMI::Proceed(sid) => { + Element::builder("proceed", ns::JINGLE_MESSAGE).attr("id", sid) + } + JingleMI::Reject(sid) => Element::builder("reject", ns::JINGLE_MESSAGE).attr("id", sid), + } + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(JingleMI, 92); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(JingleMI, 184); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + JingleMI::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = JingleMI::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in propose element."); + } +} diff --git a/xmpp-parsers/src/jingle_raw_udp.rs b/xmpp-parsers/src/jingle_raw_udp.rs new file mode 100644 index 0000000..9197980 --- /dev/null +++ b/xmpp-parsers/src/jingle_raw_udp.rs @@ -0,0 +1,102 @@ +// Copyright (c) 2020 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::jingle_ice_udp::Type; +use std::net::IpAddr; + +generate_element!( + /// Wrapper element for an raw UDP transport. + Transport, "transport", JINGLE_RAW_UDP, + children: [ + /// List of candidates for this raw UDP session. + candidates: Vec = ("candidate", JINGLE_RAW_UDP) => Candidate + ] +); + +impl Transport { + /// Create a new ICE-UDP transport. + pub fn new() -> Transport { + Transport { + candidates: Vec::new(), + } + } + + /// Add a candidate to this transport. + pub fn add_candidate(mut self, candidate: Candidate) -> Self { + self.candidates.push(candidate); + self + } +} + +generate_element!( + /// A candidate for an ICE-UDP session. + Candidate, "candidate", JINGLE_RAW_UDP, + attributes: [ + /// A Component ID as defined in ICE-CORE. + component: Required = "component", + + /// An index, starting at 0, that enables the parties to keep track of updates to the + /// candidate throughout the life of the session. + generation: Required = "generation", + + /// A unique identifier for the candidate. + id: Required = "id", + + /// The Internet Protocol (IP) address for the candidate transport mechanism; this can be + /// either an IPv4 address or an IPv6 address. + ip: Required = "ip", + + /// The port at the candidate IP address. + port: Required = "port", + + /// A Candidate Type as defined in ICE-CORE. + type_: Option = "type", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Transport, 12); + assert_size!(Candidate, 40); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Transport, 24); + assert_size!(Candidate, 56); + } + + #[test] + fn example_1() { + let elem: Element = " + + +" + .parse() + .unwrap(); + let mut transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.candidates.len(), 1); + let candidate = transport.candidates.pop().unwrap(); + assert_eq!(candidate.component, 1); + assert_eq!(candidate.generation, 0); + assert_eq!(candidate.id, "a9j3mnbtu1"); + assert_eq!(candidate.ip, "10.1.1.104".parse::().unwrap()); + assert_eq!(candidate.port, 13540u16); + assert!(candidate.type_.is_none()); + } +} diff --git a/xmpp-parsers/src/jingle_rtcp_fb.rs b/xmpp-parsers/src/jingle_rtcp_fb.rs new file mode 100644 index 0000000..d02a589 --- /dev/null +++ b/xmpp-parsers/src/jingle_rtcp_fb.rs @@ -0,0 +1,47 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +generate_element!( + /// Wrapper element for a rtcp-fb. + RtcpFb, "rtcp-fb", JINGLE_RTCP_FB, + attributes: [ + /// Type of this rtcp-fb. + type_: Required = "type", + + /// Subtype of this rtcp-fb, if relevant. + subtype: Option = "subtype", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(RtcpFb, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(RtcpFb, 48); + } + + #[test] + fn parse_simple() { + let elem: Element = + "" + .parse() + .unwrap(); + let rtcp_fb = RtcpFb::try_from(elem).unwrap(); + assert_eq!(rtcp_fb.type_, "nack"); + assert_eq!(rtcp_fb.subtype.unwrap(), "sli"); + } +} diff --git a/xmpp-parsers/src/jingle_rtp.rs b/xmpp-parsers/src/jingle_rtp.rs new file mode 100644 index 0000000..aacdf5f --- /dev/null +++ b/xmpp-parsers/src/jingle_rtp.rs @@ -0,0 +1,211 @@ +// Copyright (c) 2019-2020 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::jingle_rtcp_fb::RtcpFb; +use crate::jingle_rtp_hdrext::RtpHdrext; +use crate::jingle_ssma::{Group, Source}; + +generate_empty_element!( + /// Specifies the ability to multiplex RTP Data and Control Packets on a single port as + /// described in RFC 5761. + RtcpMux, + "rtcp-mux", + JINGLE_RTP +); + +generate_element!( + /// Wrapper element describing an RTP session. + Description, "description", JINGLE_RTP, + attributes: [ + /// Namespace of the encryption scheme used. + media: Required = "media", + + /// ssrc? + ssrc: Option = "ssrc", + + /// maximum packet time + maxptime: Option = "maxptime", + ], + children: [ + /// List of encodings that can be used for this RTP stream. + payload_types: Vec = ("payload-type", JINGLE_RTP) => PayloadType, + + /// Specifies the ability to multiplex RTP Data and Control Packets on a single port as + /// described in RFC 5761. + rtcp_mux: Option = ("rtcp-mux", JINGLE_RTP) => RtcpMux, + + /// List of ssrc-group. + ssrc_groups: Vec = ("ssrc-group", JINGLE_SSMA) => Group, + + /// List of ssrc. + ssrcs: Vec = ("source", JINGLE_SSMA) => Source, + + /// List of header extensions. + hdrexts: Vec = ("rtp-hdrext", JINGLE_RTP_HDREXT) => RtpHdrext + + // TODO: Add support for and . + ] +); + +impl Description { + /// Create a new RTP description. + pub fn new(media: String) -> Description { + Description { + media, + ssrc: None, + maxptime: None, + payload_types: Vec::new(), + rtcp_mux: None, + ssrc_groups: Vec::new(), + ssrcs: Vec::new(), + hdrexts: Vec::new(), + } + } +} + +generate_attribute!( + /// The number of channels. + Channels, + "channels", + u8, + Default = 1 +); + +generate_element!( + /// An encoding that can be used for an RTP stream. + PayloadType, "payload-type", JINGLE_RTP, + attributes: [ + /// The number of channels. + channels: Default = "channels", + + /// The sampling frequency in Hertz. + clockrate: Option = "clockrate", + + /// The payload identifier. + id: Required = "id", + + /// Maximum packet time as specified in RFC 4566. + maxptime: Option = "maxptime", + + /// The appropriate subtype of the MIME type. + name: Option = "name", + + /// Packet time as specified in RFC 4566. + ptime: Option = "ptime", + ], + children: [ + /// List of parameters specifying this payload-type. + /// + /// Their order MUST be ignored. + parameters: Vec = ("parameter", JINGLE_RTP) => Parameter, + + /// List of rtcp-fb parameters from XEP-0293. + rtcp_fbs: Vec = ("rtcp-fb", JINGLE_RTCP_FB) => RtcpFb + ] +); + +impl PayloadType { + /// Create a new RTP payload-type. + pub fn new(id: u8, name: String, clockrate: u32, channels: u8) -> PayloadType { + PayloadType { + channels: Channels(channels), + clockrate: Some(clockrate), + id, + maxptime: None, + name: Some(name), + ptime: None, + parameters: Vec::new(), + rtcp_fbs: Vec::new(), + } + } + + /// Create a new RTP payload-type without a clockrate. Warning: this is invalid as per + /// RFC 4566! + pub fn without_clockrate(id: u8, name: String) -> PayloadType { + PayloadType { + channels: Default::default(), + clockrate: None, + id, + maxptime: None, + name: Some(name), + ptime: None, + parameters: Vec::new(), + rtcp_fbs: Vec::new(), + } + } +} + +generate_element!( + /// Parameter related to a payload. + Parameter, "parameter", JINGLE_RTP, + attributes: [ + /// The name of the parameter, from the list at + /// https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml + name: Required = "name", + + /// The value of this parameter. + value: Required = "value", + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Description, 76); + assert_size!(Channels, 1); + assert_size!(PayloadType, 64); + assert_size!(Parameter, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Description, 152); + assert_size!(Channels, 1); + assert_size!(PayloadType, 104); + assert_size!(Parameter, 48); + } + + #[test] + fn test_simple() { + let elem: Element = " + + + + + + + + + + + + + + + + + + + + + + + +" + .parse() + .unwrap(); + let desc = Description::try_from(elem).unwrap(); + assert_eq!(desc.media, "audio"); + assert_eq!(desc.ssrc, None); + } +} diff --git a/xmpp-parsers/src/jingle_rtp_hdrext.rs b/xmpp-parsers/src/jingle_rtp_hdrext.rs new file mode 100644 index 0000000..199b439 --- /dev/null +++ b/xmpp-parsers/src/jingle_rtp_hdrext.rs @@ -0,0 +1,86 @@ +// Copyright (c) 2020 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +generate_attribute!( + /// Which party is allowed to send the negotiated RTP Header Extensions. + Senders, "senders", { + /// Both parties can send them. + Both => "both", + + /// Only the initiator can send them. + Initiator => "initiator", + + /// Only the responder can send them. + Responder => "responder", + }, Default = Both +); + +generate_element!( + /// Header extensions to be used in a RTP description. + RtpHdrext, "rtp-hdrext", JINGLE_RTP_HDREXT, + attributes: [ + /// The ID of the extensions. + id: Required = "id", + + /// The URI that defines the extension. + uri: Required = "uri", + + /// Which party is allowed to send the negotiated RTP Header Extensions. + senders: Default = "senders", + ] +); + +impl RtpHdrext { + /// Create a new RTP header extension element. + pub fn new(id: String, uri: String) -> RtpHdrext { + RtpHdrext { + id, + uri, + senders: Default::default(), + } + } + + /// Set the senders. + pub fn with_senders(mut self, senders: Senders) -> RtpHdrext { + self.senders = senders; + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Senders, 1); + assert_size!(RtpHdrext, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Senders, 1); + assert_size!(RtpHdrext, 56); + } + + #[test] + fn parse_exthdr() { + let elem: Element = " + " + .parse() + .unwrap(); + let rtp_hdrext = RtpHdrext::try_from(elem).unwrap(); + assert_eq!(rtp_hdrext.id, "1"); + assert_eq!(rtp_hdrext.uri, "urn:ietf:params:rtp-hdrext:toffset"); + assert_eq!(rtp_hdrext.senders, Senders::Both); + } +} diff --git a/xmpp-parsers/src/jingle_s5b.rs b/xmpp-parsers/src/jingle_s5b.rs new file mode 100644 index 0000000..e481f56 --- /dev/null +++ b/xmpp-parsers/src/jingle_s5b.rs @@ -0,0 +1,353 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::convert::TryFrom; +use std::net::IpAddr; + +generate_attribute!( + /// The type of the connection being proposed by this candidate. + Type, "type", { + /// Direct connection using NAT assisting technologies like NAT-PMP or + /// UPnP-IGD. + Assisted => "assisted", + + /// Direct connection using the given interface. + Direct => "direct", + + /// SOCKS5 relay. + Proxy => "proxy", + + /// Tunnel protocol such as Teredo. + Tunnel => "tunnel", + }, Default = Direct +); + +generate_attribute!( + /// Which mode to use for the connection. + Mode, "mode", { + /// Use TCP, which is the default. + Tcp => "tcp", + + /// Use UDP. + Udp => "udp", + }, Default = Tcp +); + +generate_id!( + /// An identifier for a candidate. + CandidateId +); + +generate_id!( + /// An identifier for a stream. + StreamId +); + +generate_element!( + /// A candidate for a connection. + Candidate, "candidate", JINGLE_S5B, + attributes: [ + /// The identifier for this candidate. + cid: Required = "cid", + + /// The host to connect to. + host: Required = "host", + + /// The JID to request at the given end. + jid: Required = "jid", + + /// The port to connect to. + port: Option = "port", + + /// The priority of this candidate, computed using this formula: + /// priority = (2^16)*(type preference) + (local preference) + priority: Required = "priority", + + /// The type of the connection being proposed by this candidate. + type_: Default = "type", + ] +); + +impl Candidate { + /// Creates a new candidate with the given parameters. + pub fn new(cid: CandidateId, host: IpAddr, jid: Jid, priority: u32) -> Candidate { + Candidate { + cid, + host, + jid, + priority, + port: Default::default(), + type_: Default::default(), + } + } + + /// Sets the port of this candidate. + pub fn with_port(mut self, port: u16) -> Candidate { + self.port = Some(port); + self + } + + /// Sets the type of this candidate. + pub fn with_type(mut self, type_: Type) -> Candidate { + self.type_ = type_; + self + } +} + +/// The payload of a transport. +#[derive(Debug, Clone, PartialEq)] +pub enum TransportPayload { + /// The responder informs the initiator that the bytestream pointed by this + /// candidate has been activated. + Activated(CandidateId), + + /// A list of suggested candidates. + Candidates(Vec), + + /// Both parties failed to use a candidate, they should fallback to another + /// transport. + CandidateError, + + /// The candidate pointed here should be used by both parties. + CandidateUsed(CandidateId), + + /// This entity can’t connect to the SOCKS5 proxy. + ProxyError, + + /// XXX: Invalid, should not be found in the wild. + None, +} + +/// Describes a Jingle transport using a direct or proxied connection. +#[derive(Debug, Clone, PartialEq)] +pub struct Transport { + /// The stream identifier for this transport. + pub sid: StreamId, + + /// The destination address. + pub dstaddr: Option, + + /// The mode to be used for the transfer. + pub mode: Mode, + + /// The payload of this transport. + pub payload: TransportPayload, +} + +impl Transport { + /// Creates a new transport element. + pub fn new(sid: StreamId) -> Transport { + Transport { + sid, + dstaddr: None, + mode: Default::default(), + payload: TransportPayload::None, + } + } + + /// Sets the destination address of this transport. + pub fn with_dstaddr(mut self, dstaddr: String) -> Transport { + self.dstaddr = Some(dstaddr); + self + } + + /// Sets the mode of this transport. + pub fn with_mode(mut self, mode: Mode) -> Transport { + self.mode = mode; + self + } + + /// Sets the payload of this transport. + pub fn with_payload(mut self, payload: TransportPayload) -> Transport { + self.payload = payload; + self + } +} + +impl TryFrom for Transport { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "transport", JINGLE_S5B); + check_no_unknown_attributes!(elem, "transport", ["sid", "dstaddr", "mode"]); + let sid = get_attr!(elem, "sid", Required); + let dstaddr = get_attr!(elem, "dstaddr", Option); + let mode = get_attr!(elem, "mode", Default); + + let mut payload = None; + for child in elem.children() { + payload = Some(if child.is("candidate", ns::JINGLE_S5B) { + let mut candidates = + match payload { + Some(TransportPayload::Candidates(candidates)) => candidates, + Some(_) => return Err(Error::ParseError( + "Non-candidate child already present in JingleS5B transport element.", + )), + None => vec![], + }; + candidates.push(Candidate::try_from(child.clone())?); + TransportPayload::Candidates(candidates) + } else if child.is("activated", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-activated child already present in JingleS5B transport element.", + )); + } + let cid = get_attr!(child, "cid", Required); + TransportPayload::Activated(cid) + } else if child.is("candidate-error", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-candidate-error child already present in JingleS5B transport element.", + )); + } + TransportPayload::CandidateError + } else if child.is("candidate-used", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-candidate-used child already present in JingleS5B transport element.", + )); + } + let cid = get_attr!(child, "cid", Required); + TransportPayload::CandidateUsed(cid) + } else if child.is("proxy-error", ns::JINGLE_S5B) { + if payload.is_some() { + return Err(Error::ParseError( + "Non-proxy-error child already present in JingleS5B transport element.", + )); + } + TransportPayload::ProxyError + } else { + return Err(Error::ParseError( + "Unknown child in JingleS5B transport element.", + )); + }); + } + let payload = payload.unwrap_or(TransportPayload::None); + Ok(Transport { + sid, + dstaddr, + mode, + payload, + }) + } +} + +impl From for Element { + fn from(transport: Transport) -> Element { + Element::builder("transport", ns::JINGLE_S5B) + .attr("sid", transport.sid) + .attr("dstaddr", transport.dstaddr) + .attr("mode", transport.mode) + .append_all(match transport.payload { + TransportPayload::Candidates(candidates) => candidates + .into_iter() + .map(Element::from) + .collect::>(), + TransportPayload::Activated(cid) => { + vec![Element::builder("activated", ns::JINGLE_S5B) + .attr("cid", cid) + .build()] + } + TransportPayload::CandidateError => { + vec![Element::builder("candidate-error", ns::JINGLE_S5B).build()] + } + TransportPayload::CandidateUsed(cid) => { + vec![Element::builder("candidate-used", ns::JINGLE_S5B) + .attr("cid", cid) + .build()] + } + TransportPayload::ProxyError => { + vec![Element::builder("proxy-error", ns::JINGLE_S5B).build()] + } + TransportPayload::None => vec![], + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Type, 1); + assert_size!(Mode, 1); + assert_size!(CandidateId, 12); + assert_size!(StreamId, 12); + assert_size!(Candidate, 84); + assert_size!(TransportPayload, 16); + assert_size!(Transport, 44); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Type, 1); + assert_size!(Mode, 1); + assert_size!(CandidateId, 24); + assert_size!(StreamId, 24); + assert_size!(Candidate, 136); + assert_size!(TransportPayload, 32); + assert_size!(Transport, 88); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let transport = Transport::try_from(elem).unwrap(); + assert_eq!(transport.sid, StreamId(String::from("coucou"))); + assert_eq!(transport.dstaddr, None); + assert_eq!(transport.mode, Mode::Tcp); + match transport.payload { + TransportPayload::None => (), + _ => panic!("Wrong element inside transport!"), + } + } + + #[test] + fn test_serialise_activated() { + let elem: Element = "".parse().unwrap(); + let transport = Transport { + sid: StreamId(String::from("coucou")), + dstaddr: None, + mode: Mode::Tcp, + payload: TransportPayload::Activated(CandidateId(String::from("coucou"))), + }; + let elem2: Element = transport.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_serialise_candidate() { + let elem: Element = "".parse().unwrap(); + let transport = Transport { + sid: StreamId(String::from("coucou")), + dstaddr: None, + mode: Mode::Tcp, + payload: TransportPayload::Candidates(vec![Candidate { + cid: CandidateId(String::from("coucou")), + host: IpAddr::from_str("127.0.0.1").unwrap(), + jid: Jid::Bare(BareJid::new("coucou", "coucou")), + port: None, + priority: 0u32, + type_: Type::Direct, + }]), + }; + let elem2: Element = transport.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/jingle_ssma.rs b/xmpp-parsers/src/jingle_ssma.rs new file mode 100644 index 0000000..29cadc5 --- /dev/null +++ b/xmpp-parsers/src/jingle_ssma.rs @@ -0,0 +1,130 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +generate_element!( + /// Source element for the ssrc SDP attribute. + Source, "source", JINGLE_SSMA, + attributes: [ + /// Maps to the ssrc-id parameter. + id: Required = "ssrc", + ], + children: [ + /// List of attributes for this source. + parameters: Vec = ("parameter", JINGLE_SSMA) => Parameter, + + /// ssrc-info for this source. + info: Option = ("ssrc-info", JITSI_MEET) => SsrcInfo + ] +); + +impl Source { + /// Create a new SSMA Source element. + pub fn new(id: String) -> Source { + Source { + id, + parameters: Vec::new(), + info: None, + } + } +} + +generate_element!( + /// Parameter associated with a ssrc. + Parameter, "parameter", JINGLE_SSMA, + attributes: [ + /// The name of the parameter. + name: Required = "name", + + /// The optional value of the parameter. + value: Option = "value", + ] +); + +generate_element!( + /// ssrc-info associated with a ssrc. + SsrcInfo, "ssrc-info", JITSI_MEET, + attributes: [ + /// The owner of the ssrc. + owner: Required = "owner" + ] +); + +generate_element!( + /// Element grouping multiple ssrc. + Group, "ssrc-group", JINGLE_SSMA, + attributes: [ + /// The semantics of this group. + semantics: Required = "semantics", + ], + children: [ + /// The various ssrc concerned by this group. + sources: Vec = ("source", JINGLE_SSMA) => Source + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Source, 24); + assert_size!(Parameter, 24); + assert_size!(Group, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Source, 48); + assert_size!(Parameter, 48); + assert_size!(Group, 48); + } + + #[test] + fn parse_source() { + let elem: Element = " + + + +" + .parse() + .unwrap(); + let mut ssrc = Source::try_from(elem).unwrap(); + assert_eq!(ssrc.id, "1656081975"); + assert_eq!(ssrc.parameters.len(), 2); + let parameter = ssrc.parameters.pop().unwrap(); + assert_eq!(parameter.name, "msid"); + assert_eq!( + parameter.value.unwrap(), + "MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIv MLTJKIHilGn71fNQoszkQ4jlPTuS5vJyKVIva0" + ); + let parameter = ssrc.parameters.pop().unwrap(); + assert_eq!(parameter.name, "cname"); + assert_eq!(parameter.value.unwrap(), "Yv/wvbCdsDW2Prgd"); + } + + #[test] + fn parse_source_group() { + let elem: Element = " + + + +" + .parse() + .unwrap(); + let mut group = Group::try_from(elem).unwrap(); + assert_eq!(group.semantics, "FID"); + assert_eq!(group.sources.len(), 2); + let source = group.sources.pop().unwrap(); + assert_eq!(source.id, "386328120"); + let source = group.sources.pop().unwrap(); + assert_eq!(source.id, "2301230316"); + } +} diff --git a/xmpp-parsers/src/lib.rs b/xmpp-parsers/src/lib.rs new file mode 100644 index 0000000..d18f399 --- /dev/null +++ b/xmpp-parsers/src/lib.rs @@ -0,0 +1,229 @@ +//! A crate parsing common XMPP elements into Rust structures. +//! +//! Each module implements the `TryFrom` trait, which takes a +//! minidom [`Element`] and returns a `Result` whose value is `Ok` if the +//! element parsed correctly, `Err(error::Error)` otherwise. +//! +//! The returned structure can be manipuled as any Rust structure, with each +//! field being public. You can also create the same structure manually, with +//! some having `new()` and `with_*()` helper methods to create them. +//! +//! Once you are happy with your structure, you can serialise it back to an +//! [`Element`], using either `From` or `Into`, which give you what +//! you want to be sending on the wire. +//! +//! [`Element`]: ../minidom/element/struct.Element.html + +// Copyright (c) 2017-2019 Emmanuel Gil Peyrot +// Copyright (c) 2017-2019 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#![deny(missing_docs)] + +pub use crate::util::error::Error; +pub use jid::{BareJid, FullJid, Jid, JidParseError}; +pub use minidom::Element; + +/// XML namespace definitions used through XMPP. +pub mod ns; + +#[macro_use] +mod util; + +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod bind; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod iq; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod message; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod presence; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod sasl; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod stanza_error; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub mod stream; + +/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence +pub mod roster; + +/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket +pub mod websocket; + +/// XEP-0004: Data Forms +pub mod data_forms; + +/// XEP-0030: Service Discovery +pub mod disco; + +/// XEP-0045: Multi-User Chat +pub mod muc; + +/// XEP-0047: In-Band Bytestreams +pub mod ibb; + +/// XEP-0048: Bookmarks +pub mod bookmarks; + +/// XEP-0059: Result Set Management +pub mod rsm; + +/// XEP-0060: Publish-Subscribe +pub mod pubsub; + +/// XEP-0071: XHTML-IM +pub mod xhtml; + +/// XEP-0077: In-Band Registration +pub mod ibr; + +/// XEP-0082: XMPP Date and Time Profiles +pub mod date; + +/// XEP-0084: User Avatar +pub mod avatar; + +/// XEP-0085: Chat State Notifications +pub mod chatstates; + +/// XEP-0092: Software Version +pub mod version; + +/// XEP-0107: User Mood +pub mod mood; + +/// XEP-0114: Jabber Component Protocol +pub mod component; + +/// XEP-0115: Entity Capabilities +pub mod caps; + +/// XEP-0118: User Tune +pub mod tune; + +/// XEP-0157: Contact Addresses for XMPP Services +pub mod server_info; + +/// XEP-0166: Jingle +pub mod jingle; + +/// XEP-0167: Jingle RTP Sessions +pub mod jingle_rtp; + +/// XEP-0172: User Nickname +pub mod nick; + +/// XEP-0176: Jingle ICE-UDP Transport Method +pub mod jingle_ice_udp; + +/// XEP-0177: Jingle Raw UDP Transport Method +pub mod jingle_raw_udp; + +/// XEP-0184: Message Delivery Receipts +pub mod receipts; + +/// XEP-0191: Blocking Command +pub mod blocking; + +/// XEP-0198: Stream Management +pub mod sm; + +/// XEP-0199: XMPP Ping +pub mod ping; + +/// XEP-0202: Entity Time +pub mod time; + +/// XEP-0203: Delayed Delivery +pub mod delay; + +/// XEP-0221: Data Forms Media Element +pub mod media_element; + +/// XEP-0224: Attention +pub mod attention; + +/// XEP-0231: Bits of Binary +pub mod bob; + +/// XEP-0234: Jingle File Transfer +pub mod jingle_ft; + +/// XEP-0257: Client Certificate Management for SASL EXTERNAL +pub mod cert_management; + +/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method +pub mod jingle_s5b; + +/// XEP-0261: Jingle In-Band Bytestreams Transport Method +pub mod jingle_ibb; + +/// XEP-0280: Message Carbons +pub mod carbons; + +/// XEP-0293: Jingle RTP Feedback Negotiation +pub mod jingle_rtcp_fb; + +/// XEP-0294: Jingle RTP Header Extensions Negociation +pub mod jingle_rtp_hdrext; + +/// XEP-0297: Stanza Forwarding +pub mod forwarding; + +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub mod hashes; + +/// XEP-0308: Last Message Correction +pub mod message_correct; + +/// XEP-0313: Message Archive Management +pub mod mam; + +/// XEP-0319: Last User Interaction in Presence +pub mod idle; + +/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions +pub mod jingle_dtls_srtp; + +/// XEP-0328: JID Prep +pub mod jid_prep; + +/// XEP-0338: Jingle Grouping Framework +pub mod jingle_grouping; + +/// XEP-0339: Source-Specific Media Attributes in Jingle +pub mod jingle_ssma; + +/// XEP-0352: Client State Indication +pub mod csi; + +/// XEP-0353: Jingle Message Initiation +pub mod jingle_message; + +/// XEP-0359: Unique and Stable Stanza IDs +pub mod stanza_id; + +/// XEP-0369: Mediated Information eXchange (MIX) +pub mod mix; + +/// XEP-0373: OpenPGP for XMPP +pub mod openpgp; + +/// XEP-0380: Explicit Message Encryption +pub mod eme; + +/// XEP-0390: Entity Capabilities 2.0 +pub mod ecaps2; + +/// XEP-0402: Bookmarks 2 (This Time it's Serious) +pub mod bookmarks2; + +/// XEP-0421: Anonymous unique occupant identifiers for MUCs +pub mod occupant_id; + +/// XEP-0441: Message Archive Management Preferences +pub mod mam_prefs; diff --git a/xmpp-parsers/src/mam.rs b/xmpp-parsers/src/mam.rs new file mode 100644 index 0000000..eff0065 --- /dev/null +++ b/xmpp-parsers/src/mam.rs @@ -0,0 +1,296 @@ +// Copyright (c) 2017-2021 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::forwarding::Forwarded; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::message::MessagePayload; +use crate::pubsub::NodeName; +use crate::rsm::{SetQuery, SetResult}; + +generate_id!( + /// An identifier matching a result message to the query requesting it. + QueryId +); + +generate_element!( + /// Starts a query to the archive. + Query, "query", MAM, + attributes: [ + /// An optional identifier for matching forwarded messages to this + /// query. + queryid: Option = "queryid", + + /// Must be set to Some when querying a PubSub node’s archive. + node: Option = "node" + ], + children: [ + /// Used for filtering the results. + form: Option = ("x", DATA_FORMS) => DataForm, + + /// Used for paging through results. + set: Option = ("set", RSM) => SetQuery + ] +); + +impl IqGetPayload for Query {} +impl IqSetPayload for Query {} +impl IqResultPayload for Query {} + +generate_element!( + /// The wrapper around forwarded stanzas. + Result_, "result", MAM, + attributes: [ + /// The stanza-id under which the archive stored this stanza. + id: Required = "id", + + /// The same queryid as the one requested in the + /// [query](struct.Query.html). + queryid: Option = "queryid", + ], + children: [ + /// The actual stanza being forwarded. + forwarded: Required = ("forwarded", FORWARD) => Forwarded + ] +); + +impl MessagePayload for Result_ {} + +generate_attribute!( + /// True when the end of a MAM query has been reached. + Complete, + "complete", + bool +); + +generate_element!( + /// Notes the end of a page in a query. + Fin, "fin", MAM, + attributes: [ + /// True when the end of a MAM query has been reached. + complete: Default = "complete", + ], + children: [ + /// Describes the current page, it should contain at least [first] + /// (with an [index]) and [last], and generally [count]. + /// + /// [first]: ../rsm/struct.SetResult.html#structfield.first + /// [index]: ../rsm/struct.SetResult.html#structfield.first_index + /// [last]: ../rsm/struct.SetResult.html#structfield.last + /// [count]: ../rsm/struct.SetResult.html#structfield.count + set: Required = ("set", RSM) => SetResult + ] +); + +impl IqResultPayload for Fin {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use minidom::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(QueryId, 12); + assert_size!(Query, 116); + assert_size!(Result_, 236); + assert_size!(Complete, 1); + assert_size!(Fin, 44); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(QueryId, 24); + assert_size!(Query, 232); + assert_size!(Result_, 456); + assert_size!(Complete, 1); + assert_size!(Fin, 88); + } + + #[test] + fn test_query() { + let elem: Element = "".parse().unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_result() { + #[cfg(not(feature = "component"))] + let elem: Element = r#" + + + + + Hail to thee + + + +"# + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = r#" + + + + + Hail to thee + + + +"#.parse().unwrap(); + Result_::try_from(elem).unwrap(); + } + + #[test] + fn test_fin() { + let elem: Element = r#" + + + 28482-98726-73623 + 09af3-cc343-b409f + + +"# + .parse() + .unwrap(); + Fin::try_from(elem).unwrap(); + } + + #[test] + fn test_query_x() { + let elem: Element = r#" + + + + urn:xmpp:mam:2 + + + juliet@capulet.lit + + + +"# + .parse() + .unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_query_x_set() { + let elem: Element = r#" + + + + urn:xmpp:mam:2 + + + 2010-08-07T00:00:00Z + + + + 10 + + +"# + .parse() + .unwrap(); + Query::try_from(elem).unwrap(); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Query::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in query element."); + } + + #[test] + fn test_serialise_empty() { + let elem: Element = "".parse().unwrap(); + let replace = Query { + queryid: None, + node: None, + form: None, + set: None, + }; + let elem2 = replace.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_serialize_query_with_form() { + let reference: Element = "urn:xmpp:mam:2juliet@capulet.lit" + .parse() + .unwrap(); + + let elem: Element = "urn:xmpp:mam:2juliet@capulet.lit" + .parse() + .unwrap(); + + let form = DataForm::try_from(elem).unwrap(); + + let query = Query { + queryid: None, + node: None, + set: None, + form: Some(form), + }; + let serialized: Element = query.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_result() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + + let forwarded = Forwarded::try_from(elem).unwrap(); + + let result = Result_ { + id: String::from("28482-98726-73623"), + queryid: Some(QueryId(String::from("f27"))), + forwarded: forwarded, + }; + let serialized: Element = result.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_fin() { + let reference: Element = "28482-98726-7362309af3-cc343-b409f" + .parse() + .unwrap(); + + let elem: Element = "28482-98726-7362309af3-cc343-b409f" + .parse() + .unwrap(); + + let set = SetResult::try_from(elem).unwrap(); + + let fin = Fin { + set: set, + complete: Complete::default(), + }; + let serialized: Element = fin.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/mam_prefs.rs b/xmpp-parsers/src/mam_prefs.rs new file mode 100644 index 0000000..04e8663 --- /dev/null +++ b/xmpp-parsers/src/mam_prefs.rs @@ -0,0 +1,175 @@ +// Copyright (c) 2021 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::util::error::Error; +use jid::Jid; +use minidom::{Element, Node}; +use std::convert::TryFrom; + +generate_attribute!( + /// Notes the default archiving preference for the user. + DefaultPrefs, "default", { + /// The default is to always log messages in the archive. + Always => "always", + + /// The default is to never log messages in the archive. + Never => "never", + + /// The default is to log messages in the archive only for contacts + /// present in the user’s [roster](../roster/index.html). + Roster => "roster", + } +); + +/// Controls the archiving preferences of the user. +#[derive(Debug, Clone)] +pub struct Prefs { + /// The default preference for JIDs in neither + /// [always](#structfield.always) or [never](#structfield.never) lists. + pub default_: DefaultPrefs, + + /// The set of JIDs for which to always store messages in the archive. + pub always: Vec, + + /// The set of JIDs for which to never store messages in the archive. + pub never: Vec, +} + +impl IqGetPayload for Prefs {} +impl IqSetPayload for Prefs {} +impl IqResultPayload for Prefs {} + +impl TryFrom for Prefs { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "prefs", MAM); + check_no_unknown_attributes!(elem, "prefs", ["default"]); + let mut always = vec![]; + let mut never = vec![]; + for child in elem.children() { + if child.is("always", ns::MAM) { + for jid_elem in child.children() { + if !jid_elem.is("jid", ns::MAM) { + return Err(Error::ParseError("Invalid jid element in always.")); + } + always.push(jid_elem.text().parse()?); + } + } else if child.is("never", ns::MAM) { + for jid_elem in child.children() { + if !jid_elem.is("jid", ns::MAM) { + return Err(Error::ParseError("Invalid jid element in never.")); + } + never.push(jid_elem.text().parse()?); + } + } else { + return Err(Error::ParseError("Unknown child in prefs element.")); + } + } + let default_ = get_attr!(elem, "default", Required); + Ok(Prefs { + default_, + always, + never, + }) + } +} + +fn serialise_jid_list(name: &str, jids: Vec) -> ::std::option::IntoIter { + if jids.is_empty() { + None.into_iter() + } else { + Some( + Element::builder(name, ns::MAM) + .append_all( + jids.into_iter() + .map(|jid| Element::builder("jid", ns::MAM).append(String::from(jid))), + ) + .into(), + ) + .into_iter() + } +} + +impl From for Element { + fn from(prefs: Prefs) -> Element { + Element::builder("prefs", ns::MAM) + .attr("default", prefs.default_) + .append_all(serialise_jid_list("always", prefs.always)) + .append_all(serialise_jid_list("never", prefs.never)) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(DefaultPrefs, 1); + assert_size!(Prefs, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(DefaultPrefs, 1); + assert_size!(Prefs, 56); + } + + #[test] + fn test_prefs_get() { + let elem: Element = "" + .parse() + .unwrap(); + let prefs = Prefs::try_from(elem).unwrap(); + assert!(prefs.always.is_empty()); + assert!(prefs.never.is_empty()); + + let elem: Element = r#" + + + + +"# + .parse() + .unwrap(); + let prefs = Prefs::try_from(elem).unwrap(); + assert!(prefs.always.is_empty()); + assert!(prefs.never.is_empty()); + } + + #[test] + fn test_prefs_result() { + let elem: Element = r#" + + + romeo@montague.lit + + + montague@montague.lit + + +"# + .parse() + .unwrap(); + let prefs = Prefs::try_from(elem).unwrap(); + assert_eq!(prefs.always, [BareJid::new("romeo", "montague.lit")]); + assert_eq!(prefs.never, [BareJid::new("montague", "montague.lit")]); + + let elem2 = Element::from(prefs.clone()); + println!("{:?}", elem2); + let prefs2 = Prefs::try_from(elem2).unwrap(); + assert_eq!(prefs.default_, prefs2.default_); + assert_eq!(prefs.always, prefs2.always); + assert_eq!(prefs.never, prefs2.never); + } +} diff --git a/xmpp-parsers/src/media_element.rs b/xmpp-parsers/src/media_element.rs new file mode 100644 index 0000000..e1d9338 --- /dev/null +++ b/xmpp-parsers/src/media_element.rs @@ -0,0 +1,249 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::util::helpers::TrimmedPlainText; + +generate_element!( + /// Represents an URI used in a media element. + URI, "uri", MEDIA_ELEMENT, + attributes: [ + /// The MIME type of the URI referenced. + /// + /// See the [IANA MIME Media Types Registry][1] for a list of + /// registered types, but unregistered or yet-to-be-registered are + /// accepted too. + /// + /// [1]: https://www.iana.org/assignments/media-types/media-types.xhtml + type_: Required = "type" + ], + text: ( + /// The actual URI contained. + uri: TrimmedPlainText + ) +); + +generate_element!( + /// References a media element, to be used in [data + /// forms](../data_forms/index.html). + MediaElement, "media", MEDIA_ELEMENT, + attributes: [ + /// The recommended display width in pixels. + width: Option = "width", + + /// The recommended display height in pixels. + height: Option = "height" + ], + children: [ + /// A list of URIs referencing this media. + uris: Vec = ("uri", MEDIA_ELEMENT) => URI + ] +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_forms::DataForm; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(URI, 24); + assert_size!(MediaElement, 28); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(URI, 48); + assert_size!(MediaElement, 56); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert!(media.width.is_none()); + assert!(media.height.is_none()); + assert!(media.uris.is_empty()); + } + + #[test] + fn test_width_height() { + let elem: Element = "" + .parse() + .unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert_eq!(media.width.unwrap(), 32); + assert_eq!(media.height.unwrap(), 32); + } + + #[test] + fn test_uri() { + let elem: Element = "https://example.org/".parse().unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert_eq!(media.uris.len(), 1); + assert_eq!(media.uris[0].type_, "text/html"); + assert_eq!(media.uris[0].uri, "https://example.org/"); + } + + #[test] + fn test_invalid_width_height() { + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.to_string(), "cannot parse integer from empty string"); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.to_string(), "invalid digit found in string"); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.to_string(), "cannot parse integer from empty string"); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.to_string(), "invalid digit found in string"); + } + + #[test] + fn test_unknown_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in media element."); + } + + #[test] + fn test_bad_uri() { + let elem: Element = + "https://example.org/" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = MediaElement::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "URI missing in uri."); + } + + #[test] + fn test_xep_ex1() { + let elem: Element = r#" + + + http://victim.example.com/challenges/speech.wav?F3A6292C + + + cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org + + + http://victim.example.com/challenges/speech.mp3?F3A6292C + +"# + .parse() + .unwrap(); + let media = MediaElement::try_from(elem).unwrap(); + assert!(media.width.is_none()); + assert!(media.height.is_none()); + assert_eq!(media.uris.len(), 3); + assert_eq!(media.uris[0].type_, "audio/x-wav"); + assert_eq!( + media.uris[0].uri, + "http://victim.example.com/challenges/speech.wav?F3A6292C" + ); + assert_eq!(media.uris[1].type_, "audio/ogg; codecs=speex"); + assert_eq!( + media.uris[1].uri, + "cid:sha1+a15a505e360702b79c75a5f67773072ed392f52a@bob.xmpp.org" + ); + assert_eq!(media.uris[2].type_, "audio/mpeg"); + assert_eq!( + media.uris[2].uri, + "http://victim.example.com/challenges/speech.mp3?F3A6292C" + ); + } + + #[test] + fn test_xep_ex2() { + let elem: Element = r#" + + [ ... ] + + + + http://www.victim.com/challenges/ocr.jpeg?F3A6292C + + + cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org + + + + [ ... ] +"# + .parse() + .unwrap(); + let form = DataForm::try_from(elem).unwrap(); + assert_eq!(form.fields.len(), 1); + assert_eq!(form.fields[0].var, "ocr"); + assert_eq!(form.fields[0].media[0].width, Some(290)); + assert_eq!(form.fields[0].media[0].height, Some(80)); + assert_eq!(form.fields[0].media[0].uris[0].type_, "image/jpeg"); + assert_eq!( + form.fields[0].media[0].uris[0].uri, + "http://www.victim.com/challenges/ocr.jpeg?F3A6292C" + ); + assert_eq!(form.fields[0].media[0].uris[1].type_, "image/jpeg"); + assert_eq!( + form.fields[0].media[0].uris[1].uri, + "cid:sha1+f24030b8d91d233bac14777be5ab531ca3b9f102@bob.xmpp.org" + ); + } +} diff --git a/xmpp-parsers/src/message.rs b/xmpp-parsers/src/message.rs new file mode 100644 index 0000000..281543a --- /dev/null +++ b/xmpp-parsers/src/message.rs @@ -0,0 +1,407 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +/// Should be implemented on every known payload of a ``. +pub trait MessagePayload: TryFrom + Into {} + +generate_attribute!( + /// The type of a message. + MessageType, "type", { + /// Standard instant messaging message. + Chat => "chat", + + /// Notifies that an error happened. + Error => "error", + + /// Standard group instant messaging message. + Groupchat => "groupchat", + + /// Used by servers to notify users when things happen. + Headline => "headline", + + /// This is an email-like message, it usually contains a + /// [subject](struct.Subject.html). + Normal => "normal", + }, Default = Normal +); + +type Lang = String; + +generate_elem_id!( + /// Represents one `` element, that is the free form text content of + /// a message. + Body, + "body", + DEFAULT_NS +); + +generate_elem_id!( + /// Defines the subject of a room, or of an email-like normal message. + Subject, + "subject", + DEFAULT_NS +); + +generate_elem_id!( + /// A thread identifier, so that other people can specify to which message + /// they are replying. + Thread, + "thread", + DEFAULT_NS +); + +/// The main structure representing the `` stanza. +#[derive(Debug, Clone, PartialEq)] +pub struct Message { + /// The JID emitting this stanza. + pub from: Option, + + /// The recipient of this stanza. + pub to: Option, + + /// The @id attribute of this stanza, which is required in order to match a + /// request with its response. + pub id: Option, + + /// The type of this message. + pub type_: MessageType, + + /// A list of bodies, sorted per language. Use + /// [get_best_body()](#method.get_best_body) to access them on reception. + pub bodies: BTreeMap, + + /// A list of subjects, sorted per language. Use + /// [get_best_subject()](#method.get_best_subject) to access them on + /// reception. + pub subjects: BTreeMap, + + /// An optional thread identifier, so that other people can reply directly + /// to this message. + pub thread: Option, + + /// A list of the extension payloads contained in this stanza. + pub payloads: Vec, +} + +impl Message { + /// Creates a new `` stanza for the given recipient. + pub fn new>>(to: J) -> Message { + Message { + from: None, + to: to.into(), + id: None, + type_: MessageType::Chat, + bodies: BTreeMap::new(), + subjects: BTreeMap::new(), + thread: None, + payloads: vec![], + } + } + + fn get_best<'a, T>( + map: &'a BTreeMap, + preferred_langs: Vec<&str>, + ) -> Option<(Lang, &'a T)> { + if map.is_empty() { + return None; + } + for lang in preferred_langs { + if let Some(value) = map.get(lang) { + return Some((Lang::from(lang), value)); + } + } + if let Some(value) = map.get("") { + return Some((Lang::new(), value)); + } + map.iter().map(|(lang, value)| (lang.clone(), value)).next() + } + + /// Returns the best matching body from a list of languages. + /// + /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English + /// body without an xml:lang attribute, and you pass ["fr", "en"] as your preferred languages, + /// `Some(("fr", the_second_body))` will be returned. + /// + /// If no body matches, an undefined body will be returned. + pub fn get_best_body(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Body)> { + Message::get_best::(&self.bodies, preferred_langs) + } + + /// Returns the best matching subject from a list of languages. + /// + /// For instance, if a message contains both an xml:lang='de', an xml:lang='fr' and an English + /// subject without an xml:lang attribute, and you pass ["fr", "en"] as your preferred + /// languages, `Some(("fr", the_second_subject))` will be returned. + /// + /// If no subject matches, an undefined subject will be returned. + pub fn get_best_subject(&self, preferred_langs: Vec<&str>) -> Option<(Lang, &Subject)> { + Message::get_best::(&self.subjects, preferred_langs) + } +} + +impl TryFrom for Message { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "message", DEFAULT_NS); + let from = get_attr!(root, "from", Option); + let to = get_attr!(root, "to", Option); + let id = get_attr!(root, "id", Option); + let type_ = get_attr!(root, "type", Default); + let mut bodies = BTreeMap::new(); + let mut subjects = BTreeMap::new(); + let mut thread = None; + let mut payloads = vec![]; + for elem in root.children() { + if elem.is("body", ns::DEFAULT_NS) { + check_no_children!(elem, "body"); + let lang = get_attr!(elem, "xml:lang", Default); + let body = Body(elem.text()); + if bodies.insert(lang, body).is_some() { + return Err(Error::ParseError( + "Body element present twice for the same xml:lang.", + )); + } + } else if elem.is("subject", ns::DEFAULT_NS) { + check_no_children!(elem, "subject"); + let lang = get_attr!(elem, "xml:lang", Default); + let subject = Subject(elem.text()); + if subjects.insert(lang, subject).is_some() { + return Err(Error::ParseError( + "Subject element present twice for the same xml:lang.", + )); + } + } else if elem.is("thread", ns::DEFAULT_NS) { + if thread.is_some() { + return Err(Error::ParseError("Thread element present twice.")); + } + check_no_children!(elem, "thread"); + thread = Some(Thread(elem.text())); + } else { + payloads.push(elem.clone()) + } + } + Ok(Message { + from, + to, + id, + type_, + bodies, + subjects, + thread, + payloads, + }) + } +} + +impl From for Element { + fn from(message: Message) -> Element { + Element::builder("message", ns::DEFAULT_NS) + .attr("from", message.from) + .attr("to", message.to) + .attr("id", message.id) + .attr("type", message.type_) + .append_all(message.subjects.into_iter().map(|(lang, subject)| { + let mut subject = Element::from(subject); + subject.set_attr( + "xml:lang", + match lang.as_ref() { + "" => None, + lang => Some(lang), + }, + ); + subject + })) + .append_all(message.bodies.into_iter().map(|(lang, body)| { + let mut body = Element::from(body); + body.set_attr( + "xml:lang", + match lang.as_ref() { + "" => None, + lang => Some(lang), + }, + ); + body + })) + .append_all(message.payloads.into_iter()) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(MessageType, 1); + assert_size!(Body, 12); + assert_size!(Subject, 12); + assert_size!(Thread, 12); + assert_size!(Message, 144); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(MessageType, 1); + assert_size!(Body, 24); + assert_size!(Subject, 24); + assert_size!(Thread, 24); + assert_size!(Message, 288); + } + + #[test] + fn test_simple() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let message = Message::try_from(elem).unwrap(); + assert_eq!(message.from, None); + assert_eq!(message.to, None); + assert_eq!(message.id, None); + assert_eq!(message.type_, MessageType::Normal); + assert!(message.payloads.is_empty()); + } + + #[test] + fn test_serialise() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let mut message = Message::new(None); + message.type_ = MessageType::Normal; + let elem2 = message.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_body() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let elem1 = elem.clone(); + let message = Message::try_from(elem).unwrap(); + assert_eq!(message.bodies[""], Body::from_str("Hello world!").unwrap()); + + { + let (lang, body) = message.get_best_body(vec!["en"]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(body, &Body::from_str("Hello world!").unwrap()); + } + + let elem2 = message.into(); + assert_eq!(elem1, elem2); + } + + #[test] + fn test_serialise_body() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let mut message = Message::new(Jid::Bare(BareJid::new("coucou", "example.org"))); + message + .bodies + .insert(String::from(""), Body::from_str("Hello world!").unwrap()); + let elem2 = message.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_subject() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let elem1 = elem.clone(); + let message = Message::try_from(elem).unwrap(); + assert_eq!( + message.subjects[""], + Subject::from_str("Hello world!").unwrap() + ); + + { + let (lang, subject) = message.get_best_subject(vec!["en"]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(subject, &Subject::from_str("Hello world!").unwrap()); + } + + let elem2 = message.into(); + assert_eq!(elem1, elem2); + } + + #[test] + fn get_best_body() { + #[cfg(not(feature = "component"))] + let elem: Element = "Hallo Welt!Salut le monde !Hello world!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Hello world!".parse().unwrap(); + let message = Message::try_from(elem).unwrap(); + + // Tests basic feature. + { + let (lang, body) = message.get_best_body(vec!["fr"]).unwrap(); + assert_eq!(lang, "fr"); + assert_eq!(body, &Body::from_str("Salut le monde !").unwrap()); + } + + // Tests order. + { + let (lang, body) = message.get_best_body(vec!["en", "de"]).unwrap(); + assert_eq!(lang, "de"); + assert_eq!(body, &Body::from_str("Hallo Welt!").unwrap()); + } + + // Tests fallback. + { + let (lang, body) = message.get_best_body(vec![]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(body, &Body::from_str("Hello world!").unwrap()); + } + + // Tests fallback. + { + let (lang, body) = message.get_best_body(vec!["ja"]).unwrap(); + assert_eq!(lang, ""); + assert_eq!(body, &Body::from_str("Hello world!").unwrap()); + } + + let message = Message::new(None); + + // Tests without a body. + assert_eq!(message.get_best_body(vec!("ja")), None); + } + + #[test] + fn test_attention() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let message = Message::try_from(elem).unwrap(); + let elem2 = message.into(); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/message_correct.rs b/xmpp-parsers/src/message_correct.rs new file mode 100644 index 0000000..374900f --- /dev/null +++ b/xmpp-parsers/src/message_correct.rs @@ -0,0 +1,99 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; + +generate_element!( + /// Defines that the message containing this payload should replace a + /// previous message, identified by the id. + Replace, "replace", MESSAGE_CORRECT, + attributes: [ + /// The 'id' attribute of the message getting corrected. + id: Required = "id", + ] +); + +impl MessagePayload for Replace {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Replace, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Replace, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + Replace::try_from(elem).unwrap(); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Replace::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in replace element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Replace::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in replace element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Replace::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let replace = Replace { + id: String::from("coucou"), + }; + let elem2 = replace.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/mix.rs b/xmpp-parsers/src/mix.rs new file mode 100644 index 0000000..7043f8a --- /dev/null +++ b/xmpp-parsers/src/mix.rs @@ -0,0 +1,398 @@ +// Copyright (c) 2020 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// TODO: validate nicks by applying the “nickname” profile of the PRECIS OpaqueString class, as +// defined in RFC 7700. + +use crate::iq::{IqResultPayload, IqSetPayload}; +use crate::message::MessagePayload; +use crate::pubsub::{NodeName, PubSubPayload}; +use jid::BareJid; + +generate_id!( + /// The identifier a participant receives when joining a channel. + ParticipantId +); + +impl ParticipantId { + /// Create a new ParticipantId. + pub fn new>(participant: P) -> ParticipantId { + ParticipantId(participant.into()) + } +} + +generate_id!( + /// A MIX channel identifier. + ChannelId +); + +generate_element!( + /// Represents a participant in a MIX channel, usually returned on the + /// urn:xmpp:mix:nodes:participants PubSub node. + Participant, "participant", MIX_CORE, + children: [ + /// The nick of this participant. + nick: Required = ("nick", MIX_CORE) => String, + + /// The bare JID of this participant. + // TODO: should be a BareJid! + jid: Required = ("jid", MIX_CORE) => String + ] +); + +impl PubSubPayload for Participant {} + +impl Participant { + /// Create a new MIX participant. + pub fn new, N: Into>(jid: J, nick: N) -> Participant { + Participant { + nick: nick.into(), + jid: jid.into(), + } + } +} + +generate_element!( + /// A node to subscribe to. + Subscribe, "subscribe", MIX_CORE, + attributes: [ + /// The PubSub node to subscribe to. + node: Required = "node", + ] +); + +impl Subscribe { + /// Create a new Subscribe element. + pub fn new>(node: N) -> Subscribe { + Subscribe { + node: NodeName(node.into()), + } + } +} + +generate_element!( + /// A request from a user’s server to join a MIX channel. + Join, "join", MIX_CORE, + attributes: [ + /// The participant identifier returned by the MIX service on successful join. + id: Option = "id", + ], + children: [ + /// The nick requested by the user or set by the service. + nick: Required = ("nick", MIX_CORE) => String, + + /// Which MIX nodes to subscribe to. + subscribes: Vec = ("subscribe", MIX_CORE) => Subscribe + ] +); + +impl IqSetPayload for Join {} +impl IqResultPayload for Join {} + +impl Join { + /// Create a new Join element. + pub fn from_nick_and_nodes>(nick: N, nodes: &[&str]) -> Join { + let subscribes = nodes + .into_iter() + .cloned() + .map(|n| Subscribe::new(n)) + .collect(); + Join { + id: None, + nick: nick.into(), + subscribes, + } + } + + /// Sets the JID on this update-subscription. + pub fn with_id>(mut self, id: I) -> Self { + self.id = Some(ParticipantId(id.into())); + self + } +} + +generate_element!( + /// Update a given subscription. + UpdateSubscription, "update-subscription", MIX_CORE, + attributes: [ + /// The JID of the user to be affected. + // TODO: why is it not a participant id instead? + jid: Option = "jid", + ], + children: [ + /// The list of additional nodes to subscribe to. + // TODO: what happens when we are already subscribed? Also, how do we unsubscribe from + // just one? + subscribes: Vec = ("subscribe", MIX_CORE) => Subscribe + ] +); + +impl IqSetPayload for UpdateSubscription {} +impl IqResultPayload for UpdateSubscription {} + +impl UpdateSubscription { + /// Create a new UpdateSubscription element. + pub fn from_nodes(nodes: &[&str]) -> UpdateSubscription { + let subscribes = nodes + .into_iter() + .cloned() + .map(|n| Subscribe::new(n)) + .collect(); + UpdateSubscription { + jid: None, + subscribes, + } + } + + /// Sets the JID on this update-subscription. + pub fn with_jid(mut self, jid: BareJid) -> Self { + self.jid = Some(jid); + self + } +} + +generate_empty_element!( + /// Request to leave a given MIX channel. It will automatically unsubscribe the user from all + /// nodes on this channel. + Leave, + "leave", + MIX_CORE +); + +impl IqSetPayload for Leave {} +impl IqResultPayload for Leave {} + +generate_element!( + /// A request to change the user’s nick. + SetNick, "setnick", MIX_CORE, + children: [ + /// The new requested nick. + nick: Required = ("nick", MIX_CORE) => String + ] +); + +impl IqSetPayload for SetNick {} +impl IqResultPayload for SetNick {} + +impl SetNick { + /// Create a new SetNick element. + pub fn new>(nick: N) -> SetNick { + SetNick { nick: nick.into() } + } +} + +generate_element!( + /// Message payload describing who actually sent the message, since unlike in MUC, all messages + /// are sent from the channel’s JID. + Mix, "mix", MIX_CORE, + children: [ + /// The nick of the user who said something. + nick: Required = ("nick", MIX_CORE) => String, + + /// The JID of the user who said something. + // TODO: should be a BareJid! + jid: Required = ("jid", MIX_CORE) => String + ] +); + +impl MessagePayload for Mix {} + +impl Mix { + /// Create a new Mix element. + pub fn new, J: Into>(nick: N, jid: J) -> Mix { + Mix { + nick: nick.into(), + jid: jid.into(), + } + } +} + +generate_element!( + /// Create a new MIX channel. + Create, "create", MIX_CORE, + attributes: [ + /// The requested channel identifier. + channel: Option = "channel", + ] +); + +impl IqSetPayload for Create {} +impl IqResultPayload for Create {} + +impl Create { + /// Create a new ad-hoc Create element. + pub fn new() -> Create { + Create { channel: None } + } + + /// Create a new Create element with a channel identifier. + pub fn from_channel_id>(channel: C) -> Create { + Create { + channel: Some(ChannelId(channel.into())), + } + } +} + +generate_element!( + /// Destroy a given MIX channel. + Destroy, "destroy", MIX_CORE, + attributes: [ + /// The channel identifier to be destroyed. + channel: Required = "channel", + ] +); + +// TODO: section 7.3.4, example 33, doesn’t mirror the in the iq result unlike every +// other section so far. +impl IqSetPayload for Destroy {} + +impl Destroy { + /// Create a new Destroy element. + pub fn new>(channel: C) -> Destroy { + Destroy { + channel: ChannelId(channel.into()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn participant() { + let elem: Element = "foo@barcoucou" + .parse() + .unwrap(); + let participant = Participant::try_from(elem).unwrap(); + assert_eq!(participant.nick, "coucou"); + assert_eq!(participant.jid, "foo@bar"); + } + + #[test] + fn join() { + let elem: Element = "coucou" + .parse() + .unwrap(); + let join = Join::try_from(elem).unwrap(); + assert_eq!(join.nick, "coucou"); + assert_eq!(join.id, None); + assert_eq!(join.subscribes.len(), 2); + assert_eq!(join.subscribes[0].node.0, "urn:xmpp:mix:nodes:messages"); + assert_eq!(join.subscribes[1].node.0, "urn:xmpp:mix:nodes:info"); + } + + #[test] + fn update_subscription() { + let elem: Element = "" + .parse() + .unwrap(); + let update_subscription = UpdateSubscription::try_from(elem).unwrap(); + assert_eq!(update_subscription.jid, None); + assert_eq!(update_subscription.subscribes.len(), 1); + assert_eq!( + update_subscription.subscribes[0].node.0, + "urn:xmpp:mix:nodes:participants" + ); + } + + #[test] + fn leave() { + let elem: Element = "".parse().unwrap(); + Leave::try_from(elem).unwrap(); + } + + #[test] + fn setnick() { + let elem: Element = "coucou" + .parse() + .unwrap(); + let setnick = SetNick::try_from(elem).unwrap(); + assert_eq!(setnick.nick, "coucou"); + } + + #[test] + fn message_mix() { + let elem: Element = + "foo@barcoucou" + .parse() + .unwrap(); + let mix = Mix::try_from(elem).unwrap(); + assert_eq!(mix.nick, "coucou"); + assert_eq!(mix.jid, "foo@bar"); + } + + #[test] + fn create() { + let elem: Element = "" + .parse() + .unwrap(); + let create = Create::try_from(elem).unwrap(); + assert_eq!(create.channel.unwrap().0, "coucou"); + + let elem: Element = "".parse().unwrap(); + let create = Create::try_from(elem).unwrap(); + assert_eq!(create.channel, None); + } + + #[test] + fn destroy() { + let elem: Element = "" + .parse() + .unwrap(); + let destroy = Destroy::try_from(elem).unwrap(); + assert_eq!(destroy.channel.0, "coucou"); + } + + #[test] + fn serialise() { + let elem: Element = Join::from_nick_and_nodes("coucou", &["foo", "bar"]).into(); + let xml = String::from(&elem); + assert_eq!(xml, "coucou"); + + let elem: Element = UpdateSubscription::from_nodes(&["foo", "bar"]).into(); + let xml = String::from(&elem); + assert_eq!(xml, ""); + + let elem: Element = Leave.into(); + let xml = String::from(&elem); + assert_eq!(xml, ""); + + let elem: Element = SetNick::new("coucou").into(); + let xml = String::from(&elem); + assert_eq!( + xml, + "coucou" + ); + + let elem: Element = Mix::new("coucou", "coucou@example").into(); + let xml = String::from(&elem); + assert_eq!( + xml, + "coucoucoucou@example" + ); + + let elem: Element = Create::new().into(); + let xml = String::from(&elem); + assert_eq!(xml, ""); + + let elem: Element = Create::from_channel_id("coucou").into(); + let xml = String::from(&elem); + assert_eq!( + xml, + "" + ); + + let elem: Element = Destroy::new("coucou").into(); + let xml = String::from(&elem); + assert_eq!( + xml, + "" + ); + } +} diff --git a/xmpp-parsers/src/mood.rs b/xmpp-parsers/src/mood.rs new file mode 100644 index 0000000..4270c58 --- /dev/null +++ b/xmpp-parsers/src/mood.rs @@ -0,0 +1,312 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +generate_element_enum!( + /// Enum representing all of the possible values of the XEP-0107 moods. + MoodEnum, "mood", MOOD, { + /// Impressed with fear or apprehension; in fear; apprehensive. + Afraid => "afraid", + + /// Astonished; confounded with fear, surprise or wonder. + Amazed => "amazed", + + /// Inclined to love; having a propensity to love, or to sexual enjoyment; loving, fond, affectionate, passionate, lustful, sexual, etc. + Amorous => "amorous", + + /// Displaying or feeling anger, i.e., a strong feeling of displeasure, hostility or antagonism towards someone or something, usually combined with an urge to harm. + Angry => "angry", + + /// To be disturbed or irritated, especially by continued or repeated acts. + Annoyed => "annoyed", + + /// Full of anxiety or disquietude; greatly concerned or solicitous, esp. respecting something future or unknown; being in painful suspense. + Anxious => "anxious", + + /// To be stimulated in one's feelings, especially to be sexually stimulated. + Aroused => "aroused", + + /// Feeling shame or guilt. + Ashamed => "ashamed", + + /// Suffering from boredom; uninterested, without attention. + Bored => "bored", + + /// Strong in the face of fear; courageous. + Brave => "brave", + + /// Peaceful, quiet. + Calm => "calm", + + /// Taking care or caution; tentative. + Cautious => "cautious", + + /// Feeling the sensation of coldness, especially to the point of discomfort. + Cold => "cold", + + /// Feeling very sure of or positive about something, especially about one's own capabilities. + Confident => "confident", + + /// Chaotic, jumbled or muddled. + Confused => "confused", + + /// Feeling introspective or thoughtful. + Contemplative => "contemplative", + + /// Pleased at the satisfaction of a want or desire; satisfied. + Contented => "contented", + + /// Grouchy, irritable; easily upset. + Cranky => "cranky", + + /// Feeling out of control; feeling overly excited or enthusiastic. + Crazy => "crazy", + + /// Feeling original, expressive, or imaginative. + Creative => "creative", + + /// Inquisitive; tending to ask questions, investigate, or explore. + Curious => "curious", + + /// Feeling sad and dispirited. + Dejected => "dejected", + + /// Severely despondent and unhappy. + Depressed => "depressed", + + /// Defeated of expectation or hope; let down. + Disappointed => "disappointed", + + /// Filled with disgust; irritated and out of patience. + Disgusted => "disgusted", + + /// Feeling a sudden or complete loss of courage in the face of trouble or danger. + Dismayed => "dismayed", + + /// Having one's attention diverted; preoccupied. + Distracted => "distracted", + + /// Having a feeling of shameful discomfort. + Embarrassed => "embarrassed", + + /// Feeling pain by the excellence or good fortune of another. + Envious => "envious", + + /// Having great enthusiasm. + Excited => "excited", + + /// In the mood for flirting. + Flirtatious => "flirtatious", + + /// Suffering from frustration; dissatisfied, agitated, or discontented because one is unable to perform an action or fulfill a desire. + Frustrated => "frustrated", + + /// Feeling appreciation or thanks. + Grateful => "grateful", + + /// Feeling very sad about something, especially something lost; mournful; sorrowful. + Grieving => "grieving", + + /// Unhappy and irritable. + Grumpy => "grumpy", + + /// Feeling responsible for wrongdoing; feeling blameworthy. + Guilty => "guilty", + + /// Experiencing the effect of favourable fortune; having the feeling arising from the consciousness of well-being or of enjoyment; enjoying good of any kind, as peace, tranquillity, comfort; contented; joyous. + Happy => "happy", + + /// Having a positive feeling, belief, or expectation that something wished for can or will happen. + Hopeful => "hopeful", + + /// Feeling the sensation of heat, especially to the point of discomfort. + Hot => "hot", + + /// Having or showing a modest or low estimate of one's own importance; feeling lowered in dignity or importance. + Humbled => "humbled", + + /// Feeling deprived of dignity or self-respect. + Humiliated => "humiliated", + + /// Having a physical need for food. + Hungry => "hungry", + + /// Wounded, injured, or pained, whether physically or emotionally. + Hurt => "hurt", + + /// Favourably affected by something or someone. + Impressed => "impressed", + + /// Feeling amazement at something or someone; or feeling a combination of fear and reverence. + InAwe => "in_awe", + + /// Feeling strong affection, care, liking, or attraction.. + InLove => "in_love", + + /// Showing anger or indignation, especially at something unjust or wrong. + Indignant => "indignant", + + /// Showing great attention to something or someone; having or showing interest. + Interested => "interested", + + /// Under the influence of alcohol; drunk. + Intoxicated => "intoxicated", + + /// Feeling as if one cannot be defeated, overcome or denied. + Invincible => "invincible", + + /// Fearful of being replaced in position or affection. + Jealous => "jealous", + + /// Feeling isolated, empty, or abandoned. + Lonely => "lonely", + + /// Unable to find one's way, either physically or emotionally. + Lost => "lost", + + /// Feeling as if one will be favored by luck. + Lucky => "lucky", + + /// Causing or intending to cause intentional harm; bearing ill will towards another; cruel; malicious. + Mean => "mean", + + /// Given to sudden or frequent changes of mind or feeling; temperamental. + Moody => "moody", + + /// Easily agitated or alarmed; apprehensive or anxious. + Nervous => "nervous", + + /// Not having a strong mood or emotional state. + Neutral => "neutral", + + /// Feeling emotionally hurt, displeased, or insulted. + Offended => "offended", + + /// Feeling resentful anger caused by an extremely violent or vicious attack, or by an offensive, immoral, or indecent act. + Outraged => "outraged", + + /// Interested in play; fun, recreational, unserious, lighthearted; joking, silly. + Playful => "playful", + + /// Feeling a sense of one's own worth or accomplishment. + Proud => "proud", + + /// Having an easy-going mood; not stressed; calm. + Relaxed => "relaxed", + + /// Feeling uplifted because of the removal of stress or discomfort. + Relieved => "relieved", + + /// Feeling regret or sadness for doing something wrong. + Remorseful => "remorseful", + + /// Without rest; unable to be still or quiet; uneasy; continually moving. + Restless => "restless", + + /// Feeling sorrow; sorrowful, mournful. + Sad => "sad", + + /// Mocking and ironical. + Sarcastic => "sarcastic", + + /// Pleased at the fulfillment of a need or desire. + Satisfied => "satisfied", + + /// Without humor or expression of happiness; grave in manner or disposition; earnest; thoughtful; solemn. + Serious => "serious", + + /// Surprised, startled, confused, or taken aback. + Shocked => "shocked", + + /// Feeling easily frightened or scared; timid; reserved or coy. + Shy => "shy", + + /// Feeling in poor health; ill. + Sick => "sick", + + /// Feeling the need for sleep. + Sleepy => "sleepy", + + /// Acting without planning; natural; impulsive. + Spontaneous => "spontaneous", + + /// Suffering emotional pressure. + Stressed => "stressed", + + /// Capable of producing great physical force; or, emotionally forceful, able, determined, unyielding. + Strong => "strong", + + /// Experiencing a feeling caused by something unexpected. + Surprised => "surprised", + + /// Showing appreciation or gratitude. + Thankful => "thankful", + + /// Feeling the need to drink. + Thirsty => "thirsty", + + /// In need of rest or sleep. + Tired => "tired", + + /// [Feeling any emotion not defined here.] + Undefined => "undefined", + + /// Lacking in force or ability, either physical or emotional. + Weak => "weak", + + /// Thinking about unpleasant things that have happened or that might happen; feeling afraid and unhappy. + Worried => "worried", + } +); + +generate_elem_id!( + /// Free-form text description of the mood. + Text, + "text", + MOOD +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(MoodEnum, 1); + assert_size!(Text, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(MoodEnum, 1); + assert_size!(Text, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let mood = MoodEnum::try_from(elem).unwrap(); + assert_eq!(mood, MoodEnum::Happy); + } + + #[test] + fn test_text() { + let elem: Element = "Yay!" + .parse() + .unwrap(); + let elem2 = elem.clone(); + let text = Text::try_from(elem).unwrap(); + assert_eq!(text.0, String::from("Yay!")); + + let elem3 = text.into(); + assert_eq!(elem2, elem3); + } +} diff --git a/xmpp-parsers/src/muc/mod.rs b/xmpp-parsers/src/muc/mod.rs new file mode 100644 index 0000000..5875e3b --- /dev/null +++ b/xmpp-parsers/src/muc/mod.rs @@ -0,0 +1,14 @@ +// Copyright (c) 2017 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// The http://jabber.org/protocol/muc protocol. +pub mod muc; + +/// The http://jabber.org/protocol/muc#user protocol. +pub mod user; + +pub use self::muc::Muc; +pub use self::user::MucUser; diff --git a/xmpp-parsers/src/muc/muc.rs b/xmpp-parsers/src/muc/muc.rs new file mode 100644 index 0000000..edce303 --- /dev/null +++ b/xmpp-parsers/src/muc/muc.rs @@ -0,0 +1,194 @@ +// Copyright (c) 2017 Maxime “pep” Buquet +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::date::DateTime; +use crate::presence::PresencePayload; + +generate_element!( + /// Represents the query for messages before our join. + #[derive(Default)] + History, "history", MUC, + attributes: [ + /// How many characters of history to send, in XML characters. + maxchars: Option = "maxchars", + + /// How many messages to send. + maxstanzas: Option = "maxstanzas", + + /// Only send messages received in these last seconds. + seconds: Option = "seconds", + + /// Only send messages after this date. + since: Option = "since", + ] +); + +impl History { + /// Create a new empty history element. + pub fn new() -> Self { + History::default() + } + + /// Set how many characters of history to send. + pub fn with_maxchars(mut self, maxchars: u32) -> Self { + self.maxchars = Some(maxchars); + self + } + + /// Set how many messages to send. + pub fn with_maxstanzas(mut self, maxstanzas: u32) -> Self { + self.maxstanzas = Some(maxstanzas); + self + } + + /// Only send messages received in these last seconds. + pub fn with_seconds(mut self, seconds: u32) -> Self { + self.seconds = Some(seconds); + self + } + + /// Only send messages received since this date. + pub fn with_since(mut self, since: DateTime) -> Self { + self.since = Some(since); + self + } +} + +generate_element!( + /// Represents a room join request. + #[derive(Default)] + Muc, "x", MUC, children: [ + /// Password to use when the room is protected by a password. + password: Option = ("password", MUC) => String, + + /// Controls how much and how old we want to receive history on join. + history: Option = ("history", MUC) => History + ] +); + +impl PresencePayload for Muc {} + +impl Muc { + /// Create a new MUC join element. + pub fn new() -> Self { + Muc::default() + } + + /// Join a room with this password. + pub fn with_password(mut self, password: String) -> Self { + self.password = Some(password); + self + } + + /// Join a room with only that much history. + pub fn with_history(mut self, history: History) -> Self { + self.history = Some(history); + self + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + use std::str::FromStr; + + #[test] + fn test_muc_simple() { + let elem: Element = "" + .parse() + .unwrap(); + Muc::try_from(elem).unwrap(); + } + + #[test] + fn test_muc_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Muc::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in x element."); + } + + #[test] + fn test_muc_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let muc = Muc { + password: None, + history: None, + }; + let elem2 = muc.into(); + assert_eq!(elem, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_muc_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Muc::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in x element."); + } + + #[test] + fn test_muc_simple_password() { + let elem: Element = + "coucou" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let muc = Muc::try_from(elem).unwrap(); + assert_eq!(muc.password, Some("coucou".to_owned())); + + let elem2 = Element::from(muc); + assert_eq!(elem1, elem2); + } + + #[test] + fn history() { + let elem: Element = " + + + " + .parse() + .unwrap(); + let muc = Muc::try_from(elem).unwrap(); + let muc2 = Muc::new().with_history(History::new().with_maxstanzas(0)); + assert_eq!(muc, muc2); + + let history = muc.history.unwrap(); + assert_eq!(history.maxstanzas, Some(0)); + assert_eq!(history.maxchars, None); + assert_eq!(history.seconds, None); + assert_eq!(history.since, None); + + let elem: Element = " + + + " + .parse() + .unwrap(); + let muc = Muc::try_from(elem).unwrap(); + assert_eq!( + muc.history.unwrap().since.unwrap(), + DateTime::from_str("1970-01-01T00:00:00+00:00").unwrap() + ); + } +} diff --git a/xmpp-parsers/src/muc/user.rs b/xmpp-parsers/src/muc/user.rs new file mode 100644 index 0000000..c608a83 --- /dev/null +++ b/xmpp-parsers/src/muc/user.rs @@ -0,0 +1,725 @@ +// Copyright (c) 2017 Maxime “pep” Buquet +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use jid::FullJid; +use std::convert::TryFrom; + +generate_attribute_enum!( +/// Lists all of the possible status codes used in MUC presences. +Status, "status", MUC_USER, "code", { + /// Inform user that any occupant is allowed to see the user's full JID + NonAnonymousRoom => 100, + + /// Inform user that his or her affiliation changed while not in the room + AffiliationChange => 101, + + /// Inform occupants that room now shows unavailable members + ConfigShowsUnavailableMembers => 102, + + /// Inform occupants that room now does not show unavailable members + ConfigHidesUnavailableMembers => 103, + + /// Inform occupants that a non-privacy-related room configuration change has occurred + ConfigNonPrivacyRelated => 104, + + /// Inform user that presence refers to itself + SelfPresence => 110, + + /// Inform occupants that room logging is now enabled + ConfigRoomLoggingEnabled => 170, + + /// Inform occupants that room logging is now disabled + ConfigRoomLoggingDisabled => 171, + + /// Inform occupants that the room is now non-anonymous + ConfigRoomNonAnonymous => 172, + + /// Inform occupants that the room is now semi-anonymous + ConfigRoomSemiAnonymous => 173, + + /// Inform user that a new room has been created + RoomHasBeenCreated => 201, + + /// Inform user that service has assigned or modified occupant's roomnick + AssignedNick => 210, + + /// Inform user that he or she has been banned from the room + Banned => 301, + + /// Inform all occupants of new room nickname + NewNick => 303, + + /// Inform user that he or she has been kicked from the room + Kicked => 307, + + /// Inform user that he or she is being removed from the room + /// because of an affiliation change + RemovalFromRoom => 321, + + /// Inform user that he or she is being removed from the room + /// because the room has been changed to members-only and the + /// user is not a member + ConfigMembersOnly => 322, + + /// Inform user that he or she is being removed from the room + /// because the MUC service is being shut down + ServiceShutdown => 332, +}); + +/// Optional element used in elements inside presence stanzas of type +/// "unavailable" that are sent to users who are kick or banned, as well as within IQs for tracking +/// purposes. -- CHANGELOG 0.17 (2002-10-23) +/// +/// Possesses a 'jid' and a 'nick' attribute, so that an action can be attributed either to a real +/// JID or to a roomnick. -- CHANGELOG 1.25 (2012-02-08) +#[derive(Debug, Clone, PartialEq)] +pub enum Actor { + /// The full JID associated with this user. + Jid(FullJid), + + /// The nickname of this user. + Nick(String), +} + +impl TryFrom for Actor { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "actor", MUC_USER); + check_no_unknown_attributes!(elem, "actor", ["jid", "nick"]); + check_no_children!(elem, "actor"); + let jid: Option = get_attr!(elem, "jid", Option); + let nick = get_attr!(elem, "nick", Option); + + match (jid, nick) { + (Some(_), Some(_)) | (None, None) => Err(Error::ParseError( + "Either 'jid' or 'nick' attribute is required.", + )), + (Some(jid), _) => Ok(Actor::Jid(jid)), + (_, Some(nick)) => Ok(Actor::Nick(nick)), + } + } +} + +impl From for Element { + fn from(actor: Actor) -> Element { + let elem = Element::builder("actor", ns::MUC_USER); + + (match actor { + Actor::Jid(jid) => elem.attr("jid", jid), + Actor::Nick(nick) => elem.attr("nick", nick), + }) + .build() + } +} + +generate_element!( + /// Used to continue a one-to-one discussion in a room, with more than one + /// participant. + Continue, "continue", MUC_USER, + attributes: [ + /// The thread to continue in this room. + thread: Option = "thread", + ] +); + +generate_elem_id!( + /// A reason for inviting, declining, etc. a request. + Reason, + "reason", + MUC_USER +); + +generate_attribute!( + /// The affiliation of an entity with a room, which isn’t tied to its + /// presence in it. + Affiliation, "affiliation", { + /// The user who created the room, or who got appointed by its creator + /// to be their equal. + Owner => "owner", + + /// A user who has been empowered by an owner to do administrative + /// operations. + Admin => "admin", + + /// A user who is whitelisted to speak in moderated rooms, or to join a + /// member-only room. + Member => "member", + + /// A user who has been banned from this room. + Outcast => "outcast", + + /// A normal participant. + None => "none", + }, Default = None +); + +generate_attribute!( + /// The current role of an entity in a room, it can be changed by an owner + /// or an administrator but will be lost once they leave the room. + Role, "role", { + /// This user can kick other participants, as well as grant and revoke + /// them voice. + Moderator => "moderator", + + /// A user who can speak in this room. + Participant => "participant", + + /// A user who cannot speak in this room, and must request voice before + /// doing so. + Visitor => "visitor", + + /// A user who is absent from the room. + None => "none", + }, Default = None +); + +generate_element!( + /// An item representing a user in a room. + Item, "item", MUC_USER, attributes: [ + /// The affiliation of this user with the room. + affiliation: Required = "affiliation", + + /// The real JID of this user, if you are allowed to see it. + jid: Option = "jid", + + /// The current nickname of this user. + nick: Option = "nick", + + /// The current role of this user. + role: Required = "role", + ], children: [ + /// The actor affected by this item. + actor: Option = ("actor", MUC_USER) => Actor, + + /// Whether this continues a one-to-one discussion. + continue_: Option = ("continue", MUC_USER) => Continue, + + /// A reason for this item. + reason: Option = ("reason", MUC_USER) => Reason + ] +); + +impl Item { + /// Creates a new item with the given affiliation and role. + pub fn new(affiliation: Affiliation, role: Role) -> Item { + Item { + affiliation, + role, + jid: None, + nick: None, + actor: None, + continue_: None, + reason: None, + } + } +} + +generate_element!( + /// The main muc#user element. + MucUser, "x", MUC_USER, children: [ + /// List of statuses applying to this item. + status: Vec = ("status", MUC_USER) => Status, + + /// List of items. + items: Vec = ("item", MUC_USER) => Item + ] +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + MucUser::try_from(elem).unwrap(); + } + + #[test] + fn statuses_and_items() { + let elem: Element = " + + + + + + " + .parse() + .unwrap(); + let muc_user = MucUser::try_from(elem).unwrap(); + assert_eq!(muc_user.status.len(), 2); + assert_eq!(muc_user.status[0], Status::AffiliationChange); + assert_eq!(muc_user.status[1], Status::ConfigShowsUnavailableMembers); + assert_eq!(muc_user.items.len(), 1); + assert_eq!(muc_user.items[0].affiliation, Affiliation::Member); + assert_eq!(muc_user.items[0].role, Role::Moderator); + } + + #[test] + fn test_invalid_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let error = MucUser::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in x element."); + } + + #[test] + fn test_serialise() { + let elem: Element = " + + " + .parse() + .unwrap(); + let muc = MucUser { + status: vec![], + items: vec![], + }; + let elem2 = muc.into(); + assert_eq!(elem, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = MucUser::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in x element."); + } + + #[test] + fn test_status_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + Status::try_from(elem).unwrap(); + } + + #[test] + fn test_status_invalid() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'code' missing."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_status_invalid_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in status element."); + } + + #[test] + fn test_status_simple_code() { + let elem: Element = " + + " + .parse() + .unwrap(); + let status = Status::try_from(elem).unwrap(); + assert_eq!(status, Status::Kicked); + } + + #[test] + fn test_status_invalid_code() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Invalid status code value."); + } + + #[test] + fn test_status_invalid_code2() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Status::try_from(elem).unwrap_err(); + let error = match error { + Error::ParseIntError(error) => error, + _ => panic!(), + }; + assert_eq!(error.to_string(), "invalid digit found in string"); + } + + #[test] + fn test_actor_required_attributes() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Actor::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Either 'jid' or 'nick' attribute is required."); + } + + #[test] + fn test_actor_required_attributes2() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Actor::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Either 'jid' or 'nick' attribute is required."); + } + + #[test] + fn test_actor_jid() { + let elem: Element = " + + " + .parse() + .unwrap(); + let actor = Actor::try_from(elem).unwrap(); + let jid = match actor { + Actor::Jid(jid) => jid, + _ => panic!(), + }; + assert_eq!(jid, "foo@bar/baz".parse::().unwrap()); + } + + #[test] + fn test_actor_nick() { + let elem: Element = " + + " + .parse() + .unwrap(); + let actor = Actor::try_from(elem).unwrap(); + let nick = match actor { + Actor::Nick(nick) => nick, + _ => panic!(), + }; + assert_eq!(nick, "baz".to_owned()); + } + + #[test] + fn test_continue_simple() { + let elem: Element = " + + " + .parse() + .unwrap(); + Continue::try_from(elem).unwrap(); + } + + #[test] + fn test_continue_thread_attribute() { + let elem: Element = " + + " + .parse() + .unwrap(); + let continue_ = Continue::try_from(elem).unwrap(); + assert_eq!(continue_.thread, Some("foo".to_owned())); + } + + #[test] + fn test_continue_invalid() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let continue_ = Continue::try_from(elem).unwrap_err(); + let message = match continue_ { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in continue element.".to_owned()); + } + + #[test] + fn test_reason_simple() { + let elem: Element = " + Reason" + .parse() + .unwrap(); + let elem2 = elem.clone(); + let reason = Reason::try_from(elem).unwrap(); + assert_eq!(reason.0, "Reason".to_owned()); + + let elem3 = reason.into(); + assert_eq!(elem2, elem3); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_reason_invalid_attribute() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Reason::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in reason element.".to_owned()); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_reason_invalid() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let error = Reason::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in reason element.".to_owned()); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_item_invalid_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Item::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in item element.".to_owned()); + } + + #[test] + fn test_item_affiliation_role_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + Item::try_from(elem).unwrap(); + } + + #[test] + fn test_item_affiliation_role_invalid_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Item::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'role' missing.".to_owned()); + } + + #[test] + fn test_item_nick_attr() { + let elem: Element = " + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + match item { + Item { nick, .. } => assert_eq!(nick, Some("foobar".to_owned())), + } + } + + #[test] + fn test_item_affiliation_role_invalid_attr2() { + let elem: Element = " + + " + .parse() + .unwrap(); + let error = Item::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Required attribute 'affiliation' missing.".to_owned() + ); + } + + #[test] + fn test_item_role_actor_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + match item { + Item { actor, .. } => assert_eq!(actor, Some(Actor::Nick("foobar".to_owned()))), + } + } + + #[test] + fn test_item_role_continue_child() { + let elem: Element = " + + + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + let continue_1 = Continue { + thread: Some("foobar".to_owned()), + }; + match item { + Item { + continue_: Some(continue_2), + .. + } => assert_eq!(continue_2.thread, continue_1.thread), + _ => panic!(), + } + } + + #[test] + fn test_item_role_reason_child() { + let elem: Element = " + + foobar + + " + .parse() + .unwrap(); + let item = Item::try_from(elem).unwrap(); + match item { + Item { reason, .. } => assert_eq!(reason, Some(Reason("foobar".to_owned()))), + } + } + + #[test] + fn test_serialize_item() { + let reference: Element = "foobar" + .parse() + .unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + let actor = Actor::try_from(elem).unwrap(); + + let elem: Element = + "" + .parse() + .unwrap(); + let continue_ = Continue::try_from(elem).unwrap(); + + let elem: Element = "foobar" + .parse() + .unwrap(); + let reason = Reason::try_from(elem).unwrap(); + + let item = Item { + affiliation: Affiliation::Member, + role: Role::Moderator, + jid: None, + nick: None, + actor: Some(actor), + reason: Some(reason), + continue_: Some(continue_), + }; + + let serialized: Element = item.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/nick.rs b/xmpp-parsers/src/nick.rs new file mode 100644 index 0000000..20ae7a9 --- /dev/null +++ b/xmpp-parsers/src/nick.rs @@ -0,0 +1,79 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +generate_elem_id!( + /// Represents a global, memorable, friendly or informal name chosen by a user. + Nick, + "nick", + NICK +); + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Nick, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Nick, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "Link Mauve" + .parse() + .unwrap(); + let nick = Nick::try_from(elem).unwrap(); + assert_eq!(&nick.0, "Link Mauve"); + } + + #[test] + fn test_serialise() { + let elem1 = Element::from(Nick(String::from("Link Mauve"))); + let elem2: Element = "Link Mauve" + .parse() + .unwrap(); + assert_eq!(elem1, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Nick::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in nick element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Nick::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in nick element."); + } +} diff --git a/xmpp-parsers/src/ns.rs b/xmpp-parsers/src/ns.rs new file mode 100644 index 0000000..4002402 --- /dev/null +++ b/xmpp-parsers/src/ns.rs @@ -0,0 +1,271 @@ +// Copyright (c) 2017-2018 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const JABBER_CLIENT: &str = "jabber:client"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const XMPP_STANZAS: &str = "urn:ietf:params:xml:ns:xmpp-stanzas"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const STREAM: &str = "http://etherx.jabber.org/streams"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const TLS: &str = "urn:ietf:params:xml:ns:xmpp-tls"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const SASL: &str = "urn:ietf:params:xml:ns:xmpp-sasl"; +/// RFC 6120: Extensible Messaging and Presence Protocol (XMPP): Core +pub const BIND: &str = "urn:ietf:params:xml:ns:xmpp-bind"; + +/// RFC 6121: Extensible Messaging and Presence Protocol (XMPP): Instant Messaging and Presence +pub const ROSTER: &str = "jabber:iq:roster"; + +/// RFC 7395: An Extensible Messaging and Presence Protocol (XMPP) Subprotocol for WebSocket +pub const WEBSOCKET: &str = "urn:ietf:params:xml:ns:xmpp-framing"; + +/// XEP-0004: Data Forms +pub const DATA_FORMS: &str = "jabber:x:data"; + +/// XEP-0030: Service Discovery +pub const DISCO_INFO: &str = "http://jabber.org/protocol/disco#info"; +/// XEP-0030: Service Discovery +pub const DISCO_ITEMS: &str = "http://jabber.org/protocol/disco#items"; + +/// XEP-0045: Multi-User Chat +pub const MUC: &str = "http://jabber.org/protocol/muc"; +/// XEP-0045: Multi-User Chat +pub const MUC_USER: &str = "http://jabber.org/protocol/muc#user"; + +/// XEP-0047: In-Band Bytestreams +pub const IBB: &str = "http://jabber.org/protocol/ibb"; + +/// XEP-0048: Bookmarks +pub const BOOKMARKS: &str = "storage:bookmarks"; + +/// XEP-0059: Result Set Management +pub const RSM: &str = "http://jabber.org/protocol/rsm"; + +/// XEP-0060: Publish-Subscribe +pub const PUBSUB: &str = "http://jabber.org/protocol/pubsub"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_ERRORS: &str = "http://jabber.org/protocol/pubsub#errors"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_EVENT: &str = "http://jabber.org/protocol/pubsub#event"; +/// XEP-0060: Publish-Subscribe +pub const PUBSUB_OWNER: &str = "http://jabber.org/protocol/pubsub#owner"; +/// XEP-0060: Publish-Subscribe node configuration +pub const PUBSUB_CONFIGURE: &str = "http://jabber.org/protocol/pubsub#node_config"; + +/// XEP-0071: XHTML-IM +pub const XHTML_IM: &str = "http://jabber.org/protocol/xhtml-im"; +/// XEP-0071: XHTML-IM +pub const XHTML: &str = "http://www.w3.org/1999/xhtml"; + +/// XEP-0077: In-Band Registration +pub const REGISTER: &str = "jabber:iq:register"; + +/// XEP-0084: User Avatar +pub const AVATAR_DATA: &str = "urn:xmpp:avatar:data"; +/// XEP-0084: User Avatar +pub const AVATAR_METADATA: &str = "urn:xmpp:avatar:metadata"; + +/// XEP-0085: Chat State Notifications +pub const CHATSTATES: &str = "http://jabber.org/protocol/chatstates"; + +/// XEP-0092: Software Version +pub const VERSION: &str = "jabber:iq:version"; + +/// XEP-0107: User Mood +pub const MOOD: &str = "http://jabber.org/protocol/mood"; + +/// XEP-0114: Jabber Component Protocol +pub const COMPONENT_ACCEPT: &str = "jabber:component:accept"; + +/// XEP-0114: Jabber Component Protocol +pub const COMPONENT: &str = "jabber:component:accept"; + +/// XEP-0115: Entity Capabilities +pub const CAPS: &str = "http://jabber.org/protocol/caps"; + +/// XEP-0118: User Tune +pub const TUNE: &str = "http://jabber.org/protocol/tune"; + +/// XEP-0157: Contact Addresses for XMPP Services +pub const SERVER_INFO: &str = "http://jabber.org/network/serverinfo"; + +/// XEP-0166: Jingle +pub const JINGLE: &str = "urn:xmpp:jingle:1"; + +/// XEP-0167: Jingle RTP Sessions +pub const JINGLE_RTP: &str = "urn:xmpp:jingle:apps:rtp:1"; +/// XEP-0167: Jingle RTP Sessions +pub const JINGLE_RTP_AUDIO: &str = "urn:xmpp:jingle:apps:rtp:audio"; +/// XEP-0167: Jingle RTP Sessions +pub const JINGLE_RTP_VIDEO: &str = "urn:xmpp:jingle:apps:rtp:video"; + +/// XEP-0172: User Nickname +pub const NICK: &str = "http://jabber.org/protocol/nick"; + +/// XEP-0176: Jingle ICE-UDP Transport Method +pub const JINGLE_ICE_UDP: &str = "urn:xmpp:jingle:transports:ice-udp:1"; + +/// XEP-0177: Jingle Raw UDP Transport Method +pub const JINGLE_RAW_UDP: &str = "urn:xmpp:jingle:transports:raw-udp:1"; + +/// XEP-0184: Message Delivery Receipts +pub const RECEIPTS: &str = "urn:xmpp:receipts"; + +/// XEP-0191: Blocking Command +pub const BLOCKING: &str = "urn:xmpp:blocking"; +/// XEP-0191: Blocking Command +pub const BLOCKING_ERRORS: &str = "urn:xmpp:blocking:errors"; + +/// XEP-0198: Stream Management +pub const SM: &str = "urn:xmpp:sm:3"; + +/// XEP-0199: XMPP Ping +pub const PING: &str = "urn:xmpp:ping"; + +/// XEP-0202: Entity Time +pub const TIME: &str = "urn:xmpp:time"; + +/// XEP-0203: Delayed Delivery +pub const DELAY: &str = "urn:xmpp:delay"; + +/// XEP-0221: Data Forms Media Element +pub const MEDIA_ELEMENT: &str = "urn:xmpp:media-element"; + +/// XEP-0224: Attention +pub const ATTENTION: &str = "urn:xmpp:attention:0"; + +/// XEP-0231: Bits of Binary +pub const BOB: &str = "urn:xmpp:bob"; + +/// XEP-0234: Jingle File Transfer +pub const JINGLE_FT: &str = "urn:xmpp:jingle:apps:file-transfer:5"; +/// XEP-0234: Jingle File Transfer +pub const JINGLE_FT_ERROR: &str = "urn:xmpp:jingle:apps:file-transfer:errors:0"; + +/// XEP-0257: Client Certificate Management for SASL EXTERNAL +pub const SASL_CERT: &str = "urn:xmpp:saslcert:1"; + +/// XEP-0260: Jingle SOCKS5 Bytestreams Transport Method +pub const JINGLE_S5B: &str = "urn:xmpp:jingle:transports:s5b:1"; + +/// XEP-0261: Jingle In-Band Bytestreams Transport Method +pub const JINGLE_IBB: &str = "urn:xmpp:jingle:transports:ibb:1"; + +/// XEP-0277: Microblogging over XMPP +pub const MICROBLOG: &str = "urn:xmpp:microblog:0"; + +/// XEP-0280: Message Carbons +pub const CARBONS: &str = "urn:xmpp:carbons:2"; + +/// XEP-0293: Jingle RTP Feedback Negotiation +pub const JINGLE_RTCP_FB: &str = "urn:xmpp:jingle:apps:rtp:rtcp-fb:0"; + +/// XEP-0294: Jingle RTP Header Extensions Negociation +pub const JINGLE_RTP_HDREXT: &str = "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0"; + +/// XEP-0297: Stanza Forwarding +pub const FORWARD: &str = "urn:xmpp:forward:0"; + +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASHES: &str = "urn:xmpp:hashes:2"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA_256: &str = "urn:xmpp:hash-function-text-names:sha-256"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA_512: &str = "urn:xmpp:hash-function-text-names:sha-512"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA3_256: &str = "urn:xmpp:hash-function-text-names:sha3-256"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_SHA3_512: &str = "urn:xmpp:hash-function-text-names:sha3-512"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_BLAKE2B_256: &str = "urn:xmpp:hash-function-text-names:id-blake2b256"; +/// XEP-0300: Use of Cryptographic Hash Functions in XMPP +pub const HASH_ALGO_BLAKE2B_512: &str = "urn:xmpp:hash-function-text-names:id-blake2b512"; + +/// XEP-0308: Last Message Correction +pub const MESSAGE_CORRECT: &str = "urn:xmpp:message-correct:0"; + +/// XEP-0313: Message Archive Management +pub const MAM: &str = "urn:xmpp:mam:2"; + +/// XEP-0319: Last User Interaction in Presence +pub const IDLE: &str = "urn:xmpp:idle:1"; + +/// XEP-0320: Use of DTLS-SRTP in Jingle Sessions +pub const JINGLE_DTLS: &str = "urn:xmpp:jingle:apps:dtls:0"; + +/// XEP-0328: JID Prep +pub const JID_PREP: &str = "urn:xmpp:jidprep:0"; + +/// XEP-0338: Jingle Grouping Framework +pub const JINGLE_GROUPING: &str = "urn:xmpp:jingle:apps:grouping:0"; + +/// XEP-0339: Source-Specific Media Attributes in Jingle +pub const JINGLE_SSMA: &str = "urn:xmpp:jingle:apps:rtp:ssma:0"; + +/// XEP-0352: Client State Indication +pub const CSI: &str = "urn:xmpp:csi:0"; + +/// XEP-0353: Jingle Message Initiation +pub const JINGLE_MESSAGE: &str = "urn:xmpp:jingle-message:0"; + +/// XEP-0359: Unique and Stable Stanza IDs +pub const SID: &str = "urn:xmpp:sid:0"; + +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_CORE: &str = "urn:xmpp:mix:core:1"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_CORE_SEARCHABLE: &str = "urn:xmpp:mix:core:1#searchable"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_CORE_CREATE_CHANNEL: &str = "urn:xmpp:mix:core:1#create-channel"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_NODES_PRESENCE: &str = "urn:xmpp:mix:nodes:presence"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_NODES_PARTICIPANTS: &str = "urn:xmpp:mix:nodes:participants"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_NODES_MESSAGES: &str = "urn:xmpp:mix:nodes:messages"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_NODES_CONFIG: &str = "urn:xmpp:mix:nodes:config"; +/// XEP-0369: Mediated Information eXchange (MIX) +pub const MIX_NODES_INFO: &str = "urn:xmpp:mix:nodes:info"; + +/// XEP-0373: OpenPGP for XMPP +pub const OX: &str = "urn:xmpp:openpgp:0"; +/// XEP-0373: OpenPGP for XMPP +pub const OX_PUBKEYS: &str = "urn:xmpp:openpgp:0:public-keys"; + +/// XEP-0380: Explicit Message Encryption +pub const EME: &str = "urn:xmpp:eme:0"; + +/// XEP-0390: Entity Capabilities 2.0 +pub const ECAPS2: &str = "urn:xmpp:caps"; +/// XEP-0390: Entity Capabilities 2.0 +pub const ECAPS2_OPTIMIZE: &str = "urn:xmpp:caps:optimize"; + +/// XEP-0402: Bookmarks 2 (This Time it's Serious) +pub const BOOKMARKS2: &str = "urn:xmpp:bookmarks:1"; +/// XEP-0402: Bookmarks 2 (This Time it's Serious) +pub const BOOKMARKS2_COMPAT: &str = "urn:xmpp:bookmarks:0#compat"; + +/// XEP-0421: Anonymous unique occupant identifiers for MUCs +pub const OID: &str = "urn:xmpp:occupant-id:0"; + +/// Alias for the main namespace of the stream, that is "jabber:client" when +/// the component feature isn’t enabled. +#[cfg(not(feature = "component"))] +pub const DEFAULT_NS: &str = JABBER_CLIENT; + +/// Alias for the main namespace of the stream, that is +/// "jabber:component:accept" when the component feature is enabled. +#[cfg(feature = "component")] +pub const DEFAULT_NS: &str = COMPONENT_ACCEPT; + +/// Jitsi Meet general namespace +pub const JITSI_MEET: &str = "http://jitsi.org/jitmeet"; + +/// Jitsi Meet Colibri namespace +pub const JITSI_COLIBRI: &str = "http://jitsi.org/protocol/colibri"; diff --git a/xmpp-parsers/src/occupant_id.rs b/xmpp-parsers/src/occupant_id.rs new file mode 100644 index 0000000..fd11b5b --- /dev/null +++ b/xmpp-parsers/src/occupant_id.rs @@ -0,0 +1,91 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; +use crate::presence::PresencePayload; + +generate_element!( + /// Unique identifier given to a MUC participant. + /// + /// It allows clients to identify a MUC participant across reconnects and + /// renames. It thus prevents impersonification of anonymous users. + OccupantId, "occupant-id", OID, + + attributes: [ + /// The id associated to the sending user by the MUC service. + id: Required = "id", + ] +); + +impl MessagePayload for OccupantId {} +impl PresencePayload for OccupantId {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(OccupantId, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(OccupantId, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let origin_id = OccupantId::try_from(elem).unwrap(); + assert_eq!(origin_id.id, "coucou"); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = OccupantId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in occupant-id element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "" + .parse() + .unwrap(); + let error = OccupantId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let occupant_id = OccupantId { + id: String::from("coucou"), + }; + let elem2 = occupant_id.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/openpgp.rs b/xmpp-parsers/src/openpgp.rs new file mode 100644 index 0000000..b862e3f --- /dev/null +++ b/xmpp-parsers/src/openpgp.rs @@ -0,0 +1,119 @@ +// Copyright (c) 2019 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::date::DateTime; +use crate::pubsub::PubSubPayload; +use crate::util::helpers::Base64; + +// TODO: Merge this container with the PubKey struct +generate_element!( + /// Data contained in the PubKey element + PubKeyData, "data", OX, + text: ( + /// Base64 data + data: Base64> + ) +); + +generate_element!( + /// Pubkey element to be used in PubSub publish payloads. + PubKey, "pubkey", OX, + attributes: [ + /// Last updated date + date: Option = "date" + ], + children: [ + /// Public key as base64 data + data: Required = ("data", OX) => PubKeyData + ] +); + +impl PubSubPayload for PubKey {} + +generate_element!( + /// Public key metadata + PubKeyMeta, "pubkey-metadata", OX, + attributes: [ + /// OpenPGP v4 fingerprint + v4fingerprint: Required = "v4-fingerprint", + /// Time the key was published or updated + date: Required = "date", + ] +); + +generate_element!( + /// List of public key metadata + PubKeysMeta, "public-key-list", OX, + children: [ + /// Public keys + pubkeys: Vec = ("pubkey-metadata", OX) => PubKeyMeta + ] +); + +impl PubSubPayload for PubKeysMeta {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use crate::pubsub::{ + pubsub::{Item as PubSubItem, Publish}, + Item, NodeName, + }; + use crate::Element; + use std::str::FromStr; + + #[test] + fn pubsub_publish_pubkey_data() { + let pubkey = PubKey { + date: None, + data: PubKeyData { + data: (&"Foo").as_bytes().to_vec(), + }, + }; + println!("Foo1: {:?}", pubkey); + + let pubsub = Publish { + node: NodeName(format!("{}:{}", ns::OX_PUBKEYS, "some-fingerprint")), + items: vec![PubSubItem(Item::new(None, None, Some(pubkey)))], + }; + println!("Foo2: {:?}", pubsub); + } + + #[test] + fn pubsub_publish_pubkey_meta() { + let pubkeymeta = PubKeysMeta { + pubkeys: vec![PubKeyMeta { + v4fingerprint: "some-fingerprint".to_owned(), + date: DateTime::from_str("2019-03-30T18:30:25Z").unwrap(), + }], + }; + println!("Foo1: {:?}", pubkeymeta); + + let pubsub = Publish { + node: NodeName("foo".to_owned()), + items: vec![PubSubItem(Item::new(None, None, Some(pubkeymeta)))], + }; + println!("Foo2: {:?}", pubsub); + } + + #[test] + fn test_serialize_pubkey() { + let reference: Element = "AAAA" + .parse() + .unwrap(); + + let pubkey = PubKey { + date: None, + data: PubKeyData { + data: b"\0\0\0".to_vec(), + }, + }; + + let serialized: Element = pubkey.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/ping.rs b/xmpp-parsers/src/ping.rs new file mode 100644 index 0000000..70663fa --- /dev/null +++ b/xmpp-parsers/src/ping.rs @@ -0,0 +1,71 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::IqGetPayload; + +generate_empty_element!( + /// Represents a ping to the recipient, which must be answered with an + /// empty `` or with an error. + Ping, + "ping", + PING +); + +impl IqGetPayload for Ping {} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(not(feature = "disable-validation"))] + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[test] + fn test_size() { + assert_size!(Ping, 0); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Ping::try_from(elem).unwrap(); + } + + #[test] + fn test_serialise() { + let elem1 = Element::from(Ping); + let elem2: Element = "".parse().unwrap(); + assert_eq!(elem1, elem2); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Ping::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in ping element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "".parse().unwrap(); + let error = Ping::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in ping element."); + } +} diff --git a/xmpp-parsers/src/presence.rs b/xmpp-parsers/src/presence.rs new file mode 100644 index 0000000..2d70714 --- /dev/null +++ b/xmpp-parsers/src/presence.rs @@ -0,0 +1,655 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// Copyright (c) 2017 Maxime “pep” Buquet +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use jid::Jid; +use minidom::{Element, IntoAttributeValue, Node}; +use std::collections::BTreeMap; +use std::convert::TryFrom; +use std::str::FromStr; + +/// Should be implemented on every known payload of a ``. +pub trait PresencePayload: TryFrom + Into {} + +/// Specifies the availability of an entity or resource. +#[derive(Debug, Clone, PartialEq)] +pub enum Show { + /// The entity or resource is temporarily away. + Away, + + /// The entity or resource is actively interested in chatting. + Chat, + + /// The entity or resource is busy (dnd = "Do Not Disturb"). + Dnd, + + /// The entity or resource is away for an extended period (xa = "eXtended + /// Away"). + Xa, +} + +impl FromStr for Show { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "away" => Show::Away, + "chat" => Show::Chat, + "dnd" => Show::Dnd, + "xa" => Show::Xa, + + _ => return Err(Error::ParseError("Invalid value for show.")), + }) + } +} + +impl Into for Show { + fn into(self) -> Node { + Element::builder("show", ns::DEFAULT_NS) + .append(match self { + Show::Away => "away", + Show::Chat => "chat", + Show::Dnd => "dnd", + Show::Xa => "xa", + }) + .build() + .into() + } +} + +type Lang = String; +type Status = String; + +type Priority = i8; + +/// +#[derive(Debug, Clone, PartialEq)] +pub enum Type { + /// This value is not an acceptable 'type' attribute, it is only used + /// internally to signal the absence of 'type'. + None, + + /// An error has occurred regarding processing of a previously sent + /// presence stanza; if the presence stanza is of type "error", it MUST + /// include an child element (refer to [XMPP‑CORE]). + Error, + + /// A request for an entity's current presence; SHOULD be generated only by + /// a server on behalf of a user. + Probe, + + /// The sender wishes to subscribe to the recipient's presence. + Subscribe, + + /// The sender has allowed the recipient to receive their presence. + Subscribed, + + /// The sender is no longer available for communication. + Unavailable, + + /// The sender is unsubscribing from the receiver's presence. + Unsubscribe, + + /// The subscription request has been denied or a previously granted + /// subscription has been canceled. + Unsubscribed, +} + +impl Default for Type { + fn default() -> Type { + Type::None + } +} + +impl FromStr for Type { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "error" => Type::Error, + "probe" => Type::Probe, + "subscribe" => Type::Subscribe, + "subscribed" => Type::Subscribed, + "unavailable" => Type::Unavailable, + "unsubscribe" => Type::Unsubscribe, + "unsubscribed" => Type::Unsubscribed, + + _ => { + return Err(Error::ParseError( + "Invalid 'type' attribute on presence element.", + )); + } + }) + } +} + +impl IntoAttributeValue for Type { + fn into_attribute_value(self) -> Option { + Some( + match self { + Type::None => return None, + + Type::Error => "error", + Type::Probe => "probe", + Type::Subscribe => "subscribe", + Type::Subscribed => "subscribed", + Type::Unavailable => "unavailable", + Type::Unsubscribe => "unsubscribe", + Type::Unsubscribed => "unsubscribed", + } + .to_owned(), + ) + } +} + +/// The main structure representing the `` stanza. +#[derive(Debug, Clone)] +pub struct Presence { + /// The sender of this presence. + pub from: Option, + + /// The recipient of this presence. + pub to: Option, + + /// The identifier, unique on this stream, of this stanza. + pub id: Option, + + /// The type of this presence stanza. + pub type_: Type, + + /// The availability of the sender of this presence. + pub show: Option, + + /// A localised list of statuses defined in this presence. + pub statuses: BTreeMap, + + /// The sender’s resource priority, if negative it won’t receive messages + /// that haven’t been directed to it. + pub priority: Priority, + + /// A list of payloads contained in this presence. + pub payloads: Vec, +} + +impl Presence { + /// Create a new presence of this type. + pub fn new(type_: Type) -> Presence { + Presence { + from: None, + to: None, + id: None, + type_, + show: None, + statuses: BTreeMap::new(), + priority: 0i8, + payloads: vec![], + } + } + + /// Set the emitter of this presence, this should only be useful for + /// servers and components, as clients can only send presences from their + /// own resource (which is implicit). + pub fn with_from>(mut self, from: J) -> Presence { + self.from = Some(from.into()); + self + } + + /// Set the recipient of this presence, this is only useful for directed + /// presences. + pub fn with_to>(mut self, to: J) -> Presence { + self.to = Some(to.into()); + self + } + + /// Set the identifier for this presence. + pub fn with_id(mut self, id: String) -> Presence { + self.id = Some(id); + self + } + + /// Set the availability information of this presence. + pub fn with_show(mut self, show: Show) -> Presence { + self.show = Some(show); + self + } + + /// Set the priority of this presence. + pub fn with_priority(mut self, priority: i8) -> Presence { + self.priority = priority; + self + } + + /// Set the payloads of this presence. + pub fn with_payloads(mut self, payloads: Vec) -> Presence { + self.payloads = payloads; + self + } + + /// Set the availability information of this presence. + pub fn set_status(&mut self, lang: L, status: S) + where + L: Into, + S: Into, + { + self.statuses.insert(lang.into(), status.into()); + } + + /// Add a payload to this presence. + pub fn add_payload(&mut self, payload: P) { + self.payloads.push(payload.into()); + } +} + +impl TryFrom for Presence { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "presence", DEFAULT_NS); + let mut show = None; + let mut priority = None; + let mut presence = Presence { + from: get_attr!(root, "from", Option), + to: get_attr!(root, "to", Option), + id: get_attr!(root, "id", Option), + type_: get_attr!(root, "type", Default), + show: None, + statuses: BTreeMap::new(), + priority: 0i8, + payloads: vec![], + }; + for elem in root.children() { + if elem.is("show", ns::DEFAULT_NS) { + if show.is_some() { + return Err(Error::ParseError( + "More than one show element in a presence.", + )); + } + check_no_attributes!(elem, "show"); + check_no_children!(elem, "show"); + show = Some(Show::from_str(elem.text().as_ref())?); + } else if elem.is("status", ns::DEFAULT_NS) { + check_no_unknown_attributes!(elem, "status", ["xml:lang"]); + check_no_children!(elem, "status"); + let lang = get_attr!(elem, "xml:lang", Default); + if presence.statuses.insert(lang, elem.text()).is_some() { + return Err(Error::ParseError( + "Status element present twice for the same xml:lang.", + )); + } + } else if elem.is("priority", ns::DEFAULT_NS) { + if priority.is_some() { + return Err(Error::ParseError( + "More than one priority element in a presence.", + )); + } + check_no_attributes!(elem, "priority"); + check_no_children!(elem, "priority"); + priority = Some(Priority::from_str(elem.text().as_ref())?); + } else { + presence.payloads.push(elem.clone()); + } + } + presence.show = show; + if let Some(priority) = priority { + presence.priority = priority; + } + Ok(presence) + } +} + +impl From for Element { + fn from(presence: Presence) -> Element { + Element::builder("presence", ns::DEFAULT_NS) + .attr("from", presence.from) + .attr("to", presence.to) + .attr("id", presence.id) + .attr("type", presence.type_) + .append_all(presence.show.into_iter()) + .append_all(presence.statuses.into_iter().map(|(lang, status)| { + Element::builder("status", ns::DEFAULT_NS) + .attr( + "xml:lang", + match lang.as_ref() { + "" => None, + lang => Some(lang), + }, + ) + .append(status) + })) + .append_all(if presence.priority == 0 { + None + } else { + Some( + Element::builder("priority", ns::DEFAULT_NS) + .append(format!("{}", presence.priority)), + ) + }) + .append_all(presence.payloads.into_iter()) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use jid::{BareJid, FullJid}; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Show, 1); + assert_size!(Type, 1); + assert_size!(Presence, 120); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Show, 1); + assert_size!(Type, 1); + assert_size!(Presence, 240); + } + + #[test] + fn test_simple() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.from, None); + assert_eq!(presence.to, None); + assert_eq!(presence.id, None); + assert_eq!(presence.type_, Type::None); + assert!(presence.payloads.is_empty()); + } + + #[test] + fn test_serialise() { + #[cfg(not(feature = "component"))] + let elem: Element = "/>" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "/>" + .parse() + .unwrap(); + let presence = Presence::new(Type::Unavailable); + let elem2 = presence.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_show() { + #[cfg(not(feature = "component"))] + let elem: Element = "chat" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "chat" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.show, Some(Show::Chat)); + } + + #[test] + fn test_empty_show_value() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.show, None); + } + + #[test] + fn test_missing_show_value() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Invalid value for show."); + } + + #[test] + fn test_invalid_show() { + // "online" used to be a pretty common mistake. + #[cfg(not(feature = "component"))] + let elem: Element = "online" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "online" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Invalid value for show."); + } + + #[test] + fn test_empty_status() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.statuses.len(), 1); + assert_eq!(presence.statuses[""], ""); + } + + #[test] + fn test_status() { + #[cfg(not(feature = "component"))] + let elem: Element = "Here!" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "Here!" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.statuses.len(), 1); + assert_eq!(presence.statuses[""], "Here!"); + } + + #[test] + fn test_multiple_statuses() { + #[cfg(not(feature = "component"))] + let elem: Element = "Here!Là!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Here!Là!".parse().unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.statuses.len(), 2); + assert_eq!(presence.statuses[""], "Here!"); + assert_eq!(presence.statuses["fr"], "Là!"); + } + + #[test] + fn test_invalid_multiple_statuses() { + #[cfg(not(feature = "component"))] + let elem: Element = "Here!Là!".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "Here!Là!".parse().unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!( + message, + "Status element present twice for the same xml:lang." + ); + } + + #[test] + fn test_priority() { + #[cfg(not(feature = "component"))] + let elem: Element = "-1" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "-1" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + assert_eq!(presence.payloads.len(), 0); + assert_eq!(presence.priority, -1i8); + } + + #[test] + fn test_invalid_priority() { + #[cfg(not(feature = "component"))] + let elem: Element = "128" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "128" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + match error { + Error::ParseIntError(_) => (), + _ => panic!(), + }; + } + + #[test] + fn test_unknown_child() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "" + .parse() + .unwrap(); + let presence = Presence::try_from(elem).unwrap(); + let payload = &presence.payloads[0]; + assert!(payload.is("test", "invalid")); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_status_child() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in status element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = + "" + .parse() + .unwrap(); + let error = Presence::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in status element."); + } + + #[test] + fn test_serialise_status() { + let status = Status::from("Hello world!"); + let mut presence = Presence::new(Type::Unavailable); + presence.statuses.insert(String::from(""), status); + let elem: Element = presence.into(); + assert!(elem.is("presence", ns::DEFAULT_NS)); + assert!(elem.children().next().unwrap().is("status", ns::DEFAULT_NS)); + } + + #[test] + fn test_serialise_priority() { + let presence = Presence::new(Type::None).with_priority(42); + let elem: Element = presence.into(); + assert!(elem.is("presence", ns::DEFAULT_NS)); + let priority = elem.children().next().unwrap(); + assert!(priority.is("priority", ns::DEFAULT_NS)); + assert_eq!(priority.text(), "42"); + } + + #[test] + fn presence_with_to() { + let presence = Presence::new(Type::None); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), None); + + let presence = Presence::new(Type::None).with_to(Jid::Bare(BareJid::domain("localhost"))); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("localhost")); + + let presence = Presence::new(Type::None).with_to(BareJid::domain("localhost")); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("localhost")); + + let presence = Presence::new(Type::None).with_to(Jid::Full(FullJid::new( + "test", + "localhost", + "coucou", + ))); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("test@localhost/coucou")); + + let presence = + Presence::new(Type::None).with_to(FullJid::new("test", "localhost", "coucou")); + let elem: Element = presence.into(); + assert_eq!(elem.attr("to"), Some("test@localhost/coucou")); + } +} diff --git a/xmpp-parsers/src/pubsub/event.rs b/xmpp-parsers/src/pubsub/event.rs new file mode 100644 index 0000000..6ad2bc9 --- /dev/null +++ b/xmpp-parsers/src/pubsub/event.rs @@ -0,0 +1,416 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::date::DateTime; +use crate::message::MessagePayload; +use crate::ns; +use crate::pubsub::{Item as PubSubItem, ItemId, NodeName, Subscription, SubscriptionId}; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::convert::TryFrom; + +/// Event wrapper for a PubSub ``. +#[derive(Debug, Clone)] +pub struct Item(pub PubSubItem); + +impl_pubsub_item!(Item, PUBSUB_EVENT); + +/// Represents an event happening to a PubSub node. +#[derive(Debug, Clone)] +pub enum PubSubEvent { + /* + Collection { + }, + */ + /// This node’s configuration changed. + Configuration { + /// The node affected. + node: NodeName, + + /// The new configuration of this node. + form: Option, + }, + + /// This node has been deleted, with an optional redirect to another node. + Delete { + /// The node affected. + node: NodeName, + + /// The xmpp: URI of another node replacing this one. + redirect: Option, + }, + + /// Some items have been published on this node. + PublishedItems { + /// The node affected. + node: NodeName, + + /// The list of published items. + items: Vec, + }, + + /// Some items have been removed from this node. + RetractedItems { + /// The node affected. + node: NodeName, + + /// The list of retracted items. + items: Vec, + }, + + /// All items of this node just got removed at once. + Purge { + /// The node affected. + node: NodeName, + }, + + /// The user’s subscription to this node has changed. + Subscription { + /// The node affected. + node: NodeName, + + /// The time at which this subscription will expire. + expiry: Option, + + /// The JID of the user affected. + jid: Option, + + /// An identifier for this subscription. + subid: Option, + + /// The state of this subscription. + subscription: Option, + }, +} + +fn parse_items(elem: Element, node: NodeName) -> Result { + let mut is_retract = None; + let mut items = vec![]; + let mut retracts = vec![]; + for child in elem.children() { + if child.is("item", ns::PUBSUB_EVENT) { + match is_retract { + None => is_retract = Some(false), + Some(false) => (), + Some(true) => { + return Err(Error::ParseError( + "Mix of item and retract in items element.", + )); + } + } + items.push(Item::try_from(child.clone())?); + } else if child.is("retract", ns::PUBSUB_EVENT) { + match is_retract { + None => is_retract = Some(true), + Some(true) => (), + Some(false) => { + return Err(Error::ParseError( + "Mix of item and retract in items element.", + )); + } + } + check_no_children!(child, "retract"); + check_no_unknown_attributes!(child, "retract", ["id"]); + let id = get_attr!(child, "id", Required); + retracts.push(id); + } else { + return Err(Error::ParseError("Invalid child in items element.")); + } + } + Ok(match is_retract { + Some(false) => PubSubEvent::PublishedItems { node, items }, + Some(true) => PubSubEvent::RetractedItems { + node, + items: retracts, + }, + None => return Err(Error::ParseError("Missing children in items element.")), + }) +} + +impl TryFrom for PubSubEvent { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "event", PUBSUB_EVENT); + check_no_attributes!(elem, "event"); + + let mut payload = None; + for child in elem.children() { + let node = get_attr!(child, "node", Required); + if child.is("configuration", ns::PUBSUB_EVENT) { + let mut payloads = child.children().cloned().collect::>(); + let item = payloads.pop(); + if !payloads.is_empty() { + return Err(Error::ParseError( + "More than a single payload in configuration element.", + )); + } + let form = match item { + None => None, + Some(payload) => Some(DataForm::try_from(payload)?), + }; + payload = Some(PubSubEvent::Configuration { node, form }); + } else if child.is("delete", ns::PUBSUB_EVENT) { + let mut redirect = None; + for item in child.children() { + if item.is("redirect", ns::PUBSUB_EVENT) { + if redirect.is_some() { + return Err(Error::ParseError( + "More than one redirect in delete element.", + )); + } + let uri = get_attr!(item, "uri", Required); + redirect = Some(uri); + } else { + return Err(Error::ParseError("Unknown child in delete element.")); + } + } + payload = Some(PubSubEvent::Delete { node, redirect }); + } else if child.is("items", ns::PUBSUB_EVENT) { + payload = Some(parse_items(child.clone(), node)?); + } else if child.is("purge", ns::PUBSUB_EVENT) { + check_no_children!(child, "purge"); + payload = Some(PubSubEvent::Purge { node }); + } else if child.is("subscription", ns::PUBSUB_EVENT) { + check_no_children!(child, "subscription"); + payload = Some(PubSubEvent::Subscription { + node, + expiry: get_attr!(child, "expiry", Option), + jid: get_attr!(child, "jid", Option), + subid: get_attr!(child, "subid", Option), + subscription: get_attr!(child, "subscription", Option), + }); + } else { + return Err(Error::ParseError("Unknown child in event element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in event element."))?) + } +} + +impl From for Element { + fn from(event: PubSubEvent) -> Element { + let payload = match event { + PubSubEvent::Configuration { node, form } => { + Element::builder("configuration", ns::PUBSUB_EVENT) + .attr("node", node) + .append_all(form.map(Element::from)) + } + PubSubEvent::Delete { node, redirect } => Element::builder("purge", ns::PUBSUB_EVENT) + .attr("node", node) + .append_all(redirect.map(|redirect| { + Element::builder("redirect", ns::PUBSUB_EVENT).attr("uri", redirect) + })), + PubSubEvent::PublishedItems { node, items } => { + Element::builder("items", ns::PUBSUB_EVENT) + .attr("node", node) + .append_all(items.into_iter()) + } + PubSubEvent::RetractedItems { node, items } => { + Element::builder("items", ns::PUBSUB_EVENT) + .attr("node", node) + .append_all( + items + .into_iter() + .map(|id| Element::builder("retract", ns::PUBSUB_EVENT).attr("id", id)), + ) + } + PubSubEvent::Purge { node } => { + Element::builder("purge", ns::PUBSUB_EVENT).attr("node", node) + } + PubSubEvent::Subscription { + node, + expiry, + jid, + subid, + subscription, + } => Element::builder("subscription", ns::PUBSUB_EVENT) + .attr("node", node) + .attr("expiry", expiry) + .attr("jid", jid) + .attr("subid", subid) + .attr("subscription", subscription), + }; + Element::builder("event", ns::PUBSUB_EVENT) + .append(payload) + .build() + } +} + +impl MessagePayload for PubSubEvent {} + +#[cfg(test)] +mod tests { + use super::*; + use jid::BareJid; + + #[test] + fn missing_items() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Missing children in items element."); + } + + #[test] + fn test_simple_items() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::PublishedItems { node, items } => { + assert_eq!(node, NodeName(String::from("coucou"))); + assert_eq!(items[0].id, Some(ItemId(String::from("test")))); + assert_eq!( + items[0].publisher.clone().unwrap(), + BareJid::new("test", "coucou") + ); + assert_eq!(items[0].payload, None); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_pep() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::PublishedItems { node, items } => { + assert_eq!(node, NodeName(String::from("something"))); + assert_eq!(items[0].id, None); + assert_eq!(items[0].publisher, None); + match items[0].payload { + Some(ref elem) => assert!(elem.is("foreign", "example:namespace")), + _ => panic!(), + } + } + _ => panic!(), + } + } + + #[test] + fn test_simple_retract() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::RetractedItems { node, items } => { + assert_eq!(node, NodeName(String::from("something"))); + assert_eq!(items[0], ItemId(String::from("coucou"))); + assert_eq!(items[1], ItemId(String::from("test"))); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_delete() { + let elem: Element = "".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Delete { node, redirect } => { + assert_eq!(node, NodeName(String::from("coucou"))); + assert_eq!(redirect, Some(String::from("hello"))); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_purge() { + let elem: Element = + "" + .parse() + .unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Purge { node } => { + assert_eq!(node, NodeName(String::from("coucou"))); + } + _ => panic!(), + } + } + + #[test] + fn test_simple_configure() { + let elem: Element = "http://jabber.org/protocol/pubsub#node_config".parse().unwrap(); + let event = PubSubEvent::try_from(elem).unwrap(); + match event { + PubSubEvent::Configuration { node, form: _ } => { + assert_eq!(node, NodeName(String::from("coucou"))); + //assert_eq!(form.type_, Result_); + } + _ => panic!(), + } + } + + #[test] + fn test_invalid() { + let elem: Element = + "" + .parse() + .unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in event element."); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid_attribute() { + let elem: Element = "" + .parse() + .unwrap(); + let error = PubSubEvent::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in event element."); + } + + #[test] + fn test_ex221_subscription() { + let elem: Element = "" + .parse() + .unwrap(); + let event = PubSubEvent::try_from(elem.clone()).unwrap(); + match event.clone() { + PubSubEvent::Subscription { + node, + expiry, + jid, + subid, + subscription, + } => { + assert_eq!(node, NodeName(String::from("princely_musings"))); + assert_eq!( + subid, + Some(SubscriptionId(String::from( + "ba49252aaa4f5d320c24d3766f0bdcade78c78d3" + ))) + ); + assert_eq!(subscription, Some(Subscription::Subscribed)); + assert_eq!(jid.unwrap(), BareJid::new("francisco", "denmark.lit")); + assert_eq!(expiry, Some("2006-02-28T23:59:59Z".parse().unwrap())); + } + _ => panic!(), + } + + let elem2: Element = event.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/pubsub/mod.rs b/xmpp-parsers/src/pubsub/mod.rs new file mode 100644 index 0000000..008d7a0 --- /dev/null +++ b/xmpp-parsers/src/pubsub/mod.rs @@ -0,0 +1,107 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// The `http://jabber.org/protocol/pubsub#event` protocol. +pub mod event; + +/// The `http://jabber.org/protocol/pubsub#owner` protocol. +pub mod owner; + +/// The `http://jabber.org/protocol/pubsub` protocol. +pub mod pubsub; + +pub use self::event::PubSubEvent; +pub use self::owner::PubSubOwner; +pub use self::pubsub::PubSub; + +use crate::{Element, Jid}; + +generate_id!( + /// The name of a PubSub node, used to identify it on a JID. + NodeName +); + +generate_id!( + /// The identifier of an item, which is unique per node. + ItemId +); + +generate_id!( + /// The identifier of a subscription to a PubSub node. + SubscriptionId +); + +generate_attribute!( + /// The state of a subscription to a node. + Subscription, "subscription", { + /// The user is not subscribed to this node. + None => "none", + + /// The user’s subscription to this node is still pending. + Pending => "pending", + + /// The user is subscribed to this node. + Subscribed => "subscribed", + + /// The user’s subscription to this node will only be valid once + /// configured. + Unconfigured => "unconfigured", + }, Default = None +); + +generate_attribute!( + /// A list of possible affiliations to a node. + AffiliationAttribute, "affiliation", { + /// You are a member of this node, you can subscribe and retrieve items. + Member => "member", + + /// You don’t have a specific affiliation with this node, you can only subscribe to it. + None => "none", + + /// You are banned from this node. + Outcast => "outcast", + + /// You are an owner of this node, and can do anything with it. + Owner => "owner", + + /// You are a publisher on this node, you can publish and retract items to it. + Publisher => "publisher", + + /// You can publish and retract items on this node, but not subscribe or retrieve items. + PublishOnly => "publish-only", + } +); + +/// An item from a PubSub node. +#[derive(Debug, Clone, PartialEq)] +pub struct Item { + /// The identifier for this item, unique per node. + pub id: Option, + + /// The JID of the entity who published this item. + pub publisher: Option, + + /// The payload of this item, in an arbitrary namespace. + pub payload: Option, +} + +impl Item { + /// Create a new item, accepting only payloads implementing `PubSubPayload`. + pub fn new( + id: Option, + publisher: Option, + payload: Option

, + ) -> Item { + Item { + id, + publisher, + payload: payload.map(Into::into), + } + } +} + +/// This trait should be implemented on any element which can be included as a PubSub payload. +pub trait PubSubPayload: ::std::convert::TryFrom + Into {} diff --git a/xmpp-parsers/src/pubsub/owner.rs b/xmpp-parsers/src/pubsub/owner.rs new file mode 100644 index 0000000..a890523 --- /dev/null +++ b/xmpp-parsers/src/pubsub/owner.rs @@ -0,0 +1,364 @@ +// Copyright (c) 2020 Paul Fariello +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::pubsub::{AffiliationAttribute, NodeName, Subscription}; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::convert::TryFrom; + +generate_element!( + /// A list of affiliations you have on a service, or on a node. + Affiliations, "affiliations", PUBSUB_OWNER, + attributes: [ + /// The node name this request pertains to. + node: Required = "node", + ], + children: [ + /// The actual list of affiliation elements. + affiliations: Vec = ("affiliation", PUBSUB_OWNER) => Affiliation + ] +); + +generate_element!( + /// An affiliation element. + Affiliation, "affiliation", PUBSUB_OWNER, + attributes: [ + /// The node this affiliation pertains to. + jid: Required = "jid", + + /// The affiliation you currently have on this node. + affiliation: Required = "affiliation", + ] +); + +generate_element!( + /// Request to configure a node. + Configure, "configure", PUBSUB_OWNER, + attributes: [ + /// The node to be configured. + node: Option = "node", + ], + children: [ + /// The form to configure it. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_element!( + /// Request to change default configuration. + Default, "default", PUBSUB_OWNER, + children: [ + /// The form to configure it. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_element!( + /// Request to delete a node. + Delete, "delete", PUBSUB_OWNER, + attributes: [ + /// The node to be configured. + node: Required = "node", + ], + children: [ + /// Redirection to replace the deleted node. + redirect: Option = ("redirect", PUBSUB_OWNER) => Redirect + ] +); + +generate_element!( + /// A redirect element. + Redirect, "redirect", PUBSUB_OWNER, + attributes: [ + /// The node this node will be redirected to. + uri: Required = "uri", + ] +); + +generate_element!( + /// Request to delete a node. + Purge, "purge", PUBSUB_OWNER, + attributes: [ + /// The node to be configured. + node: Required = "node", + ] +); + +generate_element!( + /// A request for current subscriptions. + Subscriptions, "subscriptions", PUBSUB_OWNER, + attributes: [ + /// The node to query. + node: Required = "node", + ], + children: [ + /// The list of subscription elements returned. + subscriptions: Vec = ("subscription", PUBSUB_OWNER) => SubscriptionElem + ] +); + +generate_element!( + /// A subscription element, describing the state of a subscription. + SubscriptionElem, "subscription", PUBSUB_OWNER, + attributes: [ + /// The JID affected by this subscription. + jid: Required = "jid", + + /// The state of the subscription. + subscription: Required = "subscription", + + /// Subscription unique id. + subid: Option = "subid", + ] +); + +/// Main payload used to communicate with a PubSubOwner service. +/// +/// `` +#[derive(Debug, Clone)] +pub enum PubSubOwner { + /// Manage the affiliations of a node. + Affiliations(Affiliations), + /// Request to configure a node, with optional suggested name and suggested configuration. + Configure(Configure), + /// Request the default node configuration. + Default(Default), + /// Delete a node. + Delete(Delete), + /// Purge all items from node. + Purge(Purge), + /// Request subscriptions of a node. + Subscriptions(Subscriptions), +} + +impl IqGetPayload for PubSubOwner {} +impl IqSetPayload for PubSubOwner {} +impl IqResultPayload for PubSubOwner {} + +impl TryFrom for PubSubOwner { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "pubsub", PUBSUB_OWNER); + check_no_attributes!(elem, "pubsub"); + + let mut payload = None; + for child in elem.children() { + if child.is("configure", ns::PUBSUB_OWNER) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub owner element.", + )); + } + let configure = Configure::try_from(child.clone())?; + payload = Some(PubSubOwner::Configure(configure)); + } else { + return Err(Error::ParseError("Unknown child in pubsub element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?) + } +} + +impl From for Element { + fn from(pubsub: PubSubOwner) -> Element { + Element::builder("pubsub", ns::PUBSUB_OWNER) + .append_all(match pubsub { + PubSubOwner::Affiliations(affiliations) => vec![Element::from(affiliations)], + PubSubOwner::Configure(configure) => vec![Element::from(configure)], + PubSubOwner::Default(default) => vec![Element::from(default)], + PubSubOwner::Delete(delete) => vec![Element::from(delete)], + PubSubOwner::Purge(purge) => vec![Element::from(purge)], + PubSubOwner::Subscriptions(subscriptions) => vec![Element::from(subscriptions)], + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_forms::{DataForm, DataFormType, Field, FieldType}; + use jid::BareJid; + use std::str::FromStr; + + #[test] + fn affiliations() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSubOwner::Affiliations(Affiliations { + node: NodeName(String::from("foo")), + affiliations: vec![ + Affiliation { + jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()), + affiliation: AffiliationAttribute::Owner, + }, + Affiliation { + jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()), + affiliation: AffiliationAttribute::Outcast, + }, + ], + }); + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn configure() { + let elem: Element = "http://jabber.org/protocol/pubsub#node_configwhitelist" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSubOwner::Configure(Configure { + node: Some(NodeName(String::from("foo"))), + form: Some(DataForm { + type_: DataFormType::Submit, + form_type: Some(String::from(ns::PUBSUB_CONFIGURE)), + title: None, + instructions: None, + fields: vec![Field { + var: String::from("pubsub#access_model"), + type_: FieldType::ListSingle, + label: None, + required: false, + options: vec![], + values: vec![String::from("whitelist")], + media: vec![], + }], + }), + }); + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn test_serialize_configure() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "".parse().unwrap(); + + let form = DataForm::try_from(elem).unwrap(); + + let configure = PubSubOwner::Configure(Configure { + node: Some(NodeName(String::from("foo"))), + form: Some(form), + }); + let serialized: Element = configure.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn default() { + let elem: Element = "http://jabber.org/protocol/pubsub#node_configwhitelist" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSubOwner::Default(Default { + form: Some(DataForm { + type_: DataFormType::Submit, + form_type: Some(String::from(ns::PUBSUB_CONFIGURE)), + title: None, + instructions: None, + fields: vec![Field { + var: String::from("pubsub#access_model"), + type_: FieldType::ListSingle, + label: None, + required: false, + options: vec![], + values: vec![String::from("whitelist")], + media: vec![], + }], + }), + }); + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn delete() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSubOwner::Delete(Delete { + node: NodeName(String::from("foo")), + redirect: Some(Redirect { + uri: String::from("xmpp:hamlet@denmark.lit?;node=blog"), + }), + }); + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn purge() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSubOwner::Purge(Purge { + node: NodeName(String::from("foo")), + }); + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn subscriptions() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSubOwner::Subscriptions(Subscriptions { + node: NodeName(String::from("foo")), + subscriptions: vec![ + SubscriptionElem { + jid: Jid::Bare(BareJid::from_str("hamlet@denmark.lit").unwrap()), + subscription: Subscription::Subscribed, + subid: None, + }, + SubscriptionElem { + jid: Jid::Bare(BareJid::from_str("polonius@denmark.lit").unwrap()), + subscription: Subscription::Unconfigured, + subid: None, + }, + SubscriptionElem { + jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()), + subscription: Subscription::Subscribed, + subid: Some(String::from("123-abc")), + }, + SubscriptionElem { + jid: Jid::Bare(BareJid::from_str("bernardo@denmark.lit").unwrap()), + subscription: Subscription::Subscribed, + subid: Some(String::from("004-yyy")), + }, + ], + }); + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/pubsub/pubsub.rs b/xmpp-parsers/src/pubsub/pubsub.rs new file mode 100644 index 0000000..1811a8d --- /dev/null +++ b/xmpp-parsers/src/pubsub/pubsub.rs @@ -0,0 +1,772 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::DataForm; +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use crate::ns; +use crate::pubsub::{ + AffiliationAttribute, Item as PubSubItem, NodeName, Subscription, SubscriptionId, +}; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::convert::TryFrom; + +// TODO: a better solution would be to split this into a query and a result elements, like for +// XEP-0030. +generate_element!( + /// A list of affiliations you have on a service, or on a node. + Affiliations, "affiliations", PUBSUB, + attributes: [ + /// The optional node name this request pertains to. + node: Option = "node", + ], + children: [ + /// The actual list of affiliation elements. + affiliations: Vec = ("affiliation", PUBSUB) => Affiliation + ] +); + +generate_element!( + /// An affiliation element. + Affiliation, "affiliation", PUBSUB, + attributes: [ + /// The node this affiliation pertains to. + node: Required = "node", + + /// The affiliation you currently have on this node. + affiliation: Required = "affiliation", + ] +); + +generate_element!( + /// Request to configure a new node. + Configure, "configure", PUBSUB, + children: [ + /// The form to configure it. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_element!( + /// Request to create a new node. + Create, "create", PUBSUB, + attributes: [ + /// The node name to create, if `None` the service will generate one. + node: Option = "node", + ] +); + +generate_element!( + /// Request for a default node configuration. + Default, "default", PUBSUB, + attributes: [ + /// The node targeted by this request, otherwise the entire service. + node: Option = "node", + + // TODO: do we really want to support collection nodes? + // type: Option = "type", + ] +); + +generate_element!( + /// A request for a list of items. + Items, "items", PUBSUB, + attributes: [ + // TODO: should be an xs:positiveInteger, that is, an unbounded int ≥ 1. + /// Maximum number of items returned. + max_items: Option = "max_items", + + /// The node queried by this request. + node: Required = "node", + + /// The subscription identifier related to this request. + subid: Option = "subid", + ], + children: [ + /// The actual list of items returned. + items: Vec = ("item", PUBSUB) => Item + ] +); + +impl Items { + /// Create a new items request. + pub fn new(node: &str) -> Items { + Items { + node: NodeName(String::from(node)), + max_items: None, + subid: None, + items: Vec::new(), + } + } +} + +/// Response wrapper for a PubSub ``. +#[derive(Debug, Clone, PartialEq)] +pub struct Item(pub PubSubItem); + +impl_pubsub_item!(Item, PUBSUB); + +generate_element!( + /// The options associated to a subscription request. + Options, "options", PUBSUB, + attributes: [ + /// The JID affected by this request. + jid: Required = "jid", + + /// The node affected by this request. + node: Option = "node", + + /// The subscription identifier affected by this request. + subid: Option = "subid", + ], + children: [ + /// The form describing the subscription. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_element!( + /// Request to publish items to a node. + Publish, "publish", PUBSUB, + attributes: [ + /// The target node for this operation. + node: Required = "node", + ], + children: [ + /// The items you want to publish. + items: Vec = ("item", PUBSUB) => Item + ] +); + +generate_element!( + /// The options associated to a publish request. + PublishOptions, "publish-options", PUBSUB, + children: [ + /// The form describing these options. + form: Option = ("x", DATA_FORMS) => DataForm + ] +); + +generate_attribute!( + /// Whether a retract request should notify subscribers or not. + Notify, + "notify", + bool +); + +generate_element!( + /// A request to retract some items from a node. + Retract, "retract", PUBSUB, + attributes: [ + /// The node affected by this request. + node: Required = "node", + + /// Whether a retract request should notify subscribers or not. + notify: Default = "notify", + ], + children: [ + /// The items affected by this request. + items: Vec = ("item", PUBSUB) => Item + ] +); + +/// Indicate that the subscription can be configured. +#[derive(Debug, Clone, PartialEq)] +pub struct SubscribeOptions { + /// If `true`, the configuration is actually required. + required: bool, +} + +impl TryFrom for SubscribeOptions { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "subscribe-options", PUBSUB); + check_no_attributes!(elem, "subscribe-options"); + let mut required = false; + for child in elem.children() { + if child.is("required", ns::PUBSUB) { + if required { + return Err(Error::ParseError( + "More than one required element in subscribe-options.", + )); + } + required = true; + } else { + return Err(Error::ParseError( + "Unknown child in subscribe-options element.", + )); + } + } + Ok(SubscribeOptions { required }) + } +} + +impl From for Element { + fn from(subscribe_options: SubscribeOptions) -> Element { + Element::builder("subscribe-options", ns::PUBSUB) + .append_all(if subscribe_options.required { + Some(Element::builder("required", ns::PUBSUB)) + } else { + None + }) + .build() + } +} + +generate_element!( + /// A request to subscribe a JID to a node. + Subscribe, "subscribe", PUBSUB, + attributes: [ + /// The JID being subscribed. + jid: Required = "jid", + + /// The node to subscribe to. + node: Option = "node", + ] +); + +generate_element!( + /// A request for current subscriptions. + Subscriptions, "subscriptions", PUBSUB, + attributes: [ + /// The node to query. + node: Option = "node", + ], + children: [ + /// The list of subscription elements returned. + subscription: Vec = ("subscription", PUBSUB) => SubscriptionElem + ] +); + +generate_element!( + /// A subscription element, describing the state of a subscription. + SubscriptionElem, "subscription", PUBSUB, + attributes: [ + /// The JID affected by this subscription. + jid: Required = "jid", + + /// The node affected by this subscription. + node: Option = "node", + + /// The subscription identifier for this subscription. + subid: Option = "subid", + + /// The state of the subscription. + subscription: Option = "subscription", + ], + children: [ + /// The options related to this subscription. + subscribe_options: Option = ("subscribe-options", PUBSUB) => SubscribeOptions + ] +); + +generate_element!( + /// An unsubscribe request. + Unsubscribe, "unsubscribe", PUBSUB, + attributes: [ + /// The JID affected by this request. + jid: Required = "jid", + + /// The node affected by this request. + node: Option = "node", + + /// The subscription identifier for this subscription. + subid: Option = "subid", + ] +); + +/// Main payload used to communicate with a PubSub service. +/// +/// `` +#[derive(Debug, Clone, PartialEq)] +pub enum PubSub { + /// Request to create a new node, with optional suggested name and suggested configuration. + Create { + /// The create request. + create: Create, + + /// The configure request for the new node. + configure: Option, + }, + + /// A subcribe request. + Subscribe { + /// The subscribe request. + subscribe: Option, + + /// The options related to this subscribe request. + options: Option, + }, + + /// Request to publish items to a node, with optional options. + Publish { + /// The publish request. + publish: Publish, + + /// The options related to this publish request. + publish_options: Option, + }, + + /// A list of affiliations you have on a service, or on a node. + Affiliations(Affiliations), + + /// Request for a default node configuration. + Default(Default), + + /// A request for a list of items. + Items(Items), + + /// A request to retract some items from a node. + Retract(Retract), + + /// A request about a subscription. + Subscription(SubscriptionElem), + + /// A request for current subscriptions. + Subscriptions(Subscriptions), + + /// An unsubscribe request. + Unsubscribe(Unsubscribe), +} + +impl IqGetPayload for PubSub {} +impl IqSetPayload for PubSub {} +impl IqResultPayload for PubSub {} + +impl TryFrom for PubSub { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "pubsub", PUBSUB); + check_no_attributes!(elem, "pubsub"); + + let mut payload = None; + for child in elem.children() { + if child.is("create", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let create = Create::try_from(child.clone())?; + payload = Some(PubSub::Create { + create, + configure: None, + }); + } else if child.is("subscribe", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let subscribe = Subscribe::try_from(child.clone())?; + payload = Some(PubSub::Subscribe { + subscribe: Some(subscribe), + options: None, + }); + } else if child.is("options", ns::PUBSUB) { + if let Some(PubSub::Subscribe { subscribe, options }) = payload { + if options.is_some() { + return Err(Error::ParseError( + "Options is already defined in pubsub element.", + )); + } + let options = Some(Options::try_from(child.clone())?); + payload = Some(PubSub::Subscribe { subscribe, options }); + } else if payload.is_none() { + let options = Options::try_from(child.clone())?; + payload = Some(PubSub::Subscribe { + subscribe: None, + options: Some(options), + }); + } else { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + } else if child.is("configure", ns::PUBSUB) { + if let Some(PubSub::Create { create, configure }) = payload { + if configure.is_some() { + return Err(Error::ParseError( + "Configure is already defined in pubsub element.", + )); + } + let configure = Some(Configure::try_from(child.clone())?); + payload = Some(PubSub::Create { create, configure }); + } else { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + } else if child.is("publish", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let publish = Publish::try_from(child.clone())?; + payload = Some(PubSub::Publish { + publish, + publish_options: None, + }); + } else if child.is("publish-options", ns::PUBSUB) { + if let Some(PubSub::Publish { + publish, + publish_options, + }) = payload + { + if publish_options.is_some() { + return Err(Error::ParseError( + "Publish-options are already defined in pubsub element.", + )); + } + let publish_options = Some(PublishOptions::try_from(child.clone())?); + payload = Some(PubSub::Publish { + publish, + publish_options, + }); + } else { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + } else if child.is("affiliations", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let affiliations = Affiliations::try_from(child.clone())?; + payload = Some(PubSub::Affiliations(affiliations)); + } else if child.is("default", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let default = Default::try_from(child.clone())?; + payload = Some(PubSub::Default(default)); + } else if child.is("items", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let items = Items::try_from(child.clone())?; + payload = Some(PubSub::Items(items)); + } else if child.is("retract", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let retract = Retract::try_from(child.clone())?; + payload = Some(PubSub::Retract(retract)); + } else if child.is("subscription", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let subscription = SubscriptionElem::try_from(child.clone())?; + payload = Some(PubSub::Subscription(subscription)); + } else if child.is("subscriptions", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let subscriptions = Subscriptions::try_from(child.clone())?; + payload = Some(PubSub::Subscriptions(subscriptions)); + } else if child.is("unsubscribe", ns::PUBSUB) { + if payload.is_some() { + return Err(Error::ParseError( + "Payload is already defined in pubsub element.", + )); + } + let unsubscribe = Unsubscribe::try_from(child.clone())?; + payload = Some(PubSub::Unsubscribe(unsubscribe)); + } else { + return Err(Error::ParseError("Unknown child in pubsub element.")); + } + } + Ok(payload.ok_or(Error::ParseError("No payload in pubsub element."))?) + } +} + +impl From for Element { + fn from(pubsub: PubSub) -> Element { + Element::builder("pubsub", ns::PUBSUB) + .append_all(match pubsub { + PubSub::Create { create, configure } => { + let mut elems = vec![Element::from(create)]; + if let Some(configure) = configure { + elems.push(Element::from(configure)); + } + elems + } + PubSub::Subscribe { subscribe, options } => { + let mut elems = vec![]; + if let Some(subscribe) = subscribe { + elems.push(Element::from(subscribe)); + } + if let Some(options) = options { + elems.push(Element::from(options)); + } + elems + } + PubSub::Publish { + publish, + publish_options, + } => { + let mut elems = vec![Element::from(publish)]; + if let Some(publish_options) = publish_options { + elems.push(Element::from(publish_options)); + } + elems + } + PubSub::Affiliations(affiliations) => vec![Element::from(affiliations)], + PubSub::Default(default) => vec![Element::from(default)], + PubSub::Items(items) => vec![Element::from(items)], + PubSub::Retract(retract) => vec![Element::from(retract)], + PubSub::Subscription(subscription) => vec![Element::from(subscription)], + PubSub::Subscriptions(subscriptions) => vec![Element::from(subscriptions)], + PubSub::Unsubscribe(unsubscribe) => vec![Element::from(unsubscribe)], + }) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_forms::{DataForm, DataFormType, Field, FieldType}; + use jid::FullJid; + + #[test] + fn create() { + let elem: Element = "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Create { create, configure } => { + assert!(create.node.is_none()); + assert!(configure.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Create { create, configure } => { + assert_eq!(&create.node.unwrap().0, "coucou"); + assert!(configure.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn create_and_configure_empty() { + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Create { create, configure } => { + assert!(create.node.is_none()); + assert!(configure.unwrap().form.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn create_and_configure_simple() { + // XXX: Do we want xmpp-parsers to always specify the field type in the output Element? + let elem: Element = "http://jabber.org/protocol/pubsub#node_configwhitelist" + .parse() + .unwrap(); + let elem1 = elem.clone(); + + let pubsub = PubSub::Create { + create: Create { + node: Some(NodeName(String::from("foo"))), + }, + configure: Some(Configure { + form: Some(DataForm { + type_: DataFormType::Submit, + form_type: Some(String::from(ns::PUBSUB_CONFIGURE)), + title: None, + instructions: None, + fields: vec![Field { + var: String::from("pubsub#access_model"), + type_: FieldType::ListSingle, + label: None, + required: false, + options: vec![], + values: vec![String::from("whitelist")], + media: vec![], + }], + }), + }), + }; + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn publish() { + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Publish { + publish, + publish_options, + } => { + assert_eq!(&publish.node.0, "coucou"); + assert!(publish_options.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn publish_with_publish_options() { + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Publish { + publish, + publish_options, + } => { + assert_eq!(&publish.node.0, "coucou"); + assert!(publish_options.unwrap().form.is_none()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn invalid_empty_pubsub() { + let elem: Element = "" + .parse() + .unwrap(); + let error = PubSub::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "No payload in pubsub element."); + } + + #[test] + fn publish_option() { + let elem: Element = "http://jabber.org/protocol/pubsub#publish-options".parse().unwrap(); + let publish_options = PublishOptions::try_from(elem).unwrap(); + assert_eq!( + &publish_options.form.unwrap().form_type.unwrap(), + "http://jabber.org/protocol/pubsub#publish-options" + ); + } + + #[test] + fn subscribe_options() { + let elem1: Element = "" + .parse() + .unwrap(); + let subscribe_options1 = SubscribeOptions::try_from(elem1).unwrap(); + assert_eq!(subscribe_options1.required, false); + + let elem2: Element = "".parse().unwrap(); + let subscribe_options2 = SubscribeOptions::try_from(elem2).unwrap(); + assert_eq!(subscribe_options2.required, true); + } + + #[test] + fn test_options_without_subscribe() { + let elem: Element = "".parse().unwrap(); + let elem1 = elem.clone(); + let pubsub = PubSub::try_from(elem).unwrap(); + match pubsub.clone() { + PubSub::Subscribe { subscribe, options } => { + assert!(subscribe.is_none()); + assert!(options.is_some()); + } + _ => panic!(), + } + + let elem2 = Element::from(pubsub); + assert_eq!(elem1, elem2); + } + + #[test] + fn test_serialize_options() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "".parse().unwrap(); + + let form = DataForm::try_from(elem).unwrap(); + + let options = Options { + jid: Jid::Full(FullJid::new("juliet", "capulet.lit", "balcony")), + node: None, + subid: None, + form: Some(form), + }; + let serialized: Element = options.into(); + assert_eq!(serialized, reference); + } + + #[test] + fn test_serialize_publish_options() { + let reference: Element = "" + .parse() + .unwrap(); + + let elem: Element = "".parse().unwrap(); + + let form = DataForm::try_from(elem).unwrap(); + + let options = PublishOptions { form: Some(form) }; + let serialized: Element = options.into(); + assert_eq!(serialized, reference); + } +} diff --git a/xmpp-parsers/src/receipts.rs b/xmpp-parsers/src/receipts.rs new file mode 100644 index 0000000..62fd7b7 --- /dev/null +++ b/xmpp-parsers/src/receipts.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; + +generate_empty_element!( + /// Requests that this message is acked by the final recipient once + /// received. + Request, + "request", + RECEIPTS +); + +impl MessagePayload for Request {} + +generate_element!( + /// Notes that a previous message has correctly been received, it is + /// referenced by its 'id' attribute. + Received, "received", RECEIPTS, + attributes: [ + /// The 'id' attribute of the received message. + id: Required = "id", + ] +); + +impl MessagePayload for Received {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ns; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Request, 0); + assert_size!(Received, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Request, 0); + assert_size!(Received, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + Request::try_from(elem).unwrap(); + + let elem: Element = "" + .parse() + .unwrap(); + Received::try_from(elem).unwrap(); + } + + #[test] + fn test_missing_id() { + let elem: Element = "".parse().unwrap(); + let error = Received::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_serialise() { + let receipt = Request; + let elem: Element = receipt.into(); + assert!(elem.is("request", ns::RECEIPTS)); + assert_eq!(elem.attrs().count(), 0); + + let receipt = Received { + id: String::from("coucou"), + }; + let elem: Element = receipt.into(); + assert!(elem.is("received", ns::RECEIPTS)); + assert_eq!(elem.attr("id"), Some("coucou")); + } +} diff --git a/xmpp-parsers/src/roster.rs b/xmpp-parsers/src/roster.rs new file mode 100644 index 0000000..1695296 --- /dev/null +++ b/xmpp-parsers/src/roster.rs @@ -0,0 +1,313 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqGetPayload, IqResultPayload, IqSetPayload}; +use jid::BareJid; + +generate_elem_id!( + /// Represents a group a contact is part of. + Group, + "group", + ROSTER +); + +generate_attribute!( + /// The state of your mutual subscription with a contact. + Subscription, "subscription", { + /// The user doesn’t have any subscription to this contact’s presence, + /// and neither does this contact. + None => "none", + + /// Only this contact has a subscription with you, not the opposite. + From => "from", + + /// Only you have a subscription with this contact, not the opposite. + To => "to", + + /// Both you and your contact are subscribed to each other’s presence. + Both => "both", + + /// In a roster set, this asks the server to remove this contact item + /// from your roster. + Remove => "remove", + }, Default = None +); + +generate_attribute!( + /// The sub-state of subscription with a contact. + Ask, "ask", ( + /// Pending sub-state of the 'none' subscription state. + Subscribe => "subscribe" + ) +); + +generate_element!( + /// Contact from the user’s contact list. + Item, "item", ROSTER, + attributes: [ + /// JID of this contact. + jid: Required = "jid", + + /// Name of this contact. + name: OptionEmpty = "name", + + /// Subscription status of this contact. + subscription: Default = "subscription", + + /// Indicates “Pending Out” sub-states for this contact. + ask: Default = "ask", + ], + + children: [ + /// Groups this contact is part of. + groups: Vec = ("group", ROSTER) => Group + ] +); + +generate_element!( + /// The contact list of the user. + Roster, "query", ROSTER, + attributes: [ + /// Version of the contact list. + /// + /// This is an opaque string that should only be sent back to the server on + /// a new connection, if this client is storing the contact list between + /// connections. + ver: Option = "ver" + ], + children: [ + /// List of the contacts of the user. + items: Vec = ("item", ROSTER) => Item + ] +); + +impl IqGetPayload for Roster {} +impl IqSetPayload for Roster {} +impl IqResultPayload for Roster {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use std::convert::TryFrom; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Group, 12); + assert_size!(Subscription, 1); + assert_size!(Ask, 1); + assert_size!(Item, 52); + assert_size!(Roster, 24); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Group, 24); + assert_size!(Subscription, 1); + assert_size!(Ask, 1); + assert_size!(Item, 104); + assert_size!(Roster, 48); + } + + #[test] + fn test_get() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert!(roster.items.is_empty()); + } + + #[test] + fn test_result() { + let elem: Element = "".parse().unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver7"))); + assert_eq!(roster.items.len(), 2); + + let elem2: Element = "".parse().unwrap(); + let roster2 = Roster::try_from(elem2).unwrap(); + assert_eq!(roster.items, roster2.items); + + let elem: Element = "" + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver9"))); + assert!(roster.items.is_empty()); + + let elem: Element = r#" + + + Friends + + + + + MyBuddies + + +"# + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert_eq!(roster.ver, Some(String::from("ver11"))); + assert_eq!(roster.items.len(), 4); + assert_eq!(roster.items[0].jid, BareJid::new("romeo", "example.net")); + assert_eq!(roster.items[0].name, Some(String::from("Romeo"))); + assert_eq!(roster.items[0].subscription, Subscription::Both); + assert_eq!(roster.items[0].ask, Ask::None); + assert_eq!( + roster.items[0].groups, + vec!(Group::from_str("Friends").unwrap()) + ); + + assert_eq!(roster.items[3].jid, BareJid::new("contact", "example.org")); + assert_eq!(roster.items[3].name, Some(String::from("MyContact"))); + assert_eq!(roster.items[3].subscription, Subscription::None); + assert_eq!(roster.items[3].ask, Ask::Subscribe); + assert_eq!( + roster.items[3].groups, + vec!(Group::from_str("MyBuddies").unwrap()) + ); + } + + #[test] + fn test_multiple_groups() { + let elem: Element = "AB" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!(roster.items[0].jid, BareJid::new("test", "example.org")); + assert_eq!(roster.items[0].name, None); + assert_eq!(roster.items[0].groups.len(), 2); + assert_eq!(roster.items[0].groups[0], Group::from_str("A").unwrap()); + assert_eq!(roster.items[0].groups[1], Group::from_str("B").unwrap()); + let elem2 = roster.into(); + assert_eq!(elem1, elem2); + } + + #[test] + fn test_set() { + let elem: Element = + "" + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + + let elem: Element = r#" + + + Servants + + +"# + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!(roster.items[0].jid, BareJid::new("nurse", "example.com")); + assert_eq!(roster.items[0].name, Some(String::from("Nurse"))); + assert_eq!(roster.items[0].groups.len(), 1); + assert_eq!( + roster.items[0].groups[0], + Group::from_str("Servants").unwrap() + ); + + let elem: Element = r#" + + + +"# + .parse() + .unwrap(); + let roster = Roster::try_from(elem).unwrap(); + assert!(roster.ver.is_none()); + assert_eq!(roster.items.len(), 1); + assert_eq!(roster.items[0].jid, BareJid::new("nurse", "example.com")); + assert!(roster.items[0].name.is_none()); + assert!(roster.items[0].groups.is_empty()); + assert_eq!(roster.items[0].subscription, Subscription::Remove); + } + + #[cfg(not(feature = "disable-validation"))] + #[test] + fn test_invalid() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in query element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown attribute in query element."); + } + + #[test] + fn test_invalid_item() { + let elem: Element = "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'jid' missing."); + + /* + let elem: Element = "".parse().unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let error = match error { + Error::JidParseError(error) => error, + _ => panic!(), + }; + assert_eq!(error.description(), "Invalid JID, I guess?"); + */ + + let elem: Element = + "" + .parse() + .unwrap(); + let error = Roster::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in item element."); + } +} diff --git a/xmpp-parsers/src/rsm.rs b/xmpp-parsers/src/rsm.rs new file mode 100644 index 0000000..f5649c7 --- /dev/null +++ b/xmpp-parsers/src/rsm.rs @@ -0,0 +1,302 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use std::convert::TryFrom; + +/// Requests paging through a potentially big set of items (represented by an +/// UID). +#[derive(Debug, Clone, PartialEq)] +pub struct SetQuery { + /// Limit the number of items, or use the recipient’s defaults if None. + pub max: Option, + + /// The UID after which to give results, or if None it is the element + /// “before” the first item, effectively an index of negative one. + pub after: Option, + + /// The UID before which to give results, or if None it starts with the + /// last page of the full set. + pub before: Option, + + /// Numerical index of the page (deprecated). + pub index: Option, +} + +impl TryFrom for SetQuery { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "set", RSM, "RSM set"); + let mut set = SetQuery { + max: None, + after: None, + before: None, + index: None, + }; + for child in elem.children() { + if child.is("max", ns::RSM) { + if set.max.is_some() { + return Err(Error::ParseError("Set can’t have more than one max.")); + } + set.max = Some(child.text().parse()?); + } else if child.is("after", ns::RSM) { + if set.after.is_some() { + return Err(Error::ParseError("Set can’t have more than one after.")); + } + set.after = Some(child.text()); + } else if child.is("before", ns::RSM) { + if set.before.is_some() { + return Err(Error::ParseError("Set can’t have more than one before.")); + } + set.before = Some(child.text()); + } else if child.is("index", ns::RSM) { + if set.index.is_some() { + return Err(Error::ParseError("Set can’t have more than one index.")); + } + set.index = Some(child.text().parse()?); + } else { + return Err(Error::ParseError("Unknown child in set element.")); + } + } + Ok(set) + } +} + +impl From for Element { + fn from(set: SetQuery) -> Element { + Element::builder("set", ns::RSM) + .append_all( + set.max + .map(|max| Element::builder("max", ns::RSM).append(format!("{}", max))), + ) + .append_all( + set.after + .map(|after| Element::builder("after", ns::RSM).append(after)), + ) + .append_all( + set.before + .map(|before| Element::builder("before", ns::RSM).append(before)), + ) + .append_all( + set.index + .map(|index| Element::builder("index", ns::RSM).append(format!("{}", index))), + ) + .build() + } +} + +/// Describes the paging result of a [query](struct.SetQuery.html). +#[derive(Debug, Clone, PartialEq)] +pub struct SetResult { + /// The UID of the first item of the page. + pub first: Option, + + /// The position of the [first item](#structfield.first) in the full set + /// (which may be approximate). + pub first_index: Option, + + /// The UID of the last item of the page. + pub last: Option, + + /// How many items there are in the full set (which may be approximate). + pub count: Option, +} + +impl TryFrom for SetResult { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "set", RSM, "RSM set"); + let mut set = SetResult { + first: None, + first_index: None, + last: None, + count: None, + }; + for child in elem.children() { + if child.is("first", ns::RSM) { + if set.first.is_some() { + return Err(Error::ParseError("Set can’t have more than one first.")); + } + set.first_index = get_attr!(child, "index", Option); + set.first = Some(child.text()); + } else if child.is("last", ns::RSM) { + if set.last.is_some() { + return Err(Error::ParseError("Set can’t have more than one last.")); + } + set.last = Some(child.text()); + } else if child.is("count", ns::RSM) { + if set.count.is_some() { + return Err(Error::ParseError("Set can’t have more than one count.")); + } + set.count = Some(child.text().parse()?); + } else { + return Err(Error::ParseError("Unknown child in set element.")); + } + } + Ok(set) + } +} + +impl From for Element { + fn from(set: SetResult) -> Element { + let first = set.first.clone().map(|first| { + Element::builder("first", ns::RSM) + .attr("index", set.first_index) + .append(first) + }); + Element::builder("set", ns::RSM) + .append_all(first) + .append_all( + set.last + .map(|last| Element::builder("last", ns::RSM).append(last)), + ) + .append_all( + set.count + .map(|count| Element::builder("count", ns::RSM).append(format!("{}", count))), + ) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(SetQuery, 40); + assert_size!(SetResult, 40); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(SetQuery, 80); + assert_size!(SetResult, 80); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let set = SetQuery::try_from(elem).unwrap(); + assert_eq!(set.max, None); + assert_eq!(set.after, None); + assert_eq!(set.before, None); + assert_eq!(set.index, None); + + let elem: Element = "" + .parse() + .unwrap(); + let set = SetResult::try_from(elem).unwrap(); + match set.first { + Some(_) => panic!(), + None => (), + } + assert_eq!(set.last, None); + assert_eq!(set.count, None); + } + + #[test] + fn test_unknown() { + let elem: Element = "" + .parse() + .unwrap(); + let error = SetQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a RSM set element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = SetResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "This is not a RSM set element."); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = SetQuery::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in set element."); + + let elem: Element = "" + .parse() + .unwrap(); + let error = SetResult::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in set element."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let rsm = SetQuery { + max: None, + after: None, + before: None, + index: None, + }; + let elem2 = rsm.into(); + assert_eq!(elem, elem2); + + let elem: Element = "" + .parse() + .unwrap(); + let rsm = SetResult { + first: None, + first_index: None, + last: None, + count: None, + }; + let elem2 = rsm.into(); + assert_eq!(elem, elem2); + } + + #[test] + fn test_first_index() { + let elem: Element = + "coucou" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let set = SetResult::try_from(elem).unwrap(); + assert_eq!(set.first, Some(String::from("coucou"))); + assert_eq!(set.first_index, Some(4)); + + let set2 = SetResult { + first: Some(String::from("coucou")), + first_index: Some(4), + last: None, + count: None, + }; + let elem2 = set2.into(); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/sasl.rs b/xmpp-parsers/src/sasl.rs new file mode 100644 index 0000000..a486c13 --- /dev/null +++ b/xmpp-parsers/src/sasl.rs @@ -0,0 +1,301 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::util::error::Error; +use crate::util::helpers::Base64; +use crate::Element; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +generate_attribute!( + /// The list of available SASL mechanisms. + Mechanism, "mechanism", { + /// Uses no hashing mechanism and transmit the password in clear to the + /// server, using a single step. + Plain => "PLAIN", + + /// Challenge-based mechanism using HMAC and SHA-1, allows both the + /// client and the server to avoid having to store the password in + /// clear. + /// + /// See https://tools.ietf.org/html/rfc5802 + ScramSha1 => "SCRAM-SHA-1", + + /// Same as [ScramSha1](#structfield.ScramSha1), with the addition of + /// channel binding. + ScramSha1Plus => "SCRAM-SHA-1-PLUS", + + /// Same as [ScramSha1](#structfield.ScramSha1), but using SHA-256 + /// instead of SHA-1 as the hash function. + ScramSha256 => "SCRAM-SHA-256", + + /// Same as [ScramSha256](#structfield.ScramSha256), with the addition + /// of channel binding. + ScramSha256Plus => "SCRAM-SHA-256-PLUS", + + /// Creates a temporary JID on login, which will be destroyed on + /// disconnect. + Anonymous => "ANONYMOUS", + } +); + +generate_element!( + /// The first step of the SASL process, selecting the mechanism and sending + /// the first part of the handshake. + Auth, "auth", SASL, + attributes: [ + /// The mechanism used. + mechanism: Required = "mechanism" + ], + text: ( + /// The content of the handshake. + data: Base64> + ) +); + +generate_element!( + /// In case the mechanism selected at the [auth](struct.Auth.html) step + /// requires a second step, the server sends this element with additional + /// data. + Challenge, "challenge", SASL, + text: ( + /// The challenge data. + data: Base64> + ) +); + +generate_element!( + /// In case the mechanism selected at the [auth](struct.Auth.html) step + /// requires a second step, this contains the client’s response to the + /// server’s [challenge](struct.Challenge.html). + Response, "response", SASL, + text: ( + /// The response data. + data: Base64> + ) +); + +generate_empty_element!( + /// Sent by the client at any point after [auth](struct.Auth.html) if it + /// wants to cancel the current authentication process. + Abort, + "abort", + SASL +); + +generate_element!( + /// Sent by the server on SASL success. + Success, "success", SASL, + text: ( + /// Possible data sent on success. + data: Base64> + ) +); + +generate_element_enum!( + /// List of possible failure conditions for SASL. + DefinedCondition, "defined-condition", SASL, { + /// The client aborted the authentication with + /// [abort](struct.Abort.html). + Aborted => "aborted", + + /// The account the client is trying to authenticate against has been + /// disabled. + AccountDisabled => "account-disabled", + + /// The credentials for this account have expired. + CredentialsExpired => "credentials-expired", + + /// You must enable StartTLS or use direct TLS before using this + /// authentication mechanism. + EncryptionRequired => "encryption-required", + + /// The base64 data sent by the client is invalid. + IncorrectEncoding => "incorrect-encoding", + + /// The authzid provided by the client is invalid. + InvalidAuthzid => "invalid-authzid", + + /// The client tried to use an invalid mechanism, or none. + InvalidMechanism => "invalid-mechanism", + + /// The client sent a bad request. + MalformedRequest => "malformed-request", + + /// The mechanism selected is weaker than what the server allows. + MechanismTooWeak => "mechanism-too-weak", + + /// The credentials provided are invalid. + NotAuthorized => "not-authorized", + + /// The server encountered an issue which may be fixed later, the + /// client should retry at some point. + TemporaryAuthFailure => "temporary-auth-failure", + } +); + +type Lang = String; + +/// Sent by the server on SASL failure. +#[derive(Debug, Clone)] +pub struct Failure { + /// One of the allowed defined-conditions for SASL. + pub defined_condition: DefinedCondition, + + /// A human-readable explanation for the failure. + pub texts: BTreeMap, +} + +impl TryFrom for Failure { + type Error = Error; + + fn try_from(root: Element) -> Result { + check_self!(root, "failure", SASL); + check_no_attributes!(root, "failure"); + + let mut defined_condition = None; + let mut texts = BTreeMap::new(); + + for child in root.children() { + if child.is("text", ns::SASL) { + check_no_unknown_attributes!(child, "text", ["xml:lang"]); + check_no_children!(child, "text"); + let lang = get_attr!(child, "xml:lang", Default); + if texts.insert(lang, child.text()).is_some() { + return Err(Error::ParseError( + "Text element present twice for the same xml:lang in failure element.", + )); + } + } else if child.has_ns(ns::SASL) { + if defined_condition.is_some() { + return Err(Error::ParseError( + "Failure must not have more than one defined-condition.", + )); + } + check_no_attributes!(child, "defined-condition"); + check_no_children!(child, "defined-condition"); + let condition = match DefinedCondition::try_from(child.clone()) { + Ok(condition) => condition, + // TODO: do we really want to eat this error? + Err(_) => DefinedCondition::NotAuthorized, + }; + defined_condition = Some(condition); + } else { + return Err(Error::ParseError("Unknown element in Failure.")); + } + } + let defined_condition = + defined_condition.ok_or(Error::ParseError("Failure must have a defined-condition."))?; + + Ok(Failure { + defined_condition, + texts, + }) + } +} + +impl From for Element { + fn from(failure: Failure) -> Element { + Element::builder("failure", ns::SASL) + .append(failure.defined_condition) + .append_all(failure.texts.into_iter().map(|(lang, text)| { + Element::builder("text", ns::SASL) + .attr("xml:lang", lang) + .append(text) + })) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Mechanism, 1); + assert_size!(Auth, 16); + assert_size!(Challenge, 12); + assert_size!(Response, 12); + assert_size!(Abort, 0); + assert_size!(Success, 12); + assert_size!(DefinedCondition, 1); + assert_size!(Failure, 16); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Mechanism, 1); + assert_size!(Auth, 32); + assert_size!(Challenge, 24); + assert_size!(Response, 24); + assert_size!(Abort, 0); + assert_size!(Success, 24); + assert_size!(DefinedCondition, 1); + assert_size!(Failure, 32); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let auth = Auth::try_from(elem).unwrap(); + assert_eq!(auth.mechanism, Mechanism::Plain); + assert!(auth.data.is_empty()); + } + + #[test] + fn section_6_5_1() { + let elem: Element = + "" + .parse() + .unwrap(); + let failure = Failure::try_from(elem).unwrap(); + assert_eq!(failure.defined_condition, DefinedCondition::Aborted); + assert!(failure.texts.is_empty()); + } + + #[test] + fn section_6_5_2() { + let elem: Element = " + + Call 212-555-1212 for assistance. + " + .parse() + .unwrap(); + let failure = Failure::try_from(elem).unwrap(); + assert_eq!(failure.defined_condition, DefinedCondition::AccountDisabled); + assert_eq!( + failure.texts["en"], + String::from("Call 212-555-1212 for assistance.") + ); + } + + /// Some servers apparently use a non-namespaced 'lang' attribute, which is invalid as not part + /// of the schema. This tests whether we can parse it when disabling validation. + #[cfg(feature = "disable-validation")] + #[test] + fn invalid_failure_with_non_prefixed_text_lang() { + let elem: Element = " + + Invalid username or password + " + .parse() + .unwrap(); + let failure = Failure::try_from(elem).unwrap(); + assert_eq!(failure.defined_condition, DefinedCondition::NotAuthorized); + assert_eq!( + failure.texts[""], + String::from("Invalid username or password") + ); + } +} diff --git a/xmpp-parsers/src/server_info.rs b/xmpp-parsers/src/server_info.rs new file mode 100644 index 0000000..3ad9e00 --- /dev/null +++ b/xmpp-parsers/src/server_info.rs @@ -0,0 +1,209 @@ +// Copyright (C) 2019 Maxime “pep” Buquet +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::data_forms::{DataForm, DataFormType, Field, FieldType}; +use crate::ns; +use crate::util::error::Error; +use std::convert::TryFrom; + +/// Structure representing a `http://jabber.org/network/serverinfo` form type. +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ServerInfo { + /// Abuse addresses + pub abuse: Vec, + + /// Admin addresses + pub admin: Vec, + + /// Feedback addresses + pub feedback: Vec, + + /// Sales addresses + pub sales: Vec, + + /// Security addresses + pub security: Vec, + + /// Support addresses + pub support: Vec, +} + +impl TryFrom for ServerInfo { + type Error = Error; + + fn try_from(form: DataForm) -> Result { + if form.type_ != DataFormType::Result_ { + return Err(Error::ParseError("Wrong type of form.")); + } + if form.form_type != Some(String::from(ns::SERVER_INFO)) { + return Err(Error::ParseError("Wrong FORM_TYPE for form.")); + } + let mut server_info = ServerInfo::default(); + for field in form.fields { + if field.type_ != FieldType::ListMulti { + return Err(Error::ParseError("Field is not of the required type.")); + } + if field.var == "abuse-addresses" { + server_info.abuse = field.values; + } else if field.var == "admin-addresses" { + server_info.admin = field.values; + } else if field.var == "feedback-addresses" { + server_info.feedback = field.values; + } else if field.var == "sales-addresses" { + server_info.sales = field.values; + } else if field.var == "security-addresses" { + server_info.security = field.values; + } else if field.var == "support-addresses" { + server_info.support = field.values; + } else { + return Err(Error::ParseError("Unknown form field var.")); + } + } + + Ok(server_info) + } +} + +impl From for DataForm { + fn from(server_info: ServerInfo) -> DataForm { + DataForm { + type_: DataFormType::Result_, + form_type: Some(String::from(ns::SERVER_INFO)), + title: None, + instructions: None, + fields: vec![ + generate_address_field("abuse-addresses", server_info.abuse), + generate_address_field("admin-addresses", server_info.admin), + generate_address_field("feedback-addresses", server_info.feedback), + generate_address_field("sales-addresses", server_info.sales), + generate_address_field("security-addresses", server_info.security), + generate_address_field("support-addresses", server_info.support), + ], + } + } +} + +/// Generate `Field` for addresses +pub fn generate_address_field>(var: S, values: Vec) -> Field { + Field { + var: var.into(), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values, + media: vec![], + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::data_forms::{DataForm, DataFormType, Field, FieldType}; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ServerInfo, 72); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ServerInfo, 144); + } + + #[test] + fn test_simple() { + let form = DataForm { + type_: DataFormType::Result_, + form_type: Some(String::from(ns::SERVER_INFO)), + title: None, + instructions: None, + fields: vec![ + Field { + var: String::from("abuse-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![], + media: vec![], + }, + Field { + var: String::from("admin-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![ + String::from("xmpp:admin@foo.bar"), + String::from("https://foo.bar/chat/"), + String::from("mailto:admin@foo.bar"), + ], + media: vec![], + }, + Field { + var: String::from("feedback-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![], + media: vec![], + }, + Field { + var: String::from("sales-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![], + media: vec![], + }, + Field { + var: String::from("security-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![ + String::from("xmpp:security@foo.bar"), + String::from("mailto:security@foo.bar"), + ], + media: vec![], + }, + Field { + var: String::from("support-addresses"), + type_: FieldType::ListMulti, + label: None, + required: false, + options: vec![], + values: vec![String::from("mailto:support@foo.bar")], + media: vec![], + }, + ], + }; + + let server_info = ServerInfo { + abuse: vec![], + admin: vec![ + String::from("xmpp:admin@foo.bar"), + String::from("https://foo.bar/chat/"), + String::from("mailto:admin@foo.bar"), + ], + feedback: vec![], + sales: vec![], + security: vec![ + String::from("xmpp:security@foo.bar"), + String::from("mailto:security@foo.bar"), + ], + support: vec![String::from("mailto:support@foo.bar")], + }; + + // assert_eq!(DataForm::from(server_info), form); + assert_eq!(ServerInfo::try_from(form).unwrap(), server_info); + } +} diff --git a/xmpp-parsers/src/sm.rs b/xmpp-parsers/src/sm.rs new file mode 100644 index 0000000..315761e --- /dev/null +++ b/xmpp-parsers/src/sm.rs @@ -0,0 +1,247 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::stanza_error::DefinedCondition; + +generate_element!( + /// Acknowledgement of the currently received stanzas. + A, "a", SM, + attributes: [ + /// The last handled stanza. + h: Required = "h", + ] +); + +impl A { + /// Generates a new `` element. + pub fn new(h: u32) -> A { + A { h } + } +} + +generate_attribute!( + /// Whether to allow resumption of a previous stream. + ResumeAttr, + "resume", + bool +); + +generate_element!( + /// Client request for enabling stream management. + #[derive(Default)] + Enable, "enable", SM, + attributes: [ + /// The preferred resumption time in seconds by the client. + // TODO: should be the infinite integer set ≥ 1. + max: Option = "max", + + /// Whether the client wants to be allowed to resume the stream. + resume: Default = "resume", + ] +); + +impl Enable { + /// Generates a new `` element. + pub fn new() -> Self { + Enable::default() + } + + /// Sets the preferred resumption time in seconds. + pub fn with_max(mut self, max: u32) -> Self { + self.max = Some(max); + self + } + + /// Asks for resumption to be possible. + pub fn with_resume(mut self) -> Self { + self.resume = ResumeAttr::True; + self + } +} + +generate_id!( + /// A random identifier used for stream resumption. + StreamId +); + +generate_element!( + /// Server response once stream management is enabled. + Enabled, "enabled", SM, + attributes: [ + /// A random identifier used for stream resumption. + id: Option = "id", + + /// The preferred IP, domain, IP:port or domain:port location for + /// resumption. + location: Option = "location", + + /// The preferred resumption time in seconds by the server. + // TODO: should be the infinite integer set ≥ 1. + max: Option = "max", + + /// Whether stream resumption is allowed. + resume: Default = "resume", + ] +); + +generate_element!( + /// A stream management error happened. + Failed, "failed", SM, + attributes: [ + /// The last handled stanza. + h: Option = "h", + ], + children: [ + /// The error returned. + // XXX: implement the * handling. + error: Option = ("*", XMPP_STANZAS) => DefinedCondition + ] +); + +generate_empty_element!( + /// Requests the currently received stanzas by the other party. + R, + "r", + SM +); + +generate_element!( + /// Requests a stream resumption. + Resume, "resume", SM, + attributes: [ + /// The last handled stanza. + h: Required = "h", + + /// The previous id given by the server on + /// [enabled](struct.Enabled.html). + previd: Required = "previd", + ] +); + +generate_element!( + /// The response by the server for a successfully resumed stream. + Resumed, "resumed", SM, + attributes: [ + /// The last handled stanza. + h: Required = "h", + + /// The previous id given by the server on + /// [enabled](struct.Enabled.html). + previd: Required = "previd", + ] +); + +// TODO: add support for optional and required. +generate_empty_element!( + /// Represents availability of Stream Management in ``. + StreamManagement, + "sm", + SM +); + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(A, 4); + assert_size!(ResumeAttr, 1); + assert_size!(Enable, 12); + assert_size!(StreamId, 12); + assert_size!(Enabled, 36); + assert_size!(Failed, 12); + assert_size!(R, 0); + assert_size!(Resume, 16); + assert_size!(Resumed, 16); + assert_size!(StreamManagement, 0); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(A, 4); + assert_size!(ResumeAttr, 1); + assert_size!(Enable, 12); + assert_size!(StreamId, 24); + assert_size!(Enabled, 64); + assert_size!(Failed, 12); + assert_size!(R, 0); + assert_size!(Resume, 32); + assert_size!(Resumed, 32); + assert_size!(StreamManagement, 0); + } + + #[test] + fn a() { + let elem: Element = " +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; +use crate::ns; +use crate::presence::PresencePayload; +use crate::util::error::Error; +use crate::Element; +use jid::Jid; +use std::collections::BTreeMap; +use std::convert::TryFrom; + +generate_attribute!( + /// The type of the error. + ErrorType, "type", { + /// Retry after providing credentials. + Auth => "auth", + + /// Do not retry (the error cannot be remedied). + Cancel => "cancel", + + /// Proceed (the condition was only a warning). + Continue => "continue", + + /// Retry after changing the data sent. + Modify => "modify", + + /// Retry after waiting (the error is temporary). + Wait => "wait", + } +); + +generate_element_enum!( + /// List of valid error conditions. + DefinedCondition, "condition", XMPP_STANZAS, { + /// The sender has sent a stanza containing XML that does not conform + /// to the appropriate schema or that cannot be processed (e.g., an IQ + /// stanza that includes an unrecognized value of the 'type' attribute, + /// or an element that is qualified by a recognized namespace but that + /// violates the defined syntax for the element); the associated error + /// type SHOULD be "modify". + BadRequest => "bad-request", + + /// Access cannot be granted because an existing resource exists with + /// the same name or address; the associated error type SHOULD be + /// "cancel". + Conflict => "conflict", + + /// The feature represented in the XML stanza is not implemented by the + /// intended recipient or an intermediate server and therefore the + /// stanza cannot be processed (e.g., the entity understands the + /// namespace but does not recognize the element name); the associated + /// error type SHOULD be "cancel" or "modify". + FeatureNotImplemented => "feature-not-implemented", + + /// The requesting entity does not possess the necessary permissions to + /// perform an action that only certain authorized roles or individuals + /// are allowed to complete (i.e., it typically relates to + /// authorization rather than authentication); the associated error + /// type SHOULD be "auth". + Forbidden => "forbidden", + + /// The recipient or server can no longer be contacted at this address, + /// typically on a permanent basis (as opposed to the error + /// condition, which is used for temporary addressing failures); the + /// associated error type SHOULD be "cancel" and the error stanza + /// SHOULD include a new address (if available) as the XML character + /// data of the element (which MUST be a Uniform Resource + /// Identifier [URI] or Internationalized Resource Identifier [IRI] at + /// which the entity can be contacted, typically an XMPP IRI as + /// specified in [XMPP‑URI]). + Gone => "gone", + + /// The server has experienced a misconfiguration or other internal + /// error that prevents it from processing the stanza; the associated + /// error type SHOULD be "cancel". + InternalServerError => "internal-server-error", + + /// The addressed JID or item requested cannot be found; the associated + /// error type SHOULD be "cancel". + ItemNotFound => "item-not-found", + + /// The sending entity has provided (e.g., during resource binding) or + /// communicated (e.g., in the 'to' address of a stanza) an XMPP + /// address or aspect thereof that violates the rules defined in + /// [XMPP‑ADDR]; the associated error type SHOULD be "modify". + JidMalformed => "jid-malformed", + + /// The recipient or server understands the request but cannot process + /// it because the request does not meet criteria defined by the + /// recipient or server (e.g., a request to subscribe to information + /// that does not simultaneously include configuration parameters + /// needed by the recipient); the associated error type SHOULD be + /// "modify". + NotAcceptable => "not-acceptable", + + /// The recipient or server does not allow any entity to perform the + /// action (e.g., sending to entities at a blacklisted domain); the + /// associated error type SHOULD be "cancel". + NotAllowed => "not-allowed", + + /// The sender needs to provide credentials before being allowed to + /// perform the action, or has provided improper credentials (the name + /// "not-authorized", which was borrowed from the "401 Unauthorized" + /// error of [HTTP], might lead the reader to think that this condition + /// relates to authorization, but instead it is typically used in + /// relation to authentication); the associated error type SHOULD be + /// "auth". + NotAuthorized => "not-authorized", + + /// The entity has violated some local service policy (e.g., a message + /// contains words that are prohibited by the service) and the server + /// MAY choose to specify the policy in the element or in an + /// application-specific condition element; the associated error type + /// SHOULD be "modify" or "wait" depending on the policy being + /// violated. + PolicyViolation => "policy-violation", + + /// The intended recipient is temporarily unavailable, undergoing + /// maintenance, etc.; the associated error type SHOULD be "wait". + RecipientUnavailable => "recipient-unavailable", + + /// The recipient or server is redirecting requests for this + /// information to another entity, typically in a temporary fashion (as + /// opposed to the error condition, which is used for permanent + /// addressing failures); the associated error type SHOULD be "modify" + /// and the error stanza SHOULD contain the alternate address in the + /// XML character data of the element (which MUST be a URI + /// or IRI with which the sender can communicate, typically an XMPP IRI + /// as specified in [XMPP‑URI]). + Redirect => "redirect", + + /// The requesting entity is not authorized to access the requested + /// service because prior registration is necessary (examples of prior + /// registration include members-only rooms in XMPP multi-user chat + /// [XEP‑0045] and gateways to non-XMPP instant messaging services, + /// which traditionally required registration in order to use the + /// gateway [XEP‑0100]); the associated error type SHOULD be "auth". + RegistrationRequired => "registration-required", + + /// A remote server or service specified as part or all of the JID of + /// the intended recipient does not exist or cannot be resolved (e.g., + /// there is no _xmpp-server._tcp DNS SRV record, the A or AAAA + /// fallback resolution fails, or A/AAAA lookups succeed but there is + /// no response on the IANA-registered port 5269); the associated error + /// type SHOULD be "cancel". + RemoteServerNotFound => "remote-server-not-found", + + /// A remote server or service specified as part or all of the JID of + /// the intended recipient (or needed to fulfill a request) was + /// resolved but communications could not be established within a + /// reasonable amount of time (e.g., an XML stream cannot be + /// established at the resolved IP address and port, or an XML stream + /// can be established but stream negotiation fails because of problems + /// with TLS, SASL, Server Dialback, etc.); the associated error type + /// SHOULD be "wait" (unless the error is of a more permanent nature, + /// e.g., the remote server is found but it cannot be authenticated or + /// it violates security policies). + RemoteServerTimeout => "remote-server-timeout", + + /// The server or recipient is busy or lacks the system resources + /// necessary to service the request; the associated error type SHOULD + /// be "wait". + ResourceConstraint => "resource-constraint", + + /// The server or recipient does not currently provide the requested + /// service; the associated error type SHOULD be "cancel". + ServiceUnavailable => "service-unavailable", + + /// The requesting entity is not authorized to access the requested + /// service because a prior subscription is necessary (examples of + /// prior subscription include authorization to receive presence + /// information as defined in [XMPP‑IM] and opt-in data feeds for XMPP + /// publish-subscribe as defined in [XEP‑0060]); the associated error + /// type SHOULD be "auth". + SubscriptionRequired => "subscription-required", + + /// The error condition is not one of those defined by the other + /// conditions in this list; any error type can be associated with this + /// condition, and it SHOULD NOT be used except in conjunction with an + /// application-specific condition. + UndefinedCondition => "undefined-condition", + + /// The recipient or server understood the request but was not + /// expecting it at this time (e.g., the request was out of order); the + /// associated error type SHOULD be "wait" or "modify". + UnexpectedRequest => "unexpected-request", + } +); + +type Lang = String; + +/// The representation of a stanza error. +#[derive(Debug, Clone)] +pub struct StanzaError { + /// The type of this error. + pub type_: ErrorType, + + /// The JID of the entity who set this error. + pub by: Option, + + /// One of the defined conditions for this error to happen. + pub defined_condition: DefinedCondition, + + /// Human-readable description of this error. + pub texts: BTreeMap, + + /// A protocol-specific extension for this error. + pub other: Option, +} + +impl MessagePayload for StanzaError {} +impl PresencePayload for StanzaError {} + +impl StanzaError { + /// Create a new `` with the according content. + pub fn new( + type_: ErrorType, + defined_condition: DefinedCondition, + lang: L, + text: T, + ) -> StanzaError + where + L: Into, + T: Into, + { + StanzaError { + type_, + by: None, + defined_condition, + texts: { + let mut map = BTreeMap::new(); + map.insert(lang.into(), text.into()); + map + }, + other: None, + } + } +} + +impl TryFrom for StanzaError { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "error", DEFAULT_NS); + check_no_unknown_attributes!(elem, "error", ["type", "by"]); + + let mut stanza_error = StanzaError { + type_: get_attr!(elem, "type", Required), + by: get_attr!(elem, "by", Option), + defined_condition: DefinedCondition::UndefinedCondition, + texts: BTreeMap::new(), + other: None, + }; + let mut defined_condition = None; + + for child in elem.children() { + if child.is("text", ns::XMPP_STANZAS) { + check_no_children!(child, "text"); + check_no_unknown_attributes!(child, "text", ["xml:lang"]); + let lang = get_attr!(elem, "xml:lang", Default); + if stanza_error.texts.insert(lang, child.text()).is_some() { + return Err(Error::ParseError( + "Text element present twice for the same xml:lang.", + )); + } + } else if child.has_ns(ns::XMPP_STANZAS) { + if defined_condition.is_some() { + return Err(Error::ParseError( + "Error must not have more than one defined-condition.", + )); + } + check_no_children!(child, "defined-condition"); + check_no_attributes!(child, "defined-condition"); + let condition = DefinedCondition::try_from(child.clone())?; + defined_condition = Some(condition); + } else { + if stanza_error.other.is_some() { + return Err(Error::ParseError( + "Error must not have more than one other element.", + )); + } + stanza_error.other = Some(child.clone()); + } + } + stanza_error.defined_condition = + defined_condition.ok_or(Error::ParseError("Error must have a defined-condition."))?; + + Ok(stanza_error) + } +} + +impl From for Element { + fn from(err: StanzaError) -> Element { + Element::builder("error", ns::DEFAULT_NS) + .attr("type", err.type_) + .attr("by", err.by) + .append(err.defined_condition) + .append_all(err.texts.into_iter().map(|(lang, text)| { + Element::builder("text", ns::XMPP_STANZAS) + .attr("xml:lang", lang) + .append(text) + })) + .append_all(err.other) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(ErrorType, 1); + assert_size!(DefinedCondition, 1); + assert_size!(StanzaError, 132); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(ErrorType, 1); + assert_size!(DefinedCondition, 1); + assert_size!(StanzaError, 264); + } + + #[test] + fn test_simple() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = StanzaError::try_from(elem).unwrap(); + assert_eq!(error.type_, ErrorType::Cancel); + assert_eq!( + error.defined_condition, + DefinedCondition::UndefinedCondition + ); + } + + #[test] + fn test_invalid_type() { + #[cfg(not(feature = "component"))] + let elem: Element = "".parse().unwrap(); + #[cfg(feature = "component")] + let elem: Element = "".parse().unwrap(); + let error = StanzaError::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'type' missing."); + + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaError::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown value for 'type' attribute."); + } + + #[test] + fn test_invalid_condition() { + #[cfg(not(feature = "component"))] + let elem: Element = "" + .parse() + .unwrap(); + #[cfg(feature = "component")] + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaError::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Error must have a defined-condition."); + } +} diff --git a/xmpp-parsers/src/stanza_id.rs b/xmpp-parsers/src/stanza_id.rs new file mode 100644 index 0000000..940db3f --- /dev/null +++ b/xmpp-parsers/src/stanza_id.rs @@ -0,0 +1,124 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; +use jid::Jid; + +generate_element!( + /// Gives the identifier a service has stamped on this stanza, often in + /// order to identify it inside of [an archive](../mam/index.html). + StanzaId, "stanza-id", SID, + attributes: [ + /// The id associated to this stanza by another entity. + id: Required = "id", + + /// The entity who stamped this stanza-id. + by: Required = "by", + ] +); + +impl MessagePayload for StanzaId {} + +generate_element!( + /// A hack for MUC before version 1.31 to track a message which may have + /// its 'id' attribute changed. + OriginId, "origin-id", SID, + attributes: [ + /// The id this client set for this stanza. + id: Required = "id", + ] +); + +impl MessagePayload for OriginId {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::util::error::Error; + use crate::Element; + use jid::BareJid; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(StanzaId, 52); + assert_size!(OriginId, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(StanzaId, 104); + assert_size!(OriginId, 24); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let stanza_id = StanzaId::try_from(elem).unwrap(); + assert_eq!(stanza_id.id, String::from("coucou")); + assert_eq!(stanza_id.by, BareJid::new("coucou", "coucou")); + + let elem: Element = "" + .parse() + .unwrap(); + let origin_id = OriginId::try_from(elem).unwrap(); + assert_eq!(origin_id.id, String::from("coucou")); + } + + #[test] + fn test_invalid_child() { + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Unknown child in stanza-id element."); + } + + #[test] + fn test_invalid_id() { + let elem: Element = "".parse().unwrap(); + let error = StanzaId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'id' missing."); + } + + #[test] + fn test_invalid_by() { + let elem: Element = "" + .parse() + .unwrap(); + let error = StanzaId::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Required attribute 'by' missing."); + } + + #[test] + fn test_serialise() { + let elem: Element = "" + .parse() + .unwrap(); + let stanza_id = StanzaId { + id: String::from("coucou"), + by: Jid::Bare(BareJid::new("coucou", "coucou")), + }; + let elem2 = stanza_id.into(); + assert_eq!(elem, elem2); + } +} diff --git a/xmpp-parsers/src/stream.rs b/xmpp-parsers/src/stream.rs new file mode 100644 index 0000000..c1b5e1e --- /dev/null +++ b/xmpp-parsers/src/stream.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use jid::BareJid; + +generate_element!( + /// The stream opening for client-server communications. + Stream, "stream", STREAM, + attributes: [ + /// The JID of the entity opening this stream. + from: Option = "from", + + /// The JID of the entity receiving this stream opening. + to: Option = "to", + + /// The id of the stream, used for authentication challenges. + id: Option = "id", + + /// The XMPP version used during this stream. + version: Option = "version", + + /// The default human language for all subsequent stanzas, which will + /// be transmitted to other entities for better localisation. + xml_lang: Option = "xml:lang", + ] +); + +impl Stream { + /// Creates a simple client→server `` element. + pub fn new(to: BareJid) -> Stream { + Stream { + from: None, + to: Some(to), + id: None, + version: Some(String::from("1.0")), + xml_lang: None, + } + } + + /// Sets the [@from](#structfield.from) attribute on this `` + /// element. + pub fn with_from(mut self, from: BareJid) -> Stream { + self.from = Some(from); + self + } + + /// Sets the [@id](#structfield.id) attribute on this `` + /// element. + pub fn with_id(mut self, id: String) -> Stream { + self.id = Some(id); + self + } + + /// Sets the [@xml:lang](#structfield.xml_lang) attribute on this + /// `` element. + pub fn with_lang(mut self, xml_lang: String) -> Stream { + self.xml_lang = Some(xml_lang); + self + } + + /// Checks whether the version matches the expected one. + pub fn is_version(&self, version: &str) -> bool { + match self.version { + None => false, + Some(ref self_version) => self_version == &String::from(version), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Stream, 84); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Stream, 168); + } + + #[test] + fn test_simple() { + let elem: Element = "".parse().unwrap(); + let stream = Stream::try_from(elem).unwrap(); + assert_eq!(stream.from, Some(BareJid::domain("some-server.example"))); + assert_eq!(stream.to, None); + assert_eq!(stream.id, Some(String::from("abc"))); + assert_eq!(stream.version, Some(String::from("1.0"))); + assert_eq!(stream.xml_lang, Some(String::from("en"))); + } +} diff --git a/xmpp-parsers/src/time.rs b/xmpp-parsers/src/time.rs new file mode 100644 index 0000000..d97c941 --- /dev/null +++ b/xmpp-parsers/src/time.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::date::DateTime; +use crate::iq::{IqGetPayload, IqResultPayload}; +use crate::ns; +use crate::util::error::Error; +use crate::Element; +use chrono::FixedOffset; +use std::convert::TryFrom; +use std::str::FromStr; + +generate_empty_element!( + /// An entity time query. + TimeQuery, + "time", + TIME +); + +impl IqGetPayload for TimeQuery {} + +/// An entity time result, containing an unique DateTime. +#[derive(Debug, Clone)] +pub struct TimeResult(pub DateTime); + +impl IqResultPayload for TimeResult {} + +impl TryFrom for TimeResult { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "time", TIME); + check_no_attributes!(elem, "time"); + + let mut tzo = None; + let mut utc = None; + + for child in elem.children() { + if child.is("tzo", ns::TIME) { + if tzo.is_some() { + return Err(Error::ParseError("More than one tzo element in time.")); + } + check_no_children!(child, "tzo"); + check_no_attributes!(child, "tzo"); + // TODO: Add a FromStr implementation to FixedOffset to avoid this hack. + let fake_date = String::from("2019-04-22T11:38:00") + &child.text(); + let date_time = DateTime::from_str(&fake_date)?; + tzo = Some(date_time.timezone()); + } else if child.is("utc", ns::TIME) { + if utc.is_some() { + return Err(Error::ParseError("More than one utc element in time.")); + } + check_no_children!(child, "utc"); + check_no_attributes!(child, "utc"); + let date_time = DateTime::from_str(&child.text())?; + if date_time.timezone() != FixedOffset::east(0) { + return Err(Error::ParseError("Non-UTC timezone for utc element.")); + } + utc = Some(date_time); + } else { + return Err(Error::ParseError("Unknown child in time element.")); + } + } + + let tzo = tzo.ok_or(Error::ParseError("Missing tzo child in time element."))?; + let utc = utc.ok_or(Error::ParseError("Missing utc child in time element."))?; + let date = utc.with_timezone(tzo); + + Ok(TimeResult(date)) + } +} + +impl From for Element { + fn from(time: TimeResult) -> Element { + Element::builder("time", ns::TIME) + .append(Element::builder("tzo", ns::TIME).append(format!("{}", time.0.timezone()))) + .append( + Element::builder("utc", ns::TIME) + .append(time.0.with_timezone(FixedOffset::east(0)).format("%FT%TZ")), + ) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // DateTime’s size doesn’t depend on the architecture. + #[test] + fn test_size() { + assert_size!(TimeQuery, 0); + assert_size!(TimeResult, 16); + } + + #[test] + fn parse_response() { + let elem: Element = + "" + .parse() + .unwrap(); + let elem1 = elem.clone(); + let time = TimeResult::try_from(elem).unwrap(); + assert_eq!(time.0.timezone(), FixedOffset::west(6 * 3600)); + assert_eq!( + time.0, + DateTime::from_str("2006-12-19T12:58:35-05:00").unwrap() + ); + let elem2 = Element::from(time); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/tune.rs b/xmpp-parsers/src/tune.rs new file mode 100644 index 0000000..e80dad4 --- /dev/null +++ b/xmpp-parsers/src/tune.rs @@ -0,0 +1,246 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::ns; +use crate::pubsub::PubSubPayload; +use crate::util::error::Error; +use crate::Element; +use std::convert::TryFrom; + +generate_elem_id!( + /// The artist or performer of the song or piece. + Artist, + "artist", + TUNE +); + +generate_elem_id!( + /// The duration of the song or piece in seconds. + Length, + "length", + TUNE, + u16 +); + +generate_elem_id!( + /// The user's rating of the song or piece, from 1 (lowest) to 10 (highest). + Rating, + "rating", + TUNE, + u8 +); + +generate_elem_id!( + /// The collection (e.g., album) or other source (e.g., a band website that hosts streams or + /// audio files). + Source, + "source", + TUNE +); + +generate_elem_id!( + /// The title of the song or piece. + Title, + "title", + TUNE +); + +generate_elem_id!( + /// A unique identifier for the tune; e.g., the track number within a collection or the + /// specific URI for the object (e.g., a stream or audio file). + Track, + "track", + TUNE +); + +generate_elem_id!( + /// A URI or URL pointing to information about the song, collection, or artist. + Uri, + "uri", + TUNE +); + +/// Container for formatted text. +#[derive(Debug, Clone)] +pub struct Tune { + /// The artist or performer of the song or piece. + artist: Option, + + /// The duration of the song or piece in seconds. + length: Option, + + /// The user's rating of the song or piece, from 1 (lowest) to 10 (highest). + rating: Option, + + /// The collection (e.g., album) or other source (e.g., a band website that hosts streams or + /// audio files). + source: Option, + + /// The title of the song or piece. + title: Option, + + /// A unique identifier for the tune; e.g., the track number within a collection or the + /// specific URI for the object (e.g., a stream or audio file). + track: Option<Track>, + + /// A URI or URL pointing to information about the song, collection, or artist. + uri: Option<Uri>, +} + +impl PubSubPayload for Tune {} + +impl Tune { + fn new() -> Tune { + Tune { + artist: None, + length: None, + rating: None, + source: None, + title: None, + track: None, + uri: None, + } + } +} + +impl TryFrom<Element> for Tune { + type Error = Error; + + fn try_from(elem: Element) -> Result<Tune, Error> { + check_self!(elem, "tune", TUNE); + check_no_attributes!(elem, "tune"); + + let mut tune = Tune::new(); + for child in elem.children() { + if child.is("artist", ns::TUNE) { + if tune.artist.is_some() { + return Err(Error::ParseError("Tune can’t have more than one artist.")); + } + tune.artist = Some(Artist::try_from(child.clone())?); + } else if child.is("length", ns::TUNE) { + if tune.length.is_some() { + return Err(Error::ParseError("Tune can’t have more than one length.")); + } + tune.length = Some(Length::try_from(child.clone())?); + } else if child.is("rating", ns::TUNE) { + if tune.rating.is_some() { + return Err(Error::ParseError("Tune can’t have more than one rating.")); + } + tune.rating = Some(Rating::try_from(child.clone())?); + } else if child.is("source", ns::TUNE) { + if tune.source.is_some() { + return Err(Error::ParseError("Tune can’t have more than one source.")); + } + tune.source = Some(Source::try_from(child.clone())?); + } else if child.is("title", ns::TUNE) { + if tune.title.is_some() { + return Err(Error::ParseError("Tune can’t have more than one title.")); + } + tune.title = Some(Title::try_from(child.clone())?); + } else if child.is("track", ns::TUNE) { + if tune.track.is_some() { + return Err(Error::ParseError("Tune can’t have more than one track.")); + } + tune.track = Some(Track::try_from(child.clone())?); + } else if child.is("uri", ns::TUNE) { + if tune.uri.is_some() { + return Err(Error::ParseError("Tune can’t have more than one uri.")); + } + tune.uri = Some(Uri::try_from(child.clone())?); + } else { + return Err(Error::ParseError("Unknown element in User Tune.")); + } + } + + Ok(tune) + } +} + +impl From<Tune> for Element { + fn from(tune: Tune) -> Element { + Element::builder("tune", ns::TUNE) + .append_all(tune.artist) + .append_all(tune.length) + .append_all(tune.rating) + .append_all(tune.source) + .append_all(tune.title) + .append_all(tune.track) + .append_all(tune.uri) + .build() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Tune, 68); + assert_size!(Artist, 12); + assert_size!(Length, 2); + assert_size!(Rating, 1); + assert_size!(Source, 12); + assert_size!(Title, 12); + assert_size!(Track, 12); + assert_size!(Uri, 12); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Tune, 128); + assert_size!(Artist, 24); + assert_size!(Length, 2); + assert_size!(Rating, 1); + assert_size!(Source, 24); + assert_size!(Title, 24); + assert_size!(Track, 24); + assert_size!(Uri, 24); + } + + #[test] + fn empty() { + let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'/>" + .parse() + .unwrap(); + let elem2 = elem.clone(); + let tune = Tune::try_from(elem).unwrap(); + assert!(tune.artist.is_none()); + assert!(tune.length.is_none()); + assert!(tune.rating.is_none()); + assert!(tune.source.is_none()); + assert!(tune.title.is_none()); + assert!(tune.track.is_none()); + assert!(tune.uri.is_none()); + + let elem3 = tune.into(); + assert_eq!(elem2, elem3); + } + + #[test] + fn full() { + let elem: Element = "<tune xmlns='http://jabber.org/protocol/tune'><artist>Yes</artist><length>686</length><rating>8</rating><source>Yessongs</source><title>Heart of the Sunrise3http://www.yesworld.com/lyrics/Fragile.html#9" + .parse() + .unwrap(); + let tune = Tune::try_from(elem).unwrap(); + assert_eq!(tune.artist, Some(Artist::from_str("Yes").unwrap())); + assert_eq!(tune.length, Some(Length(686))); + assert_eq!(tune.rating, Some(Rating(8))); + assert_eq!(tune.source, Some(Source::from_str("Yessongs").unwrap())); + assert_eq!( + tune.title, + Some(Title::from_str("Heart of the Sunrise").unwrap()) + ); + assert_eq!(tune.track, Some(Track::from_str("3").unwrap())); + assert_eq!( + tune.uri, + Some(Uri::from_str("http://www.yesworld.com/lyrics/Fragile.html#9").unwrap()) + ); + } +} diff --git a/xmpp-parsers/src/util/error.rs b/xmpp-parsers/src/util/error.rs new file mode 100644 index 0000000..33b2a69 --- /dev/null +++ b/xmpp-parsers/src/util/error.rs @@ -0,0 +1,105 @@ +// Copyright (c) 2017-2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use std::error::Error as StdError; +use std::fmt; + +/// Contains one of the potential errors triggered while parsing an +/// [Element](../struct.Element.html) into a specialised struct. +#[derive(Debug)] +pub enum Error { + /// The usual error when parsing something. + /// + /// TODO: use a structured error so the user can report it better, instead + /// of a freeform string. + ParseError(&'static str), + + /// Generated when some base64 content fails to decode, usually due to + /// extra characters. + Base64Error(base64::DecodeError), + + /// Generated when text which should be an integer fails to parse. + ParseIntError(std::num::ParseIntError), + + /// Generated when text which should be a string fails to parse. + ParseStringError(std::string::ParseError), + + /// Generated when text which should be an IP address (IPv4 or IPv6) fails + /// to parse. + ParseAddrError(std::net::AddrParseError), + + /// Generated when text which should be a [JID](../../jid/struct.Jid.html) + /// fails to parse. + JidParseError(jid::JidParseError), + + /// Generated when text which should be a + /// [DateTime](../date/struct.DateTime.html) fails to parse. + ChronoParseError(chrono::ParseError), +} + +impl StdError for Error { + fn cause(&self) -> Option<&dyn StdError> { + match self { + Error::ParseError(_) => None, + Error::Base64Error(e) => Some(e), + Error::ParseIntError(e) => Some(e), + Error::ParseStringError(e) => Some(e), + Error::ParseAddrError(e) => Some(e), + Error::JidParseError(e) => Some(e), + Error::ChronoParseError(e) => Some(e), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::ParseError(s) => write!(fmt, "parse error: {}", s), + Error::Base64Error(e) => write!(fmt, "base64 error: {}", e), + Error::ParseIntError(e) => write!(fmt, "integer parsing error: {}", e), + Error::ParseStringError(e) => write!(fmt, "string parsing error: {}", e), + Error::ParseAddrError(e) => write!(fmt, "IP address parsing error: {}", e), + Error::JidParseError(e) => write!(fmt, "JID parsing error: {}", e), + Error::ChronoParseError(e) => write!(fmt, "time parsing error: {}", e), + } + } +} + +impl From for Error { + fn from(err: base64::DecodeError) -> Error { + Error::Base64Error(err) + } +} + +impl From for Error { + fn from(err: std::num::ParseIntError) -> Error { + Error::ParseIntError(err) + } +} + +impl From for Error { + fn from(err: std::string::ParseError) -> Error { + Error::ParseStringError(err) + } +} + +impl From for Error { + fn from(err: std::net::AddrParseError) -> Error { + Error::ParseAddrError(err) + } +} + +impl From for Error { + fn from(err: jid::JidParseError) -> Error { + Error::JidParseError(err) + } +} + +impl From for Error { + fn from(err: chrono::ParseError) -> Error { + Error::ChronoParseError(err) + } +} diff --git a/xmpp-parsers/src/util/helpers.rs b/xmpp-parsers/src/util/helpers.rs new file mode 100644 index 0000000..570e7cf --- /dev/null +++ b/xmpp-parsers/src/util/helpers.rs @@ -0,0 +1,122 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::util::error::Error; +use jid::Jid; +use std::str::FromStr; + +/// Codec for text content. +pub struct Text; + +impl Text { + pub fn decode(s: &str) -> Result { + Ok(s.to_owned()) + } + + pub fn encode(string: &str) -> Option { + Some(string.to_owned()) + } +} + +/// Codec for plain text content. +pub struct PlainText; + +impl PlainText { + pub fn decode(s: &str) -> Result, Error> { + Ok(match s { + "" => None, + text => Some(text.to_owned()), + }) + } + + pub fn encode(string: &Option) -> Option { + string.as_ref().map(ToOwned::to_owned) + } +} + +/// Codec for trimmed plain text content. +pub struct TrimmedPlainText; + +impl TrimmedPlainText { + pub fn decode(s: &str) -> Result { + Ok(match s.trim() { + "" => return Err(Error::ParseError("URI missing in uri.")), + text => text.to_owned(), + }) + } + + pub fn encode(string: &str) -> Option { + Some(string.to_owned()) + } +} + +/// Codec wrapping base64 encode/decode. +pub struct Base64; + +impl Base64 { + pub fn decode(s: &str) -> Result, Error> { + Ok(base64::decode(s)?) + } + + pub fn encode(b: &[u8]) -> Option { + Some(base64::encode(b)) + } +} + +/// Codec wrapping base64 encode/decode, while ignoring whitespace characters. +pub struct WhitespaceAwareBase64; + +impl WhitespaceAwareBase64 { + pub fn decode(s: &str) -> Result, Error> { + let s: String = s + .chars() + .filter(|ch| *ch != ' ' && *ch != '\n' && *ch != '\t') + .collect(); + Ok(base64::decode(&s)?) + } + + pub fn encode(b: &[u8]) -> Option { + Some(base64::encode(b)) + } +} + +/// Codec for colon-separated bytes of uppercase hexadecimal. +pub struct ColonSeparatedHex; + +impl ColonSeparatedHex { + pub fn decode(s: &str) -> Result, Error> { + let mut bytes = vec![]; + for i in 0..(1 + s.len()) / 3 { + let byte = u8::from_str_radix(&s[3 * i..3 * i + 2], 16)?; + if 3 * i + 2 < s.len() { + assert_eq!(&s[3 * i + 2..3 * i + 3], ":"); + } + bytes.push(byte); + } + Ok(bytes) + } + + pub fn encode(b: &[u8]) -> Option { + let mut bytes = vec![]; + for byte in b { + bytes.push(format!("{:02X}", byte)); + } + Some(bytes.join(":")) + } +} + +/// Codec for a JID. +pub struct JidCodec; + +impl JidCodec { + pub fn decode(s: &str) -> Result { + Ok(Jid::from_str(s)?) + } + + pub fn encode(jid: &Jid) -> Option { + Some(jid.to_string()) + } +} diff --git a/xmpp-parsers/src/util/macros.rs b/xmpp-parsers/src/util/macros.rs new file mode 100644 index 0000000..702a16f --- /dev/null +++ b/xmpp-parsers/src/util/macros.rs @@ -0,0 +1,767 @@ +// Copyright (c) 2017-2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +macro_rules! get_attr { + ($elem:ident, $attr:tt, $type:tt) => { + get_attr!($elem, $attr, $type, value, value.parse()?) + }; + ($elem:ident, $attr:tt, OptionEmpty, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some("") => None, + Some($value) => Some($func), + None => None, + } + }; + ($elem:ident, $attr:tt, Option, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some($value) => Some($func), + None => None, + } + }; + ($elem:ident, $attr:tt, Required, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some($value) => $func, + None => { + return Err(crate::util::error::Error::ParseError(concat!( + "Required attribute '", + $attr, + "' missing." + ))); + } + } + }; + ($elem:ident, $attr:tt, RequiredNonEmpty, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some("") => { + return Err(crate::util::error::Error::ParseError(concat!( + "Required attribute '", + $attr, + "' must not be empty." + ))); + } + Some($value) => $func, + None => { + return Err(crate::util::error::Error::ParseError(concat!( + "Required attribute '", + $attr, + "' missing." + ))); + } + } + }; + ($elem:ident, $attr:tt, Default, $value:ident, $func:expr) => { + match $elem.attr($attr) { + Some($value) => $func, + None => ::std::default::Default::default(), + } + }; +} + +macro_rules! generate_attribute { + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}) => ( + generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+,}, Default = $default:ident) => ( + generate_attribute!($(#[$meta])* $elem, $name, {$($(#[$a_meta])* $a => $b),+}, Default = $default); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$a_meta])* + $a + ),+ + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + Ok(match s { + $($b => $elem::$a),+, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl std::fmt::Display for $elem { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + write!(fmt, "{}", match self { + $($elem::$a => $b),+ + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + Some(String::from(match self { + $($elem::$a => $b),+ + })) + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, {$($(#[$a_meta:meta])* $a:ident => $b:tt),+}, Default = $default:ident) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$a_meta])* + $a + ),+ + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + Ok(match s { + $($b => $elem::$a),+, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + #[allow(unreachable_patterns)] + fn into_attribute_value(self) -> Option { + Some(String::from(match self { + $elem::$default => return None, + $($elem::$a => $b),+ + })) + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::$default + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, ($(#[$meta_symbol:meta])* $symbol:ident => $value:tt)) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $(#[$meta_symbol])* + $symbol, + /// Value when absent. + None, + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result { + Ok(match s { + $value => $elem::$symbol, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem::$symbol => Some(String::from($value)), + $elem::None => None + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::None + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, bool) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + /// True value, represented by either 'true' or '1'. + True, + /// False value, represented by either 'false' or '0'. + False, + } + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result { + Ok(match s { + "true" | "1" => $elem::True, + "false" | "0" => $elem::False, + _ => return Err(crate::util::error::Error::ParseError(concat!("Unknown value for '", $name, "' attribute."))), + }) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem::True => Some(String::from("true")), + $elem::False => None + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem::False + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $type:tt, Default = $default:expr) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub struct $elem(pub $type); + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result { + Ok($elem($type::from_str(s)?)) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + match self { + $elem($default) => None, + $elem(value) => Some(format!("{}", value)), + } + } + } + impl ::std::default::Default for $elem { + fn default() -> $elem { + $elem($default) + } + } + ); +} + +macro_rules! generate_element_enum { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => ( + generate_element_enum!($(#[$meta])* $elem, $name, $ns, {$($(#[$enum_meta])* $enum => $enum_name),+}); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$enum_meta])* + $enum + ),+ + } + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_ns_only!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_attributes!(elem, $name); + Ok(match elem.name() { + $($enum_name => $elem::$enum,)+ + _ => return Err(crate::util::error::Error::ParseError(concat!("This is not a ", $name, " element."))), + }) + } + } + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + crate::Element::builder( + match elem { + $($elem::$enum => $enum_name,)+ + }, + crate::ns::$ns, + ) + .build() + } + } + ); +} + +macro_rules! generate_attribute_enum { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+,}) => ( + generate_attribute_enum!($(#[$meta])* $elem, $name, $ns, $attr, {$($(#[$enum_meta])* $enum => $enum_name),+}); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $attr:tt, {$($(#[$enum_meta:meta])* $enum:ident => $enum_name:tt),+}) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub enum $elem { + $( + $(#[$enum_meta])* + $enum + ),+ + } + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_ns_only!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_unknown_attributes!(elem, $name, [$attr]); + Ok(match get_attr!(elem, $attr, Required) { + $($enum_name => $elem::$enum,)+ + _ => return Err(crate::util::error::Error::ParseError(concat!("Invalid ", $name, " ", $attr, " value."))), + }) + } + } + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + crate::Element::builder($name, crate::ns::$ns) + .attr($attr, match elem { + $($elem::$enum => $enum_name,)+ + }) + .build() + } + } + ); +} + +macro_rules! check_self { + ($elem:ident, $name:tt, $ns:ident) => { + check_self!($elem, $name, $ns, $name); + }; + ($elem:ident, $name:tt, $ns:ident, $pretty_name:tt) => { + if !$elem.is($name, crate::ns::$ns) { + return Err(crate::util::error::Error::ParseError(concat!( + "This is not a ", + $pretty_name, + " element." + ))); + } + }; +} + +macro_rules! check_ns_only { + ($elem:ident, $name:tt, $ns:ident) => { + if !$elem.has_ns(crate::ns::$ns) { + return Err(crate::util::error::Error::ParseError(concat!( + "This is not a ", + $name, + " element." + ))); + } + }; +} + +macro_rules! check_no_children { + ($elem:ident, $name:tt) => { + #[cfg(not(feature = "disable-validation"))] + for _ in $elem.children() { + return Err(crate::util::error::Error::ParseError(concat!( + "Unknown child in ", + $name, + " element." + ))); + } + }; +} + +macro_rules! check_no_attributes { + ($elem:ident, $name:tt) => { + #[cfg(not(feature = "disable-validation"))] + for _ in $elem.attrs() { + return Err(crate::util::error::Error::ParseError(concat!( + "Unknown attribute in ", + $name, + " element." + ))); + } + }; +} + +macro_rules! check_no_unknown_attributes { + ($elem:ident, $name:tt, [$($attr:tt),*]) => ( + #[cfg(not(feature = "disable-validation"))] + for (_attr, _) in $elem.attrs() { + $( + if _attr == $attr { + continue; + } + )* + return Err(crate::util::error::Error::ParseError(concat!("Unknown attribute in ", $name, " element."))); + } + ); +} + +macro_rules! generate_empty_element { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub struct $elem; + + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_self!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_attributes!(elem, $name); + Ok($elem) + } + } + + impl From<$elem> for crate::Element { + fn from(_: $elem) -> crate::Element { + crate::Element::builder($name, crate::ns::$ns) + .build() + } + } + ); +} + +macro_rules! generate_id { + ($(#[$meta:meta])* $elem:ident) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct $elem(pub String); + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + // TODO: add a way to parse that differently when needed. + Ok($elem(String::from(s))) + } + } + impl ::minidom::IntoAttributeValue for $elem { + fn into_attribute_value(self) -> Option { + Some(self.0) + } + } + ); +} + +macro_rules! generate_elem_id { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident) => ( + generate_elem_id!($(#[$meta])* $elem, $name, $ns, String); + impl ::std::str::FromStr for $elem { + type Err = crate::util::error::Error; + fn from_str(s: &str) -> Result<$elem, crate::util::error::Error> { + // TODO: add a way to parse that differently when needed. + Ok($elem(String::from(s))) + } + } + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, $type:ty) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct $elem(pub $type); + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_self!(elem, $name, $ns); + check_no_children!(elem, $name); + check_no_attributes!(elem, $name); + // TODO: add a way to parse that differently when needed. + Ok($elem(elem.text().parse()?)) + } + } + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + crate::Element::builder($name, crate::ns::$ns) + .append(elem.0.to_string()) + .build() + } + } + ); +} + +macro_rules! decl_attr { + (OptionEmpty, $type:ty) => ( + Option<$type> + ); + (Option, $type:ty) => ( + Option<$type> + ); + (Required, $type:ty) => ( + $type + ); + (RequiredNonEmpty, $type:ty) => ( + $type + ); + (Default, $type:ty) => ( + $type + ); +} + +macro_rules! start_decl { + (Vec, $type:ty) => ( + Vec<$type> + ); + (Option, $type:ty) => ( + Option<$type> + ); + (Required, $type:ty) => ( + $type + ); + (Present, $type:ty) => ( + bool + ); +} + +macro_rules! start_parse_elem { + ($temp:ident: Vec) => { + let mut $temp = Vec::new(); + }; + ($temp:ident: Option) => { + let mut $temp = None; + }; + ($temp:ident: Required) => { + let mut $temp = None; + }; + ($temp:ident: Present) => { + let mut $temp = false; + }; +} + +macro_rules! do_parse { + ($elem:ident, Element) => { + $elem.clone() + }; + ($elem:ident, String) => { + $elem.text() + }; + ($elem:ident, $constructor:ident) => { + $constructor::try_from($elem.clone())? + }; +} + +macro_rules! do_parse_elem { + ($temp:ident: Vec = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + $temp.push(do_parse!($elem, $constructor)); + }; + ($temp:ident: Option = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + if $temp.is_some() { + return Err(crate::util::error::Error::ParseError(concat!( + "Element ", + $parent_name, + " must not have more than one ", + $name, + " child." + ))); + } + $temp = Some(do_parse!($elem, $constructor)); + }; + ($temp:ident: Required = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + if $temp.is_some() { + return Err(crate::util::error::Error::ParseError(concat!( + "Element ", + $parent_name, + " must not have more than one ", + $name, + " child." + ))); + } + $temp = Some(do_parse!($elem, $constructor)); + }; + ($temp:ident: Present = $constructor:ident => $elem:ident, $name:tt, $parent_name:tt) => { + if $temp { + return Err(crate::util::error::Error::ParseError(concat!( + "Element ", + $parent_name, + " must not have more than one ", + $name, + " child." + ))); + } + $temp = true; + }; +} + +macro_rules! finish_parse_elem { + ($temp:ident: Vec = $name:tt, $parent_name:tt) => { + $temp + }; + ($temp:ident: Option = $name:tt, $parent_name:tt) => { + $temp + }; + ($temp:ident: Required = $name:tt, $parent_name:tt) => { + $temp.ok_or(crate::util::error::Error::ParseError(concat!( + "Missing child ", + $name, + " in ", + $parent_name, + " element." + )))? + }; + ($temp:ident: Present = $name:tt, $parent_name:tt) => { + $temp + }; +} + +macro_rules! generate_serialiser { + ($builder:ident, $parent:ident, $elem:ident, Required, String, ($name:tt, $ns:ident)) => { + $builder.append( + crate::Element::builder($name, crate::ns::$ns) + .append(::minidom::Node::Text($parent.$elem)), + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Option, String, ($name:tt, $ns:ident)) => { + $builder.append_all($parent.$elem.map(|elem| { + crate::Element::builder($name, crate::ns::$ns).append(::minidom::Node::Text(elem)) + })) + }; + ($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, *)) => { + $builder.append_all( + $parent + .$elem + .map(|elem| ::minidom::Node::Element(crate::Element::from(elem))), + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Option, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append_all( + $parent + .$elem + .map(|elem| ::minidom::Node::Element(crate::Element::from(elem))), + ) + }; + ($builder:ident, $parent:ident, $elem:ident, Vec, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append_all($parent.$elem.into_iter()) + }; + ($builder:ident, $parent:ident, $elem:ident, Present, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append(::minidom::Node::Element( + crate::Element::builder($name, crate::ns::$ns).build(), + )) + }; + ($builder:ident, $parent:ident, $elem:ident, $_:ident, $constructor:ident, ($name:tt, $ns:ident)) => { + $builder.append(::minidom::Node::Element(crate::Element::from( + $parent.$elem, + ))) + }; +} + +macro_rules! generate_child_test { + ($child:ident, $name:tt, *) => { + $child.is($name, ::minidom::NSChoice::Any) + }; + ($child:ident, $name:tt, $ns:tt) => { + $child.is($name, crate::ns::$ns) + }; +} + +macro_rules! generate_element { + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+,]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: []); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*,], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*]) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [$($(#[$child_meta])* $child_ident: $coucou<$child_type> = ($child_name, $child_ns) => $child_constructor),*]); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),+], text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >)) => ( + generate_element!($(#[$meta])* $elem, $name, $ns, attributes: [$($(#[$attr_meta])* $attr: $attr_action<$attr_type> = $attr_name),*], children: [], text: ($(#[$text_meta])* $text_ident: $codec<$text_type>)); + ); + ($(#[$meta:meta])* $elem:ident, $name:tt, $ns:ident, attributes: [$($(#[$attr_meta:meta])* $attr:ident: $attr_action:tt<$attr_type:ty> = $attr_name:tt),*], children: [$($(#[$child_meta:meta])* $child_ident:ident: $coucou:tt<$child_type:ty> = ($child_name:tt, $child_ns:tt) => $child_constructor:ident),*] $(, text: ($(#[$text_meta:meta])* $text_ident:ident: $codec:ident < $text_type:ty >))*) => ( + $(#[$meta])* + #[derive(Debug, Clone, PartialEq)] + pub struct $elem { + $( + $(#[$attr_meta])* + pub $attr: decl_attr!($attr_action, $attr_type), + )* + $( + $(#[$child_meta])* + pub $child_ident: start_decl!($coucou, $child_type), + )* + $( + $(#[$text_meta])* + pub $text_ident: $text_type, + )* + } + + impl ::std::convert::TryFrom for $elem { + type Error = crate::util::error::Error; + + fn try_from(elem: crate::Element) -> Result<$elem, crate::util::error::Error> { + check_self!(elem, $name, $ns); + check_no_unknown_attributes!(elem, $name, [$($attr_name),*]); + $( + start_parse_elem!($child_ident: $coucou); + )* + for _child in elem.children() { + $( + if generate_child_test!(_child, $child_name, $child_ns) { + do_parse_elem!($child_ident: $coucou = $child_constructor => _child, $child_name, $name); + continue; + } + )* + return Err(crate::util::error::Error::ParseError(concat!("Unknown child in ", $name, " element."))); + } + Ok($elem { + $( + $attr: get_attr!(elem, $attr_name, $attr_action), + )* + $( + $child_ident: finish_parse_elem!($child_ident: $coucou = $child_name, $name), + )* + $( + $text_ident: $codec::decode(&elem.text())?, + )* + }) + } + } + + impl From<$elem> for crate::Element { + fn from(elem: $elem) -> crate::Element { + let mut builder = crate::Element::builder($name, crate::ns::$ns); + $( + builder = builder.attr($attr_name, elem.$attr); + )* + $( + builder = generate_serialiser!(builder, elem, $child_ident, $coucou, $child_constructor, ($child_name, $child_ns)); + )* + $( + builder = builder.append_all($codec::encode(&elem.$text_ident).map(::minidom::Node::Text).into_iter()); + )* + + builder.build() + } + } + ); +} + +#[cfg(test)] +macro_rules! assert_size ( + ($t:ty, $sz:expr) => ( + assert_eq!(::std::mem::size_of::<$t>(), $sz); + ); +); + +// TODO: move that to src/pubsub/mod.rs, once we figure out how to use macros from there. +macro_rules! impl_pubsub_item { + ($item:ident, $ns:ident) => { + impl ::std::convert::TryFrom for $item { + type Error = Error; + + fn try_from(elem: crate::Element) -> Result<$item, Error> { + check_self!(elem, "item", $ns); + check_no_unknown_attributes!(elem, "item", ["id", "publisher"]); + let mut payloads = elem.children().cloned().collect::>(); + let payload = payloads.pop(); + if !payloads.is_empty() { + return Err(Error::ParseError( + "More than a single payload in item element.", + )); + } + Ok($item(crate::pubsub::Item { + id: get_attr!(elem, "id", Option), + publisher: get_attr!(elem, "publisher", Option), + payload, + })) + } + } + + impl From<$item> for crate::Element { + fn from(item: $item) -> crate::Element { + crate::Element::builder("item", ns::$ns) + .attr("id", item.0.id) + .attr("publisher", item.0.publisher) + .append_all(item.0.payload) + .build() + } + } + + impl ::std::ops::Deref for $item { + type Target = crate::pubsub::Item; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl ::std::ops::DerefMut for $item { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + }; +} diff --git a/xmpp-parsers/src/util/mod.rs b/xmpp-parsers/src/util/mod.rs new file mode 100644 index 0000000..1d3ee64 --- /dev/null +++ b/xmpp-parsers/src/util/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/// Error type returned by every parser on failure. +pub mod error; + +/// Various helpers. +pub(crate) mod helpers; + +/// Helper macros to parse and serialise more easily. +#[macro_use] +mod macros; diff --git a/xmpp-parsers/src/version.rs b/xmpp-parsers/src/version.rs new file mode 100644 index 0000000..2bd894b --- /dev/null +++ b/xmpp-parsers/src/version.rs @@ -0,0 +1,88 @@ +// Copyright (c) 2017 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::iq::{IqGetPayload, IqResultPayload}; + +generate_empty_element!( + /// Represents a query for the software version a remote entity is using. + /// + /// It should only be used in an ``, as it can only + /// represent the request, and not a result. + VersionQuery, + "query", + VERSION +); + +impl IqGetPayload for VersionQuery {} + +generate_element!( + /// Represents the answer about the software version we are using. + /// + /// It should only be used in an ``, as it can only + /// represent the result, and not a request. + VersionResult, "query", VERSION, + children: [ + /// The name of this client. + name: Required = ("name", VERSION) => String, + + /// The version of this client. + version: Required = ("version", VERSION) => String, + + /// The OS this client is running on. + os: Option = ("os", VERSION) => String + ] +); + +impl IqResultPayload for VersionResult {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(VersionQuery, 0); + assert_size!(VersionResult, 36); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(VersionQuery, 0); + assert_size!(VersionResult, 72); + } + + #[test] + fn simple() { + let elem: Element = + "xmpp-rs0.3.0" + .parse() + .unwrap(); + let version = VersionResult::try_from(elem).unwrap(); + assert_eq!(version.name, String::from("xmpp-rs")); + assert_eq!(version.version, String::from("0.3.0")); + assert_eq!(version.os, None); + } + + #[test] + fn serialisation() { + let version = VersionResult { + name: String::from("xmpp-rs"), + version: String::from("0.3.0"), + os: None, + }; + let elem1 = Element::from(version); + let elem2: Element = + "xmpp-rs0.3.0" + .parse() + .unwrap(); + println!("{:?}", elem1); + assert_eq!(elem1, elem2); + } +} diff --git a/xmpp-parsers/src/websocket.rs b/xmpp-parsers/src/websocket.rs new file mode 100644 index 0000000..fbc1fb2 --- /dev/null +++ b/xmpp-parsers/src/websocket.rs @@ -0,0 +1,102 @@ +// Copyright (c) 2018 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use jid::BareJid; + +generate_element!( + /// The stream opening for WebSocket. + Open, "open", WEBSOCKET, + attributes: [ + /// The JID of the entity opening this stream. + from: Option = "from", + + /// The JID of the entity receiving this stream opening. + to: Option = "to", + + /// The id of the stream, used for authentication challenges. + id: Option = "id", + + /// The XMPP version used during this stream. + version: Option = "version", + + /// The default human language for all subsequent stanzas, which will + /// be transmitted to other entities for better localisation. + xml_lang: Option = "xml:lang", + ] +); + +impl Open { + /// Creates a simple client→server `` element. + pub fn new(to: BareJid) -> Open { + Open { + from: None, + to: Some(to), + id: None, + version: Some(String::from("1.0")), + xml_lang: None, + } + } + + /// Sets the [@from](#structfield.from) attribute on this `` + /// element. + pub fn with_from(mut self, from: BareJid) -> Open { + self.from = Some(from); + self + } + + /// Sets the [@id](#structfield.id) attribute on this `` element. + pub fn with_id(mut self, id: String) -> Open { + self.id = Some(id); + self + } + + /// Sets the [@xml:lang](#structfield.xml_lang) attribute on this `` + /// element. + pub fn with_lang(mut self, xml_lang: String) -> Open { + self.xml_lang = Some(xml_lang); + self + } + + /// Checks whether the version matches the expected one. + pub fn is_version(&self, version: &str) -> bool { + match self.version { + None => false, + Some(ref self_version) => self_version == &String::from(version), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Element; + use std::convert::TryFrom; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(Open, 84); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(Open, 168); + } + + #[test] + fn test_simple() { + let elem: Element = "" + .parse() + .unwrap(); + let open = Open::try_from(elem).unwrap(); + assert_eq!(open.from, None); + assert_eq!(open.to, None); + assert_eq!(open.id, None); + assert_eq!(open.version, None); + assert_eq!(open.xml_lang, None); + } +} diff --git a/xmpp-parsers/src/xhtml.rs b/xmpp-parsers/src/xhtml.rs new file mode 100644 index 0000000..fd0312f --- /dev/null +++ b/xmpp-parsers/src/xhtml.rs @@ -0,0 +1,642 @@ +// Copyright (c) 2019 Emmanuel Gil Peyrot +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +use crate::message::MessagePayload; +use crate::ns; +use crate::util::error::Error; +use minidom::{Element, Node}; +use std::collections::HashMap; +use std::convert::TryFrom; + +// TODO: Use a proper lang type. +type Lang = String; + +/// Container for formatted text. +#[derive(Debug, Clone)] +pub struct XhtmlIm { + /// Map of language to body element. + bodies: HashMap, +} + +impl XhtmlIm { + /// Serialise formatted text to HTML. + pub fn to_html(self) -> String { + let mut html = Vec::new(); + // TODO: use the best language instead. + for (lang, body) in self.bodies { + if lang.is_empty() { + assert!(body.xml_lang.is_none()); + } else { + assert_eq!(Some(lang), body.xml_lang); + } + for tag in body.children { + html.push(tag.to_html()); + } + break; + } + html.concat() + } + + /// Removes all unknown elements. + fn flatten(self) -> XhtmlIm { + let mut bodies = HashMap::new(); + for (lang, body) in self.bodies { + let children = body.children.into_iter().fold(vec![], |mut acc, child| { + match child { + Child::Tag(Tag::Unknown(children)) => acc.extend(children), + any => acc.push(any), + } + acc + }); + let body = Body { children, ..body }; + bodies.insert(lang, body); + } + XhtmlIm { bodies } + } +} + +impl MessagePayload for XhtmlIm {} + +impl TryFrom for XhtmlIm { + type Error = Error; + + fn try_from(elem: Element) -> Result { + check_self!(elem, "html", XHTML_IM); + check_no_attributes!(elem, "html"); + + let mut bodies = HashMap::new(); + for child in elem.children() { + if child.is("body", ns::XHTML) { + let child = child.clone(); + let lang = match child.attr("xml:lang") { + Some(lang) => lang, + None => "", + } + .to_string(); + let body = Body::try_from(child)?; + match bodies.insert(lang, body) { + None => (), + Some(_) => { + return Err(Error::ParseError( + "Two identical language bodies found in XHTML-IM.", + )) + } + } + } else { + return Err(Error::ParseError("Unknown element in XHTML-IM.")); + } + } + + Ok(XhtmlIm { bodies }.flatten()) + } +} + +impl From for Element { + fn from(wrapper: XhtmlIm) -> Element { + Element::builder("html", ns::XHTML_IM) + .append_all(wrapper.bodies.into_iter().map(|(lang, body)| { + if lang.is_empty() { + assert!(body.xml_lang.is_none()); + } else { + assert_eq!(Some(lang), body.xml_lang); + } + Element::from(body) + })) + .build() + } +} + +#[derive(Debug, Clone)] +enum Child { + Tag(Tag), + Text(String), +} + +impl Child { + fn to_html(self) -> String { + match self { + Child::Tag(tag) => tag.to_html(), + Child::Text(text) => text, + } + } +} + +#[derive(Debug, Clone)] +struct Property { + key: String, + value: String, +} + +type Css = Vec; + +fn get_style_string(style: Css) -> Option { + let mut result = vec![]; + for Property { key, value } in style { + result.push(format!("{}: {}", key, value)); + } + if result.is_empty() { + return None; + } + Some(result.join("; ")) +} + +#[derive(Debug, Clone)] +struct Body { + style: Css, + xml_lang: Option, + children: Vec, +} + +impl TryFrom for Body { + type Error = Error; + + fn try_from(elem: Element) -> Result { + let mut children = vec![]; + for child in elem.nodes() { + match child { + Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)), + Node::Text(text) => children.push(Child::Text(text.clone())), + } + } + + Ok(Body { + style: parse_css(elem.attr("style")), + xml_lang: elem.attr("xml:lang").map(|xml_lang| xml_lang.to_string()), + children, + }) + } +} + +impl From for Element { + fn from(body: Body) -> Element { + Element::builder("body", ns::XHTML) + .attr("style", get_style_string(body.style)) + .attr("xml:lang", body.xml_lang) + .append_all(children_to_nodes(body.children)) + .build() + } +} + +#[derive(Debug, Clone)] +enum Tag { + A { + href: Option, + style: Css, + type_: Option, + children: Vec, + }, + Blockquote { + style: Css, + children: Vec, + }, + Br, + Cite { + style: Css, + children: Vec, + }, + Em { + children: Vec, + }, + Img { + src: Option, + alt: Option, + }, // TODO: height, width, style + Li { + style: Css, + children: Vec, + }, + Ol { + style: Css, + children: Vec, + }, + P { + style: Css, + children: Vec, + }, + Span { + style: Css, + children: Vec, + }, + Strong { + children: Vec, + }, + Ul { + style: Css, + children: Vec, + }, + Unknown(Vec), +} + +impl Tag { + fn to_html(self) -> String { + match self { + Tag::A { + href, + style, + type_, + children, + } => { + let href = write_attr(href, "href"); + let style = write_attr(get_style_string(style), "style"); + let type_ = write_attr(type_, "type"); + format!( + "{}", + href, + style, + type_, + children_to_html(children) + ) + } + Tag::Blockquote { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!( + "{}", + style, + children_to_html(children) + ) + } + Tag::Br => String::from("
"), + Tag::Cite { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Em { children } => format!("{}", children_to_html(children)), + Tag::Img { src, alt } => { + let src = write_attr(src, "src"); + let alt = write_attr(alt, "alt"); + format!("", src, alt) + } + Tag::Li { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Ol { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::P { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}

", style, children_to_html(children)) + } + Tag::Span { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Strong { children } => format!("{}", children_to_html(children)), + Tag::Ul { style, children } => { + let style = write_attr(get_style_string(style), "style"); + format!("{}", style, children_to_html(children)) + } + Tag::Unknown(_) => { + panic!("No unknown element should be present in XHTML-IM after parsing.") + } + } + } +} + +impl TryFrom for Tag { + type Error = Error; + + fn try_from(elem: Element) -> Result { + let mut children = vec![]; + for child in elem.nodes() { + match child { + Node::Element(child) => children.push(Child::Tag(Tag::try_from(child.clone())?)), + Node::Text(text) => children.push(Child::Text(text.clone())), + } + } + + Ok(match elem.name() { + "a" => Tag::A { + href: elem.attr("href").map(|href| href.to_string()), + style: parse_css(elem.attr("style")), + type_: elem.attr("type").map(|type_| type_.to_string()), + children, + }, + "blockquote" => Tag::Blockquote { + style: parse_css(elem.attr("style")), + children, + }, + "br" => Tag::Br, + "cite" => Tag::Cite { + style: parse_css(elem.attr("style")), + children, + }, + "em" => Tag::Em { children }, + "img" => Tag::Img { + src: elem.attr("src").map(|src| src.to_string()), + alt: elem.attr("alt").map(|alt| alt.to_string()), + }, + "li" => Tag::Li { + style: parse_css(elem.attr("style")), + children, + }, + "ol" => Tag::Ol { + style: parse_css(elem.attr("style")), + children, + }, + "p" => Tag::P { + style: parse_css(elem.attr("style")), + children, + }, + "span" => Tag::Span { + style: parse_css(elem.attr("style")), + children, + }, + "strong" => Tag::Strong { children }, + "ul" => Tag::Ul { + style: parse_css(elem.attr("style")), + children, + }, + _ => Tag::Unknown(children), + }) + } +} + +impl From for Element { + fn from(tag: Tag) -> Element { + let (name, attrs, children) = match tag { + Tag::A { + href, + style, + type_, + children, + } => ( + "a", + { + let mut attrs = vec![]; + if let Some(href) = href { + attrs.push(("href", href)); + } + if let Some(style) = get_style_string(style) { + attrs.push(("style", style)); + } + if let Some(type_) = type_ { + attrs.push(("type", type_)); + } + attrs + }, + children, + ), + Tag::Blockquote { style, children } => ( + "blockquote", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::Br => ("br", vec![], vec![]), + Tag::Cite { style, children } => ( + "cite", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::Em { children } => ("em", vec![], children), + Tag::Img { src, alt } => { + let mut attrs = vec![]; + if let Some(src) = src { + attrs.push(("src", src)); + } + if let Some(alt) = alt { + attrs.push(("alt", alt)); + } + ("img", attrs, vec![]) + } + Tag::Li { style, children } => ( + "li", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::Ol { style, children } => ( + "ol", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::P { style, children } => ( + "p", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::Span { style, children } => ( + "span", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::Strong { children } => ("strong", vec![], children), + Tag::Ul { style, children } => ( + "ul", + match get_style_string(style) { + Some(style) => vec![("style", style)], + None => vec![], + }, + children, + ), + Tag::Unknown(_) => { + panic!("No unknown element should be present in XHTML-IM after parsing.") + } + }; + let mut builder = Element::builder(name, ns::XHTML).append_all(children_to_nodes(children)); + for (key, value) in attrs { + builder = builder.attr(key, value); + } + builder.build() + } +} + +fn children_to_nodes(children: Vec) -> impl IntoIterator { + children.into_iter().map(|child| match child { + Child::Tag(tag) => Node::Element(Element::from(tag)), + Child::Text(text) => Node::Text(text), + }) +} + +fn children_to_html(children: Vec) -> String { + children + .into_iter() + .map(|child| child.to_html()) + .collect::>() + .concat() +} + +fn write_attr(attr: Option, name: &str) -> String { + match attr { + Some(attr) => format!(" {}='{}'", name, attr), + None => String::new(), + } +} + +fn parse_css(style: Option<&str>) -> Css { + let mut properties = vec![]; + if let Some(style) = style { + // TODO: make that parser a bit more resilient to things. + for part in style.split(";") { + let mut part = part + .splitn(2, ":") + .map(|a| a.to_string()) + .collect::>(); + let key = part.pop().unwrap(); + let value = part.pop().unwrap(); + properties.push(Property { key, value }); + } + } + properties +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(target_pointer_width = "32")] + #[test] + fn test_size() { + assert_size!(XhtmlIm, 32); + assert_size!(Child, 56); + assert_size!(Tag, 52); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn test_size() { + assert_size!(XhtmlIm, 48); + assert_size!(Child, 112); + assert_size!(Tag, 104); + } + + #[test] + fn test_empty() { + let elem: Element = "" + .parse() + .unwrap(); + let xhtml = XhtmlIm::try_from(elem).unwrap(); + assert_eq!(xhtml.bodies.len(), 0); + + let elem: Element = "" + .parse() + .unwrap(); + let xhtml = XhtmlIm::try_from(elem).unwrap(); + assert_eq!(xhtml.bodies.len(), 1); + + let elem: Element = "" + .parse() + .unwrap(); + let xhtml = XhtmlIm::try_from(elem).unwrap(); + assert_eq!(xhtml.bodies.len(), 2); + } + + #[test] + fn invalid_two_same_langs() { + let elem: Element = "" + .parse() + .unwrap(); + let error = XhtmlIm::try_from(elem).unwrap_err(); + let message = match error { + Error::ParseError(string) => string, + _ => panic!(), + }; + assert_eq!(message, "Two identical language bodies found in XHTML-IM."); + } + + #[test] + fn test_tag() { + let elem: Element = "" + .parse() + .unwrap(); + let body = Body::try_from(elem).unwrap(); + assert_eq!(body.children.len(), 0); + + let elem: Element = "

Hello world!

" + .parse() + .unwrap(); + let mut body = Body::try_from(elem).unwrap(); + assert_eq!(body.style.len(), 0); + assert_eq!(body.xml_lang, None); + assert_eq!(body.children.len(), 1); + let p = match body.children.pop() { + Some(Child::Tag(tag)) => tag, + _ => panic!(), + }; + let mut children = match p { + Tag::P { style, children } => { + assert_eq!(style.len(), 0); + assert_eq!(children.len(), 1); + children + } + _ => panic!(), + }; + let text = match children.pop() { + Some(Child::Text(text)) => text, + _ => panic!(), + }; + assert_eq!(text, "Hello world!"); + } + + #[test] + fn test_unknown_element() { + let elem: Element = "Hello world!" + .parse() + .unwrap(); + let parsed = XhtmlIm::try_from(elem).unwrap(); + let parsed2 = parsed.clone(); + let html = parsed.to_html(); + assert_eq!(html, "Hello world!"); + + let elem = Element::from(parsed2); + assert_eq!(String::from(&elem), "Hello world!"); + } + + #[test] + fn test_generate_html() { + let elem: Element = "

Hello world!

" + .parse() + .unwrap(); + let xhtml_im = XhtmlIm::try_from(elem).unwrap(); + let html = xhtml_im.to_html(); + assert_eq!(html, "

Hello world!

"); + + let elem: Element = "

Hello world!

" + .parse() + .unwrap(); + let xhtml_im = XhtmlIm::try_from(elem).unwrap(); + let html = xhtml_im.to_html(); + assert_eq!(html, "

Hello world!

"); + } + + #[test] + fn generate_tree() { + let world = "world".to_string(); + + Body { + style: vec![], + xml_lang: Some("en".to_string()), + children: vec![Child::Tag(Tag::P { + style: vec![], + children: vec![ + Child::Text("Hello ".to_string()), + Child::Tag(Tag::Strong { + children: vec![Child::Text(world)], + }), + Child::Text("!".to_string()), + ], + })], + }; + } +}