├── .eslintrc ├── .github └── workflows │ ├── node.js.yml │ └── npm-publish.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── battery-level.js ├── mongodb-registry.js ├── package.json ├── server.js └── thermostat.js ├── greenkeeper.json ├── lib ├── contentFormats.js ├── errors.js ├── index.js ├── oma │ ├── acl.json │ ├── device.json │ ├── location.json │ ├── security.json │ └── server.json ├── router.js ├── schema.js ├── senml.js ├── server │ ├── bootstrap │ │ ├── index.js │ │ └── request.js │ ├── decodestream.js │ ├── deregister.js │ ├── index.js │ ├── register.js │ ├── registry.js │ ├── update.js │ └── validate.js ├── slidingWindow.js ├── tlv.js └── utils.js ├── package-lock.json ├── package.json └── test ├── registry.js ├── schema.js ├── senml.js ├── server ├── bootstrap.js ├── device-management.js ├── information-reporting.js └── registration.js ├── slidingWindow.js ├── tlv.js └── utils.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "mocha": true 5 | }, 6 | "globals": { 7 | "Promise": true, 8 | "Symbol": true 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "no-unused-vars": [ 13 | "error", 14 | { 15 | "varsIgnorePattern": "^should$", 16 | "argsIgnorePattern": "^res$" 17 | } 18 | ], 19 | "no-tabs": "error", 20 | "indent": [ 21 | "error", 22 | 2 23 | ], 24 | "comma-dangle": [ 25 | "error", 26 | "always-multiline" 27 | ], 28 | "linebreak-style": "off", 29 | "quotes": [ 30 | "error", 31 | "single" 32 | ], 33 | "semi": [ 34 | "error", 35 | "always" 36 | ], 37 | "no-sparse-arrays": "off", 38 | "no-invalid-this": "error", 39 | "no-use-before-define": "error", 40 | "handle-callback-err": "error", 41 | "no-buffer-constructor": "error" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [macos-latest, windows-latest, ubuntu-latest] 20 | node: [14, 16] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v2 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-node@v1 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 34 | 35 | publish-gpr: 36 | needs: build 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: actions/setup-node@v1 41 | with: 42 | node-version: 16 43 | registry-url: https://npm.pkg.github.com/ 44 | - run: npm ci 45 | - run: npm publish 46 | env: 47 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /examples/node_modules/ 3 | /coverage/ 4 | 5 | *~ 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.5.0] - 2017-08-31 8 | ### Added 9 | - add MongoDB registry example 10 | 11 | ## [0.4.0] - 2017-08-04 12 | ### Fixed 13 | - writeAttributes misses 'stp' attribute 14 | - return 403 when registration client is rejected 15 | - validate update registration query 16 | ### Added 17 | - add examples/battery-level.js 18 | - add coveralls 19 | ### Changed 20 | - test coverage now is performed with nyc (part of istanbul.js) 21 | ### Security 22 | - stop using deprecated Buffer API (unsafe) 23 | 24 | ## [0.1.0] - 2017-07-27 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-lwm2m 2 | 3 | [![build][build-badge]][build-url] 4 | [![coverage status][coveralls-image]][coveralls-url] 5 | [![gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/node-lwm2m/Lobby) 6 | 7 | [build-badge]: https://github.com/moleike/node-lwm2m/actions/workflows/node.js.yml/badge.svg 8 | 9 | [build-url]: https://github.com/moleike/node-lwm2m/actions/workflows/node.js.yml 10 | 11 | [coveralls-url]: https://coveralls.io/github/moleike/node-lwm2m?branch=develop 12 | 13 | [coveralls-image]: https://img.shields.io/coveralls/moleike/node-lwm2m/develop.svg 14 | 15 | [**node-lwm2m**][self] is an implementation of the Open Mobile Alliance's Lightweight M2M protocol (LWM2M). 16 | 17 | [self]: https://github.com/moleike/node-lwm2m.git 18 | 19 | [lwm2m]: http://www.openmobilealliance.org/wp/overviews/lightweightm2m_overview.html 20 | 21 | ## Install 22 | 23 | npm install --save lwm2m 24 | 25 | ## Synopsis 26 | 27 | ```js 28 | var server = require('lwm2m').createServer() 29 | 30 | server.on('register', function(params, accept) { 31 | setImmediate(function() { 32 | 33 | // read device related information 34 | server.read(params.ep, '3/0') 35 | .then(function(device) { 36 | console.log(JSON.stringify(device, null, 4)) 37 | }) 38 | 39 | // monitor device battery level 40 | server.observe(params.ep, '/3/0/9') 41 | .then(function(stream) { 42 | stream.on('data', function(value) { 43 | console.log('battery level: %d%', value) 44 | }) 45 | }) 46 | }) 47 | accept() 48 | }) 49 | 50 | server.listen(5683) 51 | ``` 52 | 53 | ## Contribute 54 | 55 | Please report bugs via the 56 | [github issue tracker](https://github.com/moleike/node-lwm2m/issues). 57 | 58 | ## API 59 | 60 | 61 | 62 | #### Table of Contents 63 | 64 | - [schemas](#schemas) 65 | - [createServer](#createserver) 66 | - [bootstrap#createServer](#bootstrapcreateserver) 67 | - [Resource](#resource) 68 | - [Schema](#schema) 69 | - [validate](#validate) 70 | - [Server](#server) 71 | - [read](#read) 72 | - [write](#write) 73 | - [execute](#execute) 74 | - [discover](#discover) 75 | - [writeAttributes](#writeattributes) 76 | - [create](#create) 77 | - [delete](#delete) 78 | - [observe](#observe) 79 | - [cancel](#cancel) 80 | - [bootstrap#Server](#bootstrapserver) 81 | - [write](#write-1) 82 | - [delete](#delete-1) 83 | - [finish](#finish) 84 | - [Registry](#registry) 85 | - [\_find](#_find) 86 | - [\_get](#_get) 87 | - [\_save](#_save) 88 | - [\_update](#_update) 89 | - [\_delete](#_delete) 90 | 91 | ### schemas 92 | 93 | Schemas for OMA-defined objects. 94 | See [oma](lib/oma). 95 | 96 | ### createServer 97 | 98 | Returns **[Server](#server)** object 99 | 100 | ### bootstrap#createServer 101 | 102 | Returns **[bootstrap#Server](#bootstrapserver)** object 103 | 104 | ### Resource 105 | 106 | Schema resource type definition 107 | 108 | Type: [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) 109 | 110 | **Properties** 111 | 112 | - `type` **(`"String"` \| `"Integer"` \| `"Float"` \| `"Boolean"` \| `"Opaque"` \| `"Time"` | \[`"type"`])** 113 | - `id` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** Resource ID 114 | - `required` **[boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** resource is mandatory. Defaults to `false` 115 | - `enum` **[Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array)?** 116 | - `range` **[object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 117 | - `range.min` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** 118 | - `range.max` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** 119 | 120 | ### Schema 121 | 122 | Schema constructor. 123 | 124 | An `Schema` describes the shape of an LwM2M Object and the type of its resources. 125 | Schemas are used throghout the API for generating/parsing payloads from/to JavaScript values. 126 | 127 | See [oma](lib/oma) directory for default definitions. 128 | See also [thermostat.js](examples/thermostat.js) for an 129 | example of a composite schema. 130 | 131 | **Note** 132 | 133 | _LwM2M types will be coerced to JavaScript types and viceversa, e.g. `Time` will return a `Date()`, 134 | `Opaque` a `Buffer()`, and `Integer`/`Float` a number._ 135 | 136 | **Parameters** 137 | 138 | - `resources` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [Resource](#resource)>** 139 | 140 | **Examples** 141 | 142 | ```javascript 143 | // IPSO light controller 144 | var lightControlSchema = new Schema({ 145 | onOff: { 146 | type: 'Boolean', 147 | id : 5850, 148 | required: true 149 | }, 150 | dimmer: { 151 | type: 'Integer', 152 | id: 5851, 153 | range: { min: 0, max: 100 } 154 | }, 155 | units: { 156 | type: 'String', 157 | id: 5701, 158 | } 159 | }); 160 | 161 | // an object literal matching the schema above 162 | var lightControl = { 163 | onOff: true, 164 | dimmer: 40, 165 | } 166 | 167 | // Bad schema 168 | var schema = new Schema({ 169 | a: { type: 'String', id: 0 }, 170 | b: { type: 'Error', id: 1 }, 171 | }); // throws TypeError 172 | ``` 173 | 174 | - Throws **any** Will throw an error if fails to validate 175 | 176 | #### validate 177 | 178 | validates `obj` with `schema`. 179 | 180 | **Parameters** 181 | 182 | - `obj` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 183 | 184 | **Examples** 185 | 186 | ```javascript 187 | var schema = new Schema({ 188 | a: { type: String, id: 0 }, 189 | b: { type: Buffer, id: 1 }, 190 | }); 191 | 192 | schema.validate({ 193 | a: 'foo', 194 | b: Buffer.from('bar'), 195 | }); // OK 196 | 197 | schema.validate({ 198 | a: 'foo', 199 | b: 'bar', 200 | }); // Throws error 201 | ``` 202 | 203 | - Throws **any** Will throw an error if fails to validate 204 | 205 | ### Server 206 | 207 | **Extends EventEmitter** 208 | 209 | Server constructor. 210 | 211 | Events: 212 | 213 | - `register`: device registration request. 214 | - `update`: device registration update. 215 | - `unregister`: device unregistration. 216 | 217 | **Parameters** 218 | 219 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 220 | - `options.type` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** IPv4 (udp4) or IPv6 (udp6) connections (optional, default `'upd6'`) 221 | - `options.piggybackReplyMs` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** milliseconds to wait for a piggyback response (optional, default `50`) 222 | - `options.registry` **[Registry](#registry)** impl. of CoRE Resource Directory (optional, default `Registry`) 223 | 224 | #### read 225 | 226 | Read `path` on device with endpoint name `endpoint`. The optional callback is given 227 | the two arguments `(err, res)`, where `res` is parsed using `schema`. 228 | 229 | Note: 230 | 231 | _If no schema is provided will return a `Buffer` if the payload is `TLV`-encoded 232 | or opaque, or an `String` otherwise._ 233 | 234 | **Parameters** 235 | 236 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 237 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** either an LWM2M Object instance or resource 238 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 239 | - `options.format` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** media type. (optional, default `'text'`) 240 | - `options.schema` **[Schema](#schema)** defining resources. 241 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 242 | 243 | **Examples** 244 | 245 | ```javascript 246 | var schema = Schema({ 247 | test: { id: 1, type: Number } 248 | }); 249 | 250 | var options = { 251 | schema: schema, 252 | format: 'json', 253 | }; 254 | 255 | server.read('test', '/1024/11', options, function(err, res) { 256 | assert(res.hasOwnProperty('test')); 257 | assert(typeof res.test == 'number'); 258 | }); 259 | ``` 260 | 261 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Buffer](https://nodejs.org/api/buffer.html) \| [number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number))>** a promise of the eventual value 262 | 263 | #### write 264 | 265 | Write `value` into `path` of device with endpoint name `endpoint`. 266 | For writing Object Instances, an schema is required. 267 | 268 | Note: 269 | 270 | _schemas can be globally added to `lwm2m.schemas`._ 271 | 272 | **Parameters** 273 | 274 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 275 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 276 | - `value` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) \| [Buffer](https://nodejs.org/api/buffer.html))** 277 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 278 | - `options.format` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** media type. (optional, default `'tlv'`) 279 | - `options.schema` **[Schema](#schema)?** schema to serialize value. 280 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 281 | 282 | **Examples** 283 | 284 | ```javascript 285 | var schema = Schema({ 286 | foo : { 287 | id: 5, 288 | type: 'String' 289 | }, 290 | bar : { 291 | id: 6, 292 | type: 'Number' 293 | }, 294 | }); 295 | 296 | var options = { 297 | schema: schema, 298 | format: 'json', 299 | }; 300 | 301 | var value = { 302 | foo: 'test', 303 | bar: 42, 304 | }; 305 | 306 | var promise = server.write('test', '/42/0', value, options) 307 | var promise = server.write('test', '/42/0/5', 'test') 308 | var promise = server.write('test', '/42/0/6', 42) 309 | 310 | // add schema for Object ID 42 globally. 311 | lwm2m.schemas[42] = schema; 312 | 313 | var promise = server.write('test', '/42/0', value) 314 | ``` 315 | 316 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 317 | 318 | #### execute 319 | 320 | Makes an Execute operation over the designed resource ID of the selected device. 321 | 322 | **Parameters** 323 | 324 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 325 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 326 | - `value` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 327 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** 328 | 329 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 330 | 331 | #### discover 332 | 333 | Execute a discover operation for the selected resource. 334 | 335 | **Parameters** 336 | 337 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 338 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 339 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** 340 | 341 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)>** a promise with an strng in link-format 342 | 343 | #### writeAttributes 344 | 345 | Write `attributes` into `path` of endpoint `endpoint`. 346 | 347 | **Parameters** 348 | 349 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 350 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 351 | - `attributes` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 352 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 353 | 354 | **Examples** 355 | 356 | ```javascript 357 | var attr = { 358 | "pmin": 5, 359 | "pmax": 10 360 | }; 361 | 362 | server.writeAttributes('dev0', '3303/0/5700', attr, function(err, res) { 363 | assert.ifError(err); 364 | }); 365 | ``` 366 | 367 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 368 | 369 | #### create 370 | 371 | Create a new LWM2M Object for `path`, where path is an Object ID. 372 | 373 | **Parameters** 374 | 375 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 376 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 377 | - `value` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) \| [Buffer](https://nodejs.org/api/buffer.html))** 378 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 379 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 380 | 381 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 382 | 383 | #### delete 384 | 385 | Deletes the LWM2M Object instance in `path` of endpoint `endpoint` 386 | 387 | **Parameters** 388 | 389 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 390 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 391 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 392 | 393 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 394 | 395 | #### observe 396 | 397 | Observe changes in `path` of device with endpoint name `endpoint`. 398 | The notification behaviour, e.g. periodic or event-triggered reporting, is configured with the 399 | `writeAttributes` method. The callback is given the two arguments `(err, stream)`, 400 | where `stream` is a `Readable Stream`. To stop receiving notifications `close()` the stream 401 | and (optionally) call `cancel()` on the same `endpoint` and `path` and . 402 | 403 | **Parameters** 404 | 405 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 406 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 407 | - `options` 408 | - `options.format` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** media type. (optional, default `'text'`) 409 | - `options.schema` **[Schema](#schema)** defining resources. 410 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 411 | 412 | **Examples** 413 | 414 | ```javascript 415 | server.observe('dev0', '/1024/10/1', function(err, stream) { 416 | stream.on('data', function(value) { 417 | console.log('new value %s', value); 418 | }); 419 | 420 | stream.on('end', function() { 421 | console.log('stopped observing'); 422 | }); 423 | }); 424 | ``` 425 | 426 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 427 | 428 | #### cancel 429 | 430 | Cancel an observation for `path` of device `endpoint`. 431 | 432 | **Parameters** 433 | 434 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 435 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 436 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 437 | 438 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 439 | 440 | ### bootstrap#Server 441 | 442 | **Extends EventEmitter** 443 | 444 | Server constructor. 445 | 446 | Events 447 | 448 | - `bootstrapRequest`: device bootstrap request. 449 | 450 | **Parameters** 451 | 452 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 453 | - `options.type` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** IPv4 (udp4) or IPv6 (udp6) connections (optional, default `'upd6'`) 454 | - `options.piggybackReplyMs` **[number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number)** milliseconds to wait for a piggyback response (optional, default `50`) 455 | 456 | **Examples** 457 | 458 | ```javascript 459 | var bootstrap = require('lwm2m').bootstrap; 460 | var server = bootstrap.createServer(); 461 | 462 | server.on('error', function(err) { 463 | throw err; 464 | }); 465 | 466 | server.on('close', function() { 467 | console.log('server is done'); 468 | }); 469 | 470 | server.on('bootstrapRequest', function(params, accept) { 471 | console.log('endpoint %s contains %s', params.ep, params.payload); 472 | accept(); 473 | }); 474 | 475 | // the default CoAP port is 5683 476 | server.listen(); 477 | ``` 478 | 479 | #### write 480 | 481 | Makes a Write operation over the designed resource ID of the selected device. 482 | 483 | **Parameters** 484 | 485 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 486 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 487 | - `value` **([Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) \| [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) \| [Number](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) \| [Buffer](https://nodejs.org/api/buffer.html))** 488 | - `options` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)?** 489 | - `options.format` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** media type. 490 | - `options.schema` **[Schema](#schema)** schema to serialize value when an object. 491 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 492 | 493 | **Examples** 494 | 495 | ```javascript 496 | var schema = Schema({ 497 | foo : { 498 | id: 5, 499 | type: 'String' 500 | }, 501 | bar : { 502 | id: 6, 503 | type: 'Number' 504 | }, 505 | }); 506 | 507 | var options = { 508 | schema: schema, 509 | format: 'json', 510 | }; 511 | 512 | var value = { 513 | foo: 'test', 514 | bar: 42, 515 | }; 516 | 517 | var promise = server.write('test', '/42/3', value, options) 518 | ``` 519 | 520 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 521 | 522 | #### delete 523 | 524 | Deletes the LWM2M Object instance in `path` of endpoint `endpoint` 525 | 526 | **Parameters** 527 | 528 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 529 | - `path` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 530 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 531 | 532 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 533 | 534 | #### finish 535 | 536 | Terminate the Bootstrap Sequence previously initiated 537 | 538 | **Parameters** 539 | 540 | - `endpoint` **[String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** client endpoint name 541 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)?** 542 | 543 | Returns **[Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)** 544 | 545 | ### Registry 546 | 547 | **Extends EventEmitter** 548 | 549 | Registry for clients. 550 | Default implementation is in-memory. 551 | 552 | For production use, extend `Registry` class and 553 | give new implementations to 554 | \_get, \_find, \_save, \_update and \_delete. 555 | 556 | See [examples](examples) for a MongoDB-backed registry. 557 | 558 | #### \_find 559 | 560 | get client by endpoint name 561 | 562 | **Parameters** 563 | 564 | - `endpoint` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 565 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** callback is given 566 | the two arguments `(err, client)` 567 | 568 | #### \_get 569 | 570 | get client by location in the registry 571 | 572 | **Parameters** 573 | 574 | - `location` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 575 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** callback is given 576 | the two arguments `(err, client)` 577 | 578 | #### \_save 579 | 580 | store a new client in the registry 581 | 582 | **Parameters** 583 | 584 | - `client` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 585 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** callback is given 586 | the two arguments `(err, location)` 587 | 588 | #### \_update 589 | 590 | update a client in the registry 591 | 592 | **Parameters** 593 | 594 | - `location` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 595 | - `params` **[Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object)** 596 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** callback is given 597 | the two arguments `(err, location)` 598 | 599 | #### \_delete 600 | 601 | delete client from the registry 602 | 603 | **Parameters** 604 | 605 | - `location` **[string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String)** 606 | - `callback` **[Function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function)** callback is given 607 | the two arguments `(err, client)` 608 | -------------------------------------------------------------------------------- /examples/battery-level.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // wakaama example client changing battery level over time: 4 | // $ ./lwm2mclient -4 -c 5 | var server = require('..').createServer(); 6 | 7 | server.on('register', function(params, accept) { 8 | setImmediate(function() { 9 | var ep = params.ep; 10 | var attr = { 11 | pmin: 1, 12 | pmax: 3, 13 | }; 14 | 15 | server 16 | .writeAttributes(ep, '/3/0/9', attr) 17 | .then(function() { 18 | return server.observe(ep, '/3/0/9'); 19 | }) 20 | .then(function(stream) { 21 | stream 22 | .on('data', function(value) { 23 | console.log('battery level: %d%', value); 24 | }); 25 | }) 26 | .catch(function(err) { 27 | console.log(err); 28 | }); 29 | }); 30 | accept(); 31 | }); 32 | 33 | server.listen(5683); 34 | -------------------------------------------------------------------------------- /examples/mongodb-registry.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Registry = require('..').Registry 4 | , MongoClient = require('mongodb').MongoClient 5 | , ObjectId = require('mongodb').ObjectId 6 | , errors = require('../lib/errors'); 7 | 8 | function MongoRegistry(url) { 9 | Registry.call(this); 10 | 11 | var _this = this; 12 | MongoClient.connect(url, function(err, db) { 13 | if (err) { 14 | _this.emit('error', err); 15 | db.close(); 16 | } else { 17 | _this.clients = db.collection('clients'); 18 | _this.clients.createIndex({ expires: 1 }, { expireAfterSeconds: 0 }); 19 | } 20 | }); 21 | } 22 | 23 | module.exports = MongoRegistry; 24 | MongoRegistry.prototype = Object.create(Registry.prototype); 25 | MongoRegistry.prototype.constructor = MongoRegistry; 26 | 27 | MongoRegistry.prototype._find = function(endpoint, callback) { 28 | this.clients.find({ ep: endpoint }).limit(1).next(function(err, result) { 29 | if (err) { 30 | callback(err); 31 | } else if (result === null) { 32 | callback(new errors.DeviceNotFound()); 33 | } else { 34 | callback(null, Object.assign({ location: result._id }, result)); 35 | } 36 | }); 37 | }; 38 | 39 | MongoRegistry.prototype._get = function(location, callback) { 40 | this.clients.find({ _id: ObjectId(location) }) 41 | .limit(1).next(function(err, result) { 42 | if (err) { 43 | callback(err); 44 | } else if (result === null) { 45 | callback(new errors.DeviceNotFound()); 46 | } else { 47 | callback(null, Object.assign({ location: result._id }, result)); 48 | } 49 | }); 50 | }; 51 | 52 | MongoRegistry.prototype._save = function(params, callback) { 53 | var client = Object.assign({ 54 | expires: new Date(Date.now() + (params.lt || 86400) * 1e3), 55 | }, params); 56 | 57 | this.clients.insertOne(client, function(err, result) { 58 | if (err) { 59 | callback(err); 60 | } else { 61 | callback(null, result.insertedId); 62 | } 63 | }); 64 | }; 65 | 66 | MongoRegistry.prototype._update = function(location, params, callback) { 67 | var _this = this; 68 | 69 | this.get(location) 70 | .then(function(result) { 71 | var client = Object.assign(result, params, { 72 | expires: new Date(Date.now() + (params.lt || result.lt) * 1e3), 73 | }); 74 | 75 | return _this.clients.updateOne({ 76 | _id: ObjectId(location), 77 | }, { 78 | $set: client, 79 | }); 80 | }) 81 | .then(function() { 82 | callback(null, location); 83 | }) 84 | .catch(callback); 85 | }; 86 | 87 | MongoRegistry.prototype._delete = function(location, callback) { 88 | this.clients.findOneAndDelete({ 89 | _id: ObjectId(location), 90 | }, function(err, result) { 91 | if (err) { 92 | callback(err); 93 | } else if (result.value === null) { 94 | callback(new errors.DeviceNotFound()); 95 | } else { 96 | callback(null, result.value); 97 | } 98 | }); 99 | }; 100 | 101 | 102 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "scripts": {}, 4 | "devDependencies": { 5 | "mongodb": "^3.2.7" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | var MongoRegistry = require('./mongodb-registry'); 4 | 5 | // Connection URL 6 | var url = 'mongodb://localhost:27017/lwm2m2'; 7 | 8 | var server = require('..').createServer({ 9 | registry: new MongoRegistry(url), 10 | }); 11 | 12 | server.on('error', function(err) { 13 | console.log(err.message); 14 | 15 | process.exit(1); 16 | }); 17 | 18 | server.on('register', function(params, accept) { 19 | console.log('client registration'); 20 | 21 | accept(); 22 | }); 23 | 24 | server.on('update', function(location) { 25 | console.log('client update'); 26 | 27 | server._registry.get(location) 28 | .then(console.log, console.log); 29 | }); 30 | 31 | server.on('deregister', function(client) { 32 | console.log('client deregistered'); 33 | 34 | console.log(client); 35 | }); 36 | 37 | server.listen(5683); 38 | -------------------------------------------------------------------------------- /examples/thermostat.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | var Schema = require('../lib/schema'); 3 | var senml = require('../lib/senml'); 4 | 5 | // IPSO temperature sensor (3303) 6 | var temperature = Schema({ 7 | value: { type: 'Float', id: 5700, required: true }, 8 | units: { type: 'String', id: 5701 }, 9 | }); 10 | 11 | // IPSO actuation (3306) 12 | var actuation = Schema({ 13 | onOff: { type: 'Boolean', id : 5850, required: true }, 14 | dimmer: { type: 'Integer', id: 5851, range: { min: 0, max: 100 } }, 15 | }); 16 | 17 | // IPSO setpoint (3308) 18 | var setPoint = Schema({ 19 | value: { type: 'Float', id: 5900, required: true }, 20 | units: { type: 'String', id: 5701 }, 21 | }); 22 | 23 | // composite schema 24 | var thermostat = Schema({ 25 | input: { 26 | type: 'Objlnk', 27 | id: 7100, 28 | schema: temperature, 29 | }, 30 | setpoint: { 31 | type: 'Objlnk', 32 | id: 7101, 33 | schema: setPoint, 34 | }, 35 | output: { 36 | type: 'Objlnk', 37 | id: 7102, 38 | schema: actuation, 39 | }, 40 | }); 41 | 42 | var sample = '{"bn":"/","e":[' + 43 | '{"n":"8300/0/7100","ov":"3303:0"},' + 44 | '{"n":"8300/0/7101","ov":"3308:0"},' + 45 | '{"n":"8300/0/7102","ov":"3306:0"},' + 46 | '{"n":"3303/0/5700","v":26.5},' + 47 | '{"n":"3303/0/5701","sv":"Cel"},' + 48 | '{"n":"3306/0/5850","bv":true},' + 49 | '{"n":"3306/0/5851","v":50},' + 50 | '{"n":"3308/0/5900","v":24},' + 51 | '{"n":"3308/0/5701","sv":"Cel"}]}'; 52 | 53 | var result = senml.parse(sample, thermostat); 54 | console.log(result); 55 | //{ input: { value: 26.5, units: 'Cel' }, 56 | // setpoint: { value: 24, units: 'Cel' }, 57 | // output: { onOff: true, dimmer: 50 } } 58 | 59 | -------------------------------------------------------------------------------- /greenkeeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "default": { 4 | "packages": [ 5 | "examples/package.json", 6 | "package.json" 7 | ] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/contentFormats.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | var text = 'text/plain'; 25 | var link = 'application/link-format'; 26 | var opaque = 'application/octet-stream'; 27 | var tlv = 'application/vnd.oma.lwm2m+tlv'; 28 | var json = 'application/vnd.oma.lwm2m+json'; 29 | 30 | var formats = [ 31 | { name: text, value : 0 }, 32 | { name: link, value : 40 }, 33 | { name: opaque, value : 42 }, 34 | { name: tlv, value : 11542 }, 35 | { name: json, value : 11543 }, 36 | ]; 37 | 38 | module.exports = { 39 | text: text, 40 | link: link, 41 | opaque: opaque, 42 | tlv: tlv, 43 | json: json, 44 | formats: formats, 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | module.exports = { 27 | BadRequestError: function(message) { 28 | this.name = 'BAD_REQUEST_ERROR'; 29 | this.message = 'The request was not build correctly: ' + message; 30 | this.code = '4.00'; 31 | }, 32 | DeviceNotFound: function() { 33 | this.name = 'DEVICE_NOT_FOUND'; 34 | this.message = 'Device not found'; 35 | this.code = '4.04'; 36 | }, 37 | ObjectNotFound: function(path) { 38 | this.name = 'OBJECT_NOT_FOUND'; 39 | this.message = 'The resource @' + path + ' was not found'; 40 | this.code = '4.04'; 41 | }, 42 | UnsupportedAttributes: function () { 43 | this.name = 'UNSUPPORTED_ATTRIBUTES'; 44 | this.message = 'Unsupported attributes'; 45 | this.code = '4.00'; 46 | }, 47 | ClientError: function(code) { 48 | this.name = 'CLIENT_ERROR'; 49 | this.message = 'Error code recieved from the client: ' + code; 50 | this.code = code; 51 | }, 52 | ClientConnectionError: function(msg) { 53 | this.name = 'CLIENT_CONNECTION_ERROR'; 54 | this.message = 'There was an error sending a request to the client: ' + msg; 55 | this.code = '5.01'; 56 | }, 57 | ClientResponseError: function(msg) { 58 | this.name = 'CLIENT_RESPONSE_ERROR'; 59 | this.message = 'Error received while waiting for a client response: ' + msg; 60 | this.code = '5.01'; 61 | }, 62 | ForbiddenError: function(msg) { 63 | this.name = 'FORBIDDEN_ERROR'; 64 | this.message = msg; 65 | this.code = '4.03'; 66 | }, 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var coap = require('coap'); 27 | var formats = require('./contentFormats').formats; 28 | var Schema = require('./schema'); 29 | 30 | /*! 31 | * register LWM2M numeric content-formats 32 | */ 33 | formats.forEach(function(format) { 34 | coap.registerFormat(format.name, format.value); 35 | }); 36 | 37 | /** 38 | * Schemas for OMA-defined objects. 39 | * See [oma](lib/oma). 40 | * @type {Array} 41 | */ 42 | exports.schemas = [ 43 | Schema(require('./oma/security.json')), 44 | Schema(require('./oma/server.json')), 45 | Schema(require('./oma/acl.json')), 46 | Schema(require('./oma/device.json')), 47 | , 48 | , 49 | Schema(require('./oma/location.json')), 50 | ]; 51 | 52 | /** 53 | * @returns {Server} object 54 | */ 55 | exports.createServer = require('./server'); 56 | exports.bootstrap = require('./server/bootstrap'); 57 | exports.Registry = require('./server/registry'); 58 | exports.Schema = Schema; 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /lib/oma/acl.json: -------------------------------------------------------------------------------- 1 | { 2 | "objectId": { 3 | "type": "Integer", 4 | "id": 0, 5 | "range": { 6 | "min": 1, 7 | "max": 65534 8 | }, 9 | "required": true 10 | }, 11 | "instanceId": { 12 | "type": "Integer", 13 | "id": 1, 14 | "range": { 15 | "min": 0, 16 | "max": 65535 17 | }, 18 | "required": true 19 | }, 20 | "acl": { 21 | "type": ["Integer"], 22 | "id": 2 23 | }, 24 | "owner": { 25 | "type": "Integer", 26 | "id": 3, 27 | "range": { 28 | "min": 0, 29 | "max": 65535 30 | }, 31 | "required": true 32 | } 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /lib/oma/device.json: -------------------------------------------------------------------------------- 1 | { 2 | "manufacturer": { 3 | "type": "String", 4 | "id": 0 5 | }, 6 | "deviceType": { 7 | "type": "String", 8 | "id": 17 9 | }, 10 | "modelNumber": { 11 | "type": "String", 12 | "id": 1 13 | }, 14 | "serialNumber": { 15 | "type": "String", 16 | "id": 2 17 | }, 18 | "hardwareVer": { 19 | "type": "String", 20 | "id": 18 21 | }, 22 | "firmwareVer": { 23 | "type": "String", 24 | "id": 3 25 | }, 26 | "softwareVer": { 27 | "type": "String", 28 | "id": 19 29 | }, 30 | "powerSrcs": { 31 | "type": ["Integer"], 32 | "id": 6 33 | }, 34 | "srcVoltage": { 35 | "type": ["Integer"], 36 | "id": 7 37 | }, 38 | "srcCurrent": { 39 | "type": ["Integer"], 40 | "id": 8 41 | }, 42 | "batteryLevel": { 43 | "type": "Integer", 44 | "id": 9 45 | }, 46 | "memoryFree": { 47 | "type": "Integer", 48 | "id": 10 49 | }, 50 | "errorCode": { 51 | "type": ["Integer"], 52 | "id": 11 53 | }, 54 | "currentTime": { 55 | "type": "Time", 56 | "id": 13 57 | }, 58 | "utcOffset": { 59 | "type": "String", 60 | "id": 14 61 | }, 62 | "timeZone": { 63 | "type": "String", 64 | "id": 15 65 | }, 66 | "binding": { 67 | "type": "String", 68 | "id": 16, 69 | "enum": [ 70 | "U", 71 | "UQ", 72 | "S", 73 | "SQ", 74 | "US", 75 | "UQS" 76 | ] 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /lib/oma/location.json: -------------------------------------------------------------------------------- 1 | { 2 | "latitude": { 3 | "type": "Float", 4 | "id": 0, 5 | "required": true 6 | }, 7 | "longitude": { 8 | "type": "Float", 9 | "id": 1, 10 | "required": true 11 | }, 12 | "altitude": { 13 | "type": "Float", 14 | "id": 2 15 | }, 16 | "radius": { 17 | "type": "Float", 18 | "id": 3 19 | }, 20 | "velocity": { 21 | "type": "Opaque", 22 | "id": 4 23 | }, 24 | "timestamp": { 25 | "type": "Time", 26 | "id": 5, 27 | "required": true 28 | }, 29 | "speed": { 30 | "type": "Float", 31 | "id": 6 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/oma/security.json: -------------------------------------------------------------------------------- 1 | { 2 | "uri": { 3 | "type": "String", 4 | "id": 0, 5 | "required": true 6 | }, 7 | "bootstrap": { 8 | "type": "Boolean", 9 | "id": 1, 10 | "required": true 11 | }, 12 | "mode": { 13 | "type": "Integer", 14 | "id": 2, 15 | "enum": [ 16 | 0, 17 | 1, 18 | 2, 19 | 3 20 | ], 21 | "required": true 22 | }, 23 | "clientCert": { 24 | "type": "Opaque", 25 | "id": 3, 26 | "required": true 27 | }, 28 | "serverId": { 29 | "type": "Integer", 30 | "id": 10, 31 | "range": { 32 | "min": 1, 33 | "max": 65535 34 | }, 35 | "required": true 36 | }, 37 | "serverCert": { 38 | "type": "Opaque", 39 | "id": 4, 40 | "required": false 41 | }, 42 | "secretKey": { 43 | "type": "Opaque", 44 | "id": 5, 45 | "required": true 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /lib/oma/server.json: -------------------------------------------------------------------------------- 1 | { 2 | "serverId": { 3 | "type": "Integer", 4 | "id": 0, 5 | "range": { 6 | "min": 1, 7 | "max": 65535 8 | }, 9 | "required": true 10 | }, 11 | "lifetime": { 12 | "type": "Integer", 13 | "id": 1, 14 | "required": true 15 | }, 16 | "notifStoring": { 17 | "type": "Boolean", 18 | "id": 6, 19 | "required": true 20 | }, 21 | "binding": { 22 | "type": "String", 23 | "id": 7, 24 | "enum": [ 25 | "U", 26 | "UQ", 27 | "S", 28 | "SQ", 29 | "US", 30 | "UQS" 31 | ], 32 | "required": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U 4 | * 5 | * This file is part of node-lwm2m 6 | * 7 | * node-lwm2m is free software: you can redistribute it and/or 8 | * modify it under the terms of the GNU Affero General Public License as 9 | * published by the Free Software Foundation, either version 3 of the License, 10 | * or (at your option) any later version. 11 | * 12 | * node-lwm2m is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 15 | * See the GNU Affero General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Affero General Public 18 | * License along with node-lwm2m. 19 | * If not, seehttp://www.gnu.org/licenses/. 20 | * 21 | * For those usages not covered by the GNU Affero General Public License 22 | * please contact with::[contacto@tid.es] 23 | */ 24 | 25 | 'use strict'; 26 | 27 | var debug = require('debug')('lwm2m'); 28 | var Window = require('./slidingWindow'); 29 | var url = require('url'); 30 | 31 | module.exports = function(options) { 32 | var routes = options.routes; 33 | var window = new Window(options.udpWindow); 34 | 35 | return function(req, res) { 36 | if (req.code === '0.00' && req._packet.confirmable && 37 | req.payload.length === 0) { 38 | return res.reset(); 39 | } 40 | 41 | var mid = req._packet.messageId; 42 | 43 | if (window.contains(mid)) { 44 | debug('Discarding duplicate message'); 45 | return; 46 | } 47 | 48 | window.push(mid); 49 | 50 | var method = req.method; 51 | var pathname = url.parse(req.url).pathname; 52 | var middlewares = []; 53 | var result = Promise.resolve(); 54 | 55 | routes.forEach(function(route) { 56 | if (method === route[0] && pathname.match(route[1])) { 57 | middlewares.push(route[2]); 58 | } 59 | }); 60 | 61 | if (middlewares.length === 0) { 62 | res.code = '4.04'; 63 | res.end(); 64 | return; 65 | } 66 | 67 | middlewares.forEach(function(m) { 68 | result = result.then(function() { 69 | return m(req, res); 70 | }); 71 | }); 72 | 73 | result.catch(function(err) { 74 | res.code = err.code || '4.00'; // TODO should be 5.00 75 | debug(err.message); 76 | res.end(); 77 | }); 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /lib/schema.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | function _matches(val, type) { 28 | function typeOf(val) { 29 | return Object.prototype.toString.call(val).slice(8, -1); 30 | } 31 | 32 | type = type.name || type; 33 | 34 | if (Array.isArray(val) && Array.isArray(type)) { 35 | return _matches(val[0], type[0]); 36 | } 37 | 38 | switch(type) { 39 | case 'Opaque': 40 | return Buffer.isBuffer(val); 41 | case 'Time': 42 | return 'Date' === typeOf(val); 43 | case 'Integer': 44 | case 'Float': 45 | return 'Number' === typeOf(val); 46 | default: 47 | return type === typeOf(val); 48 | } 49 | } 50 | 51 | function _enum(values) { 52 | return function(val) { 53 | return values.indexOf(val) !== -1; 54 | }; 55 | } 56 | 57 | function _range(values) { 58 | return function(val) { 59 | return values.min <= val && val <= values.max; 60 | }; 61 | } 62 | 63 | function _validateResource(res, val) { 64 | var ok = _matches(val, res.type); 65 | var check; 66 | 67 | if (res.enum) { 68 | check = _enum(res.enum); 69 | } else if (res.range) { 70 | check = _range(res.range); 71 | } 72 | 73 | if (ok && check !== undefined) { 74 | return check(val); 75 | } 76 | 77 | return ok; 78 | } 79 | 80 | function _validateSchema(schema, Schema) { 81 | function _validResourceType(type) { 82 | if (Array.isArray(type)) { 83 | type = type[0]; 84 | } 85 | 86 | type = type.name || type; 87 | 88 | return [ 89 | 'String', 90 | 'Integer', 91 | 'Float', 92 | 'Boolean', 93 | 'Opaque', 94 | 'Time', 95 | 'Objlnk', 96 | ].indexOf(type) > -1; 97 | } 98 | 99 | var keys = Object.keys(schema); 100 | 101 | for (var i = 0; i < keys.length; ++i) { 102 | 103 | var res = schema[keys[i]]; 104 | 105 | var ok = typeof res === 'object' 106 | && typeof res.id === 'number' 107 | && _validResourceType(res.type); 108 | 109 | if (res.range && typeof res.range !== 'object') { 110 | ok = false; 111 | } 112 | 113 | if (res.enum && !Array.isArray(res.enum)) { 114 | ok = false; 115 | } 116 | 117 | if (res.type === 'Objlnk' 118 | && !(res.schema instanceof Schema)) { 119 | if (typeof res.schema === 'object') { 120 | res.schema = new Schema(res.schema); 121 | } else { 122 | ok = false; 123 | } 124 | } 125 | 126 | if (!ok) { 127 | throw new TypeError('Bad definiton `' + keys[i] + '`'); 128 | } 129 | } 130 | } 131 | 132 | function _ids(schema) { 133 | return Object.keys(schema).reduce(function(acc, key) { 134 | var id = schema[key].id; 135 | acc[id] = key; 136 | return acc; 137 | }, {}); 138 | } 139 | 140 | /** 141 | * Schema resource type definition 142 | * @typedef {Object} Resource 143 | * @property{('String'|'Integer'|'Float'|'Boolean'|'Opaque'|'Time'|['type'])} type 144 | * @property {number} id Resource ID 145 | * @property {boolean} [required] resource is mandatory. Defaults to `false` 146 | * @property {Array} [enum] 147 | * @property {object} [range] 148 | * @property {number} range.min 149 | * @property {number} range.max 150 | */ 151 | 152 | /** 153 | * Schema constructor. 154 | * 155 | * An `Schema` describes the shape of an LwM2M Object and the type of its resources. 156 | * Schemas are used throghout the API for generating/parsing payloads from/to JavaScript values. 157 | * 158 | * See [oma](lib/oma) directory for default definitions. 159 | * See also [thermostat.js](examples/thermostat.js) for an 160 | * example of a composite schema. 161 | * 162 | * **Note** 163 | * 164 | * *LwM2M types will be coerced to JavaScript types and viceversa, e.g. `Time` will return a `Date()`, 165 | * `Opaque` a `Buffer()`, and `Integer`/`Float` a number.* 166 | * 167 | * @example 168 | * 169 | * // IPSO light controller 170 | * var lightControlSchema = new Schema({ 171 | * onOff: { 172 | * type: 'Boolean', 173 | * id : 5850, 174 | * required: true 175 | * }, 176 | * dimmer: { 177 | * type: 'Integer', 178 | * id: 5851, 179 | * range: { min: 0, max: 100 } 180 | * }, 181 | * units: { 182 | * type: 'String', 183 | * id: 5701, 184 | * } 185 | * }); 186 | * 187 | * // an object literal matching the schema above 188 | * var lightControl = { 189 | * onOff: true, 190 | * dimmer: 40, 191 | * } 192 | * 193 | * // Bad schema 194 | * var schema = new Schema({ 195 | * a: { type: 'String', id: 0 }, 196 | * b: { type: 'Error', id: 1 }, 197 | * }); // throws TypeError 198 | * 199 | * @param {Object.} resources 200 | * @throws Will throw an error if fails to validate 201 | */ 202 | function Schema(resources) { 203 | if (!(this instanceof Schema)) { 204 | return new Schema(resources); 205 | } 206 | 207 | this.resources = Object.assign({}, resources); 208 | 209 | _validateSchema(this.resources, Schema); 210 | this.id = _ids(this.resources); 211 | 212 | if (this.constructor === Schema) { 213 | Object.freeze(this.resources); 214 | } 215 | } 216 | 217 | /** 218 | * validates `obj` with `schema`. 219 | * 220 | * @param {Object} obj 221 | * @throws Will throw an error if fails to validate 222 | * @example 223 | * 224 | * var schema = new Schema({ 225 | * a: { type: String, id: 0 }, 226 | * b: { type: Buffer, id: 1 }, 227 | * }); 228 | * 229 | * schema.validate({ 230 | * a: 'foo', 231 | * b: Buffer.from('bar'), 232 | * }); // OK 233 | * 234 | * schema.validate({ 235 | * a: 'foo', 236 | * b: 'bar', 237 | * }); // Throws error 238 | * 239 | */ 240 | Schema.prototype.validate = function(obj) { 241 | var keys, ok, key, res, val; 242 | 243 | keys = Object.keys(obj); 244 | 245 | if (!keys.length) { 246 | throw new TypeError('Invalid object: `' + obj + '`'); 247 | } 248 | 249 | if (keys.length > 1) { // not single resource 250 | keys = Object.keys(this.resources); 251 | } 252 | 253 | for (var i = 0; i < keys.length; ++i) { 254 | key = keys[i]; 255 | 256 | if (!this.resources[key]) { 257 | throw new TypeError('Invalid resource: `' + key + '`'); 258 | } 259 | 260 | res = Object.assign({ required: false }, 261 | this.resources[key]); 262 | val = obj[key]; 263 | 264 | ok = _validateResource(res, val); 265 | 266 | if (val !== undefined && !ok) { 267 | throw new TypeError('Invalid resource: `' + key + '`'); 268 | } 269 | 270 | if (res.required && !ok) { 271 | throw new TypeError('Missing resource: `' + key + '`'); 272 | } 273 | } 274 | }; 275 | 276 | Schema.prototype.inspect = function() { 277 | return this.resources; 278 | }; 279 | 280 | Schema.prototype.setBasename = function(bn) { 281 | return this.basename = bn.replace(/\/?(\d+\/\d+)\/?/, '/$1/'); 282 | }; 283 | 284 | module.exports = Schema; 285 | -------------------------------------------------------------------------------- /lib/senml.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | * 23 | */ 24 | 25 | 'use strict'; 26 | 27 | var debug = require('debug')('lwm2m'); 28 | var utils = require('./utils'); 29 | 30 | function serializeToString(obj, schema) { 31 | var result = { e: [] }, 32 | keys = Object.keys(obj); 33 | 34 | schema.validate(obj); 35 | 36 | function append(arr, key, value, type) { 37 | var types = { 38 | String: function() { 39 | arr.push({ n: key, sv: value }); 40 | }, 41 | Integer: function() { 42 | arr.push({ n: key, v: value }); 43 | }, 44 | Float: function() { 45 | arr.push({ n: key, v: value }); 46 | }, 47 | Boolean: function() { 48 | arr.push({ n: key, bv: value }); 49 | }, 50 | Opaque: function() { 51 | arr.push({ 52 | n: key, 53 | sv: value.toString('base64'), 54 | }); 55 | }, 56 | Time: function() { 57 | var timestamp = value.getTime() / 1e3 >> 0; 58 | arr.push({ n: key, v: timestamp }); 59 | }, 60 | }; 61 | 62 | function skip () { 63 | debug('Skipping resource with invalid type %s', typeof value); 64 | } 65 | 66 | if (Array.isArray(type)) { 67 | value.forEach(function(val, i) { 68 | append(arr, key + '/' + i, val, type[0]); 69 | }); 70 | return arr; 71 | } 72 | 73 | (types[type] || skip)(); 74 | 75 | return arr; 76 | } 77 | 78 | for (var i = 0; i < keys.length; ++i) { 79 | var key = keys[i], 80 | res = schema.resources[key]; 81 | 82 | // skip resources not defined in schema 83 | if (!res) { 84 | debug('Resource not defined in schema: %s', key); 85 | continue; 86 | } 87 | 88 | append(result.e, String(res.id), obj[key], res.type); 89 | } 90 | 91 | if (schema.basename) { 92 | result.bn = schema.basename; 93 | } 94 | 95 | return JSON.stringify(result); 96 | } 97 | 98 | function parse(payload, schema) { 99 | var result = {}, 100 | obj = {}; 101 | 102 | function append(obj, key, attr, data, i) { 103 | var types = { 104 | String: function() { 105 | obj[key] = data[i].sv; 106 | }, 107 | Integer: function() { 108 | obj[key] = data[i].v; 109 | }, 110 | Float: function() { 111 | obj[key] = data[i].v; 112 | }, 113 | Boolean: function() { 114 | obj[key] = data[i].bv; 115 | }, 116 | Opaque: function() { 117 | obj[key] = Buffer.from(data[i].sv, 'base64'); 118 | }, 119 | Time: function() { 120 | obj[key] = new Date(data[i].v * 1e3); 121 | }, 122 | Objlnk: function() { 123 | var path = data[i].ov.split(':').join('/'); 124 | var elems = data.filter(function(elem) { 125 | return elem.n.startsWith(path); 126 | }); 127 | var inSchema = attr.schema; 128 | obj[key] = {}; 129 | 130 | elems.forEach(function(elem, j) { 131 | var inKey = inSchema.id[elem.n.split('/')[2]]; 132 | 133 | // skip resources not defined in schema. 134 | if (!inKey) { 135 | return; 136 | } 137 | 138 | append(obj[key], inKey, inSchema.resources[inKey], elems, j); 139 | }); 140 | }, 141 | }; 142 | 143 | if (Array.isArray(attr.type)) { 144 | var id = data[i].n.split('/').slice(-1)[0]; 145 | 146 | if (!obj[key]) { 147 | obj[key] = []; 148 | } 149 | 150 | append(obj[key], id, { type: attr.type[0] }, data, i); 151 | } else { 152 | (types[attr.type])(); 153 | } 154 | 155 | if (obj[key] === undefined) { 156 | throw new Error('JSON payload does not match ' + 157 | schema.name + ' definition'); 158 | } 159 | 160 | return obj; 161 | } 162 | 163 | obj = JSON.parse(payload.toString('utf8')); 164 | 165 | if (!Array.isArray(obj.e)) { 166 | throw new Error('Invalid JSON payload'); 167 | } 168 | 169 | var offset = 0; 170 | 171 | if (obj.bn) { 172 | if (obj.bn === '/') { 173 | offset = 2; 174 | } else if (utils.isObject(obj.bn)) { 175 | offset = 1; 176 | } else if (utils.isInstance(obj.bn)) { 177 | offset = 0; 178 | } else { 179 | throw new Error('Invalid JSON payload'); 180 | } 181 | } 182 | 183 | for (var i = 0; i < obj.e.length; ++i) { 184 | var key = schema.id[obj.e[i].n.split('/')[offset]]; 185 | 186 | // skip resources not defined in schema. 187 | if (!key) { 188 | continue; 189 | } 190 | 191 | append(result, key, schema.resources[key], obj.e, i); 192 | } 193 | 194 | return result; 195 | } 196 | 197 | exports.serialize = serializeToString; 198 | exports.parse = parse; 199 | -------------------------------------------------------------------------------- /lib/server/bootstrap/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var coap = require('coap'); 27 | var debug = require('debug')('lwm2m'); 28 | var Router = require('../../router'); 29 | var CoapServer = coap.createServer; 30 | var Registry = require('../registry'); 31 | var utils = require('../../utils'); 32 | var Server = require('../'); 33 | var errors = require('../../errors'); 34 | 35 | 36 | function defaultOptions(options) { 37 | return Object.assign({ 38 | udpWindow: 100, 39 | type : 'udp6', 40 | }, options || {}, { proxy: true }); 41 | } 42 | 43 | /** 44 | * Server constructor. 45 | * 46 | * Events 47 | * 48 | * - `bootstrapRequest`: device bootstrap request. 49 | * 50 | * @example 51 | * 52 | * var bootstrap = require('lwm2m').bootstrap; 53 | * var server = bootstrap.createServer(); 54 | * 55 | * server.on('error', function(err) { 56 | * throw err; 57 | * }); 58 | * 59 | * server.on('close', function() { 60 | * console.log('server is done'); 61 | * }); 62 | * 63 | * server.on('bootstrapRequest', function(params, accept) { 64 | * console.log('endpoint %s contains %s', params.ep, params.payload); 65 | * accept(); 66 | * }); 67 | * 68 | * // the default CoAP port is 5683 69 | * server.listen(); 70 | * 71 | * @name bootstrap#Server 72 | * @augments EventEmitter 73 | * @param {Object} [options] 74 | * @param {string} [options.type='upd6'] - IPv4 (udp4) or IPv6 (udp6) connections 75 | * @param {number} [options.piggybackReplyMs=50] - milliseconds to wait for a piggyback response 76 | * 77 | */ 78 | function BServer(options) { 79 | if (!(this instanceof BServer)) { 80 | return new BServer(options); 81 | } 82 | 83 | var opts = defaultOptions(options); 84 | CoapServer.call(this, opts); 85 | 86 | this._registry = new Registry(); 87 | 88 | var router = Router({ 89 | udpWindow: opts.udpWindow, 90 | routes: [ 91 | [ 'POST', /\/bs$/, this._validate.bind(this) ], 92 | [ 'POST', /\/bs$/, 93 | require('./request')(this._registry), 94 | ], 95 | ], 96 | }); 97 | 98 | this.on('request', router); 99 | } 100 | 101 | BServer.prototype = Object.create(CoapServer.prototype, { 102 | bootstrap: { 103 | value: true, 104 | }, 105 | }); 106 | 107 | BServer.prototype.constructor = BServer; 108 | 109 | BServer.prototype._validate = function(req, res) { 110 | var query = utils.query(req); 111 | var params = Object.assign({}, query, { 112 | payload: req.payload.toString(), 113 | }); 114 | 115 | var _this = this; 116 | 117 | return new Promise(function(resolve, reject) { 118 | if (utils.validateQuery(query, ['ep'], [])) { 119 | _this.emit('bootstrapRequest', params, function(err) { 120 | if (err) { 121 | reject(err); 122 | } else { 123 | resolve(); 124 | } 125 | }); 126 | } else { 127 | reject(new errors.BadRequestError()); 128 | } 129 | }); 130 | }; 131 | 132 | BServer.prototype._request = Server.prototype._request; 133 | 134 | /** 135 | * Makes a Write operation over the designed resource ID of the selected device. 136 | * @memberof bootstrap#Server 137 | * @instance 138 | * @param {String} endpoint - client endpoint name 139 | * @param {String} path 140 | * @param {Object|String|Number|Buffer} value 141 | * @param {Object} [options] 142 | * @param {string} options.format - media type. 143 | * @param {Schema} options.schema - schema to serialize value when an object. 144 | * @param {Function} [callback] 145 | * @return {Promise} 146 | * @example 147 | * 148 | * var schema = Schema({ 149 | * foo : { 150 | * id: 5, 151 | * type: 'String' 152 | * }, 153 | * bar : { 154 | * id: 6, 155 | * type: 'Number' 156 | * }, 157 | * }); 158 | * 159 | * var options = { 160 | * schema: schema, 161 | * format: 'json', 162 | * }; 163 | * 164 | * var value = { 165 | * foo: 'test', 166 | * bar: 42, 167 | * }; 168 | * 169 | * var promise = server.write('test', '/42/3', value, options) 170 | * 171 | */ 172 | BServer.prototype.write = Server.prototype.write; 173 | 174 | /** 175 | * Deletes the LWM2M Object instance in `path` of endpoint `endpoint` 176 | * 177 | * @memberof bootstrap#Server 178 | * @instance 179 | * @param {String} endpoint - client endpoint name 180 | * @param {String} path 181 | * @param {Function} [callback] 182 | * @return {Promise} 183 | */ 184 | BServer.prototype.delete = Server.prototype.delete; 185 | 186 | /** 187 | * Terminate the Bootstrap Sequence previously initiated 188 | * 189 | * @memberof bootstrap#Server 190 | * @instance 191 | * @param {String} endpoint - client endpoint name 192 | * @param {Function} [callback] 193 | * @return {Promise} 194 | */ 195 | BServer.prototype.finish = function(endpoint, callback) { 196 | var _this = this; 197 | 198 | var processResponse = function(res) { 199 | return new Promise(function(resolve, reject) { 200 | if (res.code === '2.04') { 201 | return _this._registry.find(endpoint) 202 | .then(function(client) { 203 | return _this._registry.unregister(client.location); 204 | }) 205 | .then(function() { 206 | resolve(res.payload.toString('utf8')); 207 | }); 208 | } else { 209 | reject(new errors.ClientError(res.code)); 210 | } 211 | }); 212 | }; 213 | 214 | debug('Bootstrap finish request on device [%s]', endpoint); 215 | 216 | var promise = _this._request({ 217 | endpoint: endpoint, 218 | method: 'POST', 219 | pathname: '/bs', 220 | }) 221 | .then(processResponse); 222 | 223 | if (callback) { 224 | utils.invoke(callback, promise); 225 | } 226 | 227 | return promise; 228 | }; 229 | 230 | /** 231 | * @returns {bootstrap#Server} object 232 | * @alias bootstrap#createServer 233 | */ 234 | module.exports.createServer = BServer; 235 | 236 | -------------------------------------------------------------------------------- /lib/server/bootstrap/request.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var utils = require('../../utils'); 27 | 28 | module.exports = function(registry) { 29 | return function(req, res) { 30 | return registry.register(Object.assign({}, 31 | utils.query(req), 32 | req.rsinfo) 33 | ) 34 | .then(function() { 35 | res.code = '2.04'; 36 | res.end(); 37 | }); 38 | }; 39 | }; 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/server/decodestream.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var utils = require('../utils'); 27 | var Transform = require('readable-stream').Transform; 28 | 29 | function DecodeStream(contentType, schema) { 30 | this.contentType = contentType; 31 | this.schema = schema; 32 | Transform.call(this, { readableObjectMode: true }); 33 | } 34 | 35 | DecodeStream.prototype = Object.create(Transform.prototype); 36 | DecodeStream.prototype.constructor = DecodeStream; 37 | 38 | DecodeStream.prototype._transform = function(chunk, encoding, callback) { 39 | var data = utils.parsePayload(chunk, this.contentType, this.schema); 40 | this.push(data); 41 | callback(); 42 | }; 43 | 44 | DecodeStream.prototype.close = function() { 45 | this.push(null); 46 | this.emit('close'); 47 | }; 48 | 49 | module.exports = DecodeStream; 50 | -------------------------------------------------------------------------------- /lib/server/deregister.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var utils = require('../utils'); 27 | var url = require('url'); 28 | 29 | module.exports = function(registry) { 30 | return function(req, res) { 31 | var path = url.parse(req.url).pathname; 32 | var location = utils.splitPath(path).pop(); 33 | 34 | return registry.unregister(location) 35 | .then(function() { 36 | res.code = '2.02'; 37 | res.end(); 38 | }); 39 | }; 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /lib/server/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var coap = require('coap'); 27 | var url = require('url'); 28 | var debug = require('debug')('lwm2m'); 29 | var Router = require('../router'); 30 | var errors = require('../errors'); 31 | var content = require('../contentFormats'); 32 | var utils = require('../utils'); 33 | var Registry = require('./registry'); 34 | var Schema = require('../schema'); 35 | var schemas = require('../').schemas; 36 | var DecodeStream = require('./decodestream'); 37 | var CoapServer = coap.createServer; 38 | var objectId = utils.objectId; 39 | 40 | function defaultOptions(options) { 41 | return Object.assign({ 42 | udpWindow: 100, 43 | type : 'udp6', 44 | registry : new Registry(), // default in-memory registry 45 | }, options || {}, { proxy: true }); 46 | } 47 | 48 | /** 49 | * Server constructor. 50 | * 51 | * Events: 52 | * 53 | * - `register`: device registration request. 54 | * - `update`: device registration update. 55 | * - `unregister`: device unregistration. 56 | * 57 | * @constructor 58 | * @augments EventEmitter 59 | * @param {Object} [options] 60 | * @param {string} [options.type='upd6'] - IPv4 (udp4) or IPv6 (udp6) connections 61 | * @param {number} [options.piggybackReplyMs=50] - milliseconds to wait for a piggyback response 62 | * @param {Registry} [options.registry=Registry] - impl. of CoRE Resource Directory 63 | */ 64 | function Server(options) { 65 | if (!(this instanceof Server)) { 66 | return new Server(options); 67 | } 68 | 69 | var opts = defaultOptions(options); 70 | 71 | CoapServer.call(this, opts); 72 | 73 | var registry = opts.registry; 74 | 75 | Object.defineProperty(this, '_registry', { 76 | value : registry, 77 | }); 78 | 79 | var _this = this; 80 | 81 | /* 82 | * middlewares 83 | */ 84 | var validate = require('./validate'); 85 | var register = require('./register')(registry); 86 | var update = require('./update')(registry); 87 | var deregister = require('./deregister')(registry); 88 | 89 | function authorize(req, res) { 90 | var params = Object.assign({}, utils.query(req), { 91 | payload: req.payload.toString(), 92 | }); 93 | 94 | return new Promise(function(resolve, reject) { 95 | _this.emit('register', params, function(err) { 96 | if (err) { 97 | debug(err); 98 | reject(new errors.ForbiddenError(err.message)); 99 | } else { 100 | resolve(); 101 | } 102 | }); 103 | }); 104 | } 105 | 106 | registry.on('update', function(location) { 107 | _this.emit('update', location); 108 | }); 109 | 110 | registry.on('deregister', function(location) { 111 | _this.emit('deregister', location); 112 | }); 113 | 114 | registry.on('error', function(err) { 115 | _this.emit('error', err); 116 | }); 117 | 118 | var router = Router({ 119 | udpWindow: opts.udpWindow, 120 | routes: [ 121 | ['POST', /\/rd$/, validate], 122 | ['POST', /\/rd$/, authorize], 123 | ['POST', /\/rd$/, register], 124 | ['POST', /\/rd\/.+/, validate], 125 | ['POST', /\/rd\/.+/, update], 126 | ['DELETE', /\/rd\/.+/, deregister], 127 | ], 128 | }); 129 | 130 | this.on('request', router); 131 | } 132 | 133 | module.exports = Server; 134 | Server.prototype = Object.create(CoapServer.prototype); 135 | Server.prototype.constructor = Server; 136 | 137 | Server.prototype._request = function(params) { 138 | var request = Object.assign({}, params, { 139 | host: 'localhost', 140 | port: this._port, 141 | }); 142 | var type = this._options.type; 143 | 144 | return this._registry.find(request.endpoint) 145 | .then(function(device) { 146 | request.proxyUri = url.format({ 147 | protocol: 'coap', 148 | slashes: true, 149 | hostname: device.address, 150 | port: device.port, 151 | }); 152 | return utils.send(request, type); 153 | }); 154 | }; 155 | 156 | /** 157 | * Read `path` on device with endpoint name `endpoint`. The optional callback is given 158 | * the two arguments `(err, res)`, where `res` is parsed using `schema`. 159 | * 160 | * Note: 161 | * 162 | * _If no schema is provided will return a `Buffer` if the payload is `TLV`-encoded 163 | * or opaque, or an `String` otherwise._ 164 | * 165 | * @example 166 | * 167 | * var schema = Schema({ 168 | * test: { id: 1, type: Number } 169 | * }); 170 | * 171 | * var options = { 172 | * schema: schema, 173 | * format: 'json', 174 | * }; 175 | * 176 | * server.read('test', '/1024/11', options, function(err, res) { 177 | * assert(res.hasOwnProperty('test')); 178 | * assert(typeof res.test == 'number'); 179 | * }); 180 | * 181 | * 182 | * @param {String} endpoint - client endpoint name 183 | * @param {String} path - either an LWM2M Object instance or resource 184 | * @param {Object} [options] 185 | * @param {Schema} options.format='text'] - media type. 186 | * @param {Schema} options.schema - defining resources. 187 | * @param {Function} [callback] 188 | * @return {Promise} - a promise of the eventual value 189 | */ 190 | Server.prototype.read = function(endpoint, path, options, callback) { 191 | if (typeof options === 'function') { 192 | callback = options; 193 | options = null; 194 | } 195 | 196 | if (!utils.validatePath(path)) { 197 | throw new Error('Illegal path: `' + path + '`'); 198 | } 199 | 200 | var mediaType = utils.getMediaType(path, null, options && options.format); 201 | 202 | var schema = options && options.schema 203 | || schemas[objectId(path)]; 204 | 205 | if (schema && !(schema instanceof Schema)) { 206 | throw new TypeError('Illegal schema'); 207 | } 208 | 209 | var processResponse = function(res) { 210 | return new Promise(function(resolve, reject) { 211 | switch (res.code) { 212 | case '2.05': 213 | var contentFormat = utils.getOption(res, 'Content-Format'); 214 | var body = utils.parsePayload(res.payload, contentFormat, schema); 215 | resolve(body); 216 | break; 217 | case '4.04': 218 | reject(new errors.ObjectNotFound(path)); 219 | break; 220 | default: 221 | reject(new errors.ClientError(res.code)); 222 | } 223 | }); 224 | }; 225 | 226 | debug('Reading value from %s from device %s', path, endpoint); 227 | 228 | var promise = this._request({ 229 | endpoint: endpoint, 230 | method: 'GET', 231 | pathname: path, 232 | options: { 233 | 'Accept': mediaType, 234 | }, 235 | }) 236 | .then(processResponse); 237 | 238 | if (callback) { 239 | utils.invoke(callback, promise); 240 | } 241 | 242 | return promise; 243 | }; 244 | 245 | /** 246 | * Write `value` into `path` of device with endpoint name `endpoint`. 247 | * For writing Object Instances, an schema is required. 248 | * 249 | * Note: 250 | * 251 | * _schemas can be globally added to `lwm2m.schemas`._ 252 | * 253 | * @param {String} endpoint - client endpoint name 254 | * @param {String} path 255 | * @param {Object|String|Number|Buffer} value 256 | * @param {Object} options 257 | * @param {string} [options.format='tlv'] - media type. 258 | * @param {Schema} [options.schema] - schema to serialize value. 259 | * @param {Function} [callback] 260 | * @return {Promise} 261 | * @example 262 | * 263 | * var schema = Schema({ 264 | * foo : { 265 | * id: 5, 266 | * type: 'String' 267 | * }, 268 | * bar : { 269 | * id: 6, 270 | * type: 'Number' 271 | * }, 272 | * }); 273 | * 274 | * var options = { 275 | * schema: schema, 276 | * format: 'json', 277 | * }; 278 | * 279 | * var value = { 280 | * foo: 'test', 281 | * bar: 42, 282 | * }; 283 | * 284 | * var promise = server.write('test', '/42/0', value, options) 285 | * var promise = server.write('test', '/42/0/5', 'test') 286 | * var promise = server.write('test', '/42/0/6', 42) 287 | * 288 | * // add schema for Object ID 42 globally. 289 | * lwm2m.schemas[42] = schema; 290 | * 291 | * var promise = server.write('test', '/42/0', value) 292 | * 293 | */ 294 | Server.prototype.write = function(endpoint, path, value, options, callback) { 295 | if (typeof options === 'function') { 296 | callback = options; 297 | options = {}; 298 | } 299 | 300 | if (!utils.validatePath(path)) { 301 | throw new Error('Illegal path: `' + path + '`'); 302 | } 303 | 304 | var method = utils.isResource(path) ? 'PUT' : 'POST'; 305 | var mediaType = utils.getMediaType(path, value, options && options.format); 306 | var payload; 307 | 308 | if (typeof value === 'object') { 309 | 310 | var schema = options && options.schema 311 | || schemas[objectId(path)]; 312 | 313 | if (!(schema instanceof Schema)) { 314 | throw new TypeError('Illegal schema'); 315 | } 316 | 317 | schema.setBasename(path); 318 | 319 | try { 320 | payload = utils.generatePayload(value, schema, mediaType); 321 | } catch (e) { 322 | if (callback) { 323 | callback(e); 324 | } else { 325 | return Promise.reject(e); 326 | } 327 | } 328 | } else { 329 | if (!Buffer.isBuffer(value)) { 330 | payload = String(value); 331 | } else { 332 | payload = value; 333 | } 334 | } 335 | 336 | var processResponse = function(res) { 337 | return new Promise(function(resolve, reject) { 338 | if (res.code === '2.04') { 339 | resolve(res.payload.toString('utf8')); 340 | } else if (res.code === '4.04') { 341 | reject(new errors.ObjectNotFound(path)); 342 | } else { 343 | reject(new errors.ClientError(res.code)); 344 | } 345 | }); 346 | }; 347 | 348 | debug('Writing a new value on path %s from device %s', 349 | path, endpoint); 350 | 351 | var promise = this._request({ 352 | endpoint: endpoint, 353 | method: method, 354 | pathname: path, 355 | payload: payload, 356 | options: { 357 | 'Content-Format': mediaType, 358 | }, 359 | }) 360 | .then(processResponse); 361 | 362 | if (callback) { 363 | utils.invoke(callback, promise); 364 | } 365 | 366 | return promise; 367 | }; 368 | 369 | /** 370 | * Makes an Execute operation over the designed resource ID of the selected device. 371 | * 372 | * @param {String} endpoint - client endpoint name 373 | * @param {String} path 374 | * @param {String} value 375 | * @param {Function} callback 376 | * @return {Promise} 377 | */ 378 | Server.prototype.execute = function(endpoint, path, value, callback) { 379 | if (!utils.validatePath(path)) { 380 | throw new Error('Illegal path: `' + path + '`'); 381 | } 382 | 383 | var processResponse = function(res) { 384 | return new Promise(function(resolve, reject) { 385 | if (res.code === '2.04') { 386 | resolve(res.payload.toString('utf8')); 387 | } else if (res.code === '4.04') { 388 | reject(new errors.ObjectNotFound(path)); 389 | } else { 390 | reject(new errors.ClientError(res.code)); 391 | } 392 | }); 393 | }; 394 | 395 | debug('Executing resource %s from device %s', path, endpoint); 396 | 397 | var promise = this._request({ 398 | endpoint: endpoint, 399 | method: 'POST', 400 | pathname: path, 401 | payload: value, 402 | options: { 403 | 'Content-Format': content.text, 404 | }, 405 | }) 406 | .then(processResponse); 407 | 408 | if (callback) { 409 | utils.invoke(callback, promise); 410 | } 411 | 412 | return promise; 413 | }; 414 | 415 | /** 416 | * Execute a discover operation for the selected resource. 417 | * 418 | * @param {String} endpoint - client endpoint name 419 | * @param {String} path 420 | * @param {Function} callback 421 | * @return {Promise} - a promise with an strng in link-format 422 | */ 423 | Server.prototype.discover = function(endpoint, path, callback) { 424 | if (!utils.validatePath(path)) { 425 | throw new Error('Illegal path: `' + path + '`'); 426 | } 427 | 428 | var processResponse = function(res) { 429 | return new Promise(function(resolve, reject) { 430 | if (res.code === '2.05') { 431 | resolve(res.payload.toString('utf8')); 432 | } else if (res.code === '4.04') { 433 | reject(new errors.ObjectNotFound(path)); 434 | } else { 435 | reject(new errors.ClientError(res.code)); 436 | } 437 | }); 438 | }; 439 | 440 | debug('Discover operation on path %s from device %s', 441 | path, endpoint); 442 | 443 | var promise = this._request({ 444 | endpoint: endpoint, 445 | method: 'GET', 446 | pathname: path, 447 | options: { 448 | 'Accept': content.link, 449 | }, 450 | }) 451 | .then(processResponse); 452 | 453 | 454 | if (callback) { 455 | utils.invoke(callback, promise); 456 | } 457 | 458 | return promise; 459 | }; 460 | 461 | /** 462 | * Write `attributes` into `path` of endpoint `endpoint`. 463 | * 464 | * @param {String} endpoint - client endpoint name 465 | * @param {String} path 466 | * @param {Object} attributes 467 | * @param {Function} [callback] 468 | * @return {Promise} 469 | * @example 470 | * 471 | * var attr = { 472 | * "pmin": 5, 473 | * "pmax": 10 474 | * }; 475 | * 476 | * server.writeAttributes('dev0', '3303/0/5700', attr, function(err, res) { 477 | * assert.ifError(err); 478 | * }); 479 | * 480 | */ 481 | Server.prototype.writeAttributes = function(endpoint, path, attributes, callback) { 482 | if (!utils.validatePath(path)) { 483 | throw new Error('Illegal path: `' + path + '`'); 484 | } 485 | 486 | var valid = [ 487 | 'pmin', 488 | 'pmax', 489 | 'gt', 490 | 'lt', 491 | 'stp', 492 | ]; 493 | 494 | if (!utils.validateQuery(attributes, [], valid)) { 495 | return Promise.reject(new errors.UnsupportedAttributes()); 496 | } 497 | 498 | var processResponse = function(res) { 499 | return new Promise(function(resolve, reject) { 500 | if (res.code === '2.04') { 501 | resolve(res.payload.toString('utf8')); 502 | } else if (res.code === '4.04') { 503 | reject(new errors.ObjectNotFound(path)); 504 | } else { 505 | reject(new errors.ClientError(res.code)); 506 | } 507 | }); 508 | }; 509 | 510 | debug('Writing attributes `%o` on resource %s from device %s', 511 | attributes, path, endpoint); 512 | 513 | var promise = this._request({ 514 | endpoint: endpoint, 515 | pathname: path, 516 | method: 'PUT', 517 | query: url.format({ query: attributes }).slice(1), 518 | }) 519 | .then(processResponse); 520 | 521 | if (callback) { 522 | utils.invoke(callback, promise); 523 | } 524 | 525 | return promise; 526 | }; 527 | 528 | /** 529 | * Create a new LWM2M Object for `path`, where path is an Object ID. 530 | * 531 | * @param {String} endpoint - client endpoint name 532 | * @param {String} path 533 | * @param {Object|String|Number|Buffer} value 534 | * @param {Object} [options] 535 | * @param {Function} [callback] 536 | * @return {Promise} 537 | */ 538 | Server.prototype.create = function(endpoint, path, value, options, callback) { 539 | if (!utils.validatePath(path)) { 540 | throw new Error('Illegal path: `' + path + '`'); 541 | } 542 | 543 | if (!utils.isObject(path)) { 544 | throw new Error('Not a path to Object ID: `' + path + '`'); 545 | } 546 | 547 | if (typeof options === 'function') { 548 | callback = options; 549 | options = {}; 550 | } 551 | 552 | var mediaType = utils.getMediaType(path, value, options.format); 553 | var payload; 554 | 555 | if (typeof value === 'object') { 556 | 557 | var schema = options && options.schema 558 | || schemas[objectId(path)]; 559 | 560 | if (!(schema instanceof Schema)) { 561 | throw new TypeError('Illegal schema'); 562 | } 563 | 564 | try { 565 | payload = utils.generatePayload(value, schema, mediaType); 566 | } catch (e) { 567 | if (callback) { 568 | callback(e); 569 | } else { 570 | return Promise.reject(e); 571 | } 572 | } 573 | } else { 574 | // FIXME we are not validating here 575 | payload = value; 576 | } 577 | 578 | var processResponse = function(res) { 579 | return new Promise(function(resolve, reject) { 580 | if (res.code === '2.01') { 581 | resolve(); 582 | } else if (res.code === '4.04') { 583 | reject(new errors.ObjectNotFound(path)); 584 | } else { 585 | reject(new errors.ClientError(res.code)); 586 | } 587 | }); 588 | }; 589 | 590 | debug('Creating a new Object Instance in %s from device %s', 591 | path, endpoint); 592 | 593 | var promise = this._request({ 594 | endpoint: endpoint, 595 | method: 'POST', 596 | pathname: path, 597 | payload: payload, 598 | options: { 599 | 'Content-Format': mediaType, 600 | }, 601 | }) 602 | .then(processResponse); 603 | 604 | if (callback) { 605 | utils.invoke(callback, promise); 606 | } 607 | 608 | return promise; 609 | }; 610 | 611 | /** 612 | * Deletes the LWM2M Object instance in `path` of endpoint `endpoint` 613 | * 614 | * @param {String} endpoint - client endpoint name 615 | * @param {String} path 616 | * @param {Function} [callback] 617 | * @return {Promise} 618 | */ 619 | Server.prototype.delete = function(endpoint, path, callback) { 620 | (function (bootstrap) { 621 | if (bootstrap && /^\/$/.test(path)) { 622 | return; 623 | } 624 | 625 | if (!utils.validatePath(path)) { 626 | throw new Error('Illegal path: `' + path + '`'); 627 | } 628 | 629 | if (bootstrap && utils.isObject(path)) { 630 | return; 631 | } 632 | 633 | if (!utils.isInstance(path)) { 634 | throw new Error('Not a path to an Instance: `' + path + '`'); 635 | } 636 | })(this.bootstrap); 637 | 638 | var processResponse = function(res) { 639 | return new Promise(function(resolve, reject) { 640 | if (res.code === '2.02') { 641 | resolve(); 642 | } else if (res.code === '4.04') { 643 | reject(new errors.ObjectNotFound(path)); 644 | } else { 645 | reject(new errors.ClientError(res.code)); 646 | } 647 | }); 648 | }; 649 | 650 | debug('Delete Object Instance in %s from device %s', 651 | path, endpoint); 652 | 653 | var promise = this._request({ 654 | endpoint: endpoint, 655 | method: 'DELETE', 656 | pathname: path, 657 | }) 658 | .then(processResponse); 659 | 660 | if (callback) { 661 | utils.invoke(callback, promise); 662 | } 663 | 664 | return promise; 665 | }; 666 | 667 | /** 668 | * Observe changes in `path` of device with endpoint name `endpoint`. 669 | * The notification behaviour, e.g. periodic or event-triggered reporting, is configured with the 670 | * `writeAttributes` method. The callback is given the two arguments `(err, stream)`, 671 | * where `stream` is a `Readable Stream`. To stop receiving notifications `close()` the stream 672 | * and (optionally) call `cancel()` on the same `endpoint` and `path` and . 673 | * 674 | * @example 675 | * 676 | * server.observe('dev0', '/1024/10/1', function(err, stream) { 677 | * stream.on('data', function(value) { 678 | * console.log('new value %s', value); 679 | * }); 680 | * 681 | * stream.on('end', function() { 682 | * console.log('stopped observing'); 683 | * }); 684 | * }); 685 | * 686 | * 687 | * @param {String} endpoint - client endpoint name 688 | * @param {String} path 689 | * @param {Object} [options] 690 | * @param {Schema} options.format='text'] - media type. 691 | * @param {Schema} options.schema - defining resources. 692 | * @param {Function} [callback] 693 | * @return {Promise} 694 | */ 695 | Server.prototype.observe = function(endpoint, path, options, callback) { 696 | if (!utils.validatePath(path)) { 697 | throw new Error('Illegal path: `' + path + '`'); 698 | } 699 | 700 | if (typeof options === 'function') { 701 | callback = options; 702 | options = {}; 703 | } 704 | 705 | var schema = options && options.schema 706 | || schemas[objectId(path)]; 707 | 708 | if (schema && !(schema instanceof Schema)) { 709 | throw new TypeError('Illegal schema'); 710 | } 711 | 712 | var mediaType = utils.getMediaType(path, null, options && options.format); 713 | 714 | var processStream = function(stream) { 715 | var contentType = stream.headers['Content-Type']; 716 | var decode = new DecodeStream(contentType, schema); 717 | 718 | decode.on('close', function() { 719 | debug('closing observe stream'); 720 | stream.close(); 721 | }); 722 | 723 | decode.on('error', function() { 724 | }); 725 | 726 | return Promise.resolve(stream.pipe(decode)); 727 | }; 728 | 729 | debug('Observing resource %s from device %s', path, endpoint); 730 | 731 | var promise = this._request({ 732 | endpoint: endpoint, 733 | method: 'GET', 734 | pathname: path, 735 | observe: true, 736 | options: { 737 | 'Accept': mediaType, 738 | }, 739 | }) 740 | .then(processStream); 741 | 742 | if (callback) { 743 | utils.invoke(callback, promise); 744 | } 745 | 746 | return promise; 747 | }; 748 | 749 | /** 750 | * Cancel an observation for `path` of device `endpoint`. 751 | * 752 | * @param {String} endpoint - client endpoint name 753 | * @param {String} path 754 | * @param {Function} [callback] 755 | * @return {Promise} 756 | */ 757 | Server.prototype.cancel = function(endpoint, path, callback) { 758 | if (!utils.validatePath(path)) { 759 | throw new Error('Illegal path: `' + path + '`'); 760 | } 761 | 762 | debug('Cancel observation of resource %s from device %s', 763 | path, endpoint); 764 | 765 | var promise = this._request({ 766 | endpoint: endpoint, 767 | method: 'GET', 768 | pathname: path, 769 | options: { 770 | 'Accept': content.text, 771 | 'Observe': 1, 772 | }, 773 | }); 774 | 775 | if (callback) { 776 | utils.invoke(callback, promise); 777 | } 778 | 779 | return promise; 780 | }; 781 | -------------------------------------------------------------------------------- /lib/server/register.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var utils = require('../utils'); 27 | 28 | module.exports = function(registry) { 29 | return function(req, res) { 30 | var params = Object.assign(utils.query(req), { 31 | payload: req.payload.toString(), 32 | }, req.rsinfo); 33 | 34 | return registry.register(params) 35 | .then(function(location) { 36 | res.code = '2.01'; 37 | res.setOption('Location-Path', '/rd/' + location); 38 | res.end(); 39 | }); 40 | }; 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /lib/server/registry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, see http://www.gnu.org/licenses/. 19 | * 20 | */ 21 | 22 | 'use strict'; 23 | 24 | var debug = require('debug')('lwm2m'); 25 | var errors = require('../errors'); 26 | var updated = Symbol(); 27 | var EventEmitter = require('events').EventEmitter; 28 | 29 | /** 30 | * Registry for clients. 31 | * Default implementation is in-memory. 32 | * 33 | * For production use, extend `Registry` class and 34 | * give new implementations to 35 | * _get, _find, _save, _update and _delete. 36 | * 37 | * See [examples](examples) for a MongoDB-backed registry. 38 | * 39 | * @constructor 40 | * @augments EventEmitter 41 | */ 42 | function Registry() { 43 | EventEmitter.call(this); 44 | 45 | if (this.constructor === Registry) { 46 | this.clients = []; 47 | } 48 | } 49 | 50 | module.exports = Registry; 51 | Registry.prototype = Object.create(EventEmitter.prototype); 52 | Registry.prototype.constructor = Registry; 53 | 54 | /** 55 | * get client by endpoint name 56 | * @param {string} endpoint 57 | * @param {Function} callback - callback is given 58 | * the two arguments `(err, client)` 59 | */ 60 | Registry.prototype._find = function(endpoint, callback) { 61 | var location = this.clients.findIndex(function(elem) { 62 | return elem && elem.ep === endpoint; 63 | }); 64 | 65 | this._get(location, callback); 66 | }; 67 | 68 | Registry.prototype.find = function(endpoint) { 69 | var _this = this; 70 | 71 | return new Promise(function(resolve, reject) { 72 | _this._find(endpoint, function(err, client) { 73 | if (err) { 74 | reject(err); 75 | } else { 76 | resolve(client); 77 | } 78 | }); 79 | }); 80 | }; 81 | 82 | /** 83 | * get client by location in the registry 84 | * @param {string} location 85 | * @param {Function} callback - callback is given 86 | * the two arguments `(err, client)` 87 | */ 88 | Registry.prototype._get = function(location, callback) { 89 | var result = this.clients[location]; 90 | 91 | if (result) { 92 | callback(null, result); 93 | } else { 94 | callback(new errors.DeviceNotFound()); 95 | } 96 | }; 97 | 98 | Registry.prototype.get = function(location) { 99 | var _this = this; 100 | 101 | return new Promise(function(resolve, reject) { 102 | _this._get(location, function(err, result) { 103 | if (err) { 104 | reject(err); 105 | } else { 106 | resolve(result); 107 | } 108 | }); 109 | }); 110 | }; 111 | 112 | /** 113 | * store a new client in the registry 114 | * @param {Object} client 115 | * @param {Function} callback - callback is given 116 | * the two arguments `(err, location)` 117 | */ 118 | Registry.prototype._save = function(client, callback) { 119 | var location = this.clients.findIndex(Object.is.bind(null, undefined)); 120 | 121 | if (location < 0) 122 | location = this.clients.length; 123 | 124 | var obj = Object.assign({ 125 | lt: 86400, 126 | location: location, 127 | }, client); 128 | 129 | var that = this; 130 | 131 | Object.defineProperty(obj, 'timeout', { 132 | value: setInterval(function() { 133 | if (!that.clients[location][updated]) { 134 | that._delete(location, function(err) { 135 | debug(err); 136 | }); 137 | } else { 138 | that.clients[location][updated] = false; 139 | } 140 | }, obj.lt * 1000), 141 | }); 142 | 143 | this.clients[location] = obj; 144 | return callback(null, location); 145 | 146 | }; 147 | 148 | Registry.prototype.save = function(client) { 149 | var _this = this; 150 | 151 | return new Promise(function(resolve, reject) { 152 | _this._save(client, function(err, location) { 153 | if (err) { 154 | reject(err); 155 | } else { 156 | resolve(location); 157 | } 158 | }); 159 | }); 160 | }; 161 | 162 | /** 163 | * update a client in the registry 164 | * @param {string} location 165 | * @param {Object} params 166 | * @param {Function} callback - callback is given 167 | * the two arguments `(err, location)` 168 | */ 169 | Registry.prototype._update = function(location, params, callback) { 170 | var result = this.clients[location]; 171 | 172 | if (result) { 173 | this.clients[location] = Object.assign(result, params); 174 | this.clients[location][updated] = true; 175 | 176 | callback(null, location); 177 | } else { 178 | callback(new errors.DeviceNotFound()); 179 | } 180 | }; 181 | 182 | Registry.prototype.update = function(location, params) { 183 | var _this = this; 184 | 185 | return new Promise(function(resolve, reject) { 186 | _this._update(location, params, function(err, location) { 187 | if (err) { 188 | reject(err); 189 | } else { 190 | _this.emit('update', location); 191 | resolve(location); 192 | } 193 | }); 194 | }); 195 | }; 196 | 197 | /** 198 | * delete client from the registry 199 | * @param {string} location 200 | * @param {Function} callback - callback is given 201 | * the two arguments `(err, client)` 202 | */ 203 | Registry.prototype._delete = function(location, callback) { 204 | var result = this.clients[location]; 205 | 206 | if (result) { 207 | clearTimeout(result.timeout); 208 | delete this.clients[location]; 209 | callback(null, result); 210 | } else { 211 | callback(new errors.DeviceNotFound()); 212 | } 213 | }; 214 | 215 | Registry.prototype.unregister = function(location) { 216 | var _this = this; 217 | 218 | return new Promise(function(resolve, reject) { 219 | _this._delete(location, function(err, result) { 220 | if (err) { 221 | reject(err); 222 | } else { 223 | _this.emit('deregister', result); 224 | resolve(result); 225 | } 226 | }); 227 | }); 228 | }; 229 | 230 | Registry.prototype.register = function(client) { 231 | var _this = this; 232 | 233 | if (!client.ep) { 234 | return Promise.reject(new Error('missing `ep` parameter')); 235 | } 236 | 237 | return this.find(client.ep) 238 | .then(function(result) { 239 | return _this.update(result.location, client); 240 | }) 241 | .catch(function(err) { 242 | if (err instanceof errors.DeviceNotFound) { 243 | return _this.save(client); 244 | } else { 245 | return Promise.reject(err); 246 | } 247 | }); 248 | }; 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /lib/server/update.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var utils = require('../utils'); 27 | var url = require('url'); 28 | 29 | module.exports = function(registry) { 30 | return function(req, res) { 31 | var path = url.parse(req.url).pathname; 32 | var location = utils.splitPath(path).pop(); 33 | var params = Object.assign(utils.query(req), { 34 | payload: req.payload.toString(), 35 | }, req.rsinfo); 36 | 37 | return registry.update(location, params) 38 | .then(function() { 39 | res.code = '2.04'; 40 | res.end(); 41 | }); 42 | }; 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /lib/server/validate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var utils = require('../utils'); 27 | var errors = require('../errors'); 28 | 29 | module.exports = function(req, res) { 30 | var query = utils.query(req); 31 | var required = []; 32 | var optional = ['lt', 'lwm2m', 'b']; 33 | 34 | if (/\/rd\?/.test(req.url)) { 35 | required.push('ep'); 36 | } 37 | 38 | if (utils.validateQuery(query, required, optional)) { 39 | return Promise.resolve(); 40 | } else { 41 | return Promise.reject(new errors.BadRequestError()); 42 | } 43 | }; 44 | 45 | -------------------------------------------------------------------------------- /lib/slidingWindow.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | function createSlidingWindow(size) { 27 | var window = { 28 | data: new Array(size), 29 | header: 0, 30 | }; 31 | 32 | function containNumber(number) { 33 | if (window.data.indexOf(number) < 0) { 34 | return false; 35 | } else { 36 | return true; 37 | } 38 | } 39 | 40 | function pushNumber(number) { 41 | window.data[window.header] = number; 42 | window.header = (window.header++)%window.data.length; 43 | } 44 | 45 | return { 46 | contains: containNumber, 47 | push: pushNumber, 48 | }; 49 | } 50 | 51 | module.exports = createSlidingWindow; 52 | -------------------------------------------------------------------------------- /lib/tlv.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | * 23 | */ 24 | 25 | var debug = require('debug')('lwm2m'); 26 | 27 | /* 28 | * length in bytes of a number 29 | */ 30 | function byteLength(val) { 31 | if (val < 2) { 32 | return 1; 33 | } 34 | return Math.ceil(Math.log(val) * Math.LOG2E / 8); 35 | } 36 | 37 | function length(val) { 38 | // integer size: 1, 2, 4 or 8 bytes. 39 | function size(val) { 40 | var v = byteLength(val); 41 | v--; 42 | v |= v >>> 1; 43 | v |= v >>> 2; 44 | v++; 45 | return v; 46 | } 47 | 48 | var type = Object.prototype.toString.call(val).slice(8, -1); 49 | 50 | switch(type) { 51 | case 'Number': 52 | return (val % 1 === 0 ? size(val) : 8); 53 | case 'String': 54 | case 'Uint8Array': 55 | case 'Buffer': 56 | return val.length; 57 | case 'Boolean': 58 | return 1; 59 | case 'Date': 60 | return size(val.getTime() / 1e3 >> 0); 61 | default: 62 | return 0; 63 | } 64 | } 65 | 66 | function readHeader(buf, offset, tlv) { 67 | var type, id, len; 68 | var header; 69 | 70 | header = buf.readUInt8(offset); 71 | offset += 1; 72 | 73 | type = header >>> 6; 74 | 75 | if (header & 0x20) { 76 | id = buf.readUInt16BE(offset); 77 | offset += 2; 78 | } else { 79 | id = buf.readUInt8(offset); 80 | offset += 1; 81 | } 82 | 83 | len = header & 0x7; 84 | 85 | if (!len) { 86 | switch ((header & 0x18) >>> 3) { 87 | case 1: 88 | len = buf.readUInt8(offset); 89 | offset += 1; 90 | break; 91 | case 2: 92 | len = buf.readUInt16BE(offset); 93 | offset += 2; 94 | break; 95 | case 3: 96 | len = buf.readUInt16BE(offset); 97 | len = buf.readUInt8(offset); 98 | offset += 3; 99 | break; 100 | } 101 | } 102 | 103 | tlv.len = len; 104 | tlv.id = id; 105 | tlv.type = type; 106 | 107 | return offset; 108 | } 109 | 110 | function writeHeader(buf, offset, idType, id, len) { 111 | function tlvType(type, id, len) { 112 | var byte = type << 6; 113 | 114 | if (id > 0xff) { 115 | byte |= 0x1 << 5; 116 | } 117 | 118 | if (len < 8) { 119 | byte |= len; 120 | } else { 121 | byte |= length(len) << 3; 122 | } 123 | 124 | return byte; 125 | } 126 | 127 | var type = tlvType(idType, id, len); 128 | 129 | /* type (8-bits masked field) */ 130 | buf.writeUInt8(type, offset); 131 | offset += 1; 132 | 133 | /* identifier (8-bit or 16-bit UInt) */ 134 | if (type & 0x20) { 135 | buf.writeUInt16BE(id, offset); 136 | offset += 2; 137 | } else { 138 | buf.writeUInt8(id, offset); 139 | offset += 1; 140 | } 141 | 142 | /* length (0-24-bit UInt) */ 143 | if (type & 0x18) { 144 | switch (length(len)) { 145 | case 3: 146 | buf.writeUInt8(len >>> 0x10 & 0xff, offset); 147 | offset += 1; 148 | /* falls through */ 149 | case 2: 150 | buf.writeUInt8(len >>> 0x08 & 0xff, offset); 151 | offset += 1; 152 | /* falls through */ 153 | case 1: 154 | buf.writeUInt8(len & 0xff, offset); 155 | offset += 1; 156 | break; 157 | default: 158 | throw new Error('Invalid resource `' + id + '`'); 159 | } 160 | } 161 | 162 | return offset; 163 | } 164 | 165 | function serialize(obj, schema) { 166 | var buf = Buffer.alloc(16 * 1024 /*1024*/); 167 | var keys = Object.keys(obj); 168 | 169 | schema.validate(obj); 170 | 171 | function append(buf, offset, len, value, type) { 172 | var types = { 173 | String: function() { 174 | buf.write(value, offset, buf.length - offset, 'utf8'); 175 | return offset += len; 176 | }, 177 | Integer: function() { 178 | buf.writeIntBE(value, offset, len); 179 | return offset += len; 180 | }, 181 | Float: function() { 182 | buf.writeDoubleBE(value, offset); 183 | return offset += len; 184 | }, 185 | Boolean: function() { 186 | buf.writeInt8(value, offset); 187 | return offset += len; 188 | }, 189 | Opaque: function() { 190 | value.copy(buf, offset); 191 | return offset += len; 192 | }, 193 | Time: function() { 194 | // convert date to timestamp in seconds 195 | var timestamp = value.getTime() / 1e3 >> 0; 196 | buf.writeIntBE(timestamp, offset, len); 197 | return offset += len; 198 | }, 199 | }; 200 | 201 | function skip () { 202 | debug('Skipping resource with invalid type %s', type); 203 | return offset; 204 | } 205 | 206 | return (types[type] || skip)(); 207 | } 208 | 209 | function writeRecord(buf, offset, key) { 210 | var res = schema.resources[key]; 211 | var val = obj[key]; 212 | var len; 213 | 214 | if (Array.isArray(res.type)) { 215 | var tmp = Buffer.alloc(1024); 216 | var arrLen = 0; 217 | 218 | val.forEach(function(elem, i) { 219 | len = length(elem); 220 | arrLen = writeHeader(tmp, arrLen, 1, i, len); 221 | arrLen = append(tmp, arrLen, len , elem, res.type[0]); 222 | }); 223 | 224 | offset = writeHeader(buf, offset, 2, res.id, arrLen); 225 | tmp.copy(buf, offset, 0, arrLen); 226 | offset += arrLen; 227 | } else { 228 | len = length(val); 229 | 230 | offset = writeHeader(buf, offset, 3, res.id, len); 231 | offset = append(buf, offset, len, val, res.type); 232 | } 233 | 234 | return offset; 235 | } 236 | 237 | var len = 0; 238 | 239 | for (var i = 0; i < keys.length; ++i) { 240 | var key = keys[i]; 241 | 242 | // skip resources not defined in schema 243 | if (!schema.resources[key]) { 244 | continue; 245 | } 246 | 247 | len = writeRecord(buf, len, key); 248 | } 249 | 250 | return buf.slice(0, len); 251 | } 252 | 253 | function parse(payload, schema) { 254 | function append(obj, key, type, payload, pos, len) { 255 | var types = { 256 | String: function() { 257 | obj[key] = payload.toString('utf8', pos, pos + len); 258 | pos += len; 259 | return pos; 260 | }, 261 | Integer: function() { 262 | obj[key] = payload.readIntBE(pos, len); 263 | pos += len; 264 | return pos; 265 | }, 266 | Float: function() { 267 | switch (len) { 268 | case 4: 269 | obj[key] = payload.readFloatBE(pos); 270 | break; 271 | case 8: 272 | obj[key] = payload.readDoubleBE(pos); 273 | break; 274 | } 275 | pos += len; 276 | return pos; 277 | }, 278 | Boolean: function() { 279 | obj[key] = payload.readUInt8(pos) ? true : false; 280 | pos += 1; 281 | return pos; 282 | }, 283 | Opaque: function() { 284 | var buf = Buffer.alloc(len); 285 | payload.copy(buf, 0, pos, pos + len); 286 | obj[key] = buf; 287 | 288 | pos += len; 289 | return pos; 290 | }, 291 | Time: function() { 292 | var timestamp = payload.readIntBE(pos, len); 293 | obj[key] = new Date(timestamp * 1e3); 294 | pos += len; 295 | return pos; 296 | }, 297 | }; 298 | 299 | return (types[type])(); 300 | } 301 | 302 | function readRecord(payload, pos, result) { 303 | var header = {}, key, type; 304 | 305 | pos = readHeader(payload, pos, header); 306 | key = schema.id[header.id]; 307 | 308 | // skip resources not defined in schema. 309 | if (!key) { 310 | debug('Resource not defined in schema: %s', key); 311 | return pos + header.len; 312 | } 313 | 314 | type = schema.resources[key].type; 315 | 316 | if (Array.isArray(type)) { 317 | var end = pos + header.len; 318 | var itemHeader = {}; 319 | 320 | result[key] = []; 321 | 322 | while (pos < end) { 323 | pos = readHeader(payload, pos, itemHeader); 324 | pos = append(result[key], itemHeader.id, type[0], 325 | payload, pos, itemHeader.len); 326 | } 327 | } else { 328 | pos = append(result, key, type, payload, pos, header.len); 329 | } 330 | 331 | return pos; 332 | } 333 | 334 | var result = {}; 335 | var pos = 0; 336 | 337 | while (pos < payload.length) { 338 | pos = readRecord(payload, pos, result); 339 | } 340 | 341 | return result; 342 | } 343 | 344 | exports.serialize = serialize; 345 | exports.parse = parse; 346 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var errors = require('./errors'); 27 | var senml = require('./senml'); 28 | var tlv = require('./tlv'); 29 | var content = require('./contentFormats'); 30 | var url = require('url'); 31 | var coap = require('coap'); 32 | var Readable = require('readable-stream').Readable; 33 | 34 | exports.invoke = function(callback, promise) { 35 | promise 36 | .then(callback.bind(null, null), callback) 37 | .catch(function(err) { 38 | setTimeout(function() { 39 | throw err; 40 | }); 41 | }); 42 | }; 43 | 44 | exports.setTimeoutPromise = function(delay, arg) { 45 | return new Promise(function(resolve) { 46 | setTimeout(resolve, delay, arg); 47 | }); 48 | }; 49 | 50 | function splitPath(path) { 51 | return path 52 | .replace(/^\//g, '') 53 | .split('/') 54 | .filter(Boolean); // remove empty strings 55 | } 56 | exports.splitPath = splitPath; 57 | 58 | exports.validatePath = function(path) { 59 | var path_ = splitPath(path).map(Number); 60 | 61 | function inRange(val) { 62 | return val < 65535 && val >= 0; 63 | } 64 | 65 | return path_.every(inRange) && 66 | [1,2,3].indexOf(path_.length) >= 0; 67 | }; 68 | 69 | exports.objectId = function(path) { 70 | return splitPath(path)[0]; 71 | }; 72 | 73 | function isObject(path) { 74 | return splitPath(path).length === 1; 75 | } 76 | exports.isObject = isObject; 77 | 78 | exports.instanceId = function(path) { 79 | return splitPath(path)[1]; 80 | }; 81 | 82 | function isInstance(path) { 83 | return splitPath(path).length === 2; 84 | } 85 | exports.isInstance = isInstance; 86 | 87 | exports.resourceId = function(path) { 88 | return splitPath(path)[2]; 89 | }; 90 | 91 | function isResource(path) { 92 | return splitPath(path).length === 3; 93 | } 94 | exports.isResource = isResource; 95 | 96 | exports.query = function(req) { 97 | var obj = url.parse(req.url, true); 98 | return Object.assign({}, obj.query); 99 | }; 100 | 101 | exports.validateQuery = function(query, mandatory, optional) { 102 | var params = Object.keys(query); 103 | 104 | var ok = mandatory.every(function(param) { 105 | return params.indexOf(param) >= 0; 106 | }); 107 | 108 | return ok && params.every(function(param) { 109 | return mandatory.indexOf(param) >= 0 || 110 | optional.indexOf(param) >= 0; 111 | }); 112 | }; 113 | 114 | exports.getOption = function(res, name) { 115 | var index = res.options.findIndex(function(option) { 116 | return option.name === name; 117 | }); 118 | 119 | if (index < 0) 120 | return; 121 | 122 | return res.options[index].value; 123 | }; 124 | 125 | exports.parsePayload = function(payload, contentFormat, schema) { 126 | var body; 127 | 128 | switch (contentFormat) { 129 | case content.json: 130 | if (schema) { 131 | body = senml.parse(payload, schema); 132 | } else { 133 | body = JSON.parse(payload.toString('utf8')); 134 | } 135 | break; 136 | case content.tlv: 137 | if (schema) { 138 | body = tlv.parse(payload, schema); 139 | } else { 140 | body = payload; 141 | } 142 | break; 143 | case content.opaque: 144 | body = payload; 145 | break; 146 | case content.text: 147 | body = payload.toString('utf8'); 148 | break; 149 | default: 150 | throw new Error('Unknown content format: ' + contentFormat); 151 | } 152 | 153 | return body; 154 | }; 155 | 156 | exports.generatePayload = function(value, schema, mediaType) { 157 | if (!schema) { 158 | throw new Error('missing schema'); 159 | } 160 | 161 | schema.validate(value); 162 | 163 | var payload; 164 | 165 | switch (mediaType) { 166 | case content.json: 167 | payload = senml.serialize(value, schema); 168 | break; 169 | case content.tlv: 170 | payload = tlv.serialize(value, schema); 171 | break; 172 | default: 173 | throw new Error('Uknwown media type: ' + mediaType); 174 | } 175 | 176 | return payload; 177 | }; 178 | 179 | exports.getMediaType = function(path, value, format) { 180 | if (format) { 181 | if (format.match(/json/)) { 182 | return content.json; 183 | } else if (format.match(/tlv/)) { 184 | return content.tlv; 185 | } else if (format.match(/text/)) { 186 | return content.text; 187 | } else if (format.match(/opaque/)) { 188 | return content.opaque; 189 | } else { 190 | throw new Error('Uknown media type' + JSON.stringify(format)); 191 | } 192 | } else if (isInstance(path) || isObject(path)) { 193 | if (typeof value === 'string') { 194 | return content.json; 195 | } else if (Buffer.isBuffer(value)) { 196 | return content.tlv; 197 | } else { 198 | return content.tlv; 199 | } 200 | } else if (isResource(path)) { 201 | if (Buffer.isBuffer(value)) { 202 | return content.opaque; 203 | } else { 204 | return content.text; 205 | } 206 | } else { 207 | throw new Error('Cannot get media type for ' + 208 | JSON.stringify(value) + ' at path ' + path); 209 | } 210 | }; 211 | 212 | exports.send = function(request, type) { 213 | var agent = new coap.Agent({ type: type }); 214 | var req = agent.request(request); 215 | var rs = new Readable(); 216 | 217 | return new Promise(function(resolve, reject) { 218 | req.on('response', resolve); 219 | req.on('error', function(error) { 220 | reject(new errors.ClientConnectionError(error)); 221 | }); 222 | 223 | if (request.payload) { 224 | rs.push(request.payload); 225 | rs.push(null); 226 | rs.pipe(req); 227 | } else { 228 | req.end(); 229 | } 230 | }); 231 | }; 232 | 233 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lwm2m", 3 | "description": "Library for developing servers and client of OMA Lightweight M2M", 4 | "version": "0.13.0", 5 | "homepage": "https://github.com/moleike/node-lwm2m", 6 | "main": "lib", 7 | "author": { 8 | "name": "Alexandre Moreno", 9 | "email": "alex_moreno@tutk.com" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git://github.com/moleike/node-lwm2m" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/moleike/node-lwm2m/issues" 17 | }, 18 | "scripts": { 19 | "lint": "eslint --ignore-path .gitignore \"**/*.js\"", 20 | "test": "nyc --reporter=text node_modules/mocha/bin/mocha --recursive --exit", 21 | "coverage": "nyc report --reporter=text-lcov | coveralls", 22 | "doc": "documentation readme ./lib/index.js --markdown-toc --section=API", 23 | "pretest": "npm run lint", 24 | "prepublish": "npm run test", 25 | "preversion": "npm run test", 26 | "postversion": "git push origin && git push origin --tags" 27 | }, 28 | "dependencies": { 29 | "coap": "^0.21.0", 30 | "debug": "^4.3.1", 31 | "readable-stream": "^3.6.0" 32 | }, 33 | "devDependencies": { 34 | "coveralls": "^3.1.0", 35 | "documentation": "11.0.0", 36 | "eslint": "^5.16.0", 37 | "mocha": "^6.2.3", 38 | "nyc": "^14.1.1", 39 | "should": "^13.2.3" 40 | }, 41 | "keywords": [ 42 | "iot", 43 | "lwm2m", 44 | "ipso", 45 | "coap", 46 | "sensor", 47 | "sensor streaming", 48 | "smart objects" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /test/registry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var should = require('should'); 27 | var lwm2m = require('../'); 28 | var utils = require('../lib/utils'); 29 | var registry, location; 30 | 31 | describe('Device registry', function() { 32 | beforeEach(function(done) { 33 | registry = new lwm2m.Registry(); 34 | 35 | registry.register({ 36 | ep: 'test', 37 | lt: 300, 38 | }) 39 | .then(function(loc) { 40 | location = loc; 41 | done(); 42 | }) 43 | .catch(done); 44 | }); 45 | 46 | describe('#register', function() { 47 | it('should save a new client an return its location', function() { 48 | return registry.register({ 49 | ep: 'foo', 50 | lt: 300, 51 | }).should.be.eventually.a.Number(); 52 | }); 53 | 54 | it('should save all properties', function() { 55 | return registry.register({ 56 | ep: 'foo', 57 | lt: 300, 58 | foo: 'test', 59 | bar: 42, 60 | }) 61 | .then(function(loc) { 62 | return registry.get(loc); 63 | }) 64 | .should.have.eventually.properties([ 65 | 'foo', 66 | 'bar', 67 | ]); 68 | }); 69 | 70 | it('should be ok to register an existing client', function() { 71 | return registry.register({ 72 | ep: 'test', 73 | lt: 300, 74 | }).should.be.eventually.a.Number(); 75 | }); 76 | 77 | it('should evict client when lifetime expires', function() { 78 | return registry.register({ 79 | ep: 'foo', 80 | lt: 0, 81 | }) 82 | .then(function(loc) { 83 | return utils.setTimeoutPromise(1, loc); 84 | }) 85 | .then(function(loc) { 86 | return registry.get(loc); 87 | }) 88 | .should.be.rejectedWith(/not found/); 89 | }); 90 | }); 91 | 92 | describe('#unregister', function() { 93 | it('should return the client', function() { 94 | return registry.unregister(location) 95 | .should.have.eventually.properties({ ep: 'test' }); 96 | }); 97 | 98 | it('should return an error if location is unknown', function() { 99 | return registry.unregister(123) 100 | .should.be.rejectedWith(/not found/); 101 | }); 102 | }); 103 | 104 | describe('#update', function() { 105 | it('should update client registration params', function() { 106 | return registry.update(location, { lt: 100 }) 107 | .then(function(loc) { 108 | return registry.get(loc); 109 | }) 110 | .should.have.eventually.properties({ lt: 100 }); 111 | }); 112 | 113 | it('should return an error if location is unknown', function() { 114 | return registry.update(123, { lt: 100 }) 115 | .should.be.rejectedWith(/not found/); 116 | }); 117 | }); 118 | 119 | describe('#get', function() { 120 | it('should return the client by location', function() { 121 | return registry.get(location) 122 | .should.eventually.have.property('ep').eql('test'); 123 | }); 124 | 125 | it('should return an error if location is unknown', function() { 126 | return registry.get(123) 127 | .should.be.rejectedWith(/not found/); 128 | }); 129 | }); 130 | 131 | describe('#find', function() { 132 | it('should return the client by endpoint ep', function() { 133 | return registry.find('test') 134 | .should.eventually.have.property('ep').eql('test'); 135 | }); 136 | 137 | it('should return an error if endpoint ep is unknown', function() { 138 | return registry.find('foo') 139 | .should.be.rejectedWith(/not found/); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/schema.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | * 23 | * Author: Alexandre Moreno 24 | */ 25 | 26 | 'use strict'; 27 | 28 | var should = require('should'), // jshint ignore:line 29 | Schema = require('../').Schema; 30 | 31 | describe('LWM2M Object Schema', function() { 32 | describe('#constructor', function() { 33 | 34 | it('should not throw on a valid schema', function() { 35 | var def = { 36 | a: { type: 'String', id: 0 }, 37 | b: { type: 'Integer', id: 1 }, 38 | c: { type: 'Boolean', id: 2 }, 39 | d: { type: 'Opaque', id:3 }, 40 | e: { type: 'Time', id:4 }, 41 | f: { type: ['Float'], id: 5 }, 42 | }; 43 | 44 | function validate() { 45 | return new Schema(def); 46 | } 47 | 48 | validate.should.not.throw(); 49 | }); 50 | 51 | it('should throw on invalid types', function() { 52 | var def = { 53 | a: { type: String, id: 0 }, 54 | b: { type: Error, id: 1 }, 55 | }; 56 | 57 | function validate() { 58 | return new Schema(def); 59 | } 60 | 61 | validate.should.throw(TypeError); 62 | }); 63 | 64 | it('should throw on invalid resources', function() { 65 | var def = { 66 | a: { type: String, id: 0 }, 67 | b: 'foo', 68 | }; 69 | 70 | function validate() { 71 | return new Schema(def); 72 | } 73 | 74 | validate.should.throw(TypeError); 75 | }); 76 | }); 77 | 78 | describe('#validate', function() { 79 | 80 | it('should be ok when an object matches an schema', function() { 81 | var schema = new Schema({ 82 | a: { type: 'String', id: 0 }, 83 | b: { type: 'Integer', id: 1 }, 84 | }); 85 | 86 | function validate() { 87 | return schema.validate({ a: 'foo', b: 3 }); 88 | } 89 | 90 | validate.should.not.throw(); 91 | }); 92 | 93 | it('should throw when an object does not match schema', function() { 94 | var schema = new Schema({ 95 | a: { type: 'String', id: 0 }, 96 | b: { type: 'Integer', id: 1 }, 97 | }); 98 | 99 | function validate() { 100 | return schema.validate({ a: 'foo', b: false }); 101 | } 102 | 103 | validate.should.throw(TypeError); 104 | 105 | function validate2() { 106 | return schema.validate({ a: 'foo', b: [1,2,3] }); 107 | } 108 | 109 | validate2.should.throw(TypeError); 110 | }); 111 | 112 | it('should throw when missing a required resource', function() { 113 | var schema = new Schema({ 114 | a: { type: 'String', id: 0 }, 115 | b: { type: 'Integer', id: 1, required: true }, 116 | c: { type: 'Boolean', id: 2 }, 117 | }); 118 | 119 | function validate() { 120 | return schema.validate({ a: 'foo', c: false }); 121 | } 122 | 123 | validate.should.throw(TypeError); 124 | }); 125 | 126 | it('should throw when a value is not within bounds', function() { 127 | var def = { 128 | b: { type: 'Integer', id: 1 }, 129 | }; 130 | 131 | def.b.range = { 132 | min: 1, 133 | max: 10, 134 | }; 135 | 136 | var schema = new Schema(def); 137 | 138 | function validate() { 139 | return schema.validate({ b: 11 }); 140 | } 141 | 142 | validate.should.throw(TypeError); 143 | 144 | }); 145 | 146 | it('should throw when an enumerated resource is not match', function() { 147 | var def = { 148 | a: { type: String, id: 0 }, 149 | }; 150 | 151 | def.a.enum = [ 152 | 'bar', 153 | 'baz', 154 | 'qux', 155 | ]; 156 | 157 | var schema = new Schema(def); 158 | 159 | function validate() { 160 | return schema.validate({ a: 'foo' }); 161 | } 162 | 163 | validate.should.throw(TypeError); 164 | 165 | }); 166 | }); 167 | }); 168 | 169 | -------------------------------------------------------------------------------- /test/senml.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | * 23 | * Author: Alexandre Moreno 24 | */ 25 | 26 | 'use strict'; 27 | 28 | var should = require('should'), // jshint ignore:line 29 | senml = require('../lib/senml'), 30 | Schema = require('../').Schema, 31 | deviceSchema = new Schema(require('../lib/oma/device')); 32 | 33 | var object = { 34 | manufacturer: 'Open Mobile Alliance', 35 | modelNumber: 'Lightweight M2M Client', 36 | serialNumber: '345000123', 37 | firmwareVer: '1.0', 38 | powerSrcs: [ 1, 5 ], 39 | srcVoltage: [ 3800, 5000 ], 40 | srcCurrent: [ 125, 900 ], 41 | batteryLevel: 100, 42 | memoryFree: 15, 43 | errorCode: [ 0 ], 44 | currentTime: new Date('2013-05-02T10:40:15.000Z'), 45 | utcOffset: '+02:00', 46 | timeZone: 'U', 47 | }; 48 | 49 | var payload = '{"e":[' + 50 | '{"n":"0","sv":"Open Mobile Alliance"},' + 51 | '{"n":"1","sv":"Lightweight M2M Client"},' + 52 | '{"n":"2","sv":"345000123"},' + 53 | '{"n":"3","sv":"1.0"},' + 54 | '{"n":"6/0","v":1},' + 55 | '{"n":"6/1","v":5},' + 56 | '{"n":"7/0","v":3800},' + 57 | '{"n":"7/1","v":5000},' + 58 | '{"n":"8/0","v":125},' + 59 | '{"n":"8/1","v":900},' + 60 | '{"n":"9","v":100},' + 61 | '{"n":"10","v":15},' + 62 | '{"n":"11/0","v":0},' + 63 | '{"n":"13","v":1367491215},' + 64 | '{"n":"14","sv":"+02:00"},' + 65 | '{"n":"15","sv":"U"}]}'; 66 | 67 | describe('application/vnd.oma.lwm2m+json', function() { 68 | 69 | describe('types', function() { 70 | it('should return original integer', function() { 71 | var schema = new Schema({ 72 | foo: { id:3, type:'Integer' }, 73 | }); 74 | 75 | var value = { foo: -42 }; 76 | var buf = senml.serialize(value, schema); 77 | senml.parse(buf, schema).should.be.eql(value); 78 | }); 79 | 80 | it('should return original array', function() { 81 | var schema = new Schema({ 82 | foo: { id:3, type:['Integer'] }, 83 | }); 84 | 85 | var value = { foo: [-1, 0, 1] }; 86 | var buf = senml.serialize(value, schema); 87 | senml.parse(buf, schema).should.be.eql(value); 88 | }); 89 | 90 | it('should return original float', function() { 91 | var schema = new Schema({ 92 | foo: { id:3, type:'Float' }, 93 | }); 94 | 95 | var value = { foo: 42.42 }; 96 | var buf = senml.serialize(value, schema); 97 | senml.parse(buf, schema).should.be.eql(value); 98 | }); 99 | 100 | it('should return original boolean', function() { 101 | var schema = new Schema({ 102 | foo: { id:3, type:'Boolean' }, 103 | }); 104 | 105 | var value = { foo: true }; 106 | var buf = senml.serialize(value, schema); 107 | senml.parse(buf, schema).should.be.eql(value); 108 | }); 109 | 110 | it('should return original buffer', function() { 111 | var schema = new Schema({ 112 | foo: { id:3, type:'Opaque' }, 113 | }); 114 | 115 | var value = { foo: Buffer.from('bar') }; 116 | var buf = senml.serialize(value, schema); 117 | senml.parse(buf, schema).should.be.eql(value); 118 | }); 119 | }); 120 | 121 | describe('#serialize', function() { 122 | it('should return a valid payload', function() { 123 | var dev = senml.serialize(object, deviceSchema); 124 | 125 | dev.should.equal(payload); 126 | }); 127 | 128 | it('should skip user properties', function() { 129 | 130 | var obj = Object.assign({}, object, { foo: 'bar' }); 131 | var dev = senml.serialize(obj, deviceSchema); 132 | 133 | dev.should.equal(payload); 134 | }); 135 | }); 136 | 137 | describe('#parse', function() { 138 | it('should return an object', function() { 139 | var dev = senml.parse(payload, deviceSchema); 140 | 141 | dev.should.be.an.Object().and.not.empty(); 142 | }); 143 | 144 | it('should strictly return matching resources from schema', function() { 145 | var dev = senml.parse(payload, deviceSchema), 146 | keys = Object.keys(deviceSchema.resources); 147 | 148 | Object.keys(dev).should.matchEach(function(it) { 149 | return it.should.be.oneOf(keys); 150 | }); 151 | }); 152 | 153 | it('should return a composite object when using Object links', function() { 154 | var Bar = new Schema({ 155 | bar: { 156 | type: 'String', 157 | id: 2, 158 | }, 159 | baz: { 160 | type: 'Boolean', 161 | id: 3, 162 | }, 163 | }); 164 | 165 | var Foo = new Schema({ 166 | foo: { 167 | type: 'Objlnk', 168 | id: 1, 169 | schema: Bar, 170 | }, 171 | }); 172 | 173 | var test = '{"bn":"/","e":[' + 174 | '{"n":"65/0/1","ov":"66:0"},' + 175 | '{"n":"66/0/2","sv":"test"},' + 176 | '{"n":"66/0/3","bv":false}]}'; 177 | 178 | var result = senml.parse(test, Foo); 179 | 180 | result.should.have.properties({ 181 | foo: { 182 | bar: 'test', 183 | baz: false, 184 | }, 185 | }); 186 | }); 187 | }); 188 | }); 189 | 190 | -------------------------------------------------------------------------------- /test/server/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var should = require('should'); 27 | var lwm2m = require('../../'); 28 | var bootstrap = lwm2m.bootstrap; 29 | var coap = require('coap'); 30 | var port = 5683; 31 | var server, client; 32 | var ep = 'test'; 33 | 34 | var schema = lwm2m.Schema({ 35 | foo : { id: 5, type: 'String' }, 36 | bar : { id: 6, type: 'Integer' }, 37 | }); 38 | 39 | describe('Bootstrap', function() { 40 | beforeEach(function (done) { 41 | server = bootstrap.createServer({ type: 'udp4' }); 42 | server.on('error', done); 43 | server.listen(port, done); 44 | 45 | client = coap.createServer({ type: 'udp4' }); 46 | }); 47 | 48 | afterEach(function(done) { 49 | server.close(function() { 50 | client.close(done); 51 | }); 52 | }); 53 | 54 | describe('#request', function() { 55 | it('should return a 2.04 Changed', function(done) { 56 | server.on('bootstrapRequest', function(params, accept) { 57 | accept(); 58 | }); 59 | 60 | var req = coap.request({ 61 | host: 'localhost', 62 | port: port, 63 | method: 'POST', 64 | pathname: '/bs', 65 | query: 'ep=' + ep, 66 | }); 67 | 68 | req.end(); 69 | 70 | req.on('response', function(res) { 71 | res.code.should.equal('2.04'); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should return a 4.00 Bad Request when user rejects endpoint', function(done) { 77 | server.on('bootstrapRequest', function(params, accept) { 78 | accept(new Error('unknown')); 79 | }); 80 | 81 | var req = coap.request({ 82 | host: 'localhost', 83 | port: port, 84 | method: 'POST', 85 | pathname: '/bs', 86 | query: 'ep=' + ep, 87 | }); 88 | 89 | req.end(); 90 | 91 | req.on('response', function(res) { 92 | res.code.should.equal('4.00'); 93 | done(); 94 | }); 95 | }); 96 | }); 97 | 98 | describe('#write()', function() { 99 | beforeEach(function(done) { 100 | server.on('bootstrapRequest', function(params, accept) { 101 | accept(); 102 | }); 103 | 104 | var req = coap.request({ 105 | host: 'localhost', 106 | port: port, 107 | method: 'POST', 108 | pathname: '/bs', 109 | query: 'ep=' + ep, 110 | }); 111 | 112 | req.end(); 113 | 114 | req.on('response', function(res) { 115 | res.code.should.equal('2.04'); 116 | client.listen(res.outSocket.port, done); 117 | }); 118 | }); 119 | 120 | it('should send an encoded payload', function(done) { 121 | client.on('request', function (req, res) { 122 | var payload = req.payload.toString(); 123 | 124 | req.method.should.equal('POST'); 125 | 126 | payload.should.match(/"bn":"\/3\/4\/"/); 127 | payload.should.match(/"e":\[/); 128 | payload.should.match(/{"n":"5","sv":"test"}/); 129 | payload.should.match(/{"n":"6","v":42}/); 130 | 131 | res.code = '2.04'; 132 | res.end(); 133 | }); 134 | 135 | var options = { 136 | schema: schema, 137 | format: 'json', 138 | }; 139 | 140 | var value = { 141 | foo: 'test', 142 | bar: 42, 143 | }; 144 | 145 | server.write(ep, '/3/4', value, options, function(err) { 146 | should.not.exist(err); 147 | done(); 148 | }); 149 | }); 150 | }); 151 | 152 | describe('#delete()', function() { 153 | beforeEach(function(done) { 154 | server.on('bootstrapRequest', function(params, accept) { 155 | accept(); 156 | }); 157 | 158 | var req = coap.request({ 159 | host: 'localhost', 160 | port: port, 161 | method: 'POST', 162 | pathname: '/bs', 163 | query: 'ep=' + ep, 164 | }); 165 | 166 | req.end(); 167 | 168 | req.on('response', function(res) { 169 | res.code.should.equal('2.04'); 170 | client.listen(res.outSocket.port, done); 171 | }); 172 | }); 173 | 174 | it('should delete an instance', function(done) { 175 | client.on('request', function (req, res) { 176 | req.method.should.equal('DELETE'); 177 | res.code = '2.02'; 178 | res.end(); 179 | }); 180 | 181 | server.delete(ep, '/3/4', function(err) { 182 | should.not.exist(err); 183 | done(); 184 | }); 185 | }); 186 | 187 | it('should delete all the existing Object Instances', function() { 188 | client.on('request', function (req, res) { 189 | req.method.should.equal('DELETE'); 190 | req.url.should.equal('/'); 191 | res.code = '2.02'; 192 | res.end(); 193 | }); 194 | 195 | return server.delete(ep, '/').should.be.fulfilled(); 196 | }); 197 | }); 198 | 199 | describe('#finish()', function() { 200 | beforeEach(function(done) { 201 | server.on('bootstrapRequest', function(params, accept) { 202 | accept(); 203 | }); 204 | 205 | var req = coap.request({ 206 | host: 'localhost', 207 | port: port, 208 | method: 'POST', 209 | pathname: '/bs', 210 | query: 'ep=' + ep, 211 | }); 212 | 213 | req.end(); 214 | 215 | req.on('response', function(res) { 216 | res.code.should.equal('2.04'); 217 | client.listen(res.outSocket.port, done); 218 | }); 219 | }); 220 | 221 | it('should delete an instance', function() { 222 | client.on('request', function (req, res) { 223 | req.method.should.equal('POST'); 224 | req.url.should.equal('/bs'); 225 | res.code = '2.04'; 226 | res.end(); 227 | }); 228 | 229 | return server.finish(ep).should.be.fulfilled(); 230 | }); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /test/server/device-management.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var should = require('should'); 27 | var url = require('url'); 28 | var lwm2m = require('../../'); 29 | var coap = require('coap'); 30 | var Readable = require('readable-stream').Readable; 31 | var port = 5683; 32 | var server, client; 33 | var payload = ',,,,'; 34 | var ep = 'test'; 35 | var schema = lwm2m.Schema({ 36 | foo : { id: 5, type: 'String' }, 37 | bar : { id: 6, type: 'Integer' }, 38 | }); 39 | 40 | describe('Device management' , function() { 41 | 42 | beforeEach(function (done) { 43 | server = lwm2m.createServer({ type: 'udp4' }); 44 | client = coap.createServer({ type: 'udp4' }); 45 | server.on('error', done); 46 | server.on('register', function(params, accept) { 47 | accept(); 48 | }); 49 | 50 | server.listen(port, function() { 51 | var req = coap.request({ 52 | host: 'localhost', 53 | port: port, 54 | method: 'POST', 55 | pathname: '/rd', 56 | query: 'ep=' + ep + '<=86400&lwm2m=1.0&b=U', 57 | }); 58 | 59 | req.end(payload); 60 | 61 | req.on('response', function(res) { 62 | res.code.should.equal('2.01'); 63 | client.listen(res.outSocket.port, done); 64 | }); 65 | }); 66 | }); 67 | 68 | afterEach(function(done) { 69 | server.close(function() { 70 | client.close(done); 71 | }); 72 | }); 73 | 74 | describe('#read()', function() { 75 | it('should respond with parsed object', function(done) { 76 | client.on('request', function (req, res) { 77 | req.method.should.equal('GET'); 78 | res.setOption('Content-Format', 'application/vnd.oma.lwm2m+json'); 79 | res.code = '2.05'; 80 | 81 | var rs = new Readable(); 82 | rs.push('{"e":['); 83 | rs.push('{"n":"5","sv":"test"},'); 84 | rs.push('{"n":"6","v":42}'); 85 | rs.push(']}'); 86 | rs.push(null); 87 | rs.pipe(res); 88 | }); 89 | 90 | server.read(ep, '/3/4', { schema: schema }, function(err, result) { 91 | should.not.exist(err); 92 | should.exist(result); 93 | result.should.have.properties(['foo', 'bar']); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('should respond with matching resource', function() { 99 | client.on('request', function (req, res) { 100 | req.method.should.equal('GET'); 101 | res.setOption('Content-Format', 'text/plain'); 102 | res.code = '2.05'; 103 | res.end('test'); 104 | }); 105 | 106 | return server.read(ep, '42/3/5', { schema: schema }) 107 | .should.eventually.be.equal('test'); 108 | }); 109 | 110 | it('should read current time in device object', function(done) { 111 | client.on('request', function (req, res) { 112 | req.method.should.equal('GET'); 113 | res.setOption('Content-Format', 'application/vnd.oma.lwm2m+json'); 114 | res.code = '2.05'; 115 | 116 | var rs = new Readable(); 117 | rs.push('{"e":['); 118 | rs.push('{"n":"13","v":42}'); 119 | rs.push(']}'); 120 | rs.push(null); 121 | rs.pipe(res); 122 | }); 123 | 124 | server.read(ep, '/3/0', function(err, result) { 125 | should.not.exist(err); 126 | should.exist(result); 127 | result.should.have.properties({ 128 | currentTime: new Date(42 * 1e3), 129 | }); 130 | done(); 131 | }); 132 | }); 133 | 134 | it('should return an error if object not found', function() { 135 | client.on('request', function (req, res) { 136 | res.code = '4.04'; 137 | res.end(); 138 | }); 139 | 140 | return server.read(ep, '42/3', { schema: schema }) 141 | .should.be.rejectedWith(/not found/); 142 | }); 143 | 144 | it('should respond with an 4.04 when client not found', function(done) { 145 | server.read('unknown', '42/3/5', { schema: schema }, function(err) { 146 | should.exist(err); 147 | err.should.have.property('code').eql('4.04'); 148 | done(); 149 | }); 150 | }); 151 | 152 | it('should throw when invalid schema', function() { 153 | server.read.bind(server, ep, '42/3/5', { schema: 'invalid' }).should 154 | .throw(TypeError, { message: 'Illegal schema' }); 155 | }); 156 | 157 | it('should throw when path is nonsense', function() { 158 | server.read.bind(server, ep, '/', { schema: schema }).should 159 | .throw(/Illegal path/); 160 | server.read.bind(server, ep, 'foo', { schema: schema }).should 161 | .throw(/Illegal path/); 162 | }); 163 | }); 164 | 165 | describe('#write()', function() { 166 | it('should update current time in device object', function(done) { 167 | client.on('request', function (req, res) { 168 | var payload = req.payload.toString(); 169 | 170 | req.method.should.equal('POST'); 171 | 172 | payload.should.match(/"bn":"\/3\/0\/"/); 173 | payload.should.match(/"e":\[/); 174 | payload.should.match(/{"n":"13","v":42}/); 175 | 176 | res.code = '2.04'; 177 | res.end(); 178 | 179 | done(); 180 | }); 181 | 182 | var value = { 183 | currentTime: new Date(42 * 1e3), 184 | }; 185 | 186 | var options = { 187 | format: 'json', 188 | }; 189 | 190 | server.write(ep, '/3/0', value, options) 191 | .catch(done); 192 | }); 193 | 194 | it('should send an encoded payload', function(done) { 195 | client.on('request', function (req, res) { 196 | var payload = req.payload.toString(); 197 | 198 | req.method.should.equal('POST'); 199 | 200 | payload.should.match(/"bn":"\/5\/4\/"/); 201 | payload.should.match(/"e":\[/); 202 | payload.should.match(/{"n":"5","sv":"test"}/); 203 | payload.should.match(/{"n":"6","v":42}/); 204 | 205 | res.code = '2.04'; 206 | res.end(); 207 | }); 208 | 209 | var options = { 210 | schema: schema, 211 | format: 'json', 212 | }; 213 | 214 | var value = { 215 | foo: 'test', 216 | bar: 42, 217 | }; 218 | 219 | server.write(ep, '/5/4', value, options, function(err) { 220 | should.not.exist(err); 221 | done(); 222 | }); 223 | }); 224 | 225 | it('should return an error if object not found', function() { 226 | client.on('request', function (req, res) { 227 | res.code = '4.04'; 228 | res.end(); 229 | }); 230 | 231 | var options = { 232 | schema: schema, 233 | format: 'json', 234 | }; 235 | 236 | var value = { 237 | foo: 'test', 238 | bar: 42, 239 | }; 240 | 241 | return server.write(ep, '42/3', value, options) 242 | .should.be.rejectedWith(/not found/); 243 | }); 244 | }); 245 | 246 | describe('#create()', function() { 247 | it('should send new object encoded', function(done) { 248 | client.on('request', function (req, res) { 249 | var payload = req.payload.toString(); 250 | 251 | req.method.should.equal('POST'); 252 | 253 | payload.should.match(/"e":\[/); 254 | payload.should.match(/{"n":"5","sv":"test"}/); 255 | payload.should.match(/{"n":"6","v":42}/); 256 | 257 | res.code = '2.01'; 258 | res.end(); 259 | 260 | done(); 261 | }); 262 | 263 | var options = { 264 | schema: schema, 265 | format: 'json', 266 | }; 267 | 268 | var value = { 269 | foo: 'test', 270 | bar: 42, 271 | }; 272 | 273 | server.create(ep, '/42', value, options); 274 | }); 275 | 276 | it('should throw if path not an ObjectID', function() { 277 | var options = { 278 | schema: schema, 279 | format: 'json', 280 | }; 281 | 282 | var value = { 283 | foo: 'test', 284 | bar: 42, 285 | }; 286 | 287 | server.create.bind(server, ep, '/42/3', value, options) 288 | .should.throw(); 289 | }); 290 | 291 | it('should throw when path is nonsense', function() { 292 | server.create.bind(server, ep, '/').should 293 | .throw(/Illegal path/); 294 | server.create.bind(server, ep, 'foo').should 295 | .throw(/Illegal path/); 296 | }); 297 | }); 298 | 299 | describe('#discover()', function() { 300 | it('should respond with matching payload sent by client', function(done) { 301 | client.on('request', function (req, res) { 302 | req.method.should.equal('GET'); 303 | res.code = '2.05'; 304 | res.end('test'); 305 | }); 306 | 307 | server.discover(ep, '42/3/5', function(err, result) { 308 | should.not.exist(err); 309 | should.exist(result); 310 | result.should.equal('test'); 311 | done(); 312 | }); 313 | }); 314 | 315 | it('should throw when path is nonsense', function() { 316 | server.discover.bind(server, ep, '/').should 317 | .throw(/Illegal path/); 318 | server.discover.bind(server, ep, 'foo').should 319 | .throw(/Illegal path/); 320 | }); 321 | 322 | it('should return an error if object not found', function() { 323 | client.on('request', function (req, res) { 324 | res.code = '4.04'; 325 | res.end(); 326 | }); 327 | 328 | return server.discover(ep, '42/3') 329 | .should.be.rejectedWith(/not found/); 330 | }); 331 | 332 | }); 333 | 334 | describe('#writeAttributes()', function() { 335 | it('should send query matching attributes', function(done) { 336 | client.on('request', function (req, res) { 337 | var attr = url 338 | .parse(req.url).query 339 | .split('&') 340 | .reduce(function(attr, cur) { 341 | var pair = cur.split('='); 342 | attr[pair[0]] = pair[1]; 343 | return attr; 344 | }, {}); 345 | 346 | attr.should.have.keys('pmin', 'pmax', 'lt'); 347 | req.method.should.equal('PUT'); 348 | res.code = '2.04'; 349 | res.end(); 350 | }); 351 | 352 | var attr = { pmin: 1, pmax: 5, lt: 5 }; 353 | 354 | server.writeAttributes(ep, '42/3/5', attr, function(err, result) { 355 | should.not.exist(err); 356 | should.exist(result); 357 | done(); 358 | }); 359 | }); 360 | 361 | it('should return an error if bad attributes', function() { 362 | var attr = { pmin: 1, pmax: 5, lt: 5, bad: true }; 363 | 364 | return server.writeAttributes(ep, '42/3', attr) 365 | .should.be.rejectedWith(/Unsupported attributes/); 366 | }); 367 | 368 | it('should return an error if object not found', function() { 369 | client.on('request', function (req, res) { 370 | res.code = '4.04'; 371 | res.end(); 372 | }); 373 | 374 | var attr = { pmin: 1, pmax: 5, lt: 5 }; 375 | 376 | return server.writeAttributes(ep, '42/3', attr) 377 | .should.be.rejectedWith(/not found/); 378 | }); 379 | 380 | }); 381 | 382 | describe('#delete()', function() { 383 | it('should delete an instance', function(done) { 384 | client.on('request', function (req, res) { 385 | req.method.should.equal('DELETE'); 386 | res.code = '2.02'; 387 | res.end(); 388 | }); 389 | 390 | server.delete(ep, '/3/4', function(err) { 391 | should.not.exist(err); 392 | done(); 393 | }); 394 | }); 395 | 396 | it('should throw if path not an instance', function() { 397 | server.delete.bind(server, ep, '42/3/5').should.throw(); 398 | }); 399 | 400 | it('should return an error if object not found', function() { 401 | client.on('request', function (req, res) { 402 | res.code = '4.04'; 403 | res.end(); 404 | }); 405 | 406 | return server.delete(ep, '42/3') 407 | .should.be.rejectedWith(/not found/); 408 | }); 409 | }); 410 | 411 | describe('#execute()', function() { 412 | it('should send a POST request to a resource', function(done) { 413 | client.on('request', function (req, res) { 414 | req.method.should.equal('POST'); 415 | req.payload.toString().should.equal('test'); 416 | res.code = '2.04'; 417 | res.end(); 418 | }); 419 | 420 | server.execute(ep, '42/3/5', 'test', function(err) { 421 | should.not.exist(err); 422 | done(); 423 | }); 424 | }); 425 | 426 | it('should return an error if object not found', function() { 427 | client.on('request', function (req, res) { 428 | res.code = '4.04'; 429 | res.end(); 430 | }); 431 | 432 | return server.execute(ep, '42/3') 433 | .should.be.rejectedWith(/not found/); 434 | }); 435 | }); 436 | 437 | }); 438 | -------------------------------------------------------------------------------- /test/server/information-reporting.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var should = require('should'); 27 | var lwm2m = require('../../'); 28 | var coap = require('coap'); 29 | var Writable = require('readable-stream').Writable; 30 | var Readable = require('readable-stream').Readable; 31 | var Stream = require('stream'); 32 | var port = 5683; 33 | var server, client; 34 | var payload = ',,,,'; 35 | var ep = 'test'; 36 | var schema = lwm2m.Schema({ 37 | foo : { id: 5, type: 'String' }, 38 | bar : { id: 6, type: 'Integer' }, 39 | }); 40 | 41 | describe('Information Reporting', function() { 42 | 43 | beforeEach(function (done) { 44 | server = lwm2m.createServer({ type: 'udp4' }); 45 | client = coap.createServer({ type: 'udp4' }); 46 | 47 | server.on('error', done); 48 | server.on('register', function(params, accept) { 49 | accept(); 50 | }); 51 | 52 | server.listen(port, function() { 53 | var req = coap.request({ 54 | host: 'localhost', 55 | port: port, 56 | method: 'POST', 57 | pathname: '/rd', 58 | query: 'ep=' + ep + '<=86400&lwm2m=1.0&b=U', 59 | }); 60 | 61 | req.on('response', function(res) { 62 | res.code.should.equal('2.01'); 63 | client.listen(res.outSocket.port, done); 64 | }); 65 | 66 | req.end(payload); 67 | }); 68 | }); 69 | 70 | afterEach(function(done) { 71 | server.close(function() { 72 | client.close(done); 73 | }); 74 | }); 75 | 76 | describe('#observe()', function() { 77 | it('should return an stream and pipe client messages', function(done) { 78 | client.on('request', function (req, res) { 79 | req.method.should.equal('GET'); 80 | req.headers['Observe'].should.equal(0); 81 | 82 | res.should.be.an.instanceof(Writable); 83 | 84 | var interval = setInterval(function() { 85 | res.write('test'); 86 | }, 50); 87 | 88 | res.setOption('Content-Format', 'text/plain'); 89 | res.code = '2.05'; 90 | 91 | setTimeout(function() { 92 | res.end(); 93 | }, 200); 94 | 95 | res.on('finish', function(err) { 96 | should.not.exist(err); 97 | clearInterval(interval); 98 | }); 99 | 100 | }); 101 | 102 | server.observe(ep, '/3/4/5') 103 | .then(function(stream) { 104 | stream.should.be.an.instanceof(Stream); 105 | stream.on('data', function(chunk) { 106 | chunk.should.be.equal('test'); 107 | }); 108 | stream.on('error', done); 109 | }) 110 | .then(done, done); 111 | }); 112 | 113 | it('should return parsed objects when using a schema', function(done) { 114 | client.on('request', function (req, res) { 115 | req.method.should.equal('GET'); 116 | req.headers['Observe'].should.equal(0); 117 | 118 | res.should.be.an.instanceof(Writable); 119 | 120 | res.setOption('Content-Format', 'application/vnd.oma.lwm2m+json'); 121 | res.code = '2.05'; 122 | 123 | setTimeout(function() { 124 | var rs = new Readable(); 125 | rs.push('{"e":['); 126 | rs.push('{"n":"5","sv":"test"},'); 127 | rs.push('{"n":"6","v":42}'); 128 | rs.push(']}'); 129 | rs.push(null); 130 | rs.pipe(res); 131 | 132 | }, 200); 133 | 134 | res.on('finish', function(err) { 135 | should.not.exist(err); 136 | }); 137 | 138 | }); 139 | 140 | server.observe(ep, '/3/4', { schema: schema }) 141 | .then(function(stream) { 142 | stream.should.be.an.instanceof(Stream); 143 | stream.on('data', function(data) { 144 | should.exist(data); 145 | data.should.have.properties({ foo: 'test', bar: 42 }); 146 | }); 147 | stream.on('error', done); 148 | }) 149 | .then(done, done); 150 | }); 151 | 152 | it('should emit an `end` event when closing the stream', function(done) { 153 | client.on('request', function (req, res) { 154 | req.method.should.equal('GET'); 155 | req.headers['Observe'].should.equal(0); 156 | 157 | res.should.be.an.instanceof(Writable); 158 | res.setOption('Content-Format', 'text/plain'); 159 | res.code = '2.05'; 160 | res.write('test'); 161 | }); 162 | 163 | server.observe(ep, '/3/4') 164 | .then(function(stream) { 165 | stream.should.be.an.instanceof(Stream); 166 | 167 | stream.on('data', function(chunk) { 168 | chunk.should.be.equal('test'); 169 | stream.close(); 170 | }); 171 | 172 | stream.on('error', done); 173 | }) 174 | .then(done, done); 175 | }); 176 | }); 177 | 178 | describe('#cancel()', function() { 179 | it('should stop receiving data', function() { 180 | client.on('request', function (req, res) { 181 | req.method.should.equal('GET'); 182 | req.headers['Observe'].should.equal(1); 183 | 184 | res.end(); 185 | }); 186 | 187 | return server.cancel(ep, '/3/4') 188 | .should.be.fulfilled(); 189 | }); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /test/server/registration.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var should = require('should'); 27 | var lwm2m = require('../../'); 28 | var coap = require('coap'); 29 | var port = 5683; 30 | var server; 31 | var payload = ',,,,'; 32 | var ep = 'test'; 33 | var location; 34 | 35 | describe('Registration', function() { 36 | beforeEach(function (done) { 37 | server = lwm2m.createServer({ type: 'udp4' }); 38 | server.on('error', done); 39 | server.listen(port, done); 40 | }); 41 | afterEach(function(done) { 42 | setImmediate(function() { 43 | server.close(done); 44 | }); 45 | }); 46 | 47 | describe('#register', function() { 48 | it('should return a 2.01 Created when valid request', function(done) { 49 | var req = coap.request({ 50 | host: 'localhost', 51 | port: port, 52 | method: 'POST', 53 | pathname: '/rd', 54 | query: 'ep=test<=86400&lwm2m=1.0&b=U', 55 | }); 56 | 57 | server.on('register', function(params, accept) { 58 | accept(); 59 | }); 60 | 61 | req.on('response', function(res) { 62 | res.code.should.equal('2.01'); 63 | done(); 64 | }); 65 | 66 | req.end(payload); 67 | }); 68 | 69 | it('should fail with a 4.00 Bad Request when illegal query', function(done) { 70 | var req = coap.request({ 71 | host: 'localhost', 72 | port: port, 73 | method: 'POST', 74 | pathname: '/rd', 75 | query: 'ep=test<=86400&lwm2m=1.0&b=U&bad=test', 76 | }); 77 | 78 | server.on('register', function(params, accept) { 79 | accept(); 80 | }); 81 | 82 | req.on('response', function(res) { 83 | res.code.should.equal('4.00'); 84 | done(); 85 | }); 86 | 87 | req.end(payload); 88 | }); 89 | 90 | it('should fail with a 4.00 Bad Request when missing endpoint name', function(done) { 91 | var req = coap.request({ 92 | host: 'localhost', 93 | port: port, 94 | method: 'POST', 95 | pathname: '/rd', 96 | query: 'lt=86400&lwm2m=1.0&b=U', 97 | }); 98 | 99 | server.on('register', function(params, accept) { 100 | accept(); 101 | }); 102 | 103 | req.on('response', function(res) { 104 | res.code.should.equal('4.00'); 105 | done(); 106 | }); 107 | 108 | req.end(payload); 109 | }); 110 | 111 | it('should return a 2.01 Created when missing lifetime', function(done) { 112 | var req = coap.request({ 113 | host: 'localhost', 114 | port: port, 115 | method: 'POST', 116 | pathname: '/rd', 117 | query: 'ep=test&lwm2m=1.0&b=U', 118 | }); 119 | 120 | server.on('register', function(params, accept) { 121 | accept(); 122 | }); 123 | 124 | req.on('response', function(res) { 125 | res.code.should.equal('2.01'); 126 | done(); 127 | }); 128 | 129 | req.end(payload); 130 | }); 131 | 132 | it('should emit register event with query params', function (done) { 133 | var req = coap.request({ 134 | host: 'localhost', 135 | port: port, 136 | method: 'POST', 137 | pathname: '/rd', 138 | query: 'ep=test<=86400&lwm2m=1.0&b=U', 139 | }); 140 | var request = {}; 141 | 142 | server.on('register', function(params, accept) { 143 | request = params; 144 | accept(); 145 | }); 146 | 147 | req.on('response', function(res) { 148 | res.code.should.equal('2.01'); 149 | request.should.have.properties(['ep', 'lt', 'lwm2m', 'b']); 150 | request.payload.should.be.a.String(); 151 | done(); 152 | }); 153 | 154 | req.end(payload); 155 | }); 156 | 157 | it('should set Location-Path Option', function (done) { 158 | var req = coap.request({ 159 | host: 'localhost', 160 | port: port, 161 | method: 'POST', 162 | pathname: '/rd', 163 | query: 'ep=test<=86400&lwm2m=1.0&b=U', 164 | }); 165 | 166 | server.on('register', function(params, accept) { 167 | accept(); 168 | }); 169 | 170 | req.on('response', function(res) { 171 | res.headers['Location-Path'].should.match(/rd\/\w+/); 172 | done(); 173 | }); 174 | 175 | req.end(payload); 176 | }); 177 | 178 | it('should fail with a 4.03 Forbidden when user rejects endpoint', function(done) { 179 | var req = coap.request({ 180 | host: 'localhost', 181 | port: port, 182 | method: 'POST', 183 | pathname: '/rd', 184 | query: 'ep=test<=86400&lwm2m=1.0&b=U', 185 | }); 186 | 187 | server.on('register', function(params, accept) { 188 | accept(new Error('unknown')); 189 | }); 190 | 191 | req.on('response', function(res) { 192 | res.code.should.equal('4.03'); 193 | done(); 194 | }); 195 | 196 | req.end(payload); 197 | 198 | }); 199 | }); 200 | 201 | describe('#deregister', function() { 202 | beforeEach(function(done) { 203 | server.on('register', function(params, accept) { 204 | accept(); 205 | }); 206 | 207 | var req = coap.request({ 208 | host: 'localhost', 209 | port: port, 210 | method: 'POST', 211 | pathname: '/rd', 212 | query: 'ep=' + ep + '<=86400&lwm2m=1.0&b=U', 213 | }); 214 | 215 | req.on('response', function(res) { 216 | res.code.should.equal('2.01'); 217 | location = res.headers['Location-Path']; 218 | done(); 219 | }); 220 | 221 | req.end(payload); 222 | }); 223 | 224 | it('should return a 2.02 Deleted', function(done) { 225 | var req = coap.request({ 226 | host: 'localhost', 227 | port: port, 228 | method: 'DELETE', 229 | pathname: location, 230 | }); 231 | 232 | req.on('response', function(res) { 233 | res.code.should.equal('2.02'); 234 | done(); 235 | }); 236 | 237 | req.end(); 238 | }); 239 | 240 | it('should emit the `deregister` event', function(done) { 241 | var req = coap.request({ 242 | host: 'localhost', 243 | port: port, 244 | method: 'DELETE', 245 | pathname: location, 246 | }); 247 | 248 | server.on('deregister', function(result) { 249 | location.should.endWith(result.location); 250 | done(); 251 | }); 252 | 253 | req.end(); 254 | }); 255 | 256 | it('should return a 4.04 Not Found for unknown client', function(done) { 257 | var req = coap.request({ 258 | host: 'localhost', 259 | port: port, 260 | method: 'DELETE', 261 | pathname: '/rd/136', 262 | }); 263 | 264 | req.on('response', function(res) { 265 | res.code.should.equal('4.04'); 266 | done(); 267 | }); 268 | 269 | req.end(); 270 | }); 271 | }); 272 | 273 | describe('#update', function() { 274 | 275 | beforeEach(function (done) { 276 | server.on('register', function(params, accept) { 277 | accept(); 278 | }); 279 | 280 | var req = coap.request({ 281 | host: 'localhost', 282 | port: port, 283 | method: 'POST', 284 | pathname: '/rd', 285 | query: 'ep=' + ep + '<=86400&lwm2m=1.0&b=U', 286 | }); 287 | 288 | req.on('response', function(res) { 289 | location = res.headers['Location-Path']; 290 | done(); 291 | }); 292 | 293 | req.end(payload); 294 | }); 295 | 296 | it('should return a 2.04 Changed', function(done) { 297 | var req = coap.request({ 298 | host: 'localhost', 299 | port: port, 300 | method: 'POST', 301 | pathname: location, 302 | query: 'lt=86400&lwm2m=1.0&b=U', 303 | }); 304 | 305 | req.on('response', function(res) { 306 | res.code.should.equal('2.04'); 307 | done(); 308 | }); 309 | 310 | req.end(payload); 311 | }); 312 | 313 | it('should emit the `update` event', function(done) { 314 | var req = coap.request({ 315 | host: 'localhost', 316 | port: port, 317 | method: 'POST', 318 | pathname: location, 319 | query: 'lt=86400&lwm2m=1.0&b=U', 320 | }); 321 | 322 | server.on('update', function(loc) { 323 | location.should.endWith(loc); 324 | done(); 325 | }); 326 | 327 | req.on('response', function(res) { 328 | res.code.should.equal('2.04'); 329 | }); 330 | 331 | req.end(payload); 332 | }); 333 | 334 | it('should fail with a 4.04 Not Found when unknown location', function(done) { 335 | var req = coap.request({ 336 | host: 'localhost', 337 | port: port, 338 | method: 'POST', 339 | pathname: '/rd/136', 340 | query: 'lt=86400&lwm2m=1.0&b=U', 341 | }); 342 | 343 | req.on('response', function(res) { 344 | res.code.should.equal('4.04'); 345 | done(); 346 | }); 347 | 348 | req.end(payload); 349 | }); 350 | 351 | it('should fail with a 4.00 Bad Request when illegal query', function(done) { 352 | var req = coap.request({ 353 | host: 'localhost', 354 | port: port, 355 | method: 'POST', 356 | pathname: location, 357 | query: 'lt=86400&lwm2m=1.0&b=U&bad=test', 358 | }); 359 | 360 | req.on('response', function(res) { 361 | res.code.should.equal('4.00'); 362 | done(); 363 | }); 364 | 365 | req.end(payload); 366 | }); 367 | }); 368 | 369 | }); 370 | 371 | -------------------------------------------------------------------------------- /test/slidingWindow.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var Window = require('../lib/slidingWindow'), 27 | assert = require('assert'); 28 | 29 | describe('Sliding windows test', function() { 30 | var window; 31 | 32 | beforeEach(function() { 33 | window = new Window(5); 34 | for (var i = 0; i < 5; i++) { 35 | window.push(i); 36 | } 37 | }); 38 | 39 | describe('When a new value is inserted in the sliding window', function() { 40 | beforeEach(function() { 41 | window.push(6); 42 | }); 43 | 44 | it('should be contained in the window', function() { 45 | assert(window.contains(6)); 46 | }); 47 | it('should remove one of the older values from the window', function() { 48 | assert(!window.contains(1)); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/tlv.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Telefonica Investigación y Desarrollo, S.A.U 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | * 23 | * Author: Alexandre Moreno 24 | */ 25 | 26 | 'use strict'; 27 | 28 | var should = require('should'), // jshint ignore:line 29 | tlv = require('../lib/tlv'), 30 | Schema = require('../').Schema, 31 | deviceSchema = new Schema(require('../lib/oma/device')); 32 | 33 | var object = { 34 | manufacturer: 'Open Mobile Alliance', 35 | modelNumber: 'Lightweight M2M Client', 36 | serialNumber: '345000123', 37 | firmwareVer: '1.0', 38 | powerSrcs: [ 1, 5 ], 39 | srcVoltage: [ 3800, 5000 ], 40 | srcCurrent: [ 125, 900 ], 41 | batteryLevel: 100, 42 | memoryFree: 15, 43 | errorCode: [ 0 ], 44 | currentTime: new Date('2013-05-02T10:40:15.000Z'), 45 | utcOffset: '+02:00', 46 | binding: 'U', 47 | }; 48 | 49 | // The total payload size with the TLV encoding is 121 bytes 50 | // Copied verbatim from the spec. 51 | var payload = Buffer.from([ 52 | 0xc8, 0x00, 0x14, 0x4f, 0x70, 0x65, 0x6e, 0x20, 0x4d, 0x6f, 53 | 0x62, 0x69, 0x6c, 0x65, 0x20, 0x41, 0x6c, 0x6c, 0x69, 0x61, 54 | 0x6e, 0x63, 0x65, 0xc8, 0x01, 0x16, 0x4c, 0x69, 0x67, 0x68, 55 | 0x74, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x20, 0x4d, 0x32, 56 | 0x4d, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0xc8, 0x02, 57 | 0x09, 0x33, 0x34, 0x35, 0x30, 0x30, 0x30, 0x31, 0x32, 0x33, 58 | 0xc3, 0x03, 0x31, 0x2e, 0x30, 0x86, 0x06, 0x41, 0x00, 0x01, 59 | 0x41, 0x01, 0x05, 0x88, 0x07, 0x08, 0x42, 0x00, 0x0e, 0xd8, 60 | 0x42, 0x01, 0x13, 0x88, 0x87, 0x08, 0x41, 0x00, 0x7d, 0x42, 61 | 0x01, 0x03, 0x84, 0xc1, 0x09, 0x64, 0xc1, 0x0a, 0x0f, 0x83, 62 | 0x0b, 0x41, 0x00, 0x00, 0xc4, 0x0d, 0x51, 0x82, 0x42, 0x8f, 63 | 0xc6, 0x0e, 0x2b, 0x30, 0x32, 0x3a, 0x30, 0x30, 0xc1, 0x10, 64 | 0x55 ]); 65 | 66 | describe('application/vnd.oma.lwm2m+tlv', function() { 67 | 68 | describe('types', function() { 69 | it('should return original integer', function() { 70 | var schema = new Schema({ 71 | foo: { id:3, type:'Integer' }, 72 | }); 73 | 74 | var value = { foo: -42 }; 75 | var buf = tlv.serialize(value, schema); 76 | tlv.parse(buf, schema).should.be.eql(value); 77 | }); 78 | 79 | it('should return original array', function() { 80 | var schema = new Schema({ 81 | foo: { id:3, type:['Integer'] }, 82 | }); 83 | 84 | var value = { foo: [-1, 0, 1] }; 85 | var buf = tlv.serialize(value, schema); 86 | tlv.parse(buf, schema).should.be.eql(value); 87 | }); 88 | 89 | it('should return original float', function() { 90 | var schema = new Schema({ 91 | foo: { id:3, type:'Float' }, 92 | }); 93 | 94 | var value = { foo: 42.42 }; 95 | var buf = tlv.serialize(value, schema); 96 | tlv.parse(buf, schema).should.be.eql(value); 97 | }); 98 | 99 | it('should return original boolean', function() { 100 | var schema = new Schema({ 101 | foo: { id:3, type:'Boolean' }, 102 | }); 103 | 104 | var value = { foo: true }; 105 | var buf = tlv.serialize(value, schema); 106 | tlv.parse(buf, schema).should.be.eql(value); 107 | }); 108 | 109 | it('should return original buffer', function() { 110 | var schema = new Schema({ 111 | foo: { id:3, type:'Opaque' }, 112 | }); 113 | 114 | var value = { foo: Buffer.from('bar') }; 115 | var buf = tlv.serialize(value, schema); 116 | tlv.parse(buf, schema).should.be.eql(value); 117 | }); 118 | }); 119 | describe('#serialize', function() { 120 | it('should return a valid payload', function() { 121 | var dev = tlv.serialize(object, deviceSchema); 122 | 123 | dev.should.be.an.instanceOf(Buffer); 124 | dev.toString('hex').should.equal(payload.toString('hex')); 125 | }); 126 | 127 | it('should skip user properties', function() { 128 | 129 | var obj = Object.assign({}, object, { foo: 'bar' }); 130 | var dev = tlv.serialize(obj, deviceSchema); 131 | 132 | dev.should.be.an.instanceOf(Buffer); 133 | dev.toString('hex').should.equal(payload.toString('hex')); 134 | }); 135 | }); 136 | 137 | describe('#parse', function() { 138 | it('should return a valid object', function() { 139 | var dev = tlv.parse(payload, deviceSchema); 140 | 141 | dev.should.be.eql(object); 142 | }); 143 | 144 | it('should correctly parse single-precision floats', function() { 145 | var schema = new Schema({ 146 | foo: { id:1, type:'Float' }, 147 | }); 148 | 149 | var payload = Buffer.from('c4014145851f', 'hex'); 150 | var result = tlv.parse(payload, schema); 151 | 152 | result.should.have.property('foo').which.is.a.Number(); 153 | result.foo.toFixed(3).should.be.eql('12.345'); 154 | }); 155 | }); 156 | }); 157 | 158 | 159 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Alexandre Moreno 3 | * 4 | * This file is part of node-lwm2m 5 | * 6 | * node-lwm2m is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the License, 9 | * or (at your option) any later version. 10 | * 11 | * node-lwm2m is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 14 | * See the GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public 17 | * License along with node-lwm2m. 18 | * If not, seehttp://www.gnu.org/licenses/. 19 | * 20 | * For those usages not covered by the GNU Affero General Public License 21 | * please contact with::[contacto@tid.es] 22 | */ 23 | 24 | 'use strict'; 25 | 26 | var should = require('should'), // jshint ignore:line 27 | utils = require('../lib/utils'); 28 | 29 | describe('utils', function() { 30 | describe('#getMediaType', function() { 31 | it('should return text when path is a resource and value an string', function() { 32 | utils.getMediaType('/25/4/3', 'test') 33 | .should.be.equal('text/plain'); 34 | 35 | utils.getMediaType('/25/4/3', 'test', 'text') 36 | .should.be.equal('text/plain'); 37 | }); 38 | it('should return json when path is an instance and value an string', function() { 39 | utils.getMediaType('/25/4', 'test') 40 | .should.be.equal('application/vnd.oma.lwm2m+json'); 41 | 42 | utils.getMediaType('/25/4', 'test', 'json') 43 | .should.be.equal('application/vnd.oma.lwm2m+json'); 44 | }); 45 | it('should return octet-stream when path is a resource and value a buffer', function() { 46 | utils.getMediaType('/25/4/3', Buffer.from('test')) 47 | .should.be.equal('application/octet-stream'); 48 | 49 | utils.getMediaType('/25/4/3', Buffer.from('test'), 'opaque') 50 | .should.be.equal('application/octet-stream'); 51 | }); 52 | it('should return tlv when path is an instance and value a buffer', function() { 53 | utils.getMediaType('/25/4', Buffer.from('test')) 54 | .should.be.equal('application/vnd.oma.lwm2m+tlv'); 55 | 56 | utils.getMediaType('/25/4', Buffer.from('test'), 'tlv') 57 | .should.be.equal('application/vnd.oma.lwm2m+tlv'); 58 | }); 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------