├── .formatter.exs ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── xml_rpc.ex └── xml_rpc │ ├── base64.ex │ ├── date_time.ex │ ├── decoder.ex │ ├── encoder.ex │ └── xmlrpc.xsd ├── mix.exs ├── mix.lock └── test ├── test_helper.exs └── xmlrpc_test.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where third-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | xmlrpc-*.tar 24 | 25 | # Temporary files, for example, from tests. 26 | /tmp/ 27 | 28 | # Misc. 29 | .DS_Store 30 | /bench/snapshots 31 | /bench/graphs 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.4 4 | - 1.5 5 | - 1.6 6 | 7 | otp_release: 8 | - 19.3 9 | - 20.1 10 | 11 | env: MIX_ENV=test 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | XmlRpc 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/ewildgoose/elixir-xml_rpc.svg?branch=master)](https://travis-ci.org/ewildgoose/elixir-xml_rpc) 5 | [![Module Version](https://img.shields.io/hexpm/v/xmlrpc.svg)](https://hex.pm/packages/xmlrpc) 6 | [![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/xmlrpc/) 7 | [![Total Download](https://img.shields.io/hexpm/dt/xmlrpc.svg)](https://hex.pm/packages/xmlrpc) 8 | [![License](https://img.shields.io/hexpm/l/xmlrpc.svg)](https://github.com/ewildgoose/elixir-xml_rpc/blob/master/LICENSE) 9 | [![Last Updated](https://img.shields.io/github/last-commit/ewildgoose/elixir-xml_rpc.svg)](https://github.com/ewildgoose/elixir-xml_rpc/commits/master) 10 | 11 | Encode and decode elixir terms to [XML-RPC](http://wikipedia.org/wiki/XML-RPC) parameters. 12 | All XML-RPC parameter types are supported, including arrays, structs and Nil (optional). 13 | 14 | This module handles the parsing and encoding of the datatypes, but can be used 15 | in conjunction with HTTPoison, Phoenix, etc to create fully featured XML-RPC 16 | clients and servers. 17 | 18 | XML input (ie untrusted) is validated against an [XML Schema](http://en.wikipedia.org/wiki/XML_schema), 19 | which should help enforce correctness of input. [erlsom](https://github.com/willemdj/erlsom) 20 | is used to decode the xml as xmerl creates atoms during decoding, which has 21 | the risk that a malicious client can exhaust out atom space and crash the vm. 22 | 23 | 24 | ## Installation 25 | 26 | Add XML-RPC to your mix dependencies: 27 | 28 | ```elixir 29 | def deps do 30 | [ 31 | {:xmlrpc, "~> 1.4"} 32 | ] 33 | end 34 | ``` 35 | 36 | Then run `mix deps.get` and `mix deps.compile`. 37 | 38 | 39 | ## Datatypes 40 | 41 | XML-RPC only allows limited parameter types. We map these to Elixir as follows: 42 | 43 | | XMLRPC | Elixir | 44 | | ---------------------|---------------------------| 45 | | `` | Boolean, eg true/false | 46 | | `` | Bitstring, eg "string" | 47 | | `` (``) | Integer, eg 17 | 48 | | `` | Float, eg -12.3 | 49 | | `` | List, eg [1, 2, 3] | 50 | | `` | Map, eg %{key: "value"} | 51 | | `` | %XMLRPC.DateTime | 52 | | `` | %XMLRPC.Base64 | 53 | | `` (optional) | nil | 54 | 55 | 56 | Note that array and struct parameters can be composed of the fundamental types, 57 | and you can nest to arbitrary depths. (int inside a struct, inside an array, inside a struct, etc). 58 | Common practice seems to be to use a struct (or sometimes an array) as the top 59 | level to pass (named) each way. 60 | 61 | The XML encoding is performed through a protocol and so abstract datatypes 62 | can be encoded by implementing the `XMLRPC.ValueEncoder` protocol. 63 | 64 | ### Nil 65 | 66 | Nil is not defined in the core specification, but is commonly implemented as 67 | an option. The use of nil is enabled by default for encoding and decoding. 68 | If you want a input to be treated as an error then pass 69 | [exclude_nil: true] in the `options` parameter 70 | 71 | ## API 72 | 73 | The XML-RPC api consists of a call to a remote url, passing a "method_name" 74 | and a number of parameters. 75 | 76 | %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} 77 | 78 | The response is either "failure" and a `fault_code` and `fault_string`, or a 79 | response which consists of a single parameter (use a struct/array to pass back 80 | multiple values) 81 | 82 | %XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} 83 | 84 | %XMLRPC.MethodResponse{param: 30} 85 | 86 | To encode/decode to xml use `XMLRPC.encode/2` or `XMLRPC.decode/2` 87 | 88 | ## Examples 89 | 90 | ### Client using HTTPoison 91 | 92 | [HTTPoison](https://github.com/edgurgel/httpoison) can be used to talk to the remote API. To encode the body we can 93 | simply call `XMLRPC.encode/2`, and then decode the response with `XMLRPC.decode/2` 94 | 95 | ```elixir 96 | request_body = %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} 97 | |> XMLRPC.encode! 98 | "test.sumprod23" 99 | 100 | # Now use HTTPoison to call your RPC 101 | response = HTTPoison.post!("http://www.advogato.org/XMLRPC", request_body).body 102 | 103 | # eg 104 | response = "56" 105 | |> XMLRPC.decode 106 | {:ok, %XMLRPC.MethodResponse{param: [5, 6]}} 107 | ``` 108 | See the [HTTPoison docs](https://github.com/edgurgel/httpoison#wrapping-httpoisonbase) 109 | for more details, but you can also wrap the base API and have HTTPoison 110 | automatically do your encoding and decoding. In this way its very simple to build 111 | higher level APIs: 112 | 113 | ```elixir 114 | defmodule XMLRPC do 115 | use HTTPoison.Base 116 | 117 | def process_request_body(body), do: XMLRPC.encode(body) 118 | def process_response_body(body), do: XMLRPC.decode(body) 119 | end 120 | 121 | iex> request = %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} 122 | iex> response = HTTPoison.post!("http://www.advogato.org/XMLRPC", request).body 123 | {:ok, %XMLRPC.MethodResponse{param: [5, 6]}} 124 | ``` 125 | 126 | HTTPoison allows you to hook into other parts of the request process and handle 127 | authentication, URL schemes and easily build out a complete API module. 128 | 129 | ### Server 130 | 131 | Using say Phoenix, you can handle an incoming request and decode as above. 132 | XMLRPC implements the `encode_to_iodata!` call, which allows pluggable response 133 | handlers to automatically encode your response 134 | 135 | ## Copyright and License 136 | 137 | Copyright (c) 2015 Ed Wildgoose 138 | 139 | Licensed under the Apache License, Version 2.0 (the "License"); 140 | you may not use this file except in compliance with the License. 141 | You may obtain a copy of the License at [https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0) 142 | 143 | Unless required by applicable law or agreed to in writing, software 144 | distributed under the License is distributed on an "AS IS" BASIS, 145 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 146 | See the License for the specific language governing permissions and 147 | limitations under the License. 148 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for third- 9 | # party users, it should be done in your mix.exs file. 10 | 11 | # Sample configuration: 12 | # 13 | # config :logger, :console, 14 | # level: :info, 15 | # format: "$date $time [$level] $metadata$message\n", 16 | # metadata: [:user_id] 17 | 18 | # It is also possible to import configuration files, relative to this 19 | # directory. For example, you can emulate configuration per environment 20 | # by uncommenting the line below and defining dev.exs, test.exs and such. 21 | # Configuration from the imported file will override the ones defined 22 | # here (which is why it is important to import them last). 23 | # 24 | # import_config "#{Mix.env}.exs" 25 | -------------------------------------------------------------------------------- /lib/xml_rpc.ex: -------------------------------------------------------------------------------- 1 | defmodule XMLRPC do 2 | alias XMLRPC.DecodeError 3 | alias XMLRPC.EncodeError 4 | alias XMLRPC.Decoder 5 | alias XMLRPC.Encoder 6 | 7 | 8 | @moduledoc ~S""" 9 | Encode and decode elixir terms to [XML-RPC](http://wikipedia.org/wiki/XML-RPC) parameters. 10 | All XML-RPC parameter types are supported, including arrays, structs and Nil (optional). 11 | 12 | This module handles the parsing and encoding of the datatypes, but can be used 13 | in conjunction with HTTPoison, Phoenix, etc to create fully featured XML-RPC 14 | clients and servers. 15 | 16 | XML input (ie untrusted) is validated against an [XML Schema](http://en.wikipedia.org/wiki/XML_schema), 17 | which should help enforce correctness of input. [erlsom](https://github.com/willemdj/erlsom) 18 | is used to decode the xml as xmerl creates atoms during decoding, which has 19 | the risk that a malicious client can exhaust out atom space and crash the vm. 20 | 21 | ## Example 22 | 23 | iex> _request_body = %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} |> XMLRPC.encode! 24 | "test.sumprod23" 25 | 26 | # Now use HTTPoison to call your RPC 27 | response = HTTPoison.post!("http://www.advogato.org/XMLRPC", request_body).body 28 | 29 | iex> _response = "56" |> XMLRPC.decode 30 | {:ok, %XMLRPC.MethodResponse{param: [5, 6]}} 31 | 32 | 33 | ## Datatypes 34 | 35 | XML-RPC only allows limited parameter types. We map these to Elixir as follows: 36 | 37 | | XMLRPC | Elixir | 38 | | ---------------------|---------------------------| 39 | | `` | Boolean, eg true/false | 40 | | `` | Bitstring, eg "string" | 41 | | `` (``) | Integer, eg 17 | 42 | | `` | Float, eg -12.3 | 43 | | `` | List, eg [1, 2, 3] | 44 | | `` | Map, eg %{key: "value"} | 45 | | `` | %XMLRPC.DateTime | 46 | | `` | %XMLRPC.Base64 | 47 | | `` (optional) | nil | 48 | 49 | Note that array and struct parameters can be composed of the fundamental types, 50 | and you can nest to arbitrary depths. (int inside a struct, inside an array, inside a struct, etc). 51 | Common practice seems to be to use a struct (or sometimes an array) as the top 52 | level to pass (named) each way. 53 | 54 | The XML encoding is performed through a protocol and so abstract datatypes 55 | can be encoded by implementing the `XMLRPC.ValueEncoder` protocol. 56 | 57 | ### Nil 58 | Nil is not defined in the core specification, but is commonly implemented as 59 | an option. The use of nil is enabled by default for encoding and decoding. 60 | If you want a input to be treated as an error then pass 61 | [exclude_nil: true] in the `options` parameter 62 | 63 | 64 | ## API 65 | 66 | The XML-RPC api consists of a call to a remote url, passing a "method_name" 67 | and a number of parameters. 68 | 69 | %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} 70 | 71 | The response is either "failure" and a `fault_code` and `fault_string`, or a 72 | response which consists of a single parameter (use a struct/array to pass back 73 | multiple values) 74 | 75 | %XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} 76 | 77 | %XMLRPC.MethodResponse{param: 30} 78 | 79 | To encode/decode to xml use `XMLRPC.encode/2` or `XMLRPC.decode/2` 80 | 81 | ### Options 82 | The en/decoder take an array of options: 83 | 84 | * `:iodata` - When false (default), converts output of encoder to a string 85 | * `:exclude_nil` - When false (default), allows nil to be a valid type in encoder/decoder 86 | 87 | """ 88 | 89 | defmodule Fault do 90 | @moduledoc """ 91 | struct defining an xml-rpc 'fault' response 92 | """ 93 | @type t :: %__MODULE__{fault_code: integer, fault_string: String.t} 94 | 95 | defstruct fault_code: 0, fault_string: "" 96 | end 97 | 98 | defmodule MethodCall do 99 | @moduledoc """ 100 | struct defining an xml-rpc call (note array of params) 101 | """ 102 | @type t :: %__MODULE__{method_name: String.t, params: [ XMLRPC.t ]} 103 | 104 | defstruct method_name: "", params: nil 105 | end 106 | 107 | defmodule MethodResponse do 108 | @moduledoc """ 109 | struct defining an xml-rpc response (note single param) 110 | """ 111 | @type t :: %__MODULE__{param: XMLRPC.t} 112 | 113 | defstruct param: nil 114 | end 115 | 116 | 117 | @type t :: nil | number | boolean | String.t | map() | [nil | number | boolean | String.t] 118 | 119 | 120 | @doc """ 121 | Encode an XMLRPC call or response elixir structure into XML as iodata 122 | 123 | Raises an exception on error. 124 | """ 125 | @spec encode_to_iodata!(XMLRPC.t, Keyword.t) :: {:ok, iodata} | {:error, {any, String.t}} 126 | def encode_to_iodata!(value, options \\ []) do 127 | encode!(value, [iodata: true] ++ options) 128 | end 129 | 130 | @doc """ 131 | Encode an XMLRPC call or response elixir structure into XML as iodata 132 | """ 133 | @spec encode_to_iodata(XMLRPC.t, Keyword.t) :: {:ok, iodata} | {:error, {any, String.t}} 134 | def encode_to_iodata(value, options \\ []) do 135 | encode(value, [iodata: true] ++ options) 136 | end 137 | 138 | @doc ~S""" 139 | Encode an XMLRPC call or response elixir structure into XML. 140 | 141 | Raises an exception on error. 142 | 143 | iex> %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} |> XMLRPC.encode! 144 | "test.sumprod23" 145 | 146 | """ 147 | @spec encode!(XMLRPC.t, Keyword.t) :: iodata | no_return 148 | def encode!(value, options \\ []) do 149 | iodata = Encoder.encode!(value, options) 150 | 151 | unless options[:iodata] do 152 | iodata |> IO.iodata_to_binary 153 | else 154 | iodata 155 | end 156 | end 157 | 158 | @doc ~S""" 159 | Encode an XMLRPC call or response elixir structure into XML. 160 | 161 | iex> %XMLRPC.MethodCall{method_name: "test.sumprod", params: [2,3]} |> XMLRPC.encode 162 | {:ok, "test.sumprod23"} 163 | 164 | """ 165 | @spec encode(XMLRPC.t, Keyword.t) :: {:ok, iodata} | {:ok, String.t} | {:error, {any, String.t}} 166 | def encode(value, options \\ []) do 167 | {:ok, encode!(value, options)} 168 | 169 | rescue 170 | exception in [EncodeError] -> 171 | {:error, {exception.value, exception.message}} 172 | end 173 | 174 | 175 | @doc ~S""" 176 | Decode XMLRPC call or response XML into an Elixir structure 177 | 178 | iex> XMLRPC.decode("56") 179 | {:ok, %XMLRPC.MethodResponse{param: [5, 6]}} 180 | 181 | """ 182 | @spec decode(iodata, Keyword.t) :: {:ok, Fault.t} | {:ok, MethodCall.t} | {:ok, MethodResponse.t} | {:error, String.t} 183 | def decode(value, options \\ []) do 184 | {:ok, decode!(value, options)} 185 | 186 | rescue 187 | exception in [DecodeError] -> 188 | {:error, exception.message} 189 | end 190 | 191 | @doc ~S""" 192 | Decode XMLRPC call or response XML into an Elixir structure 193 | 194 | Raises an exception on error. 195 | 196 | iex> XMLRPC.decode!("56") 197 | %XMLRPC.MethodResponse{param: [5, 6]} 198 | 199 | """ 200 | @spec decode!(iodata, Keyword.t) :: Fault.t | MethodCall.t | MethodResponse.t | no_return 201 | def decode!(value, options \\ []) do 202 | Decoder.decode!(value, options) 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/xml_rpc/base64.ex: -------------------------------------------------------------------------------- 1 | defmodule XMLRPC.Base64 do 2 | @moduledoc """ 3 | Elixir datatype to store base64 value. 4 | 5 | Note: See the `Base` module for other conversions in Elixir stdlib. 6 | """ 7 | @type t :: %__MODULE__{raw: String.t} 8 | defstruct raw: "" 9 | 10 | @doc """ 11 | Create a new Base64 struct from an binary input. 12 | """ 13 | def new(binary) do 14 | %__MODULE__{raw: Base.encode64(binary)} 15 | end 16 | 17 | @doc """ 18 | Attempt to decode a Base64 encoded value. 19 | 20 | Note: thin wrapper around `Base.decode64/1`. 21 | """ 22 | def to_binary(%__MODULE__{raw: encoded}) do 23 | # Some XMLRPC libraries put whitespace in the Base64 data. 24 | # The <1.2.0 version of elixir won't correctly parse it. 25 | # We manually remove whitespace on older versions of elixir 26 | case encoded do 27 | [] -> {:ok, encoded} 28 | _ -> 29 | if Version.compare(System.version, "1.2.3") == :lt do 30 | encoded 31 | |> String.replace(~r/\s/, "") # remove any whitespace 32 | |> Base.decode64 33 | else 34 | Base.decode64(encoded, ignore: :whitespace) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/xml_rpc/date_time.ex: -------------------------------------------------------------------------------- 1 | defmodule XMLRPC.DateTime do 2 | @moduledoc """ 3 | Struct to store a date-time in xml-rpc format (a variation on ISO 8601). 4 | 5 | Note, there is significant ambiguity in the formatting of date-time in xml-rpc. 6 | This is a thin wrapper around a basic parser, but knowledge of the API you are 7 | trying to connect to will be valuable. Consider writing your own decoder 8 | (and perhaps encoder) to speak to non standard end-points... 9 | """ 10 | 11 | @type t :: %__MODULE__{raw: String.t} 12 | defstruct raw: "" 13 | 14 | @doc """ 15 | Create a new datetime in the (odd) format that the XMLRPC spec claims is ISO 8601. 16 | 17 | iex> XMLRPC.DateTime.new({{2015,6,9},{9,7,2}}) 18 | %XMLRPC.DateTime{raw: "20150609T09:07:02"} 19 | 20 | """ 21 | def new({{year, month, day},{hour, min, sec}}) do 22 | date = :io_lib.format("~4.10.0B~2.10.0B~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B", 23 | [year, month, day, hour, min, sec]) 24 | |> IO.iodata_to_binary 25 | %__MODULE__{raw: date} 26 | end 27 | 28 | @doc """ 29 | Attempt to parse a returned date. Note there is significant ambiguity around 30 | what constitutes an valid date... The spec says no hyphens between date parts 31 | and no timezone. However, servers in the field sometimes seem to return 32 | ISO 8601 dates... 33 | 34 | We attempt to be generous in parsing, but no attempt is made to handle timezones. 35 | For more accurate parsing, including handling timezones, see the Calendar library 36 | 37 | iex>XMLRPC.DateTime.to_erlang_date(%XMLRPC.DateTime{raw: "20150609T09:07:02"}) 38 | {:ok, {{2015, 6, 9}, {9, 7, 2}}} 39 | 40 | """ 41 | def to_erlang_date(%__MODULE__{raw: date}) do 42 | case Regex.run(~r/(\d{4})-?(\d{2})-?(\d{2})T(\d{2}):(\d{2}):(\d{2})/, date, capture: :all_but_first) do 43 | nil -> {:error, "Unable to parse date"} 44 | date -> [year, mon, day, hour, min, sec] = 45 | date 46 | |> Enum.map(&to_int/1) 47 | {:ok, {{year, mon, day}, {hour, min, sec}}} 48 | end 49 | end 50 | 51 | defp to_int(str), do: str |> Integer.parse |> elem(0) 52 | end 53 | -------------------------------------------------------------------------------- /lib/xml_rpc/decoder.ex: -------------------------------------------------------------------------------- 1 | defmodule XMLRPC.DecodeError do 2 | defexception message: nil 3 | end 4 | 5 | defmodule XMLRPC.Decoder do 6 | 7 | alias XMLRPC.DecodeError 8 | alias XMLRPC.Fault 9 | alias XMLRPC.MethodCall 10 | alias XMLRPC.MethodResponse 11 | 12 | # Load our XML Schema from an external file 13 | @xmlrpc_xsd_file Path.join(__DIR__, "xmlrpc.xsd") 14 | @external_resource @xmlrpc_xsd_file 15 | @xmlrpc_xsd File.read!(@xmlrpc_xsd_file) 16 | 17 | @moduledoc """ 18 | This module does the work of decoding an XML-RPC call or response. 19 | """ 20 | 21 | @doc """ 22 | Decode an XML-RPC Call or Response object 23 | 24 | Input: 25 | iodata consisting of the input XML string 26 | options: 27 | exclude_nil: false (default) - allow decoding of values 28 | 29 | Output: 30 | On any parse failure raises XMLRPC.DecodeError 31 | 32 | On success the decoded result will be a struct, either: 33 | * XMLRPC.MethodCall 34 | * XMLRPC.MethodResponse 35 | * XMLRPC.Fault 36 | """ 37 | def decode!(iodata, options) do 38 | {:ok, model} = :erlsom.compile_xsd(@xmlrpc_xsd) 39 | xml = IO.iodata_to_binary(iodata) 40 | 41 | case :erlsom.scan(xml, model, [{:output_encoding, :utf8}]) do 42 | {:error, [{:exception, {_error_type, {error}}}, _stack, _received]} when is_list(error) -> 43 | raise DecodeError, message: List.to_string(error) 44 | {:error, [{:exception, {_error_type, error}}, _stack, _received]} -> 45 | raise DecodeError, message: error 46 | {:error, message} when is_list(message) -> 47 | raise DecodeError, message: List.to_string(message) 48 | {:ok, struct, _rest} -> 49 | parse(struct, options) 50 | end 51 | 52 | end 53 | 54 | # ########################################################################## 55 | # Top level parsers. 56 | # Pickup the main type of the thing being parsed and setup appropriate result objects 57 | 58 | # Parse a method 'Call' 59 | defp parse( {:methodCall, [], method_name, 60 | {:"methodCall/params", [], params }}, 61 | options ) 62 | when is_list(params) 63 | do 64 | %MethodCall{ method_name: method_name, params: parse_params(params, options) } 65 | end 66 | 67 | # Parse a method 'Call' with no (:undefined) params 68 | defp parse( {:methodCall, [], method_name, 69 | {:"methodCall/params", [], :undefined }}, 70 | options ) 71 | do 72 | %MethodCall{ method_name: method_name, params: parse_params([], options) } 73 | end 74 | 75 | # Parse a method 'Call' with completely missing params array 76 | defp parse( {:methodCall, [], method_name, 77 | :undefined}, 78 | options ) 79 | do 80 | %MethodCall{ method_name: method_name, params: parse_params([], options) } 81 | end 82 | 83 | # Parse a 'fault' Response 84 | defp parse( {:methodResponse, [], 85 | {:"methodResponse/fault", [], 86 | {:"methodResponse/fault/value", [], 87 | {:"methodResponse/fault/value/struct", [], fault_struct} }}}, 88 | options ) 89 | when is_list(fault_struct) 90 | do 91 | fault = parse_struct(fault_struct, options) 92 | fault_code = Map.get(fault, "faultCode") 93 | fault_string = Map.get(fault, "faultString") 94 | %Fault{ fault_code: fault_code, fault_string: fault_string } 95 | end 96 | 97 | # Parse any other 'Response' 98 | defp parse( {:methodResponse, [], 99 | {:"methodResponse/params", [], param}}, 100 | options ) 101 | when is_tuple(param) 102 | do 103 | %MethodResponse{ param: parse_param(param, options) } 104 | end 105 | 106 | # ########################################################################## 107 | 108 | # Parse an 'array' atom 109 | defp parse_value( {:ValueType, [], [{:ArrayType, [], {:"ArrayType/data", [], array}}]}, options ) do 110 | parse_array(array, options) 111 | end 112 | 113 | # Parse a 'struct' atom 114 | defp parse_value( {:ValueType, [], [{:StructType, [], struct}]}, options) 115 | when is_list(struct) 116 | do 117 | parse_struct(struct, options) 118 | end 119 | 120 | defp parse_value( {:ValueType, [], [{:StructType, [], _struct}]}, _options) do 121 | %{} 122 | end 123 | 124 | # Parse an 'integer' atom 125 | defp parse_value( {:ValueType, [], [{:"ValueType-int", [], int}]}, _options) 126 | when is_integer(int) 127 | do 128 | int 129 | end 130 | 131 | # Parse an 'i4' atom (32 bit integer) 132 | defp parse_value( {:ValueType, [], [{:"ValueType-i4", [], int}]}, _options) 133 | when is_integer(int) 134 | do 135 | int 136 | end 137 | 138 | # Parse an 'i8' atom (64 bit integer) 139 | defp parse_value( {:ValueType, [], [{:"ValueType-i8", [], int}]}, _options) 140 | when is_integer(int) 141 | do 142 | int 143 | end 144 | 145 | # Parse a 'float' atom 146 | defp parse_value( {:ValueType, [], [{:"ValueType-double", [], float}]}, _options) do 147 | Float.parse(float) 148 | |> elem(0) 149 | end 150 | 151 | # Parse a 'boolean' atom 152 | defp parse_value( {:ValueType, [], [{:"ValueType-boolean", [], boolean}]}, _options) do 153 | case boolean do 154 | "0" -> false 155 | "1" -> true 156 | end 157 | end 158 | 159 | # Parse a 'datetime' atom (needs decoding from bolloxed iso8601 alike format...) 160 | defp parse_value( {:ValueType, [], [{:"ValueType-dateTime.iso8601", [], datetime}]}, _options) do 161 | %XMLRPC.DateTime{raw: datetime} 162 | end 163 | 164 | # Parse a 'base64' atom 165 | defp parse_value( {:ValueType, [], [{:"ValueType-base64", [], string}]}, _options) do 166 | %XMLRPC.Base64{raw: string} 167 | end 168 | 169 | # Parse an empty 'string' atom 170 | defp parse_value( {:ValueType, [], [{:"ValueType-string", [], []}]}, _options) do 171 | "" 172 | end 173 | 174 | # Parse a 'string' atom 175 | defp parse_value( {:ValueType, [], [{:"ValueType-string", [], string}]}, _options) do 176 | string 177 | end 178 | 179 | # A string value can optionally drop the type specifier. The node is assumed to be a string value 180 | defp parse_value( {:ValueType, [], [string] }, _options) when is_binary(string) do 181 | string 182 | end 183 | 184 | # An empty string that drops the type specifier will parse as :undefined instead of an empty binary. 185 | defp parse_value( {:ValueType, [], :undefined }, _options) do 186 | "" 187 | end 188 | 189 | # Parse a 'nil' atom 190 | # Note: this is an xml-rpc extension 191 | defp parse_value( {:ValueType, [], [NilType: []]}, options) do 192 | if options[:exclude_nil] do 193 | raise XMLRPC.DecodeError, message: "unable to decode " 194 | else 195 | nil 196 | end 197 | end 198 | 199 | # ########################################################################## 200 | 201 | # Parse the 'struct' 202 | # 'structs' are a list of key-value pairs 203 | # Note: values can be 'structs'/'arrays' as well as other atom types 204 | defp parse_struct(doc, options) when is_list(doc) do 205 | doc 206 | |> Enum.reduce(Map.new, 207 | fn(member, acc) -> 208 | parse_member(member, options) 209 | |> Enum.into(acc) 210 | end) 211 | end 212 | 213 | # Parse the 'array' 214 | # 'arrays' are just an ordered list of other atom values 215 | # Note: values can be 'structs'/'arrays' as well as other atom types 216 | defp parse_array(doc, options) when is_list(doc) do 217 | doc 218 | |> Enum.map(fn v -> parse_value(v, options) end) 219 | end 220 | 221 | # Empty array, ie 222 | defp parse_array(:undefined, _options), do: [] 223 | 224 | # ########################################################################## 225 | 226 | # Parse a list of Parameter values (implies a Request) 227 | defp parse_params(values, options) when is_list(values) do 228 | values 229 | |> Enum.map(fn p -> parse_param(p, options) end) 230 | end 231 | 232 | # Parse a single Parameter 233 | defp parse_param( {:ParamType, [], value }, options ), do: parse_value(value, options) 234 | 235 | # ########################################################################## 236 | 237 | # Parse one member of a Struct 238 | defp parse_member( {:MemberType, [], name, value }, options ) do 239 | [{name, parse_value(value, options)}] 240 | end 241 | 242 | end 243 | -------------------------------------------------------------------------------- /lib/xml_rpc/encoder.ex: -------------------------------------------------------------------------------- 1 | defmodule XMLRPC.EncodeError do 2 | defexception value: nil, message: nil 3 | end 4 | 5 | 6 | defmodule XMLRPC.Encode do 7 | @moduledoc """ 8 | Utility functions helpful for encoding XML. 9 | """ 10 | 11 | @doc """ 12 | Wrap a value in an XML tag. 13 | 14 | Note: For xml-rpc we need only a very minimal XML generator. 15 | """ 16 | def tag(tag, value) do 17 | ["<#{tag}>", value, ""] 18 | end 19 | 20 | @doc """ 21 | Escape special characters in XML attributes. 22 | 23 | Note: technically you only need to escape "&" and "<" in tags, however, 24 | its common to also escape ">". For attributes you must additionally escape 25 | both single and double quotes, but its convenient to also escape \r and \n 26 | """ 27 | def escape_attr(string) do 28 | string 29 | |> String.replace("&", "&") 30 | |> String.replace("<", "<") 31 | |> String.replace(">", ">") 32 | |> String.replace("\"", """) 33 | |> String.replace("'", "'") 34 | |> String.replace("\x0d", " ") 35 | |> String.replace("\x0a", " ") 36 | end 37 | end 38 | 39 | 40 | defmodule XMLRPC.Encoder do 41 | @moduledoc """ 42 | This module does the work of encoding an XML-RPC call or response. 43 | """ 44 | 45 | import XMLRPC.Encode, only: [tag: 2] 46 | 47 | def encode!(%XMLRPC.MethodCall{method_name: method_name, params: params}, options) do 48 | [""] ++ 49 | tag("methodCall", 50 | tag("methodName", 51 | method_name) ++ 52 | tag("params", 53 | encode_params(params, options))) 54 | end 55 | 56 | def encode!(%XMLRPC.MethodResponse{ param: param }, options) do 57 | [""] ++ 58 | tag("methodResponse", 59 | tag("params", 60 | encode_param(param, options))) 61 | end 62 | 63 | def encode!(%XMLRPC.Fault{ fault_code: fault_code, fault_string: fault_string }, options) do 64 | fault = %{faultCode: fault_code, faultString: fault_string} 65 | 66 | [""] ++ 67 | tag("methodResponse", 68 | tag("fault", 69 | encode_value(fault, options))) 70 | end 71 | 72 | # ########################################################################## 73 | 74 | defp encode_params(params, options) do 75 | Enum.map params, fn p -> encode_param(p, options) end 76 | end 77 | 78 | defp encode_param(param, options) do 79 | tag "param", encode_value(param, options) 80 | end 81 | 82 | # ########################################################################## 83 | 84 | def encode_value(value, options) do 85 | tag("value", XMLRPC.ValueEncoder.encode(value, options)) 86 | end 87 | 88 | end 89 | 90 | 91 | # ########################################################################## 92 | 93 | defprotocol XMLRPC.ValueEncoder do 94 | @fallback_to_any true 95 | 96 | def encode(value, options) 97 | end 98 | 99 | 100 | defimpl XMLRPC.ValueEncoder, for: Atom do 101 | import XMLRPC.Encode, only: [tag: 2, escape_attr: 1] 102 | 103 | # encode nil value (default to enabled) 104 | def encode(nil, options) do 105 | if options[:exclude_nil] do 106 | raise XMLRPC.EncodeError, value: nil, message: "unable to encode value: nil" 107 | else 108 | [""] 109 | end 110 | end 111 | 112 | def encode(true, _options), do: tag("boolean", "1") 113 | def encode(false, _options), do: tag("boolean", "0") 114 | 115 | def encode(atom, _options), do: tag("string", 116 | atom 117 | |> Atom.to_string 118 | |> escape_attr ) 119 | end 120 | 121 | 122 | defimpl XMLRPC.ValueEncoder, for: BitString do 123 | import XMLRPC.Encode, only: [tag: 2, escape_attr: 1] 124 | 125 | def encode(string, _options) do 126 | tag("string", 127 | escape_attr(string)) 128 | end 129 | end 130 | 131 | 132 | defimpl XMLRPC.ValueEncoder, for: Integer do 133 | import XMLRPC.Encode, only: [tag: 2] 134 | 135 | def encode(int, _options), do: tag("int", Integer.to_string(int)) 136 | end 137 | 138 | 139 | defimpl XMLRPC.ValueEncoder, for: Float do 140 | import XMLRPC.Encode, only: [tag: 2] 141 | 142 | def encode(double, _options) do 143 | # Something of a format hack in the absence of a proper pretty printer 144 | # On average will round trip a float back to the original simple string 145 | tag("double", Float.to_string(double)) 146 | end 147 | end 148 | 149 | defimpl XMLRPC.ValueEncoder, for: Decimal do 150 | import XMLRPC.Encode, only: [tag: 2] 151 | 152 | def encode(double, _options) do 153 | # Something of a format hack in the absence of a proper pretty printer 154 | # On average will round trip a float back to the original simple string 155 | tag("double", double |> Decimal.normalize() |> Decimal.to_string()) 156 | end 157 | end 158 | 159 | 160 | defimpl XMLRPC.ValueEncoder, for: XMLRPC.DateTime do 161 | import XMLRPC.Encode, only: [tag: 2] 162 | 163 | def encode(%XMLRPC.DateTime{raw: datetime}, _options) do 164 | tag("dateTime.iso8601", datetime) 165 | end 166 | end 167 | 168 | 169 | defimpl XMLRPC.ValueEncoder, for: XMLRPC.Base64 do 170 | import XMLRPC.Encode, only: [tag: 2] 171 | 172 | def encode(%XMLRPC.Base64{raw: base64}, _options) do 173 | tag("base64", base64) 174 | end 175 | end 176 | 177 | 178 | defimpl XMLRPC.ValueEncoder, for: List do 179 | import XMLRPC.Encode, only: [tag: 2] 180 | 181 | def encode(array, options) do 182 | tag("array", 183 | tag("data", 184 | array |> Enum.map(fn v -> XMLRPC.Encoder.encode_value(v, options) end) ) ) 185 | end 186 | end 187 | 188 | defimpl XMLRPC.ValueEncoder, for: Map do 189 | import XMLRPC.Encode, only: [tag: 2, escape_attr: 1] 190 | 191 | # Parse a general map structure. 192 | # Note: This will also match structs, so define those above this definition 193 | def encode(struct, options) do 194 | tag("struct", 195 | struct |> Enum.map(fn m -> encode_member(m, options) end)) 196 | end 197 | 198 | # Individual items of a struct. Basically key/value pair 199 | def encode_member({key, value}, options) when is_atom(key) do 200 | encode_member({Atom.to_string(key), value}, options) 201 | end 202 | 203 | def encode_member({key, value}, options) when is_bitstring(key) do 204 | tag("member", 205 | tag("name", escape_attr(key)) ++ 206 | XMLRPC.Encoder.encode_value(value, options) ) 207 | end 208 | end 209 | 210 | defimpl XMLRPC.ValueEncoder, for: Any do 211 | def encode(%{__struct__: _} = struct, options) do 212 | XMLRPC.ValueEncoder.Map.encode(Map.from_struct(struct), options) 213 | end 214 | 215 | def encode(value, _options) do 216 | raise XMLRPC.EncodeError, 217 | value: value, 218 | message: "unable to encode value: #{inspect value}" 219 | end 220 | end 221 | 222 | # defp encode_value(_) do 223 | # throw({:error, "Unknown value type"}) 224 | # end 225 | -------------------------------------------------------------------------------- /lib/xml_rpc/xmlrpc.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule XmlRpc.Mixfile do 2 | use Mix.Project 3 | 4 | @source_url "https://github.com/ewildgoose/elixir-xml_rpc" 5 | @version "1.4.3" 6 | 7 | def project do 8 | [ 9 | app: :xmlrpc, 10 | version: @version, 11 | elixir: "~> 1.4", 12 | name: "XMLRPC", 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | docs: docs(), 17 | package: package() 18 | ] 19 | end 20 | 21 | def application do 22 | [extra_applications: []] 23 | end 24 | 25 | defp deps do 26 | [ 27 | {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, 28 | {:erlsom, "~> 1.4"}, 29 | {:decimal, "~> 2.0"} 30 | ] 31 | end 32 | 33 | defp package do 34 | [ 35 | description: 36 | "XML-RPC encoder/decder for Elixir. Supports all valid " <> 37 | "datatypes. Input (ie untrusted) is parsed with erlsom against " <> 38 | "an xml-schema for security.", 39 | files: ~w(lib mix.exs README.md LICENSE), 40 | maintainers: ["Ed Wildgoose"], 41 | licenses: ["Apache-2.0"], 42 | links: %{"GitHub" => @source_url} 43 | ] 44 | end 45 | 46 | defp docs do 47 | [ 48 | extras: [ 49 | LICENSE: [title: "License"], 50 | "README.md": [title: "Overview"] 51 | ], 52 | main: "readme", 53 | source_url: @source_url, 54 | source_ref: "master", 55 | formatters: ["html"] 56 | ] 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "calendar": {:hex, :calendar, "0.6.7"}, 3 | "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, 4 | "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"}, 5 | "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, 6 | "erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"}, 7 | "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, 8 | "hackney": {:hex, :hackney, "1.1.0"}, 9 | "httpoison": {:hex, :httpoison, "0.7.0"}, 10 | "idna": {:hex, :idna, "1.0.2"}, 11 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 12 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 13 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 14 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, 15 | "ssl_verify_hostname": {:hex, :ssl_verify_hostname, "1.0.5"}, 16 | "tzdata": {:hex, :tzdata, "0.1.5"}, 17 | } 18 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/xmlrpc_test.exs: -------------------------------------------------------------------------------- 1 | defmodule XMLRPC.DecoderTest do 2 | use ExUnit.Case, async: true 3 | doctest XMLRPC 4 | doctest XMLRPC.DateTime 5 | doctest XMLRPC.Base64 6 | 7 | @rpc_simple_call_1 """ 8 | 9 | 10 | sample.sum 11 | 12 | 13 | 17 14 | 15 | 16 | 17 | 13 18 | 19 | 20 | 21 | """ 22 | 23 | @rpc_simple_call_1_elixir %XMLRPC.MethodCall{method_name: "sample.sum", params: [17, 13]} 24 | 25 | 26 | # It seems to be valid to either have an empty section or no section at all 27 | @rpc_no_params_1 """ 28 | 29 | 30 | GetAlive 31 | 32 | 33 | 34 | """ 35 | 36 | @rpc_no_params_2 """ 37 | 38 | 39 | GetAlive 40 | 41 | """ 42 | 43 | @rpc_no_params_elixir %XMLRPC.MethodCall{method_name: "GetAlive", params: []} 44 | 45 | 46 | @rpc_simple_response_1 """ 47 | 48 | 49 | 50 | 51 | 30 52 | 53 | 54 | 55 | """ 56 | 57 | @rpc_simple_response_1_elixir %XMLRPC.MethodResponse{param: 30} 58 | 59 | 60 | # 2^50 = 1125899906842624 (more than 32 bit) 61 | @rpc_bitsize_integer_response_1 """ 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 17 70 | 1125899906842624 71 | 72 | 73 | 74 | 75 | 76 | 77 | """ 78 | 79 | @rpc_bitsize_integer_response_1_elixir %XMLRPC.MethodResponse{param: [17, 1125899906842624]} 80 | 81 | 82 | 83 | @rpc_fault_1 """ 84 | 85 | 86 | 87 | 88 | 89 | 90 | faultCode 91 | 4 92 | 93 | 94 | faultString 95 | Too many parameters. 96 | 97 | 98 | 99 | 100 | 101 | """ 102 | 103 | @rpc_fault_1_elixir %XMLRPC.Fault{fault_code: 4, fault_string: "Too many parameters."} 104 | 105 | 106 | @rpc_response_all_array """ 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 30 115 | 1 116 | 19980717T14:08:55 117 | -12.53 118 | Something here 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | """ 127 | @rpc_response_all_array_elixir %XMLRPC.MethodResponse{param: 128 | [30, true, 129 | %XMLRPC.DateTime{raw: "19980717T14:08:55"}, 130 | -12.53, "Something here", nil]} 131 | 132 | 133 | @rpc_response_all_struct """ 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | bool 142 | 1 143 | 144 | 145 | datetime 146 | 19980717T14:08:55 147 | 148 | 149 | double 150 | -12.53 151 | 152 | 153 | int 154 | 30 155 | 156 | 157 | nil 158 | 159 | 160 | 161 | string 162 | Something here 163 | 164 | 165 | 166 | 167 | 168 | 169 | """ 170 | 171 | @rpc_response_all_struct_elixir %XMLRPC.MethodResponse{param: 172 | %{"bool" => true, 173 | "datetime" => %XMLRPC.DateTime{raw: "19980717T14:08:55"}, 174 | "double" => -12.53, "int" => 30, "nil" => nil, 175 | "string" => "Something here"}} 176 | 177 | 178 | @rpc_response_nested """ 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 30 188 | 189 | 190 | 191 | 192 | 193 | array 194 | 195 | 196 | 197 | 198 | 30 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | """ 215 | 216 | @rpc_response_nested_elixir %XMLRPC.MethodResponse{param: 217 | [30, nil, %{"array" => [30]} ]} 218 | 219 | 220 | @rpc_response_empty_array """ 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | """ 234 | 235 | @rpc_response_empty_array_elixir %XMLRPC.MethodResponse{param: []} 236 | 237 | 238 | @rpc_response_optional_string_tag """ 239 | 240 | 241 | 242 | 243 | a4sdfff7dad8 244 | 245 | 246 | 247 | """ 248 | 249 | @rpc_response_optional_string_tag_elixir %XMLRPC.MethodResponse{param: "a4sdfff7dad8"} 250 | 251 | @rpc_response_empty_string_tag """ 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | """ 261 | 262 | @rpc_response_empty_string_tag_elixir %XMLRPC.MethodResponse{param: ""} 263 | 264 | @rpc_response_optional_empty_string_tag """ 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | """ 274 | 275 | @rpc_response_optional_empty_string_tag_elixir %XMLRPC.MethodResponse{param: ""} 276 | 277 | @rpc_base64_call_1 """ 278 | 279 | 280 | sample.fun1 281 | 282 | 283 | 1 284 | 285 | 286 | YWFiYmNjZGRlZWZmYWFiYmNjZGRlZWZmMDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDExMjIzMzQ0NTU2Njc3ODg5OQ== 287 | 288 | 289 | 290 | """ 291 | 292 | @rpc_base64_call_with_whitespace """ 293 | 294 | 295 | sample.fun1 296 | 297 | 298 | 1 299 | 300 | 301 | 302 | YWFiYmNjZGRlZWZmYWFiYmNjZGRlZWZm 303 | MDAxMTIyMzM0NDU1NjY3Nzg4OTkwMDEx 304 | MjIzMzQ0NTU2Njc3ODg5OQ== 305 | 306 | 307 | 308 | 309 | """ 310 | 311 | @rpc_base64_value "aabbccddeeffaabbccddeeff0011223344556677889900112233445566778899" 312 | 313 | @rpc_base64_call_1_elixir_to_encode %XMLRPC.MethodCall{method_name: "sample.fun1", params: [true, 314 | XMLRPC.Base64.new(@rpc_base64_value)]} 315 | 316 | # Various malformed tags 317 | @rpc_response_invalid_1 """ 318 | 319 | 320 | 321 | 322 | 30 323 | 324 | 325 | 326 | 30 327 | 328 | 329 | 330 | """ 331 | 332 | # Various malformed tags 333 | @rpc_response_invalid_2 """ 334 | 335 | 336 | 337 | 338 | 30 339 | 340 | 341 | 342 | """ 343 | 344 | # Raise an error when trying to encode unsupported param type (function in this case) 345 | @rpc_response_invalid_3_elixir %XMLRPC.MethodResponse{param: &Kernel.exit/1} 346 | 347 | @rpc_response_empty_struct """ 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | """ 360 | 361 | @rpc_response_empty_struct_elixir %XMLRPC.MethodResponse{param: %{}} 362 | 363 | 364 | # ########################################################################## 365 | 366 | 367 | test "decode rpc_simple_call_1" do 368 | decode = XMLRPC.decode(@rpc_simple_call_1) 369 | assert decode == {:ok, @rpc_simple_call_1_elixir} 370 | end 371 | 372 | test "decode rpc_no_params_1" do 373 | decode = XMLRPC.decode(@rpc_no_params_1) 374 | assert decode == {:ok, @rpc_no_params_elixir} 375 | end 376 | 377 | test "decode rpc_no_params_2" do 378 | decode = XMLRPC.decode(@rpc_no_params_2) 379 | assert decode == {:ok, @rpc_no_params_elixir} 380 | end 381 | 382 | test "decode rpc_simple_response_1" do 383 | decode = XMLRPC.decode!(@rpc_simple_response_1) 384 | assert decode == @rpc_simple_response_1_elixir 385 | end 386 | 387 | test "decode rpc_bitsize_integer_response_1" do 388 | decode = XMLRPC.decode!(@rpc_bitsize_integer_response_1) 389 | assert decode == @rpc_bitsize_integer_response_1_elixir 390 | end 391 | 392 | test "decode rpc_fault_1" do 393 | decode = XMLRPC.decode(@rpc_fault_1) 394 | assert decode == {:ok, @rpc_fault_1_elixir} 395 | end 396 | 397 | test "decode rpc_response_all_array" do 398 | decode = XMLRPC.decode(@rpc_response_all_array) 399 | assert decode == {:ok, @rpc_response_all_array_elixir} 400 | end 401 | 402 | test "decode rpc_response_all_struct" do 403 | decode = XMLRPC.decode(@rpc_response_all_struct) 404 | assert decode == {:ok, @rpc_response_all_struct_elixir} 405 | end 406 | 407 | test "decode rpc_response_nested" do 408 | decode = XMLRPC.decode(@rpc_response_nested) 409 | assert decode == {:ok, @rpc_response_nested_elixir} 410 | end 411 | 412 | test "decode rpc_response_empty_array" do 413 | decode = XMLRPC.decode(@rpc_response_empty_array) 414 | assert decode == {:ok, @rpc_response_empty_array_elixir} 415 | end 416 | 417 | test "decode rpc_response_optional_string_tag" do 418 | decode = XMLRPC.decode(@rpc_response_optional_string_tag) 419 | assert decode == {:ok, @rpc_response_optional_string_tag_elixir} 420 | end 421 | 422 | test "decode rpc_response_empty_string_tag" do 423 | decode = XMLRPC.decode(@rpc_response_empty_string_tag) 424 | assert decode == {:ok, @rpc_response_empty_string_tag_elixir} 425 | end 426 | 427 | test "decode rpc_response_optional_empty_string_tag" do 428 | decode = XMLRPC.decode(@rpc_response_optional_empty_string_tag) 429 | assert decode == {:ok, @rpc_response_optional_empty_string_tag_elixir} 430 | end 431 | 432 | test "decode base64 data" do 433 | decode = XMLRPC.decode(@rpc_base64_call_1) 434 | assert decode == {:ok, @rpc_base64_call_1_elixir_to_encode} 435 | end 436 | 437 | test "decode base64 data with whitespace" do 438 | {:ok, decode} = XMLRPC.decode(@rpc_base64_call_with_whitespace) 439 | assert {:ok, @rpc_base64_value} == decode.params |> List.last |> XMLRPC.Base64.to_binary 440 | end 441 | 442 | test "decode rpc_response_invalid_1" do 443 | decode = XMLRPC.decode(@rpc_response_invalid_1) 444 | assert decode == {:error, "1 - Unexpected event, expected end-tag"} 445 | end 446 | 447 | test "decode rpc_response_invalid_2" do 448 | decode = XMLRPC.decode(@rpc_response_invalid_2) 449 | assert decode == {:error, "Malformed: Tags don\'t match"} 450 | end 451 | 452 | test "decode rpc_response_empty_struct" do 453 | decode = XMLRPC.decode(@rpc_response_empty_struct) 454 | assert decode == {:ok, @rpc_response_empty_struct_elixir} 455 | end 456 | 457 | # ########################################################################## 458 | 459 | 460 | test "encode rpc_simple_call_1" do 461 | encode = XMLRPC.encode!(@rpc_simple_call_1_elixir) 462 | |> IO.iodata_to_binary 463 | 464 | assert encode == strip_space(@rpc_simple_call_1) 465 | end 466 | 467 | test "encode rpc_simple_response_1" do 468 | encode = XMLRPC.encode!(@rpc_simple_response_1_elixir) 469 | |> IO.iodata_to_binary 470 | 471 | assert encode == strip_space(@rpc_simple_response_1) 472 | end 473 | 474 | test "encode rpc_fault_1" do 475 | encode = XMLRPC.encode!(@rpc_fault_1_elixir) 476 | |> IO.iodata_to_binary 477 | 478 | assert encode == strip_space(@rpc_fault_1) 479 | end 480 | 481 | test "encode rpc_response_all_array" do 482 | encode = XMLRPC.encode!(@rpc_response_all_array_elixir) 483 | |> IO.iodata_to_binary 484 | 485 | assert encode == strip_space(@rpc_response_all_array) 486 | end 487 | 488 | test "encode rpc_response_all_struct" do 489 | encode = XMLRPC.encode!(@rpc_response_all_struct_elixir) 490 | |> IO.iodata_to_binary 491 | 492 | assert encode == strip_space(@rpc_response_all_struct) 493 | end 494 | 495 | test "encode rpc_response_nested" do 496 | encode = XMLRPC.encode!(@rpc_response_nested_elixir) 497 | 498 | assert encode == strip_space(@rpc_response_nested) 499 | end 500 | 501 | test "encode rpc_response_empty_array" do 502 | encode = XMLRPC.encode!(@rpc_response_empty_array_elixir) 503 | 504 | assert encode == strip_space(@rpc_response_empty_array) 505 | end 506 | 507 | test "encode base64 data" do 508 | encode = XMLRPC.encode!(@rpc_base64_call_1_elixir_to_encode) 509 | |> IO.iodata_to_binary 510 | 511 | assert encode == strip_space(@rpc_base64_call_1) 512 | end 513 | 514 | test "encode rpc_response_invalid_3" do 515 | assert_raise XMLRPC.EncodeError, fn -> 516 | XMLRPC.encode!(@rpc_response_invalid_3_elixir) 517 | end 518 | end 519 | 520 | test "encode rpc_response_empty_struct" do 521 | encode = XMLRPC.encode!(@rpc_response_empty_struct_elixir) 522 | |> IO.iodata_to_binary 523 | 524 | assert encode == strip_space(@rpc_response_empty_struct) 525 | end 526 | 527 | test "floating point doesn't round arbitrarily" do 528 | assert "127.39" == 127.39 |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary 529 | assert "128.39" == 128.39 |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary 530 | end 531 | 532 | test "Decimal type outputs with expected precision" do 533 | assert "127.39" == Decimal.new("127.3900") |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary 534 | assert "128.39" == Decimal.new("128.3900") |> XMLRPC.ValueEncoder.encode(nil) |> IO.iodata_to_binary 535 | end 536 | 537 | # ########################################################################## 538 | 539 | 540 | # Helper functions 541 | # 542 | defp strip_space(string) do 543 | Regex.replace(~r/>\s+<") 544 | |> String.trim 545 | end 546 | 547 | 548 | end 549 | --------------------------------------------------------------------------------