Compare commits
153 Commits
Author | SHA1 | Date | |
---|---|---|---|
e1adb78f7d | |||
1f717fa987 | |||
50c50ef9c7 | |||
a18646f3a9 | |||
2e34ffcedc | |||
cf0d59ab61 | |||
c5d0562493 | |||
0085d47d16 | |||
7ec4f0c52e | |||
e40bb3a2f4 | |||
cb535188b2 | |||
273f91b3d4 | |||
de334aa52e | |||
72e8463717 | |||
7dad6999a7 | |||
55199e666a | |||
d5c2f6b8e3 | |||
e58129e46a | |||
6aff0344a5 | |||
22aad9836f | |||
2c9042ef7d | |||
16a5e8b884 | |||
9edc0ae53e | |||
37251cbcb3 | |||
44263d9b26 | |||
04ac5b0bef | |||
af5d980154 | |||
442fb6aef4 | |||
be523f5e35 | |||
2048781b8f | |||
08b125f5e4 | |||
df5a9a60a5 | |||
03bf21ee52 | |||
82d294b3a7 | |||
26792beb5a | |||
869d302b28 | |||
f9b3dd8aa4 | |||
c93203b914 | |||
1810e89a45 | |||
45bc108e94 | |||
27fa1b31b0 | |||
b2f23cdaad | |||
44973255d8 | |||
18da573825 | |||
969f6328fb | |||
0b011eaa72 | |||
a2928361d9 | |||
fcd2a54754 | |||
8a39b639b9 | |||
88837ef572 | |||
b9f1e3f4df | |||
66d3ea28e0 | |||
eecdde0548 | |||
72aad1872e | |||
1a25505425 | |||
58f1b3cfa2 | |||
7936f753bc | |||
0cb0a307e6 | |||
b17dcc6198 | |||
9f2601d3ac | |||
81e99b319b | |||
4c99ab0402 | |||
bf822dad13 | |||
3d2c3aeb50 | |||
bd0b7ebbb2 | |||
83bfdf00a2 | |||
391c84cf76 | |||
b8512b66d9 | |||
90155bd60c | |||
c721c9ab48 | |||
8c2e870068 | |||
1eb1769ef4 | |||
9e2a8b0c1b | |||
64ba16d4f3 | |||
9673965345 | |||
541d9ad5da | |||
da4409c539 | |||
e6843e2781 | |||
b043b4b589 | |||
0ea28c8b5b | |||
434042a9d1 | |||
7468ac3d14 | |||
f57fa701d0 | |||
e1d292fff0 | |||
6bf929b62a | |||
7c90ced960 | |||
58a91be062 | |||
b897fc7e0f | |||
4a347ea086 | |||
2541c66292 | |||
d8e9639d0f | |||
32820a06ab | |||
1d263da0df | |||
d57af170b3 | |||
1a6c1f56e8 | |||
70afd93f2f | |||
563e85fe70 | |||
9e61a99c6d | |||
d54dce8c25 | |||
92739a725f | |||
fdfb187bde | |||
76fd7d3130 | |||
b072978cfb | |||
48617ebf19 | |||
8edfcc9c02 | |||
7a26124025 | |||
959fe71f32 | |||
a70177ec64 | |||
a6ae4c8405 | |||
fadfd0d0cf | |||
3e861bb749 | |||
8fa7c75e24 | |||
3fa499b198 | |||
48d1ff2915 | |||
50667e8196 | |||
7c0553c311 | |||
07f2db8d15 | |||
b4d71d1bc9 | |||
ffaabdd9af | |||
27d3df3403 | |||
65a8435141 | |||
acc97b01c9 | |||
ad4e96cf4c | |||
5a0b34889d | |||
be4eb2ed39 | |||
8f56d68bb7 | |||
38468b278e | |||
868f990f13 | |||
2ffc33eda0 | |||
435a667acb | |||
571240e349 | |||
4e05b4df06 | |||
68e42522f8 | |||
1139824349 | |||
87ee709eac | |||
7e3ca87c14 | |||
7d803faaf8 | |||
c6058b0ee9 | |||
3eec980855 | |||
275d3d707f | |||
dac6c2a26e | |||
1fcfb176ba | |||
7d36216ab0 | |||
153bb23f0c | |||
433f786a50 | |||
f354dd9c30 | |||
4f80d19e3a | |||
c636af7ead | |||
6e3b85ac97 | |||
48687daccc | |||
66667de240 | |||
eda5699f38 | |||
902377b92a |
66
.github/workflows/sync.yaml
vendored
Normal file
66
.github/workflows/sync.yaml
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
# GitHub Actions workflow file to sync an external repository to this GitHub mirror.
|
||||
# This file was automatically generated by go-github-sync.
|
||||
#
|
||||
# The workflow does the following:
|
||||
# - Runs on a scheduled basis (and can also be triggered manually)
|
||||
# - Clones the GitHub mirror repository
|
||||
# - Fetches changes from the primary external repository
|
||||
# - Applies those changes to the mirror repository
|
||||
# - Pushes the updated content back to the GitHub mirror
|
||||
#
|
||||
# Authentication is handled by the GITHUB_TOKEN secret provided by GitHub Actions.
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate Github Actions Environment
|
||||
run: if [ "$GITHUB_ACTIONS" != "true" ]; then echo 'This script must be run in a GitHub Actions environment.'; exit 1; fi
|
||||
- name: Checkout GitHub Mirror
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Configure Git
|
||||
run: |-
|
||||
git config user.name 'GitHub Actions'
|
||||
git config user.email 'actions@github.com'
|
||||
- env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
name: Sync Primary Repository
|
||||
run: |-
|
||||
# Add the primary repository as a remote
|
||||
git remote add primary https://i2pgit.org/I2P_Developers/i2p.plugins.i2pcontrol.git
|
||||
|
||||
# Fetch the latest changes from the primary repository
|
||||
git fetch primary
|
||||
|
||||
# Check if the primary branch exists in the primary repository
|
||||
if git ls-remote --heads primary master | grep -q master; then
|
||||
echo "Primary branch master found in primary repository"
|
||||
else
|
||||
echo "Error: Primary branch master not found in primary repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're already on the mirror branch
|
||||
if git rev-parse --verify --quiet master; then
|
||||
git checkout master
|
||||
else
|
||||
# Create the mirror branch if it doesn't exist
|
||||
git checkout -b master
|
||||
fi
|
||||
|
||||
|
||||
# Force-apply all changes from primary, overriding any conflicts
|
||||
echo "Performing force sync from primary/master to master"
|
||||
git reset --hard primary/master
|
||||
|
||||
|
||||
# Push changes back to the mirror repository
|
||||
git push origin master
|
||||
name: Sync Primary Repository to GitHub Mirror
|
||||
"on":
|
||||
push: {}
|
||||
schedule:
|
||||
- cron: 0 * * * *
|
||||
workflow_dispatch: {}
|
62
CHANGES.txt
Normal file
62
CHANGES.txt
Normal file
@ -0,0 +1,62 @@
|
||||
Version 0.12.0 2018-02-07 zzz
|
||||
* Fixes for Jetty 9 / I2P 0.9.30
|
||||
* Convert to RouterApp interface
|
||||
* Register with PortMapper
|
||||
* HTML password change form
|
||||
* Update SSL cert parameters
|
||||
* Convert Timer to SimpleTimer2
|
||||
* Remove hardcoded salt; generate if necessary
|
||||
* Constant-time password hash comparison
|
||||
* Update jBCrypt to version 0.4 2015-01-30
|
||||
* Update jsonrpc2 libs: Base 1.38.1; Server 1.11; Mini 2015-10-23
|
||||
* Update makeplugin.sh
|
||||
* Add DNS rebinding protection
|
||||
* Implement destroy() for servlet
|
||||
* Don't stop all running threads on shutdown
|
||||
* Set restrictive permissions on configuration file
|
||||
* Disable changing host/port via RPC
|
||||
* Use I2P libs for setting supported ciphers and for configuration file
|
||||
* Change maintainer, signer, and update URLs
|
||||
* Smaller update file
|
||||
* i2pcontrol.py enhancements and options
|
||||
* Bundle i2pcontrol.py test script
|
||||
* Add support for building and running as a console webapp
|
||||
* Remove all static references
|
||||
* Various code cleanups
|
||||
|
||||
Version 0.11 2016-01-18 hottuna
|
||||
* Implemented AdvancedSettings RPC method.
|
||||
* Fix 2 bugs.
|
||||
|
||||
Version 0.1.0
|
||||
* Fix 2 bugs.
|
||||
|
||||
Version 0.0.9
|
||||
* Add support for I2P 0.9.16.
|
||||
|
||||
Version 0.0.8
|
||||
* Add support for Java 8.
|
||||
|
||||
Version 0.0.7
|
||||
* Add support for I2P v0.9.8 and greater.
|
||||
|
||||
Version 0.0.6
|
||||
* Migrated to jetty7. Reflected changes in the API.
|
||||
|
||||
Version 0.0.5
|
||||
* I2PControl has been updated to reflect changes in the reseed API of I2P.
|
||||
|
||||
Version 0.0.4
|
||||
* I2PControl has been ported to Jetty6 as I2P is no longer including Jetty5. Shouldn't affect end users.
|
||||
|
||||
Version 0.0.3
|
||||
* Switched signature file of I2PControl. Meaning that updates have to be made from scratch.
|
||||
|
||||
Version 0.0.2
|
||||
* Support for monitoring netdb status and initiating a reseed if needed.</tt><br>
|
||||
* Added support for changing which IP addresses I2PControl accepts. 127.0.0.1 is default, 0.0.0.0 is an option.</tt><br>
|
||||
* Improved looks by realigning components and adding gradients to panels.
|
||||
|
||||
Version 0.0.1
|
||||
* Added graphs.
|
||||
* Added support for changing the port of I2PControl.
|
18
LICENSE-jBCrypt.txt
Normal file
18
LICENSE-jBCrypt.txt
Normal file
@ -0,0 +1,18 @@
|
||||
jBCrypt is subject to the following license:
|
||||
|
||||
/*
|
||||
* Copyright (c) 2006 Damien Miller <djm@mindrot.org>
|
||||
*
|
||||
* Permission to use, copy, modify, and distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
205
LICENSE.txt
205
LICENSE.txt
@ -1,4 +1,192 @@
|
||||
Copyright 2010 zzz (zzz@mail.i2p)
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2011 uRobert Foss / hottuna
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -12,18 +200,3 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
========================================================================
|
||||
Includes code from Jetty 5.1.15:
|
||||
|
||||
Copyright 199-2004 Mort Bay Consulting Pty. Ltd.
|
||||
------------------------------------------------------------------------
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
66
README.txt
66
README.txt
@ -1,42 +1,40 @@
|
||||
This is a very simple in-memory open tracker, wrapped into an I2P plugin.
|
||||
I2PControl
|
||||
Server implementing JSON-RPC2 API for remote control of I2P.
|
||||
|
||||
The plugin starts a new http serer tunnel, eepsite, and Jetty server running at port 7662.
|
||||
The tracker status is available at http://127.0.0.1:7661/tracker/ .
|
||||
If other files are desired on the eepsite, they can be added at eepsite/docroot .
|
||||
See i2pcontrol.py for a test client.
|
||||
Default host is 127.0.0.1.
|
||||
Default port is 7650.
|
||||
Default password is "itoopie".
|
||||
|
||||
The open tracker code and jsps were written from scratch, but depend on some code
|
||||
in i2psnark.jar from the I2P installation for bencoding, and of course
|
||||
on other i2p libraries.
|
||||
See the license files in I2P for i2p and i2psnark licenses.
|
||||
There is also some code modified from Jetty 5.1.15.
|
||||
See LICENSES.txt for the zzzot and Jetty licenses.
|
||||
You may change the API password via the API,
|
||||
or via a browser at https://127.0.0.1:7650/
|
||||
|
||||
I2P source must be installed and built in ../i2p.i2p to compile this package.
|
||||
Version 1 API specification:
|
||||
http://i2p-projekt.i2p/en/docs/api/i2pcontrol
|
||||
https://geti2p.net/en/docs/api/i2pcontrol
|
||||
|
||||
Sure, as a standalone program in its own JVM with Jetty, this would be a pig -
|
||||
you should use the C opentracker instead. But since you're already running
|
||||
the JVM and Jetty, running this in the same JVM probably doesn't hog to much more memory.
|
||||
Version 2 API proposal:
|
||||
http://i2p-projekt.i2p/spec/proposals/118-i2pcontrol-api-2
|
||||
https://geti2p.net/spec/proposals/118-i2pcontrol-api-2
|
||||
|
||||
Valid announce URLs:
|
||||
/a
|
||||
/announce
|
||||
/announce.jsp
|
||||
/announce.php
|
||||
/tracker/a
|
||||
/tracker/announce
|
||||
/tracker/announce.jsp
|
||||
/tracker/announce.php
|
||||
To build as a router console plugin: ant
|
||||
To build as a router console webapp: ant war
|
||||
|
||||
Valid scrape URLs:
|
||||
/scrape
|
||||
/scrape.jsp
|
||||
/scrape.php
|
||||
/tracker/scrape
|
||||
/tracker/scrape.jsp
|
||||
/tracker/scrape.php
|
||||
Command line test client:
|
||||
scripts/i2pcontrol.py in this package
|
||||
|
||||
The tracker also responds to seedless queries at
|
||||
/Seedless/index.jsp
|
||||
GUI client itoopie version 0.3 (2015-03-02):
|
||||
Clearnet installer: https://github.com/robertfoss/itoopie.net/raw/master/files/itoopie-install.exe
|
||||
Clearnet SHA512: https://raw.githubusercontent.com/robertfoss/itoopie.net/master/files/itoopie-install.exe.sha512
|
||||
I2P installer: http://stats.i2p/i2p/plugins/others/itoopie-install.exe
|
||||
I2P SHA512: http://stats.i2p/i2p/plugins/others/itoopie-install.exe.sha512
|
||||
Source: i2p.itoopie branch in monotone, or https://github.com/i2p/i2p.itoopie
|
||||
java -jar itoopie-install.exe to install on non-Windows.
|
||||
|
||||
You may use the rest of the eepsite for other purposes, for example you
|
||||
may place torrent files in eepsite/docroot/torrents.
|
||||
Discussion forum:
|
||||
http://zzz.i2p/forums/16
|
||||
|
||||
Bugs:
|
||||
Report on above forum, or http://trac.i2p2.i2p/ or https://trac.i2p2.de/
|
||||
|
||||
License: Apache 2
|
||||
|
23
TODO.txt
23
TODO.txt
@ -1,19 +1,8 @@
|
||||
Configuration file:
|
||||
- interval
|
||||
- clean time
|
||||
- max peers in response
|
||||
- disable full scrapes
|
||||
- disable all scrapes
|
||||
- disable seedless
|
||||
http://zzz.i2p/topics/888
|
||||
|
||||
Stop the cleaner
|
||||
https://geti2p.net/spec/proposals/118-i2pcontrol-api-2
|
||||
|
||||
Throttles:
|
||||
- full scrapes
|
||||
- per-requestor
|
||||
|
||||
Bans:
|
||||
- refuse non-GETs
|
||||
|
||||
Verifier:
|
||||
- Check dest vs. b32 in header
|
||||
http://zzz.i2p/topics/2030
|
||||
Prep for bundling into router package
|
||||
Review auth requirements and implementation
|
||||
bcrypt merge or move to PasswordManager
|
||||
|
102
build.xml
102
build.xml
@ -1,67 +1,111 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<project basedir="." default="all" name="zzzot">
|
||||
|
||||
<target name="all" depends="clean,plugin" />
|
||||
<project basedir="." default="all" name="I2PControl">
|
||||
|
||||
<target name="war" >
|
||||
<!-- Include property files so that values can be easily overridden.
|
||||
Users should create an override.properties file to make changes.
|
||||
-->
|
||||
<property file="override.properties"/>
|
||||
|
||||
<target name="all" depends="clean,plugin,release" />
|
||||
|
||||
<target name="local" depends="clean,plugin">
|
||||
<property name="i2p.plugindir" value="${user.home}/.i2p/plugins/I2PControl" />
|
||||
<delete dir="${i2p.plugindir}"/>
|
||||
<mkdir dir="${i2p.plugindir}"/>
|
||||
<mkdir dir="${i2p.plugindir}/lib"/>
|
||||
<copy file="src/build/I2PControl.jar" todir="${i2p.plugindir}/lib" overwrite="true" />
|
||||
<copy todir="${i2p.plugindir}" >
|
||||
<fileset dir="plugin" includes="**"/>
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
<target name="jar">
|
||||
<ant dir="src" target="build" />
|
||||
</target>
|
||||
|
||||
<target name="plugin" depends="war">
|
||||
<delete file="plugin/i2ptunnel.config" />
|
||||
<target name="war" depends="clean" >
|
||||
<ant dir="src" target="war" />
|
||||
<copy file="src/build/jsonrpc.war" todir="." />
|
||||
</target>
|
||||
|
||||
<target name="plugin" depends="jar">
|
||||
<!-- get version number -->
|
||||
<buildnumber file="scripts/build.number" />
|
||||
<property name="release.number" value="0.1" />
|
||||
<!-- change in I2PControlVersion.java also! -->
|
||||
<property name="release.number" value="0.12.0" />
|
||||
|
||||
<!-- make the update xpi2p -->
|
||||
<!-- this contains everything except i2ptunnel.config -->
|
||||
<copy file="LICENSE.txt" todir="plugin/" overwrite="true" />
|
||||
<copy file="README.txt" todir="plugin/" overwrite="true" />
|
||||
<mkdir dir="plugin/lib"/>
|
||||
<copy file="scripts/plugin.config" todir="plugin/" overwrite="true" />
|
||||
|
||||
<exec executable="pack200" failonerror="true">
|
||||
<arg value="--no-gzip"/>
|
||||
<arg value="--effort=9"/>
|
||||
<arg value="plugin/lib/I2PControl.jar.pack" />
|
||||
<arg value="src/build/I2PControl.jar" />
|
||||
</exec>
|
||||
|
||||
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
|
||||
<arg value="update-only=true" />
|
||||
</exec>
|
||||
|
||||
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
|
||||
<arg value="version=${release.number}-b${build.number}" />
|
||||
</exec>
|
||||
<exec executable="pack200" failonerror="true">
|
||||
<arg value="-g" />
|
||||
<arg value="plugin/lib/zzzot.jar.pack" />
|
||||
<arg value="src/build/zzzot.jar" />
|
||||
</exec>
|
||||
<exec executable="pack200" failonerror="true">
|
||||
<arg value="-g" />
|
||||
<arg value="plugin/eepsite/webapps/tracker.war.pack" />
|
||||
<arg value="src/build/tracker.war.jar" />
|
||||
</exec>
|
||||
<exec executable="scripts/makeplugin.sh" failonerror="true" >
|
||||
|
||||
<input message="Enter su3 signing key password:" addproperty="release.password.su3" />
|
||||
<fail message="You must enter a password." >
|
||||
<condition>
|
||||
<equals arg1="${release.password.su3}" arg2=""/>
|
||||
</condition>
|
||||
</fail>
|
||||
<!-- this will fail if no su3 keys exist, as it needs the password twice -->
|
||||
<exec executable="scripts/makeplugin.sh" inputstring="${release.password.su3}" failonerror="true" >
|
||||
<arg value="plugin" />
|
||||
</exec>
|
||||
<move file="zzzot.xpi2p" tofile="zzzot-update.xpi2p" overwrite="true" />
|
||||
|
||||
<move file="I2PControl.xpi2p" tofile="I2PControl-update.xpi2p" overwrite="true" />
|
||||
<move file="I2PControl.su3" tofile="I2PControl-update.su3" overwrite="true" />
|
||||
|
||||
<!-- make the install xpi2p -->
|
||||
<copy file="scripts/i2ptunnel.config" todir="plugin/" overwrite="true" />
|
||||
<copy file="LICENSE.txt" todir="plugin/" overwrite="true" />
|
||||
<copy file="LICENSE-jBCrypt.txt" todir="plugin/" overwrite="true" />
|
||||
<copy file="README.txt" todir="plugin/" overwrite="true" />
|
||||
<copy file="scripts/clients.config" todir="plugin/" overwrite="true" />
|
||||
<copy file="scripts/plugin.config" todir="plugin/" overwrite="true" />
|
||||
<copy file="scripts/i2pcontrol.py" todir="plugin/" overwrite="true" />
|
||||
<exec executable="echo" osfamily="unix" failonerror="true" output="plugin/plugin.config" append="true">
|
||||
<arg value="version=${release.number}-b${build.number}" />
|
||||
</exec>
|
||||
<exec executable="scripts/makeplugin.sh" failonerror="true" >
|
||||
<exec executable="scripts/makeplugin.sh" inputstring="${release.password.su3}" failonerror="true" >
|
||||
<arg value="plugin" />
|
||||
</exec>
|
||||
</target>
|
||||
|
||||
<target name="release" depends="plugin" />
|
||||
|
||||
<target name="distclean" depends="clean" />
|
||||
|
||||
<target name="format">
|
||||
<exec executable="scripts/format.sh" failonerror="true" />
|
||||
</target>
|
||||
|
||||
<target name="clean" >
|
||||
<ant dir="src" target="clean" />
|
||||
<delete file="plugin/i2ptunnel.config" />
|
||||
<delete file="plugin/clients.config" />
|
||||
<delete file="plugin/plugin.config" />
|
||||
<delete file="plugin/lib/zzzot.jar.pack" />
|
||||
<delete file="plugin/eepsite/webapps/tracker.war.pack" />
|
||||
<delete file="plugin/console/webapp.config" />
|
||||
<delete file="plugin/lib/I2PControl.jar.pack" />
|
||||
<delete file="plugin/console/webapps/I2PControl.war.pack" />
|
||||
<delete file="plugin/LICENSE.txt" />
|
||||
<delete file="plugin/LICENSE-jBCrypt.txt" />
|
||||
<delete file="plugin/README.txt" />
|
||||
<delete file="zzzot.xpi2p" />
|
||||
<delete file="zzzot-update.xpi2p" />
|
||||
<delete file="plugin/i2pcontrol.py" />
|
||||
<delete file="I2PControl.xpi2p" />
|
||||
<delete file="I2PControl-update.xpi2p" />
|
||||
<delete file="I2PControl.su3" />
|
||||
<delete file="I2PControl-update.su3" />
|
||||
<delete file="jsonrpc.war" />
|
||||
</target>
|
||||
|
||||
</project>
|
||||
|
@ -1,6 +0,0 @@
|
||||
<html><head>
|
||||
<!-- edit this file if you want to change your home page -->
|
||||
<title>zzzot</title>
|
||||
</head><body style="background-color: #000; color: #c30; font-size: 2000%;">
|
||||
<center><b>zzzot</b></center>
|
||||
</body></html>
|
@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow:
|
@ -1,56 +0,0 @@
|
||||
<html><head><title>ZzzOT Plugin Help</title></head>
|
||||
<body style="background-color: #ddd; color: #a30;">
|
||||
<h2>Welcome to the ZzzOT I2P Plugin!</h2>
|
||||
|
||||
A new eepsite tunnel and Jetty server have been started for your open tracker.
|
||||
|
||||
<p><a href="/tracker/index.jsp">Click here to see the current stats</a>.
|
||||
This link is also at the top of your router console when ZzzOT is running.
|
||||
|
||||
<p>Report bugs or add comments on
|
||||
<a href="htp://zzz.i2p//forums/16">the plugin forum on zzz.i2p</a>.
|
||||
|
||||
<h3>Eepsite Key and Helpful Hints for I2P</h3>
|
||||
|
||||
<p>Your Base 32 address is <a href="http://$B32/">$B32</a>.
|
||||
Others may access your eepsite using this address, even if you do not publish a hostname.
|
||||
<p>Once you decide on a host name, you may
|
||||
<a href="http://127.0.0.1:7657/susidns/addressbook.jsp?book=private&destination=$B64">add the key to your local addressbook here</a>.
|
||||
<p>Your Base 64 key is: <textarea rows="1" style="height: 3em;" cols="40" readonly="readonly" wrap="off">$B64</textarea>
|
||||
<br>You will need this key to register a hostname at <a href="http://stats.i2p/i2p/addkey.html">stats.i2p</a>.
|
||||
<p>Your private key file is $PLUGIN/eepPriv.dat - back it up!!!
|
||||
<p>Your eepsite document root is $PLUGIN/eepsite/docroot,
|
||||
you may put other files there is you wish to have additional content on your eepsite.
|
||||
<p>The supported announce URLs are:
|
||||
<ul>
|
||||
<li><a href="http://$B32/a">http://$B32/a</a>
|
||||
<li><a href="http://$B32/announce">http://$B32/announce</a>
|
||||
<li><a href="http://$B32/announce.jsp">http://$B32/announce.jsp</a>
|
||||
<li><a href="http://$B32/announce.php">http://$B32/announce.php</a>
|
||||
<li><a href="http://$B32/tracker/a">http://$B32/tracker/a</a>
|
||||
<li><a href="http://$B32/tracker/announce">http://$B32/tracker/announce</a>
|
||||
<li><a href="http://$B32/tracker/announce.jsp">http://$B32/tracker/announce.jsp</a>
|
||||
<li><a href="http://$B32/tracker/announce.php">http://$B32/tracker/announce.php</a>
|
||||
</ul>
|
||||
<p>The supported scrape URLs are:
|
||||
<ul>
|
||||
<li><a href="http://$B32/scrape">http://$B32/scrape</a>
|
||||
<li><a href="http://$B32/scrape.jsp">http://$B32/scrape.jsp</a>
|
||||
<li><a href="http://$B32/scrape.php">http://$B32/scrape.php</a>
|
||||
<li><a href="http://$B32/tracker/scrape">http://$B32/tracker/scrape</a>
|
||||
<li><a href="http://$B32/tracker/scrape.jsp">http://$B32/tracker/scrape.jsp</a>
|
||||
<li><a href="http://$B32/tracker/scrape.php">http://$B32/tracker/scrape.php</a>
|
||||
</ul>
|
||||
<p>Your eepsite tunnel is configured for 2 inbound and 2 outbound tunnels, 3 hops each.
|
||||
You may change tunnel settings by editing $PLUGIN/i2ptunnel.config and restarting the plugin.
|
||||
The tunnel will not appear in <a href="http://127.0.0.1:7657/i2ptunnel/index.jsp">i2ptunnel</a>.
|
||||
If your tracker gets over 1000 peers, you will probably want to increase the number of tunnels.
|
||||
<p>The Jetty webserver port is 7662. If you must change it, edit jetty.xml, i2ptunnel.config, and plugins.config
|
||||
in the directory $PLUGIN. Then stop and restart the plugin.
|
||||
<p>This help file is $PLUGIN/eepsite/docroot/help.html, you should probably move it
|
||||
outside of the document root before you announce your eepsite as it may contain your user name.
|
||||
<p>As you probably know, an open tracker does not require torrents to be registered,
|
||||
and it does not host torrent files. You can, however, host torrent files elsewhere on
|
||||
the eepsite, for example at <a href="http://$B32/torrents/">/torrents</a>.
|
||||
|
||||
</body></html>
|
@ -1,186 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure 1.2//EN" "http://jetty.mortbay.org/configure_1_2.dtd">
|
||||
|
||||
<!-- ========================================================================= -->
|
||||
<!-- This file configures the Jetty server. -->
|
||||
<!-- All changes require a restart of I2P. -->
|
||||
<!-- -->
|
||||
<!-- Commonly changed settings: -->
|
||||
<!-- * host: Change 127.0.0.1 to 0.0.0.0 in the addListener section -->
|
||||
<!-- to access the server directly (bypassing i2p) -->
|
||||
<!-- from other computers. The included version of Jetty has -->
|
||||
<!-- been patched to allow IPv6 addresses as well, -->
|
||||
<!-- enclosed in brackets e.g. [::1] -->
|
||||
<!-- * port: Default 7662 in the addListener section -->
|
||||
<!-- * threads: Raise MaxThreads in the addListener section -->
|
||||
<!-- if you have a high-traffic site and get a lot of warnings. -->
|
||||
<!-- -->
|
||||
<!-- I2P uses Jetty 5.1.15. We have no plans to upgrade to Jetty 6, due to -->
|
||||
<!-- the significant changes in the API. If you need web server features not -->
|
||||
<!-- found in Jetty 5, you may install and run Jetty 6 in a different JVM, -->
|
||||
<!-- or run any other web server such as Apache. If you do run another -->
|
||||
<!-- web server instead, be sure and disable the Jetty 5 server for your -->
|
||||
<!-- eepsite on http://127.0.0.1/configclients.jsp . -->
|
||||
<!-- -->
|
||||
<!-- Jetty errors and warnings will appear in wrapper.log, check there -->
|
||||
<!-- to diagnose problems. -->
|
||||
<!-- ========================================================================= -->
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Jetty Server -->
|
||||
<!-- =============================================================== -->
|
||||
<Configure class="org.mortbay.jetty.Server">
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Request Listeners -->
|
||||
<!-- =============================================================== -->
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Add and configure a HTTP listener to port 8080 -->
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<Call name="addListener">
|
||||
<Arg>
|
||||
<New class="org.mortbay.http.SocketListener">
|
||||
<Arg>
|
||||
<New class="org.mortbay.util.InetAddrPort">
|
||||
<Set name="host">127.0.0.1</Set>
|
||||
<Set name="port">7662</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
<Set name="MinThreads">3</Set>
|
||||
<Set name="MaxThreads">10</Set>
|
||||
<Set name="MaxIdleTimeMs">60000</Set>
|
||||
<Set name="LowResourcePersistTimeMs">1000</Set>
|
||||
<Set name="ConfidentialPort">8443</Set>
|
||||
<Set name="IntegralPort">8443</Set>
|
||||
<Set name="PoolName">main</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Contexts -->
|
||||
<!-- =============================================================== -->
|
||||
|
||||
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<!-- Add a all web application within the webapps directory. -->
|
||||
<!-- + No virtual host specified -->
|
||||
<!-- + Look in the webapps directory relative to jetty.home or . -->
|
||||
<!-- + Use the default webdefault.xml in jetty's install -->
|
||||
<!-- + Upack the war file -->
|
||||
<!-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -->
|
||||
<Set name="rootWebApp">root</Set>
|
||||
<Call name="addWebApplications">
|
||||
<Arg></Arg>
|
||||
<Arg>$PLUGIN/eepsite/webapps/</Arg>
|
||||
<Arg></Arg>
|
||||
<Arg type="boolean">true</Arg>
|
||||
</Call>
|
||||
|
||||
<Call name="addContext">
|
||||
<Arg>
|
||||
<New class="org.mortbay.http.HttpContext">
|
||||
<Set name="contextPath">/</Set>
|
||||
<Set name="resourceBase">$PLUGIN/eepsite/docroot</Set>
|
||||
<Call name="addHandler">
|
||||
<Arg>
|
||||
<New class="org.mortbay.http.handler.ResourceHandler">
|
||||
<Set name="redirectWelcome">FALSE</Set>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
<!-- This custom handler is like Jetty's ForwardHandler,
|
||||
- but it passes CGI query parameters through.
|
||||
- Note that it is somewhat misnamed - it does NOT
|
||||
- return a 301/302, it handles the request directly.
|
||||
-->
|
||||
<Call name="addHandler">
|
||||
<Arg>
|
||||
<New class="net.i2p.zzzot.QForwardHandler">
|
||||
<Call name="setHandleQueries">
|
||||
<Arg type="boolean">true</Arg>
|
||||
</Call>
|
||||
<!-- Forward announce requests to /tracker/announce.jsp -->
|
||||
<Call name="addForward">
|
||||
<Arg>/a</Arg>
|
||||
<Arg>/tracker/announce.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/announce</Arg>
|
||||
<Arg>/tracker/announce.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/announce.jsp</Arg>
|
||||
<Arg>/tracker/announce.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/announce.php</Arg>
|
||||
<Arg>/tracker/announce.jsp</Arg>
|
||||
</Call>
|
||||
<!-- Forward scrape requests to /tracker/scrape.jsp -->
|
||||
<Call name="addForward">
|
||||
<Arg>/scrape</Arg>
|
||||
<Arg>/tracker/scrape.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/scrape.jsp</Arg>
|
||||
<Arg>/tracker/scrape.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/scrape.php</Arg>
|
||||
<Arg>/tracker/scrape.jsp</Arg>
|
||||
</Call>
|
||||
<!-- Forward Seedless requests to /tracker/seedless.jsp -->
|
||||
<Call name="addForward">
|
||||
<Arg>/Seedless</Arg>
|
||||
<Arg>/tracker/seedless.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/Seedless/</Arg>
|
||||
<Arg>/tracker/seedless.jsp</Arg>
|
||||
</Call>
|
||||
<Call name="addForward">
|
||||
<Arg>/Seedless/index.jsp</Arg>
|
||||
<Arg>/tracker/seedless.jsp</Arg>
|
||||
</Call>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
</New>
|
||||
</Arg>
|
||||
</Call>
|
||||
|
||||
<Call name="addContext">
|
||||
<Arg>/cgi-bin/*</Arg>
|
||||
<Set name="ResourceBase">$PLUGIN/eepsite/cgi-bin</Set>
|
||||
<Call name="addServlet">
|
||||
<Arg>Common Gateway Interface</Arg>
|
||||
<Arg>/</Arg>
|
||||
<Arg>org.mortbay.servlet.CGI</Arg>
|
||||
<Put name="Path">/usr/local/bin:/usr/ucb:/bin:/usr/bin</Put>
|
||||
</Call>
|
||||
</Call>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Request Log -->
|
||||
<!-- =============================================================== -->
|
||||
<Set name="RequestLog">
|
||||
<New class="org.mortbay.http.I2PRequestLog">
|
||||
<Arg>$PLUGIN/eepsite/logs/yyyy_mm_dd.request.log</Arg>
|
||||
<Set name="retainDays">30</Set>
|
||||
<Set name="append">true</Set>
|
||||
<Set name="extended">false</Set>
|
||||
<Set name="buffered">false</Set>
|
||||
<Set name="LogTimeZone">GMT</Set>
|
||||
</New>
|
||||
</Set>
|
||||
|
||||
<!-- =============================================================== -->
|
||||
<!-- Configure the Other Server Options -->
|
||||
<!-- =============================================================== -->
|
||||
<Set name="requestsPerGC">2000</Set>
|
||||
<Set name="statsOn">false</Set>
|
||||
|
||||
</Configure>
|
@ -1,8 +1,8 @@
|
||||
clientApp.0.main=net.i2p.zzzot.ZzzOTController
|
||||
clientApp.0.name=ZzzOT
|
||||
clientApp.0.main=net.i2p.i2pcontrol.I2PControlController
|
||||
clientApp.0.name=I2PControl
|
||||
clientApp.0.args=-d $PLUGIN start
|
||||
clientApp.0.stopargs=-d $PLUGIN stop
|
||||
clientApp.0.delay=15
|
||||
clientApp.0.startOnLoad=true
|
||||
# we also use i2p.jar and i2ptunnel.jar, they are in the standard router classpath
|
||||
clientApp.0.classpath=$PLUGIN/lib/zzzot.jar,$I2P/lib/i2psnark.jar
|
||||
clientApp.0.classpath=$PLUGIN/lib/I2PControl.jar
|
18
scripts/format.sh
Executable file
18
scripts/format.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
|
||||
if ! command -v astyle >/dev/null 2>&1; then
|
||||
echo "astyle required, but couldn't be found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Formating source files..."
|
||||
# Modified kdelibs coding style as defined in
|
||||
# http://techbase.kde.org/Policies/Kdelibs_Coding_Style
|
||||
|
||||
find -regex ".*\.\(java\)" -exec \
|
||||
astyle --mode=java --indent=spaces=4 \
|
||||
--indent-labels --pad-oper --unpad-paren --pad-header \
|
||||
--keep-one-line-statements --convert-tabs \
|
||||
--indent-preprocessor "{}" \;
|
||||
|
||||
echo "Done!"
|
361
scripts/i2pcontrol.py
Executable file
361
scripts/i2pcontrol.py
Executable file
@ -0,0 +1,361 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# If it fails "No module named yaml"
|
||||
# then sudo apt install python-yaml
|
||||
#
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import urllib2
|
||||
import httplib
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import yaml
|
||||
from urllib2 import HTTPError, URLError
|
||||
from string import whitespace
|
||||
|
||||
# Info about requestable data can be found at https://geti2p.net/i2pcontrol.html & https://geti2p.net/ratestats.html
|
||||
|
||||
address = "127.0.0.1" # Default I2PControl Address
|
||||
port = 7650 # Default I2PControl Port
|
||||
usessl = 1 # Change to 0 for HTTP
|
||||
apiPassword = "itoopie" # Default I2PControl password
|
||||
|
||||
|
||||
## Do not edit below
|
||||
apiVersion = 1 # Default API Version
|
||||
msgId = 1
|
||||
token = None
|
||||
|
||||
def checkToken():
|
||||
global token
|
||||
if (token == None):
|
||||
token = getToken()
|
||||
if (token == None):
|
||||
print("Unable to login. Quitting..")
|
||||
sys.exit()
|
||||
|
||||
def getToken():
|
||||
loginStr = "{\"id\":" + str(msgId) + ", \"method\":\"Authenticate\",\"params\":{\"API\":" + str(apiVersion) + ", \"Password\":\"" + apiPassword + "\"}, \"jsonrpc\":\"2.0\"}"
|
||||
|
||||
try:
|
||||
jsonResp = sendMsg(loginStr)
|
||||
return jsonResp.get("result").get("Token")
|
||||
|
||||
except HTTPError, e:
|
||||
print("HTTPError: %s" % e.reason)
|
||||
except URLError, e:
|
||||
print("URLError: %s" % e.reason)
|
||||
|
||||
def getRate(rateName, ratePeriod):
|
||||
checkToken()
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"GetRate\",\"params\":{\"Stat\":\"" + rateName + "\", \"Period\":" + str(ratePeriod) + ", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
jsonResp = sendMsg(msgStr)
|
||||
return jsonResp.get("result").get("Result")
|
||||
|
||||
def getRouterInfo(infoName):
|
||||
checkToken()
|
||||
## The parameter names in 'params' defines which answers are requested
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"RouterInfo\",\"params\":{\""+infoName+"\":\"\", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
jsonResp = sendMsg(msgStr)
|
||||
return jsonResp.get("result").get(infoName)
|
||||
|
||||
def getControlInfo(infoName):
|
||||
checkToken()
|
||||
## The parameter names in 'params' defines which answers are requested
|
||||
if ("=" in infoName):
|
||||
toks = infoName.split("=", 2);
|
||||
infoName = toks[0];
|
||||
infoValue = toks[1];
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"I2PControl\",\"params\":{\""+infoName+"\":\""+infoValue+"\", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
else:
|
||||
return "Parameter value required for " + infoName
|
||||
jsonResp = sendMsg(msgStr)
|
||||
return "I2PControl setting " + infoName + " set to " + infoValue
|
||||
|
||||
def getRouterManagerInfo(infoName):
|
||||
checkToken()
|
||||
## The parameter names in 'params' defines which answers are requested
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"RouterManager\",\"params\":{\""+infoName+"\":\"\", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
jsonResp = sendMsg(msgStr)
|
||||
if (infoName == "FindUpdates" or infoName == "Update"):
|
||||
return jsonResp.get("result").get(infoName)
|
||||
else:
|
||||
return "Sent Router Manager command: " + infoName
|
||||
|
||||
def getNetworkInfo(infoName):
|
||||
checkToken()
|
||||
isset = 0;
|
||||
## The parameter names in 'params' defines which answers are requested
|
||||
if ("=" in infoName):
|
||||
toks = infoName.split("=", 2);
|
||||
isset = 1;
|
||||
infoName = toks[0];
|
||||
infoValue = toks[1];
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"NetworkSetting\",\"params\":{\""+infoName+"\":\""+infoValue+"\", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
else:
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"NetworkSetting\",\"params\":{\""+infoName+"\":null, \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
jsonResp = sendMsg(msgStr)
|
||||
if (isset == 1):
|
||||
return "Network setting " + infoName + " set to " + infoValue
|
||||
else:
|
||||
return jsonResp.get("result").get(infoName)
|
||||
|
||||
def getAdvancedInfo(infoName):
|
||||
checkToken()
|
||||
isset = 0;
|
||||
## The parameter names in 'params' defines which answers are requested
|
||||
if (infoName == "GetAll"):
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"AdvancedSettings\",\"params\":{\""+infoName+"\":\"\", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
elif (infoName == "SetAll"):
|
||||
return "SetAll unsupported"
|
||||
elif ("=" in infoName):
|
||||
toks = infoName.split("=", 2);
|
||||
isset = 1;
|
||||
infoName = toks[0];
|
||||
infoValue = toks[1];
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"AdvancedSettings\",\"params\":{\"set\":{\""+infoName+"\":\""+infoValue+"\"}, \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
else:
|
||||
msgStr = "{\"id\":" + str(msgId) + ", \"method\":\"AdvancedSettings\",\"params\":{\"get\":\""+infoName+"\", \"Token\":\"" + token +"\" }, \"jsonrpc\":\"2.0\"}"
|
||||
jsonResp = sendMsg(msgStr)
|
||||
if (infoName == "GetAll"):
|
||||
return jsonResp.get("result").get(infoName)
|
||||
elif (isset == 1):
|
||||
return "Advanced configuration " + infoName + " set to " + infoValue
|
||||
else:
|
||||
return jsonResp.get("result").get("get").get(infoName)
|
||||
|
||||
def sendMsg(jsonStr):
|
||||
global msgId
|
||||
https_handler = UnauthenticatedHTTPSHandler()
|
||||
url_opener = urllib2.build_opener(https_handler)
|
||||
if (usessl != 0):
|
||||
handle = url_opener.open("https://"+address+":"+ str(port) + "/jsonrpc/", jsonStr)
|
||||
else:
|
||||
handle = url_opener.open("http://"+address+":"+ str(port) + "/jsonrpc/", jsonStr)
|
||||
response = handle.read()
|
||||
handle.close()
|
||||
msgId = msgId + 1;
|
||||
|
||||
jsonResp = json.loads(response)
|
||||
if (jsonResp.has_key("error")):
|
||||
print ("Remote server: I2PControl Error: " + str(jsonResp.get("error").get("code")) + ", " + jsonResp.get("error").get("message"))
|
||||
sys.exit()
|
||||
return jsonResp
|
||||
|
||||
###
|
||||
# Overrides the version in httplib so that we can ignore server certificate authenticity
|
||||
###
|
||||
class UnauthenticatedHTTPSConnection(httplib.HTTPSConnection):
|
||||
def connect(self):
|
||||
#
|
||||
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||
if self._tunnel_host:
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
self.sock = ssl.wrap_socket(sock,
|
||||
cert_reqs=ssl.CERT_NONE)
|
||||
|
||||
###
|
||||
# HTTPS handler which uses SSLv3 and ignores server cert authenticity
|
||||
###
|
||||
class UnauthenticatedHTTPSHandler(urllib2.HTTPSHandler):
|
||||
def __init__(self, connection_class = UnauthenticatedHTTPSConnection):
|
||||
self.specialized_conn_class = connection_class
|
||||
urllib2.HTTPSHandler.__init__(self)
|
||||
def https_open(self, req):
|
||||
return self.do_open(self.specialized_conn_class, req)
|
||||
|
||||
def zabbix_config(fileName, outfile):
|
||||
yamlDict = dict()
|
||||
for line in open(fileName):
|
||||
li=line.strip()
|
||||
if li.startswith("UserParameter"):
|
||||
i2pCtrlOpt = li.strip("UserParameter=").split(",")
|
||||
i2pCtrlOpt[1] = i2pCtrlOpt[1].split()
|
||||
i2pCtrlOpt[1].pop(0) # Remove path of this script (i2pcontrol)
|
||||
i2pCtrlParams = i2pCtrlOpt[1]
|
||||
#print i2pCtrlOpt #Delete me!
|
||||
result = ""
|
||||
if (i2pCtrlParams[0] == "-i" or i2pCtrlParams[0] == "--router-info"):
|
||||
result = getRouterInfo(i2pCtrlParams[1])
|
||||
elif (i2pCtrlParams[0] == "-s" or i2pCtrlParams[0] == "--rate-stat"):
|
||||
result = getRate(i2pCtrlParams[1], i2pCtrlParams[2])
|
||||
else:
|
||||
result = "Bad query syntax."
|
||||
yamlDict[i2pCtrlParams[1]] = result
|
||||
#print yaml.dump(yamlDict)
|
||||
yaml.dump(yamlDict, open(outfile,'w'))
|
||||
|
||||
def from_file(infile, parameter):
|
||||
try:
|
||||
yamlDict = yaml.load(open(infile,'r'))
|
||||
print yamlDict[parameter]
|
||||
except IOError, e:
|
||||
print "File \""+ infile +"\" couldn't be read."
|
||||
def main():
|
||||
global address
|
||||
global port
|
||||
global usessl
|
||||
parser = argparse.ArgumentParser(description='Fetch I2P info via the I2PControl API.')
|
||||
parser.add_argument("-l",
|
||||
"--host",
|
||||
nargs=1,
|
||||
metavar="host",
|
||||
dest="address",
|
||||
action="store",
|
||||
help="Listen host address of the i2pcontrol server")
|
||||
|
||||
parser.add_argument("-p",
|
||||
"--port",
|
||||
nargs=1,
|
||||
metavar="port",
|
||||
dest="port",
|
||||
action="store",
|
||||
help="Port of the i2pcontrol server")
|
||||
|
||||
parser.add_argument("-x",
|
||||
"--no-ssl",
|
||||
dest="http",
|
||||
action="store_true",
|
||||
help="Use HTTP instead of HTTPS")
|
||||
|
||||
parser.add_argument("-i",
|
||||
"--router-info",
|
||||
nargs=1,
|
||||
metavar="info",
|
||||
dest="router_info",
|
||||
action="store",
|
||||
help="Request info such as I2P version and uptime. Returned info can be of any type. Full list of options at https://geti2p.net/i2pcontrol.html. Usage: \"-i i2p.router.version\"")
|
||||
|
||||
parser.add_argument("-c",
|
||||
"--i2pcontrol-info",
|
||||
nargs=1,
|
||||
metavar="key[=value]",
|
||||
dest="i2pcontrol_info",
|
||||
action="store",
|
||||
help="Change settings such as password. Usage: \"-c i2pcontrol.password=foo\"")
|
||||
|
||||
parser.add_argument("-r",
|
||||
"--routermanager-info",
|
||||
nargs=1,
|
||||
metavar="command",
|
||||
dest="routermanager_info",
|
||||
action="store",
|
||||
help="Send a command to the router. Usage: \"-r FindUpdates|Reseed|Restart|RestartGraceful|Shutdown|ShutdownGraceful|Update\"")
|
||||
|
||||
parser.add_argument("-n",
|
||||
"--network-info",
|
||||
nargs=1,
|
||||
metavar="key[=value]",
|
||||
dest="network_info",
|
||||
action="store",
|
||||
help="Request info such as bandwidth. Usage: \"-n i2p.router.net.bw.in[=xxx]\"")
|
||||
|
||||
parser.add_argument("-a",
|
||||
"--advanced-info",
|
||||
nargs=1,
|
||||
metavar="key[=value]",
|
||||
dest="advanced_info",
|
||||
action="store",
|
||||
help="Request configuration info. Usage: \"-a GetAll|foo[=bar]\"")
|
||||
|
||||
parser.add_argument("-s",
|
||||
"--rate-stat",
|
||||
nargs=2,
|
||||
metavar=("rateStatName", "period"),
|
||||
dest="rate_stat",
|
||||
action="store",
|
||||
help="Request info such as bandwidth, number active peers, clock skew, etc.. The period is measured in ms and must be longer than 60s. Full list at https://geti2p.net/ratestats.html. Usage: \"-s bw.receiveBps 3600000\"")
|
||||
|
||||
parser.add_argument("-z",
|
||||
"--zabbix",
|
||||
nargs=2,
|
||||
metavar=("\"path to zabbix_agent.conf\"", "\"path to output file\""),
|
||||
dest="zabbix",
|
||||
action="store",
|
||||
help="Parse options to request, by reading a zabbix config file for \"UserParameter\"s relating to I2P. Usage: \"-z /etc/zabbix/zabbix_agent.conf\"")
|
||||
|
||||
parser.add_argument("-f",
|
||||
"--from-file",
|
||||
nargs=1,
|
||||
metavar=("\"path to input file\""),
|
||||
dest="from_file",
|
||||
action="store",
|
||||
help="Parse options to request, by reading a zabbix config file for \"UserParameter\"s relating to I2P. Usage: \"-z /etc/zabbix/zabbix_agent.conf\"")
|
||||
|
||||
if (len(sys.argv) == 1):
|
||||
parser.parse_args(["-h"])
|
||||
options = parser.parse_args()
|
||||
|
||||
# todo we don't check all the options
|
||||
if ((options.rate_stat != None) and (options.router_info != None)):
|
||||
print("Error: Choose _one_ option. \n\n")
|
||||
parser.parse_args(["-h"])
|
||||
|
||||
# todo we don't check all the options
|
||||
if ((options.zabbix != None) and ((options.rate_stat != None) or (options.router_info != None) or (options.from_file != None))):
|
||||
print("Error: Don't combine option --zabbix with other options.\n")
|
||||
parser.parse_args(["-h"])
|
||||
|
||||
# From-file can only be used when either router-info or rate-stat is enabled.
|
||||
# todo we don't check all the options
|
||||
if ((options.from_file != None) and (options.rate_stat == None) and (options.router_info == None)):
|
||||
print("Error: --from-file must be used with either --router-info or --rate-stat.\n")
|
||||
parser.parse_args(["-h"])
|
||||
|
||||
if (options.port != None):
|
||||
port = int(options.port[0]);
|
||||
if (options.address != None):
|
||||
address = options.address[0];
|
||||
if (options.http):
|
||||
usessl = 0;
|
||||
|
||||
if (options.from_file != None):
|
||||
if (options.router_info != None):
|
||||
from_file(options.from_file[0], options.router_info[0])
|
||||
if (options.rate_stat != None):
|
||||
from_file(options.from_file[0], options.rate_stat[0])
|
||||
sys.exit()
|
||||
|
||||
if (options.rate_stat != None):
|
||||
try:
|
||||
period = int(options.rate_stat[1])
|
||||
if (period < 60000):
|
||||
raise ValueError
|
||||
print getRate(options.rate_stat[0], period)
|
||||
except ValueError, e:
|
||||
print("Error: \""+options.rate_stat[1]+"\" is not an integer > 60000 \n\n")
|
||||
parser.parse_args(["-h"])
|
||||
sys.exit()
|
||||
|
||||
if (options.router_info != None):
|
||||
print getRouterInfo(options.router_info[0])
|
||||
sys.exit()
|
||||
|
||||
if (options.i2pcontrol_info != None):
|
||||
print getControlInfo(options.i2pcontrol_info[0])
|
||||
sys.exit()
|
||||
|
||||
if (options.routermanager_info != None):
|
||||
print getRouterManagerInfo(options.routermanager_info[0])
|
||||
sys.exit()
|
||||
|
||||
if (options.network_info != None):
|
||||
print getNetworkInfo(options.network_info[0])
|
||||
sys.exit()
|
||||
|
||||
if (options.advanced_info != None):
|
||||
print getAdvancedInfo(options.advanced_info[0])
|
||||
sys.exit()
|
||||
|
||||
if (options.zabbix != None):
|
||||
zabbix_config(options.zabbix[0], options.zabbix[1])
|
||||
sys.exit()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,25 +0,0 @@
|
||||
tunnel.0.description=ZzzOT
|
||||
tunnel.0.i2cpHost=127.0.0.1
|
||||
tunnel.0.i2cpPort=7654
|
||||
tunnel.0.name=zzzot
|
||||
tunnel.0.option.i2cp.enableAccessList=false
|
||||
tunnel.0.option.i2cp.encryptLeaseSet=false
|
||||
tunnel.0.option.i2cp.reduceIdleTime=1200000
|
||||
tunnel.0.option.i2cp.reduceOnIdle=true
|
||||
tunnel.0.option.i2cp.reduceQuantity=1
|
||||
tunnel.0.option.i2p.streaming.connectDelay=0
|
||||
tunnel.0.option.inbound.backupQuantity=0
|
||||
tunnel.0.option.inbound.length=3
|
||||
tunnel.0.option.inbound.lengthVariance=0
|
||||
tunnel.0.option.inbound.nickname=ZzzOT
|
||||
tunnel.0.option.inbound.quantity=2
|
||||
tunnel.0.option.outbound.backupQuantity=0
|
||||
tunnel.0.option.outbound.length=3
|
||||
tunnel.0.option.outbound.lengthVariance=0
|
||||
tunnel.0.option.outbound.nickname=ZzzOT
|
||||
tunnel.0.option.outbound.quantity=2
|
||||
tunnel.0.privKeyFile=plugins/zzzot/eepPriv.dat
|
||||
tunnel.0.startOnLoad=true
|
||||
tunnel.0.targetHost=127.0.0.1
|
||||
tunnel.0.targetPort=7662
|
||||
tunnel.0.type=httpserver
|
@ -5,58 +5,95 @@
|
||||
# usage: makeplugin.sh plugindir
|
||||
#
|
||||
# zzz 2010-02
|
||||
# zzz 2014-08 added support for su3 files
|
||||
#
|
||||
|
||||
if [ -z "$I2P" -a -d "$PWD/../i2p/pkg-temp" ]; then
|
||||
export I2P=$PWD/../i2p/pkg-temp
|
||||
fi
|
||||
|
||||
if [ ! -d "$I2P" ]; then
|
||||
echo "Can't locate your I2P installation. Please add a environment variable named I2P with the path to the folder as value"
|
||||
echo "On OSX this solved with running: export I2P=/Applications/i2p if default install directory is used."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CPATH=$I2P/lib/i2p.jar:/usr/share/java/gnu-getopt.jar
|
||||
PUBKEYDIR=$HOME/.i2p-plugin-keys
|
||||
PUBKEYFILE=$PUBKEYDIR/plugin-public-signing.key
|
||||
PRIVKEYFILE=$PUBKEYDIR/plugin-private-signing.key
|
||||
B64KEYFILE=$PUBKEYDIR/plugin-public-signing.txt
|
||||
export I2P=../i2p/pkg-temp
|
||||
PUBKEYSTORE=$PUBKEYDIR/plugin-su3-public-signing.crt
|
||||
PRIVKEYSTORE=$PUBKEYDIR/plugin-su3-keystore.ks
|
||||
KEYTYPE=RSA_SHA512_4096
|
||||
|
||||
# put your files in here
|
||||
PLUGINDIR=${1:-plugin}
|
||||
|
||||
PC=plugin.config
|
||||
PCT=${PC}.tmp
|
||||
|
||||
if [ ! -f $PRIVKEYFILE ]
|
||||
then
|
||||
mkdir -p $PUBKEYDIR
|
||||
java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate keygen $PUBKEYFILE $PRIVKEYFILE || exit 1
|
||||
java -cp $I2P/lib/i2p.jar net.i2p.data.Base64 encode $PUBKEYFILE $B64KEYFILE || exit 1
|
||||
rm -rf logs/
|
||||
chmod 444 $PUBKEYFILE $B64KEYFILE
|
||||
chmod 400 $PRIVKEYFILE
|
||||
echo "Created new keys: $PUBKEYFILE $PRIVKEYFILE"
|
||||
fi
|
||||
|
||||
rm -f plugin.zip
|
||||
if [ ! -d $PLUGINDIR ]
|
||||
if [ ! -d "$PLUGINDIR" ]
|
||||
then
|
||||
echo "You must have a $PLUGINDIR directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OPWD=$PWD
|
||||
cd $PLUGINDIR
|
||||
|
||||
if [ ! -f $PC ]
|
||||
if [ ! -f "$PLUGINDIR/$PC" ]
|
||||
then
|
||||
echo "You must have a $PC file"
|
||||
echo "You must have a $PLUGINDIR/$PC file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
grep -q '^signer=' $PC
|
||||
SIGNER=`grep '^signer=' "$PLUGINDIR/$PC"`
|
||||
if [ "$?" -ne "0" ]
|
||||
then
|
||||
echo "You must have a signer in $PC"
|
||||
echo 'For example signer=joe@mail.i2p'
|
||||
echo "You must have a signer name in $PC"
|
||||
echo 'For example name=foo'
|
||||
exit 1
|
||||
fi
|
||||
SIGNER=`echo $SIGNER | cut -f 2 -d '='`
|
||||
|
||||
if [ ! -f "$PRIVKEYFILE" ]
|
||||
then
|
||||
echo "Creating new XPI2P DSA keys"
|
||||
mkdir -p "$PUBKEYDIR" || exit 1
|
||||
java -cp "$CPATH" net.i2p.crypto.TrustedUpdate keygen "$PUBKEYFILE" "$PRIVKEYFILE" || exit 1
|
||||
java -cp "$CPATH" net.i2p.data.Base64 encode "$PUBKEYFILE" "$B64KEYFILE" || exit 1
|
||||
rm -rf logs/
|
||||
chmod 444 "$PUBKEYFILE" "$B64KEYFILE"
|
||||
chmod 400 "$PRIVKEYFILE"
|
||||
echo "Created new XPI2P keys: $PUBKEYFILE $PRIVKEYFILE"
|
||||
fi
|
||||
|
||||
if [ ! -f "$PRIVKEYSTORE" ]
|
||||
then
|
||||
echo "Creating new SU3 $KEYTYPE keys for $SIGNER"
|
||||
java -cp "$CPATH" net.i2p.crypto.SU3File keygen -t $KEYTYPE "$PUBKEYSTORE" "$PRIVKEYSTORE" $SIGNER || exit 1
|
||||
echo '*** Save your password in a safe place!!! ***'
|
||||
rm -rf logs/
|
||||
# copy to the router dir so verify will work
|
||||
CDIR=$I2P/certificates/plugin
|
||||
mkdir -p "$CDIR" || exit 1
|
||||
CFILE=$CDIR/`echo $SIGNER | sed s/@/_at_/`.crt
|
||||
cp "$PUBKEYSTORE" "$CFILE"
|
||||
chmod 444 "$PUBKEYSTORE"
|
||||
chmod 400 "$PRIVKEYSTORE"
|
||||
chmod 644 "$CFILE"
|
||||
echo "Created new SU3 keys: $PUBKEYSTORE $PRIVKEYSTORE"
|
||||
echo "Copied public key to $CFILE for testing"
|
||||
fi
|
||||
|
||||
rm -f plugin.zip
|
||||
|
||||
OPWD=$PWD
|
||||
cd "$PLUGINDIR"
|
||||
|
||||
grep -q '^name=' $PC
|
||||
if [ "$?" -ne "0" ]
|
||||
then
|
||||
echo "You must have a plugin name in $PC"
|
||||
echo 'For example name=foo'
|
||||
echo 'For example name=foo'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@ -64,41 +101,49 @@ grep -q '^version=' $PC
|
||||
if [ "$?" -ne "0" ]
|
||||
then
|
||||
echo "You must have a version in $PC"
|
||||
echo 'For example version=0.1.2'
|
||||
echo 'For example version=0.1.2'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# update the date
|
||||
grep -v '^date=' $PC > $PCT
|
||||
DATE=`date '+%s000'`
|
||||
echo "date=$DATE" >> $PCT
|
||||
mv $PCT $PC
|
||||
echo "date=$DATE" >> $PCT || exit 1
|
||||
mv $PCT $PC || exit 1
|
||||
|
||||
# add our Base64 key
|
||||
grep -v '^key=' $PC > $PCT
|
||||
B64KEY=`cat $B64KEYFILE`
|
||||
B64KEY=`cat "$B64KEYFILE"`
|
||||
echo "key=$B64KEY" >> $PCT || exit 1
|
||||
mv $PCT $PC
|
||||
mv $PCT $PC || exit 1
|
||||
|
||||
# zip it
|
||||
zip -r $OPWD/plugin.zip * -x \*.jar || exit 1
|
||||
zip -r "$OPWD/plugin.zip" * || exit 1
|
||||
|
||||
# get the version and use it for the sud header
|
||||
VERSION=`grep '^version=' $PC | cut -f 2 -d '='`
|
||||
# get the name and use it for the file name
|
||||
NAME=`grep '^name=' $PC | cut -f 2 -d '='`
|
||||
XPI2P=${NAME}.xpi2p
|
||||
cd $OPWD
|
||||
SU3=${NAME}.su3
|
||||
cd "$OPWD"
|
||||
|
||||
# sign it
|
||||
java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate sign plugin.zip $XPI2P $PRIVKEYFILE $VERSION || exit 1
|
||||
echo 'Signing. ...'
|
||||
java -cp "$CPATH" net.i2p.crypto.TrustedUpdate sign plugin.zip "$XPI2P" "$PRIVKEYFILE" "$VERSION" || exit 1
|
||||
java -cp "$CPATH" net.i2p.crypto.SU3File sign -c PLUGIN -t $KEYTYPE plugin.zip "$SU3" "$PRIVKEYSTORE" "$VERSION" "$SIGNER" || exit 1
|
||||
rm -f plugin.zip
|
||||
|
||||
# verify
|
||||
echo 'Verifying. ...'
|
||||
java -cp $I2P/lib/i2p.jar net.i2p.crypto.TrustedUpdate showversion $XPI2P || exit 1
|
||||
java -cp $I2P/lib/i2p.jar -Drouter.trustedUpdateKeys=$B64KEY net.i2p.crypto.TrustedUpdate verifysig $XPI2P || exit 1
|
||||
java -cp "$CPATH" net.i2p.crypto.TrustedUpdate showversion "$XPI2P" || exit 1
|
||||
java -cp "$CPATH" -Drouter.trustedUpdateKeys=$B64KEY net.i2p.crypto.TrustedUpdate verifysig "$XPI2P" || exit 1
|
||||
java -cp "$CPATH" net.i2p.crypto.SU3File showversion "$SU3" || exit 1
|
||||
java -cp "$CPATH" net.i2p.crypto.SU3File verifysig -k "$PUBKEYSTORE" "$SU3" || exit 1
|
||||
rm -rf logs/
|
||||
|
||||
echo -n 'Plugin created: '
|
||||
wc -c $XPI2P
|
||||
echo 'Plugin files created: '
|
||||
wc -c "$XPI2P"
|
||||
wc -c "$SU3"
|
||||
|
||||
exit 0
|
||||
|
@ -1,8 +1,11 @@
|
||||
name=zzzot
|
||||
name=I2PControl
|
||||
signer=zzz-plugin@mail.i2p
|
||||
consoleLinkName=ZzzOT
|
||||
consoleLinkURL=http://127.0.0.1:7662/tracker/index.jsp
|
||||
description=Open tracker
|
||||
author=zzz
|
||||
updateURL=http://stats.i2p/i2p/plugins/zzzot-update.xpi2p
|
||||
consoleLinkName=I2PControl
|
||||
description=Remote Control Service
|
||||
author=hottuna
|
||||
websiteURL=http://zzz.i2p/forums/16
|
||||
updateURL=http://stats.i2p/i2p/plugins/I2PControl-update.xpi2p
|
||||
updateURL.su3=http://stats.i2p/i2p/plugins/I2PControl-update.su3
|
||||
license=Apache 2.0
|
||||
min-jetty-version=9
|
||||
min-i2p-version=0.9.30
|
||||
|
15
src/.classpath
Normal file
15
src/.classpath
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="java"/>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/i2p.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/javax.servlet.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/commons-logging.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/router.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/wrapper.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/jetty7/jetty-server-7.6.8.v20121106.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/jetty7/jetty-util-7.6.8.v20121106.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/jetty7/jetty-servlet-7.6.8.v20121106.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/jetty7/jetty-security-7.6.8.v20121106.jar"/>
|
||||
<classpathentry kind="output" path="build/obj"/>
|
||||
</classpath>
|
28
src/.project
Normal file
28
src/.project
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>i2pcontrol</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1746916390735</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
@ -1,110 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project basedir="." default="all" name="i2psnark">
|
||||
<project basedir="." default="all" name="source">
|
||||
<property name="i2pbase" value="../../i2p.i2p"/>
|
||||
<property name="i2plib" value="${i2pbase}/build"/>
|
||||
<property name="jettylib" value="${i2pbase}/apps/jetty/jettylib"/>
|
||||
<property name="war" value="../plugin/eepsite/webapps/blog"/>
|
||||
<property name="lib" value="${war}/WEB-INF/lib"/>
|
||||
<property name="wrapperlib" value="${i2pbase}/installer/lib/wrapper/all"/>
|
||||
|
||||
<path id="cp">
|
||||
<pathelement path="${java.class.path}" />
|
||||
<pathelement location="${i2plib}/i2p.jar" />
|
||||
<pathelement location="${i2plib}/i2ptunnel.jar" />
|
||||
<pathelement location="${i2plib}/i2psnark.jar" />
|
||||
<pathelement location="${i2plib}/routerconsole.jar" />
|
||||
<pathelement location="${jettylib}/ant.jar"/>
|
||||
<pathelement location="${jettylib}/org.mortbay.jetty.jar"/>
|
||||
<pathelement location="${jettylib}/jasper-compiler.jar" />
|
||||
<pathelement location="${jettylib}/jasper-runtime.jar" />
|
||||
<pathelement location="${jettylib}/javax.servlet.jar" />
|
||||
<pathelement location="${jettylib}/commons-logging.jar" />
|
||||
<pathelement location="${jettylib}/commons-el.jar" />
|
||||
<pathelement location="${i2plib}/router.jar" />
|
||||
<pathelement location="${i2plib}/org.mortbay.jetty.jar" />
|
||||
<pathelement location="${i2plib}/javax.servlet.jar" />
|
||||
<pathelement location="${jettylib}/jetty-servlet.jar" />
|
||||
<pathelement location="${wrapperlib}/wrapper.jar" />
|
||||
</path>
|
||||
|
||||
<target name="all" depends="clean, build" />
|
||||
<target name="build" depends="jar, war" />
|
||||
<target name="build" depends="jar" />
|
||||
<target name="builddep">
|
||||
</target>
|
||||
<condition property="depend.available">
|
||||
<typefound name="depend" />
|
||||
</condition>
|
||||
<target name="depend" if="depend.available">
|
||||
<depend
|
||||
cache="build"
|
||||
srcdir="./src"
|
||||
destdir="./build/obj" >
|
||||
<!-- Depend on classes instead of jars where available -->
|
||||
<classpath>
|
||||
<pathelement location="../../../core/java/build/obj" />
|
||||
<pathelement location="../../ministreaming/java/build/obj" />
|
||||
<pathelement location="../../jetty/jettylib/org.mortbay.jetty.jar" />
|
||||
<pathelement location="../../jetty/jettylib/javax.servlet.jar" />
|
||||
</classpath>
|
||||
</depend>
|
||||
</target>
|
||||
|
||||
<property name="javac.compilerargs" value="" />
|
||||
|
||||
<target name="compile" depends="depend">
|
||||
<target name="compile" depends="builddep" >
|
||||
<mkdir dir="./build" />
|
||||
<mkdir dir="./build/obj" />
|
||||
<javac
|
||||
srcdir="./java"
|
||||
debug="true" deprecation="on" source="1.5" target="1.5"
|
||||
debug="true" deprecation="on" source="1.7" target="1.7"
|
||||
includeAntRuntime="false"
|
||||
destdir="./build/obj"
|
||||
classpath="${i2plib}/i2p.jar:${i2plib}/i2ptunnel.jar:${i2plib}/i2psnark.jar:${i2plib}/systray.jar:${i2plib}/org.mortbay.jetty.jar" >
|
||||
classpath="${cp}">
|
||||
<compilerarg line="${javac.compilerargs}" />
|
||||
<classpath refid="cp"/>
|
||||
</javac>
|
||||
</target>
|
||||
|
||||
<target name="jar" depends="builddep, compile">
|
||||
<jar destfile="build/zzzot.jar" basedir="./build/obj" includes="**/*.class" >
|
||||
<target name="jar" depends="compile">
|
||||
<jar destfile="build/I2PControl.jar" basedir="./build/obj" includes="**/*.class" >
|
||||
</jar>
|
||||
</target>
|
||||
|
||||
<target name="precompilejsp" depends="compile" >
|
||||
<mkdir dir="build" />
|
||||
<mkdir dir="build/war/WEB-INF/classes" />
|
||||
<path id="jspcp">
|
||||
<path refid="cp" />
|
||||
<pathelement location="build/obj" />
|
||||
</path>
|
||||
<java classname="org.apache.jasper.JspC" fork="true" classpathref="jspcp" failonerror="true">
|
||||
<arg value="-d" />
|
||||
<arg value="build/jspjava" />
|
||||
<arg value="-v" />
|
||||
<arg value="-p" />
|
||||
<arg value="net.i2p.zzzot" />
|
||||
<arg value="-webinc" />
|
||||
<arg value="build/web-fragment.xml" />
|
||||
<arg value="-webapp" />
|
||||
<arg value="jsp/" />
|
||||
</java>
|
||||
|
||||
<javac
|
||||
debug="true"
|
||||
deprecation="on"
|
||||
source="1.5" target="1.5"
|
||||
destdir="build/war/WEB-INF/classes"
|
||||
srcdir="./build/jspjava"
|
||||
includes="**/*.java"
|
||||
classpathref="jspcp"
|
||||
failonerror="true" />
|
||||
|
||||
<copy file="jsp/WEB-INF/web.xml" tofile="build/web.xml" />
|
||||
<loadfile property="jspc.web.fragment" srcfile="build/web-fragment.xml" />
|
||||
<replace file="build/web.xml">
|
||||
<replacefilter token="<!-- precompiled servlets -->" value="${jspc.web.fragment}" />
|
||||
</replace>
|
||||
|
||||
</target>
|
||||
|
||||
<target name="war" depends="precompilejsp">
|
||||
<copy file="jsp/index.html" todir="build/war" />
|
||||
<war destfile="build/tracker.war.jar" webxml="build/web.xml">
|
||||
<fileset dir="build/war" />
|
||||
<target name="war" depends="compile" >
|
||||
<war destfile="build/jsonrpc.war" webxml="web.xml" >
|
||||
<classes dir="./build/obj" excludes="net/i2p/i2pcontrol/I2PControlController.class net/i2p/i2pcontrol/HostCheckHandler.class" />
|
||||
</war>
|
||||
</target>
|
||||
|
||||
|
||||
<target name="clean">
|
||||
<delete dir="./build" />
|
||||
</target>
|
||||
|
10
src/java/.classpath
Normal file
10
src/java/.classpath
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry exported="true" kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/commons-logging.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/i2p.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/javax.servlet.jar"/>
|
||||
<classpathentry kind="lib" path="/home/hottuna/Apps/i2p/lib/org.mortbay.jetty.jar"/>
|
||||
<classpathentry kind="output" path="bin"/>
|
||||
</classpath>
|
28
src/java/.project
Normal file
28
src/java/.project
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>i2pcontrol</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
<filteredResources>
|
||||
<filter>
|
||||
<id>1746916390737</id>
|
||||
<name></name>
|
||||
<type>30</type>
|
||||
<matcher>
|
||||
<id>org.eclipse.core.resources.regexFilterMatcher</id>
|
||||
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
|
||||
</matcher>
|
||||
</filter>
|
||||
</filteredResources>
|
||||
</projectDescription>
|
274
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Error.java
Normal file
274
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Error.java
Normal file
@ -0,0 +1,274 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a JSON-RPC 2.0 error that occurred during the processing of a
|
||||
* request. This class is immutable.
|
||||
*
|
||||
* <p>The protocol expects error objects to be structured like this:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code code} An integer that indicates the error type.
|
||||
* <li>{@code message} A string providing a short description of the
|
||||
* error. The message should be limited to a concise single sentence.
|
||||
* <li>{@code data} Additional information, which may be omitted. Its
|
||||
* contents is entirely defined by the application.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that the "Error" word in the class name was put there solely to
|
||||
* comply with the parlance of the JSON-RPC spec. This class doesn't inherit
|
||||
* from {@code java.lang.Error}. It's a regular subclass of
|
||||
* {@code java.lang.Exception} and, if thrown, it's to indicate a condition
|
||||
* that a reasonable application might want to catch.
|
||||
*
|
||||
* <p>This class also includes convenient final static instances for all
|
||||
* standard JSON-RPC 2.0 errors:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #PARSE_ERROR} JSON parse error (-32700)
|
||||
* <li>{@link #INVALID_REQUEST} Invalid JSON-RPC 2.0 Request (-32600)
|
||||
* <li>{@link #METHOD_NOT_FOUND} Method not found (-32601)
|
||||
* <li>{@link #INVALID_PARAMS} Invalid parameters (-32602)
|
||||
* <li>{@link #INTERNAL_ERROR} Internal error (-32603)
|
||||
* </ul>
|
||||
*
|
||||
* <p>Note that the range -32099..-32000 is reserved for additional server
|
||||
* errors.
|
||||
*
|
||||
* <p id="map">The mapping between JSON and Java entities (as defined by the
|
||||
* underlying JSON Smart library):
|
||||
* <pre>
|
||||
* true|false <---> java.lang.Boolean
|
||||
* number <---> java.lang.Number
|
||||
* string <---> java.lang.String
|
||||
* array <---> java.util.List
|
||||
* object <---> java.util.Map
|
||||
* null <---> null
|
||||
* </pre>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class JSONRPC2Error extends Exception {
|
||||
|
||||
|
||||
/**
|
||||
* Serial version UID.
|
||||
*/
|
||||
private static final long serialVersionUID = 4682571044532698806L;
|
||||
|
||||
|
||||
/**
|
||||
* JSON parse error (-32700).
|
||||
*/
|
||||
public static final JSONRPC2Error PARSE_ERROR = new JSONRPC2Error(-32700, "JSON parse error");
|
||||
|
||||
|
||||
/**
|
||||
* Invalid JSON-RPC 2.0 request error (-32600).
|
||||
*/
|
||||
public static final JSONRPC2Error INVALID_REQUEST = new JSONRPC2Error(-32600, "Invalid request");
|
||||
|
||||
|
||||
/**
|
||||
* Method not found error (-32601).
|
||||
*/
|
||||
public static final JSONRPC2Error METHOD_NOT_FOUND = new JSONRPC2Error(-32601, "Method not found");
|
||||
|
||||
|
||||
/**
|
||||
* Invalid parameters error (-32602).
|
||||
*/
|
||||
public static final JSONRPC2Error INVALID_PARAMS = new JSONRPC2Error(-32602, "Invalid parameters");
|
||||
|
||||
|
||||
/**
|
||||
* Internal JSON-RPC 2.0 error (-32603).
|
||||
*/
|
||||
public static final JSONRPC2Error INTERNAL_ERROR = new JSONRPC2Error(-32603, "Internal error");
|
||||
|
||||
|
||||
/**
|
||||
* The error code.
|
||||
*/
|
||||
private final int code;
|
||||
|
||||
|
||||
/**
|
||||
* The optional error data.
|
||||
*/
|
||||
private final Object data;
|
||||
|
||||
|
||||
/**
|
||||
* Appends the specified string to the message of a JSON-RPC 2.0 error.
|
||||
*
|
||||
* @param err The JSON-RPC 2.0 error. Must not be {@code null}.
|
||||
* @param apx The string to append to the original error message.
|
||||
*
|
||||
* @return A new JSON-RPC 2.0 error with the appended message.
|
||||
*/
|
||||
@Deprecated
|
||||
public static JSONRPC2Error appendMessage(final JSONRPC2Error err, final String apx) {
|
||||
|
||||
return new JSONRPC2Error(err.getCode(), err.getMessage() + apx, err.getData());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the specified data to a JSON-RPC 2.0 error.
|
||||
*
|
||||
* @param err The JSON-RPC 2.0 error to have its data field set. Must
|
||||
* not be {@code null}.
|
||||
* @param data Optional error data, must <a href="#map">map</a> to a
|
||||
* valid JSON type.
|
||||
*
|
||||
* @return A new JSON-RPC 2.0 error with the set data.
|
||||
*/
|
||||
@Deprecated
|
||||
public static JSONRPC2Error setData(final JSONRPC2Error err, final Object data) {
|
||||
|
||||
return new JSONRPC2Error(err.getCode(), err.getMessage(), data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 error with the specified code and
|
||||
* message. The optional data is omitted.
|
||||
*
|
||||
* @param code The error code (standard pre-defined or
|
||||
* application-specific).
|
||||
* @param message The error message.
|
||||
*/
|
||||
public JSONRPC2Error(int code, String message) {
|
||||
|
||||
this(code, message, null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 error with the specified code,
|
||||
* message and data.
|
||||
*
|
||||
* @param code The error code (standard pre-defined or
|
||||
* application-specific).
|
||||
* @param message The error message.
|
||||
* @param data Optional error data, must <a href="#map">map</a>
|
||||
* to a valid JSON type.
|
||||
*/
|
||||
public JSONRPC2Error(int code, String message, Object data) {
|
||||
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the JSON-RPC 2.0 error code.
|
||||
*
|
||||
* @return The error code.
|
||||
*/
|
||||
public int getCode() {
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the JSON-RPC 2.0 error data.
|
||||
*
|
||||
* @return The error data, {@code null} if none was specified.
|
||||
*/
|
||||
public Object getData() {
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the specified data to a JSON-RPC 2.0 error.
|
||||
*
|
||||
* @param data Optional error data, must <a href="#map">map</a> to a
|
||||
* valid JSON type.
|
||||
*
|
||||
* @return A new JSON-RPC 2.0 error with the set data.
|
||||
*/
|
||||
public JSONRPC2Error setData(final Object data) {
|
||||
|
||||
return new JSONRPC2Error(code, getMessage(), data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends the specified string to the message of this JSON-RPC 2.0
|
||||
* error.
|
||||
*
|
||||
* @param apx The string to append to the original error message.
|
||||
*
|
||||
* @return A new JSON-RPC 2.0 error with the appended message.
|
||||
*/
|
||||
public JSONRPC2Error appendMessage(final String apx) {
|
||||
|
||||
return new JSONRPC2Error(code, getMessage() + apx, data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @see #toJSONObject
|
||||
*/
|
||||
@Deprecated
|
||||
public JSONObject toJSON() {
|
||||
|
||||
return toJSONObject();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a JSON object representation of this JSON-RPC 2.0 error.
|
||||
*
|
||||
* @return A JSON object representing this error object.
|
||||
*/
|
||||
public JSONObject toJSONObject() {
|
||||
|
||||
JSONObject out = new JSONObject();
|
||||
|
||||
out.put("code", code);
|
||||
out.put("message", super.getMessage());
|
||||
if (data != null)
|
||||
out.put("data", data);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serialises the error object to a JSON string.
|
||||
*
|
||||
* @return A JSON-encoded string representing this error object.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return toJSON().toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Overrides {@code Object.equals()}.
|
||||
*
|
||||
* @param object The object to compare to.
|
||||
*
|
||||
* @return {@code true} if both objects are instances if this class and
|
||||
* their error codes are identical, {@code false} if not.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
|
||||
return object != null &&
|
||||
object instanceof JSONRPC2Error &&
|
||||
code == ((JSONRPC2Error)object).getCode();
|
||||
}
|
||||
}
|
251
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Message.java
Normal file
251
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Message.java
Normal file
@ -0,0 +1,251 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minidev.json.JSONAware;
|
||||
import net.minidev.json.JSONObject;
|
||||
|
||||
|
||||
/**
|
||||
* The base abstract class for JSON-RPC 2.0 requests, notifications and
|
||||
* responses. Provides common methods for parsing (from JSON string) and
|
||||
* serialisation (to JSON string) of these three message types.
|
||||
*
|
||||
* <p>Example parsing and serialisation back to JSON:
|
||||
*
|
||||
* <pre>
|
||||
* String jsonString = "{\"method\":\"progressNotify\",\"params\":[\"75%\"],\"jsonrpc\":\"2.0\"}";
|
||||
*
|
||||
* JSONRPC2Message message = null;
|
||||
*
|
||||
* // parse
|
||||
* try {
|
||||
* message = JSONRPC2Message.parse(jsonString);
|
||||
* } catch (JSONRPC2ParseException e) {
|
||||
* // handle parse exception
|
||||
* }
|
||||
*
|
||||
* if (message instanceof JSONRPC2Request)
|
||||
* System.out.println("The message is a request");
|
||||
* else if (message instanceof JSONRPC2Notification)
|
||||
* System.out.println("The message is a notification");
|
||||
* else if (message instanceof JSONRPC2Response)
|
||||
* System.out.println("The message is a response");
|
||||
*
|
||||
* // serialise back to JSON string
|
||||
* System.out.println(message);
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* <p id="map">The mapping between JSON and Java entities (as defined by the
|
||||
* underlying JSON Smart library):
|
||||
*
|
||||
* <pre>
|
||||
* true|false <---> java.lang.Boolean
|
||||
* number <---> java.lang.Number
|
||||
* string <---> java.lang.String
|
||||
* array <---> java.util.List
|
||||
* object <---> java.util.Map
|
||||
* null <---> null
|
||||
* </pre>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public abstract class JSONRPC2Message implements JSONAware {
|
||||
|
||||
|
||||
/**
|
||||
* Map of non-standard JSON-RPC 2.0 message attributes, {@code null} if
|
||||
* none.
|
||||
*/
|
||||
private Map <String,Object> nonStdAttributes = null;
|
||||
|
||||
|
||||
/**
|
||||
* Provides common parsing of JSON-RPC 2.0 requests, notifications
|
||||
* and responses. Use this method if you don't know which type of
|
||||
* JSON-RPC message the input JSON string represents.
|
||||
*
|
||||
* <p>Batched requests / notifications are not supported.
|
||||
*
|
||||
* <p>This method is thread-safe.
|
||||
*
|
||||
* <p>If you are certain about the message type use the dedicated
|
||||
* {@link JSONRPC2Request#parse}, {@link JSONRPC2Notification#parse}
|
||||
* or {@link JSONRPC2Response#parse} methods. They are more efficient
|
||||
* and provide a more detailed parse error reporting.
|
||||
*
|
||||
* <p>The member order of parsed JSON objects will not be preserved
|
||||
* (for efficiency reasons) and the JSON-RPC 2.0 version field must be
|
||||
* set to "2.0". To change this behaviour check the optional {@link
|
||||
* #parse(String,boolean,boolean)} method.
|
||||
*
|
||||
* @param jsonString A JSON string representing a JSON-RPC 2.0 request,
|
||||
* notification or response, UTF-8 encoded. Must not
|
||||
* be {@code null}.
|
||||
*
|
||||
* @return An instance of {@link JSONRPC2Request},
|
||||
* {@link JSONRPC2Notification} or {@link JSONRPC2Response}.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Message parse(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provides common parsing of JSON-RPC 2.0 requests, notifications
|
||||
* and responses. Use this method if you don't know which type of
|
||||
* JSON-RPC message the input string represents.
|
||||
*
|
||||
* <p>Batched requests / notifications are not supported.
|
||||
*
|
||||
* <p>This method is thread-safe.
|
||||
*
|
||||
* <p>If you are certain about the message type use the dedicated
|
||||
* {@link JSONRPC2Request#parse}, {@link JSONRPC2Notification#parse}
|
||||
* or {@link JSONRPC2Response#parse} methods. They are more efficient
|
||||
* and provide a more detailed parse error reporting.
|
||||
*
|
||||
* @param jsonString A JSON string representing a JSON-RPC 2.0
|
||||
* request, notification or response, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
* @param preserveOrder If {@code true} the member order of JSON objects
|
||||
* in parameters and results must be preserved.
|
||||
* @param ignoreVersion If {@code true} the {@code "jsonrpc":"2.0"}
|
||||
* version field in the JSON-RPC 2.0 message will
|
||||
* not be checked.
|
||||
*
|
||||
* @return An instance of {@link JSONRPC2Request},
|
||||
* {@link JSONRPC2Notification} or {@link JSONRPC2Response}.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Message parse(final String jsonString, final boolean preserveOrder, final boolean ignoreVersion)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, ignoreVersion);
|
||||
|
||||
return parser.parseJSONRPC2Message(jsonString);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Appends a non-standard attribute to this JSON-RPC 2.0 message. This is
|
||||
* done by adding a new member (key / value pair) to the top level JSON
|
||||
* object representing the message.
|
||||
*
|
||||
* <p>You may use this method to add meta and debugging attributes,
|
||||
* such as the request processing time, to a JSON-RPC 2.0 message.
|
||||
*
|
||||
* @param name The attribute name. Must not conflict with the existing
|
||||
* "method", "id", "params", "result", "error" and "jsonrpc"
|
||||
* attributes reserved by the JSON-RPC 2.0 protocol, else
|
||||
* an {@code IllegalArgumentException} will be thrown. Must
|
||||
* not be {@code null} either.
|
||||
* @param value The attribute value. Must be of type String, boolean,
|
||||
* number, List, Map or null, else an
|
||||
* {@code IllegalArgumentException} will be thrown.
|
||||
*/
|
||||
public void appendNonStdAttribute(final String name, final Object value) {
|
||||
|
||||
// Name check
|
||||
if (name == null ||
|
||||
name.equals("method") ||
|
||||
name.equals("id") ||
|
||||
name.equals("params") ||
|
||||
name.equals("result") ||
|
||||
name.equals("error") ||
|
||||
name.equals("jsonrpc") )
|
||||
|
||||
throw new IllegalArgumentException("Non-standard attribute name violation");
|
||||
|
||||
// Value check
|
||||
if ( value != null &&
|
||||
! (value instanceof Boolean) &&
|
||||
! (value instanceof Number) &&
|
||||
! (value instanceof String) &&
|
||||
! (value instanceof List) &&
|
||||
! (value instanceof Map) )
|
||||
|
||||
throw new IllegalArgumentException("Illegal non-standard attribute value, must map to a valid JSON type");
|
||||
|
||||
|
||||
if (nonStdAttributes == null)
|
||||
nonStdAttributes = new HashMap<String,Object>();
|
||||
|
||||
nonStdAttributes.put(name, value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a non-standard JSON-RPC 2.0 message attribute.
|
||||
*
|
||||
* @param name The name of the non-standard attribute to retrieve. Must
|
||||
* not be {@code null}.
|
||||
*
|
||||
* @return The value of the non-standard attribute (may also be
|
||||
* {@code null}, {@code null} if not found.
|
||||
*/
|
||||
public Object getNonStdAttribute(final String name) {
|
||||
|
||||
if (nonStdAttributes == null)
|
||||
return null;
|
||||
|
||||
return nonStdAttributes.get(name);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the non-standard JSON-RPC 2.0 message attributes.
|
||||
*
|
||||
* @return The non-standard attributes as a map, {@code null} if none.
|
||||
*/
|
||||
public Map<String,Object> getNonStdAttributes() {
|
||||
|
||||
return nonStdAttributes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a JSON object representing this JSON-RPC 2.0 message.
|
||||
*
|
||||
* @return The JSON object.
|
||||
*/
|
||||
public abstract JSONObject toJSONObject();
|
||||
|
||||
|
||||
/**
|
||||
* Returns a JSON string representation of this JSON-RPC 2.0 message.
|
||||
*
|
||||
* @see #toString
|
||||
*
|
||||
* @return The JSON object string representing this JSON-RPC 2.0
|
||||
* message.
|
||||
*/
|
||||
public String toJSONString() {
|
||||
|
||||
return toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serialises this JSON-RPC 2.0 message to a JSON object string.
|
||||
*
|
||||
* @return The JSON object string representing this JSON-RPC 2.0
|
||||
* message.
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return toJSONObject().toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,448 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a JSON-RPC 2.0 notification.
|
||||
*
|
||||
* <p>Notifications provide a mean for calling a remote procedure without
|
||||
* generating a response. Note that notifications are inherently unreliable
|
||||
* as no confirmation is sent back to the caller.
|
||||
*
|
||||
* <p>Notifications have the same JSON structure as requests, except that they
|
||||
* lack an identifier:
|
||||
* <ul>
|
||||
* <li>{@code method} The name of the remote method to call.
|
||||
* <li>{@code params} The required method parameters (if any), which can
|
||||
* be packed into a JSON array or object.
|
||||
* <li>{@code jsonrpc} A string indicating the JSON-RPC protocol version
|
||||
* set to "2.0".
|
||||
* </ul>
|
||||
*
|
||||
* <p>Here is a sample JSON-RPC 2.0 notification string:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "method" : "progressNotify",
|
||||
* "params" : ["75%"],
|
||||
* "jsonrpc" : "2.0"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>This class provides two methods to obtain a request object:
|
||||
* <ul>
|
||||
* <li>Pass a JSON-RPC 2.0 notification string to the static
|
||||
* {@link #parse} method, or
|
||||
* <li>Invoke one of the constructors with the appropriate arguments.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example 1: Parsing a notification string:
|
||||
*
|
||||
* <pre>
|
||||
* String jsonString = "{\"method\":\"progressNotify\",\"params\":[\"75%\"],\"jsonrpc\":\"2.0\"}";
|
||||
*
|
||||
* JSONRPC2Notification notification = null;
|
||||
*
|
||||
* try {
|
||||
* notification = JSONRPC2Notification.parse(jsonString);
|
||||
*
|
||||
* } catch (JSONRPC2ParseException e) {
|
||||
* // handle exception
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Example 2: Recreating the above request:
|
||||
*
|
||||
* <pre>
|
||||
* String method = "progressNotify";
|
||||
* List<Object> params = new Vector<Object>();
|
||||
* params.add("75%");
|
||||
*
|
||||
* JSONRPC2Notification notification = new JSONRPC2Notification(method, params);
|
||||
*
|
||||
* System.out.println(notification);
|
||||
* </pre>
|
||||
*
|
||||
* <p id="map">The mapping between JSON and Java entities (as defined by the
|
||||
* underlying JSON Smart library):
|
||||
*
|
||||
* <pre>
|
||||
* true|false <---> java.lang.Boolean
|
||||
* number <---> java.lang.Number
|
||||
* string <---> java.lang.String
|
||||
* array <---> java.util.List
|
||||
* object <---> java.util.Map
|
||||
* null <---> null
|
||||
* </pre>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class JSONRPC2Notification extends JSONRPC2Message {
|
||||
|
||||
|
||||
/**
|
||||
* The requested method name.
|
||||
*/
|
||||
private String method;
|
||||
|
||||
|
||||
/**
|
||||
* The positional parameters, {@code null} if none.
|
||||
*/
|
||||
private List<Object> positionalParams;
|
||||
|
||||
|
||||
/**
|
||||
* The named parameters, {@code null} if none.
|
||||
*/
|
||||
private Map<String,Object> namedParams;
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 notification string. This method is
|
||||
* thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 notification string, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 notification object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Notification parse(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, false, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 notification string. This method is
|
||||
* thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 notification string, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members in parameters.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 notification object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Notification parse(final String jsonString,
|
||||
final boolean preserveOrder)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, preserveOrder, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 notification string. This method is
|
||||
* thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 notification string, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members in parameters.
|
||||
* @param ignoreVersion {@code true} to skip a check of the
|
||||
* {@code "jsonrpc":"2.0"} version attribute in the
|
||||
* JSON-RPC 2.0 message.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 notification object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Notification parse(final String jsonString,
|
||||
final boolean preserveOrder,
|
||||
final boolean ignoreVersion)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, preserveOrder, ignoreVersion, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 notification string. This method is
|
||||
* thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 notification string,
|
||||
* UTF-8 encoded. Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of
|
||||
* JSON object members in parameters.
|
||||
* @param ignoreVersion {@code true} to skip a check of the
|
||||
* {@code "jsonrpc":"2.0"} version
|
||||
* attribute in the JSON-RPC 2.0 message.
|
||||
* @param parseNonStdAttributes {@code true} to parse non-standard
|
||||
* attributes found in the JSON-RPC 2.0
|
||||
* message.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 notification object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Notification parse(final String jsonString,
|
||||
final boolean preserveOrder,
|
||||
final boolean ignoreVersion,
|
||||
final boolean parseNonStdAttributes)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, ignoreVersion, parseNonStdAttributes);
|
||||
|
||||
return parser.parseJSONRPC2Notification(jsonString);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new JSON-RPC 2.0 notification with no parameters.
|
||||
*
|
||||
* @param method The name of the requested method. Must not be
|
||||
* {@code null}.
|
||||
*/
|
||||
public JSONRPC2Notification(final String method) {
|
||||
|
||||
setMethod(method);
|
||||
setParams(null);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new JSON-RPC 2.0 notification with positional (JSON
|
||||
* array) parameters.
|
||||
*
|
||||
* @param method The name of the requested method. Must not
|
||||
* be {@code null}.
|
||||
* @param positionalParams The positional (JSON array) parameters,
|
||||
* {@code null} if none.
|
||||
*/
|
||||
public JSONRPC2Notification(final String method,
|
||||
final List<Object> positionalParams) {
|
||||
|
||||
setMethod(method);
|
||||
setPositionalParams(positionalParams);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new JSON-RPC 2.0 notification with named (JSON object)
|
||||
* parameters.
|
||||
*
|
||||
* @param method The name of the requested method.
|
||||
* @param namedParams The named (JSON object) parameters, {@code null}
|
||||
* if none.
|
||||
*/
|
||||
public JSONRPC2Notification(final String method,
|
||||
final Map <String,Object> namedParams) {
|
||||
|
||||
setMethod(method);
|
||||
setNamedParams(namedParams);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the name of the requested method.
|
||||
*
|
||||
* @return The method name.
|
||||
*/
|
||||
public String getMethod() {
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the name of the requested method.
|
||||
*
|
||||
* @param method The method name. Must not be {@code null}.
|
||||
*/
|
||||
public void setMethod(final String method) {
|
||||
|
||||
// The method name is mandatory
|
||||
if (method == null)
|
||||
throw new IllegalArgumentException("The method name must not be null");
|
||||
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the parameters type ({@link JSONRPC2ParamsType#ARRAY positional},
|
||||
* {@link JSONRPC2ParamsType#OBJECT named} or
|
||||
* {@link JSONRPC2ParamsType#NO_PARAMS none}).
|
||||
*
|
||||
* @return The parameters type.
|
||||
*/
|
||||
public JSONRPC2ParamsType getParamsType() {
|
||||
|
||||
if (positionalParams == null && namedParams == null)
|
||||
return JSONRPC2ParamsType.NO_PARAMS;
|
||||
|
||||
if (positionalParams != null)
|
||||
return JSONRPC2ParamsType.ARRAY;
|
||||
|
||||
if (namedParams != null)
|
||||
return JSONRPC2ParamsType.OBJECT;
|
||||
|
||||
else
|
||||
return JSONRPC2ParamsType.NO_PARAMS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the notification parameters.
|
||||
*
|
||||
* <p>This method was deprecated in version 1.30. Use
|
||||
* {@link #getPositionalParams} or {@link #getNamedParams} instead.
|
||||
*
|
||||
* @return The parameters as {@code List<Object>} for positional
|
||||
* (JSON array), {@code Map<String,Object>} for named
|
||||
* (JSON object), or {@code null} if none.
|
||||
*/
|
||||
@Deprecated
|
||||
public Object getParams() {
|
||||
|
||||
switch (getParamsType()) {
|
||||
|
||||
case ARRAY:
|
||||
return positionalParams;
|
||||
|
||||
case OBJECT:
|
||||
return namedParams;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the positional (JSON array) parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @return The positional (JSON array) parameters, {@code null} if none
|
||||
* or named.
|
||||
*/
|
||||
public List<Object> getPositionalParams() {
|
||||
|
||||
return positionalParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the named parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @return The named (JSON object) parameters, {@code null} if none or
|
||||
* positional.
|
||||
*/
|
||||
public Map<String,Object> getNamedParams() {
|
||||
|
||||
return namedParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the notification parameters.
|
||||
*
|
||||
* <p>This method was deprecated in version 1.30. Use
|
||||
* {@link #setPositionalParams} or {@link #setNamedParams} instead.
|
||||
*
|
||||
* @param params The parameters. For positional (JSON array) pass a
|
||||
* {@code List<Object>}. For named (JSON object)
|
||||
* pass a {@code Map<String,Object>}. If there are
|
||||
* no parameters pass {@code null}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setParams(final Object params) {
|
||||
|
||||
if (params == null) {
|
||||
positionalParams = null;
|
||||
namedParams = null;
|
||||
} else if (params instanceof List) {
|
||||
positionalParams = (List<Object>) params;
|
||||
} else if (params instanceof Map) {
|
||||
namedParams = (Map<String, Object>) params;
|
||||
} else {
|
||||
throw new IllegalArgumentException("The notification parameters must be of type List, Map or null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the positional (JSON array) request parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @param positionalParams The positional (JSON array) request
|
||||
* parameters, {@code null} if none.
|
||||
*/
|
||||
public void setPositionalParams(final List<Object> positionalParams) {
|
||||
|
||||
if (positionalParams == null)
|
||||
return;
|
||||
|
||||
this.positionalParams = positionalParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the named (JSON object) request parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @param namedParams The named (JSON object) request parameters,
|
||||
* {@code null} if none.
|
||||
*/
|
||||
public void setNamedParams(final Map<String,Object> namedParams) {
|
||||
|
||||
if (namedParams == null)
|
||||
return;
|
||||
|
||||
this.namedParams = namedParams;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JSONObject toJSONObject() {
|
||||
|
||||
JSONObject notf = new JSONObject();
|
||||
|
||||
notf.put("method", method);
|
||||
|
||||
// The params can be omitted if none
|
||||
switch (getParamsType()) {
|
||||
|
||||
case ARRAY:
|
||||
notf.put("params", positionalParams);
|
||||
break;
|
||||
|
||||
case OBJECT:
|
||||
notf.put("params", namedParams);
|
||||
break;
|
||||
}
|
||||
|
||||
notf.put("jsonrpc", "2.0");
|
||||
|
||||
|
||||
Map <String,Object> nonStdAttributes = getNonStdAttributes();
|
||||
|
||||
if (nonStdAttributes != null) {
|
||||
|
||||
for (final Map.Entry<String,Object> attr: nonStdAttributes.entrySet())
|
||||
notf.put(attr.getKey(), attr.getValue());
|
||||
}
|
||||
|
||||
return notf;
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
/**
|
||||
* Enumeration of the three parameter types in JSON-RPC 2.0 requests and
|
||||
* notifications.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #NO_PARAMS} The method takes no parameters.
|
||||
* <li>{@link #ARRAY} The method takes positional parameters, packed as a
|
||||
* JSON array, e.g. {@code ["val1", "val2", ...]}.
|
||||
* <li>{@link #OBJECT} The method takes named parameters, packed as a JSON
|
||||
* object, e.g. {@code {"param1":"val1", "param2":"val2", ...}}.
|
||||
* </ul>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public enum JSONRPC2ParamsType {
|
||||
|
||||
|
||||
/**
|
||||
* No parameters.
|
||||
*/
|
||||
NO_PARAMS,
|
||||
|
||||
|
||||
/**
|
||||
* Positional parameters, packed as a JSON array.
|
||||
*/
|
||||
ARRAY,
|
||||
|
||||
|
||||
/**
|
||||
* Named parameters, packed as a JSON object.
|
||||
*/
|
||||
OBJECT
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
/**
|
||||
* Thrown to indicate an exception during the parsing of a JSON-RPC 2.0
|
||||
* message string.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class JSONRPC2ParseException extends Exception {
|
||||
|
||||
|
||||
/**
|
||||
* Serial version UID.
|
||||
*/
|
||||
private static final long serialVersionUID = 3376608778436136410L;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates a parse exception caused by a JSON message not conforming
|
||||
* to the JSON-RPC 2.0 protocol.
|
||||
*/
|
||||
public static final int PROTOCOL = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates a parse exception caused by invalid JSON.
|
||||
*/
|
||||
public static final int JSON = 1;
|
||||
|
||||
|
||||
/**
|
||||
* The parse exception cause type. Default is {@link #PROTOCOL}.
|
||||
*/
|
||||
private int causeType = PROTOCOL;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* The string that could't be parsed.
|
||||
*/
|
||||
private String unparsableString = null;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new parse exception with the specified message. The cause
|
||||
* type is set to {@link #PROTOCOL}.
|
||||
*
|
||||
* @param message The exception message.
|
||||
*/
|
||||
public JSONRPC2ParseException(final String message) {
|
||||
|
||||
super(message);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new parse exception with the specified message and the
|
||||
* original string that didn't parse. The cause type is set to
|
||||
* {@link #PROTOCOL}.
|
||||
*
|
||||
* @param message The exception message.
|
||||
* @param unparsableString The unparsable string.
|
||||
*/
|
||||
public JSONRPC2ParseException(final String message, final String unparsableString) {
|
||||
|
||||
super(message);
|
||||
this.unparsableString = unparsableString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new parse exception with the specified message, cause type
|
||||
* and the original string that didn't parse.
|
||||
*
|
||||
* @param message The exception message.
|
||||
* @param causeType The exception cause type, either
|
||||
* {@link #PROTOCOL} or {@link #JSON}.
|
||||
* @param unparsableString The unparsable string.
|
||||
*/
|
||||
public JSONRPC2ParseException(final String message, final int causeType, final String unparsableString) {
|
||||
|
||||
super(message);
|
||||
|
||||
if (causeType != PROTOCOL && causeType != JSON)
|
||||
throw new IllegalArgumentException("Cause type must be either PROTOCOL or JSON");
|
||||
|
||||
this.causeType = causeType;
|
||||
this.unparsableString = unparsableString;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the parse exception cause type.
|
||||
*
|
||||
* @return The cause type, either {@link #PROTOCOL} or {@link #JSON}.
|
||||
*/
|
||||
public int getCauseType() {
|
||||
|
||||
return causeType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets original string that caused the parse exception (if specified).
|
||||
*
|
||||
* @return The string that didn't parse, {@code null} if none.
|
||||
*/
|
||||
public String getUnparsableString() {
|
||||
|
||||
return unparsableString;
|
||||
}
|
||||
}
|
654
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Parser.java
Normal file
654
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Parser.java
Normal file
@ -0,0 +1,654 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minidev.json.parser.ContainerFactory;
|
||||
import net.minidev.json.parser.JSONParser;
|
||||
import net.minidev.json.parser.ParseException;
|
||||
|
||||
|
||||
/**
|
||||
* Parses JSON-RPC 2.0 request, notification and response messages.
|
||||
*
|
||||
* <p>Parsing of batched requests / notifications is not supported.
|
||||
*
|
||||
* <p>This class is not thread-safe. A parser instance should not be used by
|
||||
* more than one thread unless properly synchronised. Alternatively, you may
|
||||
* use the thread-safe {@link JSONRPC2Message#parse} and its sister methods.
|
||||
*
|
||||
* <p>Example:
|
||||
*
|
||||
* <pre>
|
||||
* String jsonString = "{\"method\":\"makePayment\"," +
|
||||
* "\"params\":{\"recipient\":\"Penny Adams\",\"amount\":175.05}," +
|
||||
* "\"id\":\"0001\","+
|
||||
* "\"jsonrpc\":\"2.0\"}";
|
||||
*
|
||||
* JSONRPC2Request req = null;
|
||||
*
|
||||
* JSONRPC2Parser parser = new JSONRPC2Parser();
|
||||
*
|
||||
* try {
|
||||
* req = parser.parseJSONRPC2Request(jsonString);
|
||||
*
|
||||
* } catch (JSONRPC2ParseException e) {
|
||||
* // handle exception
|
||||
* }
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* <p id="map">The mapping between JSON and Java entities (as defined by the
|
||||
* underlying JSON Smart library):
|
||||
*
|
||||
* <pre>
|
||||
* true|false <---> java.lang.Boolean
|
||||
* number <---> java.lang.Number
|
||||
* string <---> java.lang.String
|
||||
* array <---> java.util.List
|
||||
* object <---> java.util.Map
|
||||
* null <---> null
|
||||
* </pre>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class JSONRPC2Parser {
|
||||
|
||||
|
||||
/**
|
||||
* Reusable JSON parser. Not thread-safe!
|
||||
*/
|
||||
private final JSONParser parser;
|
||||
|
||||
|
||||
/**
|
||||
* If {@code true} the order of the parsed JSON object members must be
|
||||
* preserved.
|
||||
*/
|
||||
private boolean preserveOrder;
|
||||
|
||||
|
||||
/**
|
||||
* If {@code true} the {@code "jsonrpc":"2.0"} version attribute in the
|
||||
* JSON-RPC 2.0 message must be ignored during parsing.
|
||||
*/
|
||||
private boolean ignoreVersion;
|
||||
|
||||
|
||||
/**
|
||||
* If {@code true} non-standard JSON-RPC 2.0 message attributes must be
|
||||
* parsed too.
|
||||
*/
|
||||
private boolean parseNonStdAttributes;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 message parser.
|
||||
*
|
||||
* <p>The member order of parsed JSON objects in parameters and results
|
||||
* will not be preserved; strict checking of the 2.0 JSON-RPC version
|
||||
* attribute will be enforced; non-standard message attributes will be
|
||||
* ignored. Check the other constructors if you want to specify
|
||||
* different behaviour.
|
||||
*/
|
||||
public JSONRPC2Parser() {
|
||||
|
||||
this(false, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 message parser.
|
||||
*
|
||||
* <p>Strict checking of the 2.0 JSON-RPC version attribute will be
|
||||
* enforced; non-standard message attributes will be ignored. Check the
|
||||
* other constructors if you want to specify different behaviour.
|
||||
*
|
||||
* @param preserveOrder If {@code true} the member order of JSON objects
|
||||
* in parameters and results will be preserved.
|
||||
*/
|
||||
public JSONRPC2Parser(final boolean preserveOrder) {
|
||||
|
||||
this(preserveOrder, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 message parser.
|
||||
*
|
||||
* <p>Non-standard message attributes will be ignored. Check the other
|
||||
* constructors if you want to specify different behaviour.
|
||||
*
|
||||
* @param preserveOrder If {@code true} the member order of JSON objects
|
||||
* in parameters and results will be preserved.
|
||||
* @param ignoreVersion If {@code true} the {@code "jsonrpc":"2.0"}
|
||||
* version attribute in the JSON-RPC 2.0 message
|
||||
* will not be checked.
|
||||
*/
|
||||
public JSONRPC2Parser(final boolean preserveOrder,
|
||||
final boolean ignoreVersion) {
|
||||
|
||||
this(preserveOrder, ignoreVersion, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 message parser.
|
||||
*
|
||||
* <p>This constructor allows full specification of the available
|
||||
* JSON-RPC message parsing properties.
|
||||
*
|
||||
* @param preserveOrder If {@code true} the member order of JSON
|
||||
* objects in parameters and results will
|
||||
* be preserved.
|
||||
* @param ignoreVersion If {@code true} the
|
||||
* {@code "jsonrpc":"2.0"} version
|
||||
* attribute in the JSON-RPC 2.0 message
|
||||
* will not be checked.
|
||||
* @param parseNonStdAttributes If {@code true} non-standard attributes
|
||||
* found in the JSON-RPC 2.0 messages will
|
||||
* be parsed too.
|
||||
*/
|
||||
public JSONRPC2Parser(final boolean preserveOrder,
|
||||
final boolean ignoreVersion,
|
||||
final boolean parseNonStdAttributes) {
|
||||
|
||||
// Numbers parsed as long/double, requires JSON Smart 1.0.9+
|
||||
parser = new JSONParser(JSONParser.MODE_JSON_SIMPLE);
|
||||
|
||||
this.preserveOrder = preserveOrder;
|
||||
this.ignoreVersion = ignoreVersion;
|
||||
this.parseNonStdAttributes = parseNonStdAttributes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON object string. Provides the initial parsing of
|
||||
* JSON-RPC 2.0 messages. The member order of JSON objects will be
|
||||
* preserved if {@link #preserveOrder} is set to {@code true}.
|
||||
*
|
||||
* @param jsonString The JSON string to parse. Must not be
|
||||
* {@code null}.
|
||||
*
|
||||
* @return The parsed JSON object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Map<String,Object> parseJSONObject(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
if (jsonString.trim().length()==0)
|
||||
throw new JSONRPC2ParseException("Invalid JSON: Empty string",
|
||||
JSONRPC2ParseException.JSON,
|
||||
jsonString);
|
||||
|
||||
Object json;
|
||||
|
||||
// Parse the JSON string
|
||||
try {
|
||||
if (preserveOrder)
|
||||
json = parser.parse(jsonString, ContainerFactory.FACTORY_ORDERED);
|
||||
|
||||
else
|
||||
json = parser.parse(jsonString);
|
||||
|
||||
} catch (ParseException e) {
|
||||
|
||||
// Terse message, do not include full parse exception message
|
||||
throw new JSONRPC2ParseException("Invalid JSON",
|
||||
JSONRPC2ParseException.JSON,
|
||||
jsonString);
|
||||
}
|
||||
|
||||
if (json instanceof List)
|
||||
throw new JSONRPC2ParseException("JSON-RPC 2.0 batch requests/notifications not supported", jsonString);
|
||||
|
||||
if (! (json instanceof Map))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message: Message must be a JSON object", jsonString);
|
||||
|
||||
return (Map<String,Object>)json;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensures the specified parameter is a {@code String} object set to
|
||||
* "2.0". This method is intended to check the "jsonrpc" attribute
|
||||
* during parsing of JSON-RPC messages.
|
||||
*
|
||||
* @param version The version parameter. Must not be {@code null}.
|
||||
* @param jsonString The original JSON string.
|
||||
*
|
||||
* @throws JSONRPC2ParseException If the parameter is not a string that
|
||||
* equals "2.0".
|
||||
*/
|
||||
private static void ensureVersion2(final Object version, final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
if (version == null)
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version string missing", jsonString);
|
||||
|
||||
else if (! (version instanceof String))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version not a JSON string", jsonString);
|
||||
|
||||
else if (! version.equals("2.0"))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0: Version must be \"2.0\"", jsonString);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provides common parsing of JSON-RPC 2.0 requests, notifications
|
||||
* and responses. Use this method if you don't know which type of
|
||||
* JSON-RPC message the input string represents.
|
||||
*
|
||||
* <p>If a particular message type is expected use the dedicated
|
||||
* {@link #parseJSONRPC2Request}, {@link #parseJSONRPC2Notification}
|
||||
* and {@link #parseJSONRPC2Response} methods. They are more efficient
|
||||
* and would provide you with more detailed parse error reporting.
|
||||
*
|
||||
* @param jsonString A JSON string representing a JSON-RPC 2.0 request,
|
||||
* notification or response, UTF-8 encoded. Must not
|
||||
* be {@code null}.
|
||||
*
|
||||
* @return An instance of {@link JSONRPC2Request},
|
||||
* {@link JSONRPC2Notification} or {@link JSONRPC2Response}.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if the parsing
|
||||
* failed.
|
||||
*/
|
||||
public JSONRPC2Message parseJSONRPC2Message(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
// Try each of the parsers until one succeeds (or all fail)
|
||||
|
||||
try {
|
||||
return parseJSONRPC2Request(jsonString);
|
||||
|
||||
} catch (JSONRPC2ParseException e) {
|
||||
|
||||
// throw on JSON error, ignore on protocol error
|
||||
if (e.getCauseType() == JSONRPC2ParseException.JSON)
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
return parseJSONRPC2Notification(jsonString);
|
||||
|
||||
} catch (JSONRPC2ParseException e) {
|
||||
|
||||
// throw on JSON error, ignore on protocol error
|
||||
if (e.getCauseType() == JSONRPC2ParseException.JSON)
|
||||
throw e;
|
||||
}
|
||||
|
||||
try {
|
||||
return parseJSONRPC2Response(jsonString);
|
||||
|
||||
} catch (JSONRPC2ParseException e) {
|
||||
|
||||
// throw on JSON error, ignore on protocol error
|
||||
if (e.getCauseType() == JSONRPC2ParseException.JSON)
|
||||
throw e;
|
||||
}
|
||||
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 message",
|
||||
JSONRPC2ParseException.PROTOCOL,
|
||||
jsonString);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 request string.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 request object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public JSONRPC2Request parseJSONRPC2Request(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
// Initial JSON object parsing
|
||||
Map<String,Object> jsonObject = parseJSONObject(jsonString);
|
||||
|
||||
|
||||
// Check for JSON-RPC version "2.0"
|
||||
Object version = jsonObject.remove("jsonrpc");
|
||||
|
||||
if (! ignoreVersion)
|
||||
ensureVersion2(version, jsonString);
|
||||
|
||||
|
||||
// Extract method name
|
||||
Object method = jsonObject.remove("method");
|
||||
|
||||
if (method == null)
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name missing", jsonString);
|
||||
|
||||
else if (! (method instanceof String))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name not a JSON string", jsonString);
|
||||
|
||||
else if (((String)method).length() == 0)
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method name is an empty string", jsonString);
|
||||
|
||||
|
||||
// Extract ID
|
||||
if (! jsonObject.containsKey("id"))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Missing identifier", jsonString);
|
||||
|
||||
Object id = jsonObject.remove("id");
|
||||
|
||||
if ( id != null &&
|
||||
!(id instanceof Number ) &&
|
||||
!(id instanceof Boolean) &&
|
||||
!(id instanceof String ) )
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Identifier not a JSON scalar", jsonString);
|
||||
|
||||
|
||||
// Extract params
|
||||
Object params = jsonObject.remove("params");
|
||||
|
||||
|
||||
JSONRPC2Request request;
|
||||
|
||||
if (params == null)
|
||||
request = new JSONRPC2Request((String)method, id);
|
||||
|
||||
else if (params instanceof List)
|
||||
request = new JSONRPC2Request((String)method, (List<Object>)params, id);
|
||||
|
||||
else if (params instanceof Map)
|
||||
request = new JSONRPC2Request((String)method, (Map<String,Object>)params, id);
|
||||
|
||||
else
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 request: Method parameters have unexpected JSON type", jsonString);
|
||||
|
||||
|
||||
// Extract remaining non-std params?
|
||||
if (parseNonStdAttributes) {
|
||||
|
||||
for (Map.Entry<String,Object> entry: jsonObject.entrySet()) {
|
||||
|
||||
request.appendNonStdAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 notification string.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 notification string, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 notification object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public JSONRPC2Notification parseJSONRPC2Notification(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
// Initial JSON object parsing
|
||||
Map<String,Object> jsonObject = parseJSONObject(jsonString);
|
||||
|
||||
|
||||
// Check for JSON-RPC version "2.0"
|
||||
Object version = jsonObject.remove("jsonrpc");
|
||||
|
||||
if (! ignoreVersion)
|
||||
ensureVersion2(version, jsonString);
|
||||
|
||||
|
||||
// Extract method name
|
||||
Object method = jsonObject.remove("method");
|
||||
|
||||
if (method == null)
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name missing", jsonString);
|
||||
|
||||
else if (! (method instanceof String))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name not a JSON string", jsonString);
|
||||
|
||||
else if (((String)method).length() == 0)
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method name is an empty string", jsonString);
|
||||
|
||||
|
||||
// Extract params
|
||||
Object params = jsonObject.get("params");
|
||||
|
||||
JSONRPC2Notification notification;
|
||||
|
||||
if (params == null)
|
||||
notification = new JSONRPC2Notification((String)method);
|
||||
|
||||
else if (params instanceof List)
|
||||
notification = new JSONRPC2Notification((String)method, (List<Object>)params);
|
||||
|
||||
else if (params instanceof Map)
|
||||
notification = new JSONRPC2Notification((String)method, (Map<String,Object>)params);
|
||||
else
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 notification: Method parameters have unexpected JSON type", jsonString);
|
||||
|
||||
// Extract remaining non-std params?
|
||||
if (parseNonStdAttributes) {
|
||||
|
||||
for (Map.Entry<String,Object> entry: jsonObject.entrySet()) {
|
||||
|
||||
notification.appendNonStdAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 response string.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 response object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public JSONRPC2Response parseJSONRPC2Response(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
// Initial JSON object parsing
|
||||
Map<String,Object> jsonObject = parseJSONObject(jsonString);
|
||||
|
||||
// Check for JSON-RPC version "2.0"
|
||||
Object version = jsonObject.remove("jsonrpc");
|
||||
|
||||
if (! ignoreVersion)
|
||||
ensureVersion2(version, jsonString);
|
||||
|
||||
|
||||
// Extract request ID
|
||||
Object id = jsonObject.remove("id");
|
||||
|
||||
if ( id != null &&
|
||||
! (id instanceof Boolean) &&
|
||||
! (id instanceof Number ) &&
|
||||
! (id instanceof String ) )
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Identifier not a JSON scalar", jsonString);
|
||||
|
||||
|
||||
// Extract result/error and create response object
|
||||
// Note: result and error are mutually exclusive
|
||||
|
||||
JSONRPC2Response response;
|
||||
|
||||
if (jsonObject.containsKey("result") && ! jsonObject.containsKey("error")) {
|
||||
|
||||
// Success
|
||||
Object res = jsonObject.remove("result");
|
||||
|
||||
response = new JSONRPC2Response(res, id);
|
||||
|
||||
}
|
||||
else if (! jsonObject.containsKey("result") && jsonObject.containsKey("error")) {
|
||||
|
||||
// Error JSON object
|
||||
Object errorJSON = jsonObject.remove("error");
|
||||
|
||||
if (errorJSON == null)
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Missing error object", jsonString);
|
||||
|
||||
|
||||
if (! (errorJSON instanceof Map))
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error object not a JSON object");
|
||||
|
||||
|
||||
Map<String,Object> error = (Map<String,Object>)errorJSON;
|
||||
|
||||
|
||||
int errorCode;
|
||||
|
||||
try {
|
||||
errorCode = ((Number)error.get("code")).intValue();
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error code missing or not an integer", jsonString);
|
||||
}
|
||||
|
||||
String errorMessage;
|
||||
|
||||
try {
|
||||
errorMessage = (String)error.get("message");
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Error message missing or not a string", jsonString);
|
||||
}
|
||||
|
||||
Object errorData = error.get("data");
|
||||
|
||||
response = new JSONRPC2Response(new JSONRPC2Error(errorCode, errorMessage, errorData), id);
|
||||
|
||||
}
|
||||
else if (jsonObject.containsKey("result") && jsonObject.containsKey("error")) {
|
||||
|
||||
// Invalid response
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: You cannot have result and error at the same time", jsonString);
|
||||
}
|
||||
else if (! jsonObject.containsKey("result") && ! jsonObject.containsKey("error")){
|
||||
|
||||
// Invalid response
|
||||
throw new JSONRPC2ParseException("Invalid JSON-RPC 2.0 response: Neither result nor error specified", jsonString);
|
||||
}
|
||||
else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
|
||||
// Extract remaining non-std params?
|
||||
if (parseNonStdAttributes) {
|
||||
|
||||
for (Map.Entry<String,Object> entry: jsonObject.entrySet()) {
|
||||
|
||||
response.appendNonStdAttribute(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Controls the preservation of JSON object member order in parsed
|
||||
* JSON-RPC 2.0 messages.
|
||||
*
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members, else {@code false}.
|
||||
*/
|
||||
public void preserveOrder(final boolean preserveOrder) {
|
||||
|
||||
this.preserveOrder = preserveOrder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the order of JSON object members in parsed
|
||||
* JSON-RPC 2.0 messages is preserved, else {@code false}.
|
||||
*
|
||||
* @return {@code true} if order is preserved, else {@code false}.
|
||||
*/
|
||||
public boolean preservesOrder() {
|
||||
|
||||
return preserveOrder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies whether to ignore the {@code "jsonrpc":"2.0"} version
|
||||
* attribute during parsing of JSON-RPC 2.0 messages.
|
||||
*
|
||||
* <p>You may with to disable strict 2.0 version checking if the parsed
|
||||
* JSON-RPC 2.0 messages don't include a version attribute or if you
|
||||
* wish to achieve limited compatibility with older JSON-RPC protocol
|
||||
* versions.
|
||||
*
|
||||
* @param ignore {@code true} to skip checks of the
|
||||
* {@code "jsonrpc":"2.0"} version attribute in parsed
|
||||
* JSON-RPC 2.0 messages, else {@code false}.
|
||||
*/
|
||||
public void ignoreVersion(final boolean ignore) {
|
||||
|
||||
ignoreVersion = ignore;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the {@code "jsonrpc":"2.0"} version
|
||||
* attribute in parsed JSON-RPC 2.0 messages is ignored, else
|
||||
* {@code false}.
|
||||
*
|
||||
* @return {@code true} if the {@code "jsonrpc":"2.0"} version
|
||||
* attribute in parsed JSON-RPC 2.0 messages is ignored, else
|
||||
* {@code false}.
|
||||
*/
|
||||
public boolean ignoresVersion() {
|
||||
|
||||
return ignoreVersion;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specifies whether to parse non-standard attributes found in JSON-RPC
|
||||
* 2.0 messages.
|
||||
*
|
||||
* @param enable {@code true} to parse non-standard attributes, else
|
||||
* {@code false}.
|
||||
*/
|
||||
public void parseNonStdAttributes(final boolean enable) {
|
||||
|
||||
parseNonStdAttributes = enable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@code true} if non-standard attributes in JSON-RPC 2.0
|
||||
* messages are parsed.
|
||||
*
|
||||
* @return {@code true} if non-standard attributes are parsed, else
|
||||
* {@code false}.
|
||||
*/
|
||||
public boolean parsesNonStdAttributes() {
|
||||
|
||||
return parseNonStdAttributes;
|
||||
}
|
||||
}
|
507
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Request.java
Normal file
507
src/java/com/thetransactioncompany/jsonrpc2/JSONRPC2Request.java
Normal file
@ -0,0 +1,507 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a JSON-RPC 2.0 request.
|
||||
*
|
||||
* <p>A request carries four pieces of data:
|
||||
* <ul>
|
||||
* <li>{@code method} The name of the remote method to call.
|
||||
* <li>{@code params} The required method parameters (if any), which can
|
||||
* be packed into a JSON array or object.
|
||||
* <li>{@code id} An identifier which is echoed back to the client with
|
||||
* the response.
|
||||
* <li>{@code jsonrpc} A string indicating the JSON-RPC protocol version
|
||||
* set to "2.0".
|
||||
* </ul>
|
||||
*
|
||||
* <p>Here is a sample JSON-RPC 2.0 request string:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "method" : "makePayment",
|
||||
* "params" : { "recipient" : "Penny Adams", "amount":175.05 },
|
||||
* "id" : "0001",
|
||||
* "jsonrpc" : "2.0"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>This class provides two methods to obtain a request object:
|
||||
* <ul>
|
||||
* <li>Pass a JSON-RPC 2.0 request string to the static
|
||||
* {@link #parse} method, or
|
||||
* <li>Invoke one of the constructors with the appropriate arguments.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Example 1: Parsing a request string:
|
||||
*
|
||||
* <pre>
|
||||
* String jsonString = "{\"method\":\"makePayment\"," +
|
||||
* "\"params\":{\"recipient\":\"Penny Adams\",\"amount\":175.05}," +
|
||||
* "\"id\":\"0001\","+
|
||||
* "\"jsonrpc\":\"2.0\"}";
|
||||
*
|
||||
* JSONRPC2Request req = null;
|
||||
*
|
||||
* try {
|
||||
* req = JSONRPC2Request.parse(jsonString);
|
||||
*
|
||||
* } catch (JSONRPC2ParseException e) {
|
||||
* // handle exception
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Example 2: Recreating the above request:
|
||||
*
|
||||
* <pre>
|
||||
* String method = "makePayment";
|
||||
* Map<String,Object> params = new HashMap<String,Object>();
|
||||
* params.put("recipient", "Penny Adams");
|
||||
* params.put("amount", 175.05);
|
||||
* String id = "0001";
|
||||
*
|
||||
* JSONRPC2Request req = new JSONRPC2Request(method, params, id);
|
||||
*
|
||||
* System.out.println(req);
|
||||
* </pre>
|
||||
*
|
||||
* <p id="map">The mapping between JSON and Java entities (as defined by the
|
||||
* underlying JSON Smart library):
|
||||
*
|
||||
* <pre>
|
||||
* true|false <---> java.lang.Boolean
|
||||
* number <---> java.lang.Number
|
||||
* string <---> java.lang.String
|
||||
* array <---> java.util.List
|
||||
* object <---> java.util.Map
|
||||
* null <---> null
|
||||
* </pre>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class JSONRPC2Request extends JSONRPC2Message {
|
||||
|
||||
|
||||
/**
|
||||
* The method name.
|
||||
*/
|
||||
private String method;
|
||||
|
||||
|
||||
/**
|
||||
* The positional parameters, {@code null} if none.
|
||||
*/
|
||||
private List<Object> positionalParams;
|
||||
|
||||
|
||||
/**
|
||||
* The named parameters, {@code null} if none.
|
||||
*/
|
||||
private Map<String,Object> namedParams;
|
||||
|
||||
|
||||
/**
|
||||
* The request identifier.
|
||||
*/
|
||||
private Object id;
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 request string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 request object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Request parse(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, false, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 request string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members in parameters.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 request object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Request parse(final String jsonString,
|
||||
final boolean preserveOrder)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, preserveOrder, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 request string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 request string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members in parameters.
|
||||
* @param ignoreVersion {@code true} to skip a check of the
|
||||
* {@code "jsonrpc":"2.0"} version attribute in the
|
||||
* JSON-RPC 2.0 message.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 request object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Request parse(final String jsonString,
|
||||
final boolean preserveOrder,
|
||||
final boolean ignoreVersion)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, preserveOrder, ignoreVersion, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 request string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 request string, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of
|
||||
* JSON object members in parameters.
|
||||
* @param ignoreVersion {@code true} to skip a check of the
|
||||
* {@code "jsonrpc":"2.0"} version
|
||||
* attribute in the JSON-RPC 2.0 message.
|
||||
* @param parseNonStdAttributes {@code true} to parse non-standard
|
||||
* attributes found in the JSON-RPC 2.0
|
||||
* message.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 request object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Request parse(final String jsonString,
|
||||
final boolean preserveOrder,
|
||||
final boolean ignoreVersion,
|
||||
final boolean parseNonStdAttributes)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder,
|
||||
ignoreVersion,
|
||||
parseNonStdAttributes);
|
||||
|
||||
return parser.parseJSONRPC2Request(jsonString);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new JSON-RPC 2.0 request with no parameters.
|
||||
*
|
||||
* @param method The name of the requested method. Must not be
|
||||
* {@code null}.
|
||||
* @param id The request identifier echoed back to the caller.
|
||||
* The value must <a href="#map">map</a> to a JSON
|
||||
* scalar ({@code null} and fractions, however, should
|
||||
* be avoided).
|
||||
*/
|
||||
public JSONRPC2Request(final String method, final Object id) {
|
||||
|
||||
setMethod(method);
|
||||
setID(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new JSON-RPC 2.0 request with positional (JSON array)
|
||||
* parameters.
|
||||
*
|
||||
* @param method The name of the requested method. Must not
|
||||
* be {@code null}.
|
||||
* @param positionalParams The positional (JSON array) parameters,
|
||||
* {@code null} if none.
|
||||
* @param id The request identifier echoed back to the
|
||||
* caller. The value must <a href="#map">map</a>
|
||||
* to a JSON scalar ({@code null} and
|
||||
* fractions, however, should be avoided).
|
||||
*/
|
||||
public JSONRPC2Request(final String method,
|
||||
final List<Object> positionalParams,
|
||||
final Object id) {
|
||||
|
||||
setMethod(method);
|
||||
setPositionalParams(positionalParams);
|
||||
setID(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a new JSON-RPC 2.0 request with named (JSON object)
|
||||
* parameters.
|
||||
*
|
||||
* @param method The name of the requested method.
|
||||
* @param namedParams The named (JSON object) parameters, {@code null}
|
||||
* if none.
|
||||
* @param id The request identifier echoed back to the caller.
|
||||
* The value must <a href="#map">map</a> to a JSON
|
||||
* scalar ({@code null} and fractions, however,
|
||||
* should be avoided).
|
||||
*/
|
||||
public JSONRPC2Request(final String method,
|
||||
final Map <String,Object> namedParams,
|
||||
final Object id) {
|
||||
|
||||
setMethod(method);
|
||||
setNamedParams(namedParams);
|
||||
setID(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the name of the requested method.
|
||||
*
|
||||
* @return The method name.
|
||||
*/
|
||||
public String getMethod() {
|
||||
|
||||
return method;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the name of the requested method.
|
||||
*
|
||||
* @param method The method name. Must not be {@code null}.
|
||||
*/
|
||||
public void setMethod(final String method) {
|
||||
|
||||
// The method name is mandatory
|
||||
if (method == null)
|
||||
throw new IllegalArgumentException("The method name must not be null");
|
||||
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the parameters type ({@link JSONRPC2ParamsType#ARRAY positional},
|
||||
* {@link JSONRPC2ParamsType#OBJECT named} or
|
||||
* {@link JSONRPC2ParamsType#NO_PARAMS none}).
|
||||
*
|
||||
* @return The parameters type.
|
||||
*/
|
||||
public JSONRPC2ParamsType getParamsType() {
|
||||
|
||||
if (positionalParams == null && namedParams == null)
|
||||
return JSONRPC2ParamsType.NO_PARAMS;
|
||||
|
||||
if (positionalParams != null)
|
||||
return JSONRPC2ParamsType.ARRAY;
|
||||
|
||||
if (namedParams != null)
|
||||
return JSONRPC2ParamsType.OBJECT;
|
||||
|
||||
else
|
||||
return JSONRPC2ParamsType.NO_PARAMS;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the request parameters.
|
||||
*
|
||||
* <p>This method was deprecated in version 1.30. Use
|
||||
* {@link #getPositionalParams} or {@link #getNamedParams} instead.
|
||||
*
|
||||
* @return The parameters as {@code List<Object>} for positional
|
||||
* (JSON array), {@code Map<String,Object>} for named
|
||||
* (JSON object), or {@code null} if none.
|
||||
*/
|
||||
@Deprecated
|
||||
public Object getParams() {
|
||||
|
||||
switch (getParamsType()) {
|
||||
|
||||
case ARRAY:
|
||||
return positionalParams;
|
||||
|
||||
case OBJECT:
|
||||
return namedParams;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the positional (JSON array) parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @return The positional (JSON array) parameters, {@code null} if none
|
||||
* or named.
|
||||
*/
|
||||
public List<Object> getPositionalParams() {
|
||||
|
||||
return positionalParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the named parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @return The named (JSON object) parameters, {@code null} if none or
|
||||
* positional.
|
||||
*/
|
||||
public Map<String,Object> getNamedParams() {
|
||||
|
||||
return namedParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the request parameters.
|
||||
*
|
||||
* <p>This method was deprecated in version 1.30. Use
|
||||
* {@link #setPositionalParams} or {@link #setNamedParams} instead.
|
||||
*
|
||||
* @param params The parameters. For positional (JSON array) pass a
|
||||
* {@code List<Object>}. For named (JSON object)
|
||||
* pass a {@code Map<String,Object>}. If there are
|
||||
* no parameters pass {@code null}.
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setParams(final Object params) {
|
||||
|
||||
if (params == null) {
|
||||
positionalParams = null;
|
||||
namedParams = null;
|
||||
} else if (params instanceof List) {
|
||||
positionalParams = (List<Object>) params;
|
||||
} else if (params instanceof Map) {
|
||||
namedParams = (Map<String, Object>) params;
|
||||
} else {
|
||||
throw new IllegalArgumentException("The request parameters must be of type List, Map or null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the positional (JSON array) request parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @param positionalParams The positional (JSON array) request
|
||||
* parameters, {@code null} if none.
|
||||
*/
|
||||
public void setPositionalParams(final List<Object> positionalParams) {
|
||||
|
||||
if (positionalParams == null)
|
||||
return;
|
||||
|
||||
this.positionalParams = positionalParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the named (JSON object) request parameters.
|
||||
*
|
||||
* @since 1.30
|
||||
*
|
||||
* @param namedParams The named (JSON object) request parameters,
|
||||
* {@code null} if none.
|
||||
*/
|
||||
public void setNamedParams(final Map<String,Object> namedParams) {
|
||||
|
||||
if (namedParams == null)
|
||||
return;
|
||||
|
||||
this.namedParams = namedParams;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the request identifier.
|
||||
*
|
||||
* @return The request identifier ({@code Number}, {@code Boolean},
|
||||
* {@code String}) or {@code null}.
|
||||
*/
|
||||
public Object getID() {
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the request identifier (ID).
|
||||
*
|
||||
* @param id The request identifier echoed back to the caller.
|
||||
* The value must <a href="#map">map</a> to a JSON
|
||||
* scalar ({@code null} and fractions, however, should
|
||||
* be avoided).
|
||||
*/
|
||||
public void setID(final Object id) {
|
||||
|
||||
if (id == null ||
|
||||
id instanceof Boolean ||
|
||||
id instanceof Number ||
|
||||
id instanceof String
|
||||
) {
|
||||
this.id = id;
|
||||
} else {
|
||||
this.id = id.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JSONObject toJSONObject() {
|
||||
|
||||
JSONObject req = new JSONObject();
|
||||
|
||||
req.put("method", method);
|
||||
|
||||
// The params can be omitted if none
|
||||
switch (getParamsType()) {
|
||||
|
||||
case ARRAY:
|
||||
req.put("params", positionalParams);
|
||||
break;
|
||||
|
||||
case OBJECT:
|
||||
req.put("params", namedParams);
|
||||
break;
|
||||
}
|
||||
|
||||
req.put("id", id);
|
||||
|
||||
req.put("jsonrpc", "2.0");
|
||||
|
||||
Map <String,Object> nonStdAttributes = getNonStdAttributes();
|
||||
|
||||
if (nonStdAttributes != null) {
|
||||
|
||||
for (final Map.Entry<String,Object> attr: nonStdAttributes.entrySet())
|
||||
req.put(attr.getKey(), attr.getValue());
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
}
|
@ -0,0 +1,414 @@
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import net.minidev.json.JSONObject;
|
||||
|
||||
|
||||
/**
|
||||
* Represents a JSON-RPC 2.0 response.
|
||||
*
|
||||
* <p>A response is returned to the caller after a JSON-RPC 2.0 request has
|
||||
* been processed (notifications, however, don't produce a response). The
|
||||
* response can take two different forms depending on the outcome:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The request was successful. The corresponding response returns
|
||||
* a JSON object with the following information:
|
||||
* <ul>
|
||||
* <li>{@code result} The result, which can be of any JSON type
|
||||
* - a number, a boolean value, a string, an array, an object
|
||||
* or null.
|
||||
* <li>{@code id} The request identifier which is echoed back back
|
||||
* to the caller.
|
||||
* <li>{@code jsonrpc} A string indicating the JSON-RPC protocol
|
||||
* version set to "2.0".
|
||||
* </ul>
|
||||
* <li>The request failed. The returned JSON object contains:
|
||||
* <ul>
|
||||
* <li>{@code error} An object with:
|
||||
* <ul>
|
||||
* <li>{@code code} An integer indicating the error type.
|
||||
* <li>{@code message} A brief error messsage.
|
||||
* <li>{@code data} Optional error data.
|
||||
* </ul>
|
||||
* <li>{@code id} The request identifier. If it couldn't be
|
||||
* determined, e.g. due to a request parse error, the ID is
|
||||
* set to {@code null}.
|
||||
* <li>{@code jsonrpc} A string indicating the JSON-RPC protocol
|
||||
* version set to "2.0".
|
||||
* </ul>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Here is an example JSON-RPC 2.0 response string where the request
|
||||
* has succeeded:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "result" : true,
|
||||
* "id" : "req-002",
|
||||
* "jsonrpc" : "2.0"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* <p>And here is an example JSON-RPC 2.0 response string indicating a failure:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "error" : { "code" : -32601, "message" : "Method not found" },
|
||||
* "id" : "req-003",
|
||||
* "jsonrpc" : "2.0"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>A response object is obtained either by passing a valid JSON-RPC 2.0
|
||||
* response string to the static {@link #parse} method or by invoking the
|
||||
* appropriate constructor.
|
||||
*
|
||||
* <p>Here is how parsing is done:
|
||||
*
|
||||
* <pre>
|
||||
* String jsonString = "{\"result\":true,\"id\":\"req-002\",\"jsonrpc\":\"2.0\"}";
|
||||
*
|
||||
* JSONRPC2Response response = null;
|
||||
*
|
||||
* try {
|
||||
* response = JSONRPC2Response.parse(jsonString);
|
||||
*
|
||||
* } catch (JSONRPC2Exception e) {
|
||||
* // handle exception
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>And here is how you can replicate the above example response strings:
|
||||
*
|
||||
* <pre>
|
||||
* // success example
|
||||
* JSONRPC2Response resp = new JSONRPC2Response(true, "req-002");
|
||||
* System.out.println(resp);
|
||||
*
|
||||
* // failure example
|
||||
* JSONRPC2Error err = new JSONRPC2Error(-32601, "Method not found");
|
||||
* resp = new JSONRPC2Response(err, "req-003");
|
||||
* System.out.println(resp);
|
||||
*
|
||||
* </pre>
|
||||
*
|
||||
* <p id="map">The mapping between JSON and Java entities (as defined by the
|
||||
* underlying JSON Smart library):
|
||||
*
|
||||
* <pre>
|
||||
* true|false <---> java.lang.Boolean
|
||||
* number <---> java.lang.Number
|
||||
* string <---> java.lang.String
|
||||
* array <---> java.util.List
|
||||
* object <---> java.util.Map
|
||||
* null <---> null
|
||||
* </pre>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class JSONRPC2Response extends JSONRPC2Message {
|
||||
|
||||
|
||||
/**
|
||||
* The result.
|
||||
*/
|
||||
private Object result = null;
|
||||
|
||||
|
||||
/**
|
||||
* The error object.
|
||||
*/
|
||||
private JSONRPC2Error error = null;
|
||||
|
||||
|
||||
/**
|
||||
* The echoed request identifier.
|
||||
*/
|
||||
private Object id = null;
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 response string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 response object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Response parse(final String jsonString)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, false, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 response string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members in results.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 response object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Response parse(final String jsonString,
|
||||
final boolean preserveOrder)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, preserveOrder, false, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 response string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 response string, UTF-8 encoded.
|
||||
* Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of JSON
|
||||
* object members in results.
|
||||
* @param ignoreVersion {@code true} to skip a check of the
|
||||
* {@code "jsonrpc":"2.0"} version attribute in the
|
||||
* JSON-RPC 2.0 message.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 response object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if the parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Response parse(final String jsonString,
|
||||
final boolean preserveOrder,
|
||||
final boolean ignoreVersion)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
return parse(jsonString, preserveOrder, ignoreVersion, false);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses a JSON-RPC 2.0 response string. This method is thread-safe.
|
||||
*
|
||||
* @param jsonString The JSON-RPC 2.0 response string, UTF-8
|
||||
* encoded. Must not be {@code null}.
|
||||
* @param preserveOrder {@code true} to preserve the order of
|
||||
* JSON object members in results.
|
||||
* @param ignoreVersion {@code true} to skip a check of the
|
||||
* {@code "jsonrpc":"2.0"} version
|
||||
* attribute in the JSON-RPC 2.0 message.
|
||||
* @param parseNonStdAttributes {@code true} to parse non-standard
|
||||
* attributes found in the JSON-RPC 2.0
|
||||
* message.
|
||||
*
|
||||
* @return The corresponding JSON-RPC 2.0 response object.
|
||||
*
|
||||
* @throws JSONRPC2ParseException With detailed message if the parsing
|
||||
* failed.
|
||||
*/
|
||||
public static JSONRPC2Response parse(final String jsonString,
|
||||
final boolean preserveOrder,
|
||||
final boolean ignoreVersion,
|
||||
final boolean parseNonStdAttributes)
|
||||
throws JSONRPC2ParseException {
|
||||
|
||||
JSONRPC2Parser parser = new JSONRPC2Parser(preserveOrder, ignoreVersion, parseNonStdAttributes);
|
||||
|
||||
return parser.parseJSONRPC2Response(jsonString);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 response to a successful request.
|
||||
*
|
||||
* @param result The result. The value can <a href="#map">map</a>
|
||||
* to any JSON type. May be {@code null}.
|
||||
* @param id The request identifier echoed back to the caller. May
|
||||
* be {@code null} though not recommended.
|
||||
*/
|
||||
public JSONRPC2Response(final Object result, final Object id) {
|
||||
|
||||
setResult(result);
|
||||
setID(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 response to a successful request which
|
||||
* result is {@code null}.
|
||||
*
|
||||
* @param id The request identifier echoed back to the caller. May be
|
||||
* {@code null} though not recommended.
|
||||
*/
|
||||
public JSONRPC2Response(final Object id) {
|
||||
|
||||
setResult(null);
|
||||
setID(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 response to a failed request.
|
||||
*
|
||||
* @param error A JSON-RPC 2.0 error instance indicating the
|
||||
* cause of the failure. Must not be {@code null}.
|
||||
* @param id The request identifier echoed back to the caller.
|
||||
* Pass a {@code null} if the request identifier couldn't
|
||||
* be determined (e.g. due to a parse error).
|
||||
*/
|
||||
public JSONRPC2Response(final JSONRPC2Error error, final Object id) {
|
||||
|
||||
setError(error);
|
||||
setID(id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates a successful JSON-RPC 2.0 request and sets the result.
|
||||
* Note that if the response was previously indicating failure this
|
||||
* will turn it into a response indicating success. Any previously set
|
||||
* error data will be invalidated.
|
||||
*
|
||||
* @param result The result. The value can <a href="#map">map</a> to
|
||||
* any JSON type. May be {@code null}.
|
||||
*/
|
||||
public void setResult(final Object result) {
|
||||
|
||||
// result and error are mutually exclusive
|
||||
this.result = result;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the result of the request. The returned value has meaning
|
||||
* only if the request was successful. Use the
|
||||
* {@link #getError getError} method to check this.
|
||||
*
|
||||
* @return The result.
|
||||
*/
|
||||
public Object getResult() {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates a failed JSON-RPC 2.0 request and sets the error details.
|
||||
* Note that if the response was previously indicating success this
|
||||
* will turn it into a response indicating failure. Any previously set
|
||||
* result data will be invalidated.
|
||||
*
|
||||
* @param error A JSON-RPC 2.0 error instance indicating the cause of
|
||||
* the failure. Must not be {@code null}.
|
||||
*/
|
||||
public void setError(final JSONRPC2Error error) {
|
||||
|
||||
if (error == null)
|
||||
throw new IllegalArgumentException("The error object cannot be null");
|
||||
|
||||
// result and error are mutually exclusive
|
||||
this.error = error;
|
||||
this.result = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the error object indicating the cause of the request failure.
|
||||
* If a {@code null} is returned, the request succeeded and there was
|
||||
* no error.
|
||||
*
|
||||
* @return A JSON-RPC 2.0 error object, {@code null} if the response
|
||||
* indicates success.
|
||||
*/
|
||||
public JSONRPC2Error getError() {
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A convinience method to check if the response indicates success or
|
||||
* failure of the request. Alternatively, you can use the
|
||||
* {@code #getError} method for this purpose.
|
||||
*
|
||||
* @return {@code true} if the request succeeded, {@code false} if
|
||||
* there was an error.
|
||||
*/
|
||||
public boolean indicatesSuccess() {
|
||||
|
||||
return error == null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the request identifier echoed back to the caller.
|
||||
*
|
||||
* @param id The value must <a href="#map">map</a> to a JSON scalar.
|
||||
* Pass a {@code null} if the request identifier couldn't
|
||||
* be determined (e.g. due to a parse error).
|
||||
*/
|
||||
public void setID(final Object id) {
|
||||
|
||||
if (id == null ||
|
||||
id instanceof Boolean ||
|
||||
id instanceof Number ||
|
||||
id instanceof String
|
||||
) {
|
||||
this.id = id;
|
||||
} else {
|
||||
this.id = id.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the request identifier that is echoed back to the caller.
|
||||
*
|
||||
* @return The request identifier. If there was an error during the
|
||||
* the request retrieval (e.g. parse error) and the identifier
|
||||
* couldn't be determined, the value will be {@code null}.
|
||||
*/
|
||||
public Object getID() {
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JSONObject toJSONObject() {
|
||||
|
||||
JSONObject out = new JSONObject();
|
||||
|
||||
// Result and error are mutually exclusive
|
||||
if (error != null) {
|
||||
out.put("error", error.toJSONObject());
|
||||
}
|
||||
else {
|
||||
out.put("result", result);
|
||||
}
|
||||
|
||||
out.put("id", id);
|
||||
|
||||
out.put("jsonrpc", "2.0");
|
||||
|
||||
|
||||
Map <String,Object> nonStdAttributes = getNonStdAttributes();
|
||||
|
||||
if (nonStdAttributes != null) {
|
||||
|
||||
for (final Map.Entry<String,Object> attr: nonStdAttributes.entrySet())
|
||||
out.put(attr.getKey(), attr.getValue());
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Classes to represent, parse and serialise JSON-RPC 2.0 requests,
|
||||
* notifications and responses.
|
||||
*
|
||||
* <p>JSON-RPC is a protocol for
|
||||
* <a href="http://en.wikipedia.org/wiki/Remote_procedure_call">remote
|
||||
* procedure calls</a> (RPC) using <a href="http://www.json.org" >JSON</a>
|
||||
* - encoded requests and responses. It can be easily relayed over HTTP
|
||||
* and is of JavaScript origin, making it ideal for use in dynamic web
|
||||
* applications in the spirit of Ajax and Web 2.0.
|
||||
*
|
||||
* <p>This package implements <b>version 2.0</b> of the protocol, with the
|
||||
* exception of <i>batching / multicall</i>. This feature is deliberately left
|
||||
* out as it tends to confuse users (judging by posts in the JSON-RPC forum).
|
||||
*
|
||||
* <p>See the <a href="http://www.jsonrpc.org/specification"></a>JSON-RPC 2.0
|
||||
* specification</a> for more information or write to the
|
||||
* <a href="https://groups.google.com/forum/#!forum/json-rpc">user group</a> if
|
||||
* you have questions.
|
||||
*
|
||||
* <p><b>Package dependencies:</b> The classes in this package rely on the
|
||||
* {@code net.minidev.json} and {@code net.minidev.json.parser} packages
|
||||
* (version 1.1.1 and compabile) for JSON encoding and decoding. You can obtain
|
||||
* them from the <a href="http://code.google.com/p/json-smart/">JSON-Smart</a>
|
||||
* website.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
package com.thetransactioncompany.jsonrpc2;
|
||||
|
||||
|
||||
|
@ -0,0 +1,263 @@
|
||||
package com.thetransactioncompany.jsonrpc2.server;
|
||||
|
||||
|
||||
import java.util.Hashtable;
|
||||
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Error;
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification;
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Request;
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Response;
|
||||
|
||||
|
||||
/**
|
||||
* Dispatcher for JSON-RPC 2.0 requests and notifications. This class is
|
||||
* tread-safe.
|
||||
*
|
||||
* <p>Use the {@code register()} methods to add a request or notification
|
||||
* handler for an RPC method.
|
||||
*
|
||||
* <p>Use the {@code process()} methods to have an incoming request or
|
||||
* notification processed by the matching handler.
|
||||
*
|
||||
* <p>The {@code reportProcTime()} method enables reporting of request
|
||||
* processing time (in microseconds) by appending a non-standard "xProcTime"
|
||||
* attribute to the resulting JSON-RPC 2.0 response message.
|
||||
*
|
||||
* <p>Example:
|
||||
*
|
||||
* <pre>
|
||||
* {
|
||||
* "result" : "xyz",
|
||||
* "id" : 1,
|
||||
* "jsonrpc" : "2.0",
|
||||
* "xProcTime" : "189 us"
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>Note: The dispatch(...) methods were deprecated in version 1.7. Use
|
||||
* process(...) instead.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class Dispatcher implements RequestHandler, NotificationHandler {
|
||||
|
||||
|
||||
/**
|
||||
* Hashtable of request name / handler pairs.
|
||||
*/
|
||||
private final Hashtable<String,RequestHandler> requestHandlers;
|
||||
|
||||
|
||||
/**
|
||||
* Hashtable of notification name / handler pairs.
|
||||
*/
|
||||
private final Hashtable<String,NotificationHandler> notificationHandlers;
|
||||
|
||||
|
||||
/**
|
||||
* Controls reporting of request processing time by appending a
|
||||
* non-standard "xProcTime" attribute to the JSON-RPC 2.0 response.
|
||||
*/
|
||||
private boolean reportProcTime = false;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new dispatcher with no registered handlers.
|
||||
*/
|
||||
public Dispatcher() {
|
||||
|
||||
requestHandlers = new Hashtable<String,RequestHandler>();
|
||||
notificationHandlers = new Hashtable<String,NotificationHandler>();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers a new JSON-RPC 2.0 request handler.
|
||||
*
|
||||
* @param handler The request handler to register. Must not be
|
||||
* {@code null}.
|
||||
*
|
||||
* @throws IllegalArgumentException On attempting to register a handler
|
||||
* that duplicates an existing request
|
||||
* name.
|
||||
*/
|
||||
public void register(final RequestHandler handler) {
|
||||
|
||||
for (String name: handler.handledRequests()) {
|
||||
|
||||
if (requestHandlers.containsKey(name))
|
||||
throw new IllegalArgumentException("Cannot register a duplicate JSON-RPC 2.0 handler for request " + name);
|
||||
|
||||
requestHandlers.put(name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Registers a new JSON-RPC 2.0 notification handler.
|
||||
*
|
||||
* @param handler The notification handler to register. Must not be
|
||||
* {@code null}.
|
||||
*
|
||||
* @throws IllegalArgumentException On attempting to register a handler
|
||||
* that duplicates an existing
|
||||
* notification name.
|
||||
*/
|
||||
public void register(final NotificationHandler handler) {
|
||||
|
||||
for (String name: handler.handledNotifications()) {
|
||||
|
||||
if (notificationHandlers.containsKey(name))
|
||||
throw new IllegalArgumentException("Cannot register a duplicate JSON-RPC 2.0 handler for notification " + name);
|
||||
|
||||
notificationHandlers.put(name, handler);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String[] handledRequests() {
|
||||
|
||||
java.util.Set<String> var = requestHandlers.keySet();
|
||||
return var.toArray(new String[var.size()]);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String[] handledNotifications() {
|
||||
|
||||
java.util.Set<String> var = notificationHandlers.keySet();
|
||||
return var.toArray(new String[var.size()]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the handler for the specified JSON-RPC 2.0 request name.
|
||||
*
|
||||
* @param requestName The request name to lookup.
|
||||
*
|
||||
* @return The corresponding request handler or {@code null} if none
|
||||
* was found.
|
||||
*/
|
||||
public RequestHandler getRequestHandler(final String requestName) {
|
||||
|
||||
return requestHandlers.get(requestName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the handler for the specified JSON-RPC 2.0 notification name.
|
||||
*
|
||||
* @param notificationName The notification name to lookup.
|
||||
*
|
||||
* @return The corresponding notification handler or {@code null} if
|
||||
* none was found.
|
||||
*/
|
||||
public NotificationHandler getNotificationHandler(final String notificationName) {
|
||||
|
||||
return notificationHandlers.get(notificationName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public JSONRPC2Response dispatch(final JSONRPC2Request request, final MessageContext requestCtx) {
|
||||
|
||||
return process(request, requestCtx);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public JSONRPC2Response process(final JSONRPC2Request request, final MessageContext requestCtx) {
|
||||
|
||||
long startNanosec = 0;
|
||||
|
||||
// Measure request processing time?
|
||||
if (reportProcTime)
|
||||
startNanosec = System.nanoTime();
|
||||
|
||||
|
||||
final String method = request.getMethod();
|
||||
|
||||
RequestHandler handler = getRequestHandler(method);
|
||||
|
||||
if (handler == null) {
|
||||
|
||||
// We didn't find a handler for the requested RPC
|
||||
|
||||
Object id = request.getID();
|
||||
|
||||
return new JSONRPC2Response(JSONRPC2Error.METHOD_NOT_FOUND, id);
|
||||
}
|
||||
|
||||
// Process the request
|
||||
|
||||
JSONRPC2Response response = handler.process(request, requestCtx);
|
||||
|
||||
if (reportProcTime) {
|
||||
|
||||
final long procTimeNanosec = System.nanoTime() - startNanosec;
|
||||
|
||||
response.appendNonStdAttribute("xProcTime", procTimeNanosec / 1000 + " us");
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public void dispatch(final JSONRPC2Notification notification, final MessageContext notificationCtx) {
|
||||
|
||||
process(notification, notificationCtx);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void process(final JSONRPC2Notification notification, final MessageContext notificationCtx) {
|
||||
|
||||
final String method = notification.getMethod();
|
||||
|
||||
NotificationHandler handler = getNotificationHandler(method);
|
||||
|
||||
if (handler == null) {
|
||||
|
||||
// We didn't find a handler for the requested RPC
|
||||
return;
|
||||
}
|
||||
|
||||
// Process the notification
|
||||
|
||||
handler.process(notification, notificationCtx);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Controls reporting of request processing time by appending a
|
||||
* non-standard "xProcTime" attribute to the JSON-RPC 2.0 response.
|
||||
* Reporting is disabled by default.
|
||||
*
|
||||
* @param enable {@code true} to enable proccessing time reporting,
|
||||
* {@code false} to disable it.
|
||||
*/
|
||||
public void reportProcTime(final boolean enable) {
|
||||
|
||||
reportProcTime = enable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns {@code true} if reporting of request processing time is
|
||||
* enabled. See the {@link #reportProcTime} description for more
|
||||
* information.
|
||||
*
|
||||
* @return {@code true} if reporting of request processing time is
|
||||
* enabled, else {@code false}.
|
||||
*/
|
||||
public boolean reportsProcTime() {
|
||||
|
||||
return reportProcTime;
|
||||
}
|
||||
}
|
@ -0,0 +1,428 @@
|
||||
package com.thetransactioncompany.jsonrpc2.server;
|
||||
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.URLConnection;
|
||||
import java.security.Principal;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
|
||||
/**
|
||||
* Context information about JSON-RPC 2.0 request and notification messages.
|
||||
* This class is immutable.
|
||||
*
|
||||
* <ul>
|
||||
* <li>The client's host name.
|
||||
* <li>The client's IP address.
|
||||
* <li>Whether the request / notification was transmitted securely (e.g.
|
||||
* via HTTPS).
|
||||
* <li>The client principal(s) (user), if authenticated.
|
||||
* </ul>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public class MessageContext {
|
||||
|
||||
|
||||
/**
|
||||
* The client hostname, {@code null} if none was specified.
|
||||
*/
|
||||
private String clientHostName = null;
|
||||
|
||||
|
||||
/**
|
||||
* The client IP address, {@code null} if none was specified.
|
||||
*/
|
||||
private String clientInetAddress = null;
|
||||
|
||||
|
||||
/**
|
||||
* Indicates whether the request was received over a secure channel
|
||||
* (typically HTTPS).
|
||||
*/
|
||||
private boolean secure = false;
|
||||
|
||||
|
||||
/**
|
||||
* The authenticated client principals, {@code null} if none were
|
||||
* specified.
|
||||
*/
|
||||
private Principal[] principals = null;
|
||||
|
||||
|
||||
/**
|
||||
* Minimal implementation of the {@link java.security.Principal}
|
||||
* interface.
|
||||
*/
|
||||
public class BasicPrincipal implements Principal {
|
||||
|
||||
/**
|
||||
* The principal name.
|
||||
*/
|
||||
private String name;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new principal.
|
||||
*
|
||||
* @param name The principal name, must not be {@code null} or
|
||||
* empty string.
|
||||
*
|
||||
* @throws IllegalArgumentException On a {@code null} or empty
|
||||
* principal name.
|
||||
*/
|
||||
public BasicPrincipal(final String name) {
|
||||
|
||||
if (name == null || name.trim().isEmpty())
|
||||
throw new IllegalArgumentException("The principal name must be defined");
|
||||
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks for equality.
|
||||
*
|
||||
* @param another The object to compare to.
|
||||
*/
|
||||
public boolean equals(final Object another) {
|
||||
|
||||
return another != null &&
|
||||
another instanceof Principal &&
|
||||
((Principal)another).getName().equals(this.getName());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a hash code for this principal.
|
||||
*
|
||||
* @return The hash code.
|
||||
*/
|
||||
public int hashCode() {
|
||||
|
||||
return getName().hashCode();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the principal name.
|
||||
*
|
||||
* @return The principal name.
|
||||
*/
|
||||
public String getName() {
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context.
|
||||
*
|
||||
* @param clientHostName The client host name, {@code null} if
|
||||
* unknown.
|
||||
* @param clientInetAddress The client IP address, {@code null} if
|
||||
* unknown.
|
||||
* @param secure Specifies a request received over HTTPS.
|
||||
* @param principalName Specifies the authenticated client principle
|
||||
* name, {@code null} if unknown. The name must
|
||||
* not be an empty or blank string.
|
||||
*/
|
||||
public MessageContext(final String clientHostName,
|
||||
final String clientInetAddress,
|
||||
final boolean secure,
|
||||
final String principalName) {
|
||||
|
||||
this.clientHostName = clientHostName;
|
||||
this.clientInetAddress = clientInetAddress;
|
||||
this.secure = secure;
|
||||
|
||||
if (principalName != null) {
|
||||
principals = new Principal[1];
|
||||
principals[0] = new BasicPrincipal(principalName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context.
|
||||
*
|
||||
* @param clientHostName The client host name, {@code null} if
|
||||
* unknown.
|
||||
* @param clientInetAddress The client IP address, {@code null} if
|
||||
* unknown.
|
||||
* @param secure Specifies a request received over HTTPS.
|
||||
* @param principalNames Specifies the authenticated client principle
|
||||
* names, {@code null} if unknown. The names
|
||||
* must not be an empty or blank string.
|
||||
*/
|
||||
public MessageContext(final String clientHostName,
|
||||
final String clientInetAddress,
|
||||
final boolean secure,
|
||||
final String[] principalNames) {
|
||||
|
||||
this.clientHostName = clientHostName;
|
||||
this.clientInetAddress = clientInetAddress;
|
||||
this.secure = secure;
|
||||
|
||||
if (principalNames != null) {
|
||||
principals = new Principal[principalNames.length];
|
||||
|
||||
for (int i=0; i < principals.length; i++)
|
||||
principals[0] = new BasicPrincipal(principalNames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context. No
|
||||
* authenticated client principal is specified.
|
||||
*
|
||||
* @param clientHostName The client host name, {@code null} if
|
||||
* unknown.
|
||||
* @param clientInetAddress The client IP address, {@code null} if
|
||||
* unknown.
|
||||
* @param secure Specifies a request received over HTTPS.
|
||||
*/
|
||||
public MessageContext(final String clientHostName,
|
||||
final String clientInetAddress,
|
||||
final boolean secure) {
|
||||
|
||||
this.clientHostName = clientHostName;
|
||||
this.clientInetAddress = clientInetAddress;
|
||||
this.secure = secure;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context. Indicates
|
||||
* an insecure transport (plain HTTP) and no authenticated client
|
||||
* principal.
|
||||
*
|
||||
* @param clientHostName The client host name, {@code null} if
|
||||
* unknown.
|
||||
* @param clientInetAddress The client IP address, {@code null} if
|
||||
* unknown.
|
||||
*/
|
||||
public MessageContext(final String clientHostName,
|
||||
final String clientInetAddress) {
|
||||
|
||||
this.clientHostName = clientHostName;
|
||||
this.clientInetAddress = clientInetAddress;
|
||||
this.secure = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context. Indicates
|
||||
* an insecure transport (plain HTTP) and no authenticated client
|
||||
* principal. Not client host name / IP is specified.
|
||||
*/
|
||||
public MessageContext() {
|
||||
|
||||
this.secure = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context from the
|
||||
* specified HTTP request.
|
||||
*
|
||||
* @param httpRequest The HTTP request.
|
||||
*/
|
||||
public MessageContext(final HttpServletRequest httpRequest) {
|
||||
|
||||
clientInetAddress = httpRequest.getRemoteAddr();
|
||||
|
||||
clientHostName = httpRequest.getRemoteHost();
|
||||
|
||||
if (clientHostName != null && clientHostName.equals(clientInetAddress))
|
||||
clientHostName = null; // not resolved actually
|
||||
|
||||
secure = httpRequest.isSecure();
|
||||
|
||||
X509Certificate[] certs = (X509Certificate[])httpRequest.getAttribute("javax.servlet.request.X509Certificate");
|
||||
|
||||
if (certs != null && certs.length > 0) {
|
||||
|
||||
principals = new Principal[certs.length];
|
||||
|
||||
for (int i=0; i < principals.length; i++)
|
||||
principals[i] = certs[i].getSubjectX500Principal();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new JSON-RPC 2.0 request / notification context from the
|
||||
* specified URL connection. Use this constructor in cases when the
|
||||
* HTTP server is the origin of the JSON-RPC 2.0 requests /
|
||||
* notifications. If the IP address of the HTTP server cannot be
|
||||
* resolved {@link #getClientInetAddress} will return {@code null}.
|
||||
*
|
||||
* @param connection The URL connection, must be established and not
|
||||
* {@code null}.
|
||||
*/
|
||||
public MessageContext(final URLConnection connection) {
|
||||
|
||||
clientHostName = connection.getURL().getHost();
|
||||
|
||||
InetAddress ip = null;
|
||||
|
||||
if (clientHostName != null) {
|
||||
|
||||
try {
|
||||
ip = InetAddress.getByName(clientHostName);
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
// UnknownHostException, SecurityException
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (ip != null)
|
||||
clientInetAddress = ip.getHostAddress();
|
||||
|
||||
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
|
||||
secure = true;
|
||||
|
||||
HttpsURLConnection httpsConnection = (HttpsURLConnection)connection;
|
||||
|
||||
Principal prn = null;
|
||||
|
||||
try {
|
||||
prn = httpsConnection.getPeerPrincipal();
|
||||
|
||||
} catch (Exception e) {
|
||||
|
||||
// SSLPeerUnverifiedException, IllegalStateException
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (prn != null) {
|
||||
|
||||
principals = new Principal[1];
|
||||
principals[0] = prn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the host name of the client that sent the request /
|
||||
* notification.
|
||||
*
|
||||
* @return The client host name, {@code null} if unknown.
|
||||
*/
|
||||
public String getClientHostName() {
|
||||
|
||||
return clientHostName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the IP address of the client that sent the request /
|
||||
* notification.
|
||||
*
|
||||
* @return The client IP address, {@code null} if unknown.
|
||||
*/
|
||||
public String getClientInetAddress() {
|
||||
|
||||
return clientInetAddress;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Indicates whether the request / notification was received over a
|
||||
* secure HTTPS connection.
|
||||
*
|
||||
* @return {@code true} If the request was received over HTTPS,
|
||||
* {@code false} if it was received over plain HTTP.
|
||||
*/
|
||||
public boolean isSecure() {
|
||||
|
||||
return secure;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the first authenticated client principal, {@code null} if
|
||||
* none.
|
||||
*
|
||||
* @return The first client principal, {@code null} if none.
|
||||
*/
|
||||
public Principal getPrincipal() {
|
||||
|
||||
if (principals != null)
|
||||
return principals[0];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the authenticated client principals, {@code null} if
|
||||
* none.
|
||||
*
|
||||
* @return The client principals, {@code null} if none.
|
||||
*/
|
||||
public Principal[] getPrincipals() {
|
||||
|
||||
return principals;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the first authenticated client principal name, {@code null}
|
||||
* if none.
|
||||
*
|
||||
* @return The first client principal name, {@code null} if none.
|
||||
*/
|
||||
public String getPrincipalName() {
|
||||
|
||||
if (principals != null)
|
||||
return principals[0].getName();
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the authenticated client principal names, {@code null}
|
||||
* if none.
|
||||
*
|
||||
* @return The client principal names, {@code null} if none.
|
||||
*/
|
||||
public String[] getPrincipalNames() {
|
||||
|
||||
String[] names = new String[principals.length];
|
||||
|
||||
for (int i=0; i < names.length; i++)
|
||||
names[i] = principals[i].getName();
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
String s = "[host=" + clientHostName + " hostIP=" + clientInetAddress + " secure=" + secure;
|
||||
|
||||
if (principals != null) {
|
||||
|
||||
int i = 0;
|
||||
|
||||
for (Principal p: principals)
|
||||
s += " principal[" + (i++) + "]=" + p;
|
||||
}
|
||||
|
||||
return s + "]";
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.thetransactioncompany.jsonrpc2.server;
|
||||
|
||||
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Notification;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for handling JSON-RPC 2.0 notifications.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public interface NotificationHandler {
|
||||
|
||||
|
||||
/**
|
||||
* Gets the names of the handled JSON-RPC 2.0 notification methods.
|
||||
*
|
||||
* @return The names of the handled JSON-RPC 2.0 notification methods.
|
||||
*/
|
||||
public String[] handledNotifications();
|
||||
|
||||
|
||||
/**
|
||||
* Processes a JSON-RPC 2.0 notification.
|
||||
*
|
||||
* <p>Note that JSON-RPC 2.0 notifications don't produce a response!
|
||||
*
|
||||
* @param notification A valid JSON-RPC 2.0 notification instance.
|
||||
* Must not be {@code null}.
|
||||
* @param notificationCtx Context information about the notification
|
||||
* message, may be {@code null} if undefined.
|
||||
*/
|
||||
public void process(final JSONRPC2Notification notification, final MessageContext notificationCtx);
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.thetransactioncompany.jsonrpc2.server;
|
||||
|
||||
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Request;
|
||||
import com.thetransactioncompany.jsonrpc2.JSONRPC2Response;
|
||||
|
||||
|
||||
/**
|
||||
* Interface for handling JSON-RPC 2.0 requests.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public interface RequestHandler {
|
||||
|
||||
|
||||
/**
|
||||
* Gets the names of the handled JSON-RPC 2.0 request methods.
|
||||
*
|
||||
* @return The names of the handled JSON-RPC 2.0 request methods.
|
||||
*/
|
||||
public String[] handledRequests();
|
||||
|
||||
|
||||
/**
|
||||
* Processes a JSON-RPC 2.0 request.
|
||||
*
|
||||
* @param request A valid JSON-RPC 2.0 request instance. Must not be
|
||||
* {@code null}.
|
||||
* @param requestCtx Context information about the request message, may
|
||||
* be {@code null} if undefined.
|
||||
*
|
||||
* @return The resulting JSON-RPC 2.0 response. It indicates success
|
||||
* or an error, such as METHOD_NOT_FOUND.
|
||||
*/
|
||||
public JSONRPC2Response process(final JSONRPC2Request request, final MessageContext requestCtx);
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Simple server framework for processing JSON-RPC 2.0 requests and
|
||||
* notifications.
|
||||
*
|
||||
* <p>Usage:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Implement {@link com.thetransactioncompany.jsonrpc2.server.RequestHandler request}
|
||||
* and / or {@link com.thetransactioncompany.jsonrpc2.server.NotificationHandler notification}
|
||||
* handlers for the various expected JSON-RPC 2.0 messages. A handler
|
||||
* may process one or more request/notification methods (identified by
|
||||
* method name).
|
||||
* <li>Create a new {@link com.thetransactioncompany.jsonrpc2.server.Dispatcher}
|
||||
* and register the handlers with it.
|
||||
* <li>Pass the received JSON-RPC 2.0 requests and notifications to the
|
||||
* appropriate {@code Dispatcher.dispatch(...)} method, then, if the
|
||||
* message is a request, pass the resulting JSON-RPC 2.0 response back
|
||||
* to the client.
|
||||
* </ol>
|
||||
*
|
||||
* <p>Direct package dependencies:
|
||||
*
|
||||
* <ul>
|
||||
* <li><b><a href="http://software.dzhuvinov.com/json-rpc-2.0-base.html">JSON-RPC 2.0 Base</a></b>
|
||||
* [<i>com.thetransactioncompany.jsonrpc2</i>] to construct and represent
|
||||
* JSON-RPC 2.0 messages.
|
||||
* <li><b>Java Servlet API</b> [<i>javax.servlet.http</i>] for constructing
|
||||
* {@link com.thetransactioncompany.jsonrpc2.server.MessageContext}
|
||||
* objects from HTTP servlet requests.
|
||||
* </ul>
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
package com.thetransactioncompany.jsonrpc2.server;
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,80 @@
|
||||
package com.thetransactioncompany.jsonrpc2.util;
|
||||
|
||||
|
||||
/**
|
||||
* The base abstract class for the JSON-RPC 2.0 parameter retrievers.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
public abstract class ParamsRetriever {
|
||||
|
||||
|
||||
/**
|
||||
* Returns the parameter count.
|
||||
*
|
||||
* @return The parameters count.
|
||||
*/
|
||||
public abstract int size();
|
||||
|
||||
|
||||
/**
|
||||
* Matches a string against an array of acceptable values.
|
||||
*
|
||||
* @param input The string to match.
|
||||
* @param enumStrings The acceptable string values. Must not be
|
||||
* {@code null}.
|
||||
* @param ignoreCase {@code true} for a case insensitive match.
|
||||
*
|
||||
* @return The matching string value, {@code null} if no match was
|
||||
* found.
|
||||
*/
|
||||
protected static String getEnumStringMatch(final String input,
|
||||
final String[] enumStrings,
|
||||
final boolean ignoreCase) {
|
||||
|
||||
for (final String en: enumStrings) {
|
||||
|
||||
if (ignoreCase) {
|
||||
if (en.equalsIgnoreCase(input))
|
||||
return en;
|
||||
}
|
||||
else {
|
||||
if (en.equals(input))
|
||||
return en;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Matches a string against an enumeration of acceptable values.
|
||||
*
|
||||
* @param input The string to match.
|
||||
* @param enumClass The enumeration class specifying the acceptable
|
||||
* string values. Must not be {@code null}.
|
||||
* @param ignoreCase {@code true} for a case insensitive match.
|
||||
*
|
||||
* @return The matching enumeration constant, {@code null} if no match
|
||||
* was found.
|
||||
*/
|
||||
protected static <T extends Enum<T>> T getEnumStringMatch(final String input,
|
||||
final Class<T> enumClass,
|
||||
final boolean ignoreCase) {
|
||||
|
||||
for (T en: enumClass.getEnumConstants()) {
|
||||
|
||||
if (ignoreCase) {
|
||||
if (en.toString().equalsIgnoreCase(input))
|
||||
return en;
|
||||
}
|
||||
else {
|
||||
if (en.toString().equals(input))
|
||||
return en;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Utility classes for typed retrieval of JSON-RPC 2.0 request parameters on the
|
||||
* server side.
|
||||
*
|
||||
* <p>The following parameter type conversion choices are available:
|
||||
*
|
||||
* <ul>
|
||||
* <li>JSON true/false to Java {@code boolean}
|
||||
* <li>JSON number to Java {@code int}, {@code long}, {@code float} or
|
||||
* {@code double}
|
||||
* <li>JSON string to {@code java.lang.String}
|
||||
* <li>Predefined (enumerated) JSON string to a Java {@code enum} constant
|
||||
* or {@code java.lang.String}
|
||||
* <li>JSON array to Java {@code boolean[]}, {@code int[]}, {@code long[]},
|
||||
* {@code float[]}, {@code double[]} or {@code string[]} array, or
|
||||
* to mixed type {@code java.util.List}
|
||||
* <li>JSON object to {@code java.util.Map}
|
||||
* </ul>
|
||||
*
|
||||
* <p>If a parameter cannot be retrieved, either because it's missing or
|
||||
* is of the wrong type, a standard
|
||||
* {@link com.thetransactioncompany.jsonrpc2.JSONRPC2Error#INVALID_PARAMS}
|
||||
* exception is thrown.
|
||||
*
|
||||
* <p>There are two concrete classes:
|
||||
*
|
||||
* <ul>
|
||||
* <li>The {@link com.thetransactioncompany.jsonrpc2.util.PositionalParamsRetriever}
|
||||
* class is for extracting <em>positional parameters</em> (packed in a
|
||||
* JSON array).
|
||||
* <li>The {@link com.thetransactioncompany.jsonrpc2.util.NamedParamsRetriever}
|
||||
* class is for extracting <em>named parameters</em> (packed in a JSON
|
||||
* object).
|
||||
* </ul>
|
||||
*
|
||||
*
|
||||
* <p><b>Package dependencies:</b> The classes in this package depend on the
|
||||
* sister {@link com.thetransactioncompany.jsonrpc2} package.
|
||||
*
|
||||
* @author Vladimir Dzhuvinov
|
||||
*/
|
||||
package com.thetransactioncompany.jsonrpc2.util;
|
||||
|
||||
|
127
src/java/net/i2p/i2pcontrol/HostCheckHandler.java
Normal file
127
src/java/net/i2p/i2pcontrol/HostCheckHandler.java
Normal file
@ -0,0 +1,127 @@
|
||||
package net.i2p.i2pcontrol;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
|
||||
import org.apache.http.conn.util.InetAddressUtils;
|
||||
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.handler.HandlerWrapper;
|
||||
|
||||
/**
|
||||
* Block certain Host headers to prevent DNS rebinding attacks.
|
||||
*
|
||||
* This Handler wraps the ContextHandlerCollection, which handles
|
||||
* all the webapps (not just routerconsole).
|
||||
* Therefore, this protects all the webapps.
|
||||
*
|
||||
* @since 0.12 copied from routerconsole
|
||||
*/
|
||||
public class HostCheckHandler extends HandlerWrapper
|
||||
{
|
||||
private final I2PAppContext _context;
|
||||
private final Set<String> _listenHosts;
|
||||
|
||||
/**
|
||||
* MUST call setListenHosts() afterwards.
|
||||
*/
|
||||
public HostCheckHandler(I2PAppContext ctx) {
|
||||
super();
|
||||
_context = ctx;
|
||||
_listenHosts = new HashSet<String>(8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the legal hosts.
|
||||
* Not synched. Call this BEFORE starting.
|
||||
* If empty, all are allowed.
|
||||
*
|
||||
* @param hosts contains hostnames or IPs. But we allow all IPs anyway.
|
||||
*/
|
||||
public void setListenHosts(Set<String> hosts) {
|
||||
_listenHosts.clear();
|
||||
_listenHosts.addAll(hosts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Block by Host header, pass everything else to the delegate.
|
||||
*/
|
||||
public void handle(String pathInContext,
|
||||
Request baseRequest,
|
||||
HttpServletRequest httpRequest,
|
||||
HttpServletResponse httpResponse)
|
||||
throws IOException, ServletException
|
||||
{
|
||||
|
||||
String host = httpRequest.getHeader("Host");
|
||||
if (!allowHost(host)) {
|
||||
Log log = _context.logManager().getLog(HostCheckHandler.class);
|
||||
host = DataHelper.stripHTML(getHost(host));
|
||||
String s = "Console request denied.\n" +
|
||||
" To allow access using the hostname \"" + host + "\", add the line \"" +
|
||||
I2PControlController.PROP_ALLOWED_HOSTS + '=' + host +
|
||||
"\" to I2PControl.conf and restart.";
|
||||
log.logAlways(Log.WARN, s);
|
||||
httpResponse.sendError(403, s);
|
||||
return;
|
||||
}
|
||||
|
||||
super.handle(pathInContext, baseRequest, httpRequest, httpResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should we allow a request with this Host header?
|
||||
*
|
||||
* ref: https://en.wikipedia.org/wiki/DNS_rebinding
|
||||
*
|
||||
* @param host the HTTP Host header, null ok
|
||||
* @return true if OK
|
||||
*/
|
||||
private boolean allowHost(String host) {
|
||||
if (host == null)
|
||||
return true;
|
||||
// common cases
|
||||
if (host.equals("127.0.0.1:7650") ||
|
||||
host.equals("localhost:7650"))
|
||||
return true;
|
||||
// all allowed?
|
||||
if (_listenHosts.isEmpty())
|
||||
return true;
|
||||
host = getHost(host);
|
||||
if (_listenHosts.contains(host))
|
||||
return true;
|
||||
// allow all IP addresses
|
||||
if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host))
|
||||
return true;
|
||||
//System.out.println(host + " not found in " + s);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip [] and port from a host header
|
||||
*
|
||||
* @param host the HTTP Host header non-null
|
||||
*/
|
||||
private static String getHost(String host) {
|
||||
if (host.startsWith("[")) {
|
||||
host = host.substring(1);
|
||||
int brack = host.indexOf(']');
|
||||
if (brack >= 0)
|
||||
host = host.substring(0, brack);
|
||||
} else {
|
||||
int colon = host.indexOf(':');
|
||||
if (colon >= 0)
|
||||
host = host.substring(0, colon);
|
||||
}
|
||||
return host;
|
||||
}
|
||||
}
|
403
src/java/net/i2p/i2pcontrol/I2PControlController.java
Normal file
403
src/java/net/i2p/i2pcontrol/I2PControlController.java
Normal file
@ -0,0 +1,403 @@
|
||||
package net.i2p.i2pcontrol;
|
||||
/*
|
||||
* Copyright 2010 hottuna (dev@robertfoss.se)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.app.ClientAppManager;
|
||||
import net.i2p.app.ClientAppState;
|
||||
import static net.i2p.app.ClientAppState.*;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.router.app.RouterApp;
|
||||
import net.i2p.util.I2PSSLSocketFactory;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.PortMapper;
|
||||
|
||||
import net.i2p.i2pcontrol.security.KeyStoreProvider;
|
||||
import net.i2p.i2pcontrol.security.SecurityManager;
|
||||
import net.i2p.i2pcontrol.servlets.JSONRPC2Servlet;
|
||||
import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager;
|
||||
|
||||
import org.eclipse.jetty.server.Connector;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.HttpConnectionFactory;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.eclipse.jetty.server.SslConnectionFactory;
|
||||
import org.eclipse.jetty.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.KeyStore;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
|
||||
/**
|
||||
* This handles the starting and stopping of Jetty
|
||||
* from a single static class so it can be called via clients.config.
|
||||
*
|
||||
* This makes installation of a new eepsite a turnkey operation.
|
||||
*
|
||||
* Usage: I2PControlController -d $PLUGIN [start|stop]
|
||||
*
|
||||
* @author hottuna
|
||||
*/
|
||||
public class I2PControlController implements RouterApp {
|
||||
// non-null
|
||||
private final I2PAppContext _appContext;
|
||||
// warning, null in app context
|
||||
private final RouterContext _context;
|
||||
private final ClientAppManager _mgr;
|
||||
private final Log _log;
|
||||
private final String _pluginDir;
|
||||
private final ConfigurationManager _conf;
|
||||
private final KeyStoreProvider _ksp;
|
||||
private final SecurityManager _secMan;
|
||||
private final Server _server;
|
||||
private ClientAppState _state = UNINITIALIZED;
|
||||
// only for main()
|
||||
private static I2PControlController _instance;
|
||||
static final String PROP_ALLOWED_HOSTS = "i2pcontrol.allowedhosts";
|
||||
private static final String SVC_HTTPS_I2PCONTROL = "https_i2pcontrol";
|
||||
|
||||
/**
|
||||
* RouterApp (new way)
|
||||
*/
|
||||
public I2PControlController(RouterContext ctx, ClientAppManager mgr, String args[]) {
|
||||
_appContext = _context = ctx;
|
||||
_mgr = mgr;
|
||||
_log = _appContext.logManager().getLog(I2PControlController.class);
|
||||
File pluginDir = new File(_context.getAppDir(), "plugins/I2PControl");
|
||||
_pluginDir = pluginDir.getAbsolutePath();
|
||||
_conf = new ConfigurationManager(_appContext, pluginDir, true);
|
||||
_ksp = new KeyStoreProvider(_pluginDir);
|
||||
_secMan = new SecurityManager(_appContext, _ksp, _conf);
|
||||
_server = buildServer();
|
||||
_state = INITIALIZED;
|
||||
}
|
||||
|
||||
/**
|
||||
* From main() (old way)
|
||||
*/
|
||||
public I2PControlController(File pluginDir) {
|
||||
_appContext = I2PAppContext.getGlobalContext();
|
||||
if (_appContext instanceof RouterContext)
|
||||
_context = (RouterContext) _appContext;
|
||||
else
|
||||
_context = null;
|
||||
_mgr = null;
|
||||
_log = _appContext.logManager().getLog(I2PControlController.class);
|
||||
_pluginDir = pluginDir.getAbsolutePath();
|
||||
_conf = new ConfigurationManager(_appContext, pluginDir, true);
|
||||
_ksp = new KeyStoreProvider(_pluginDir);
|
||||
_secMan = new SecurityManager(_appContext, _ksp, _conf);
|
||||
_server = buildServer();
|
||||
_state = INITIALIZED;
|
||||
}
|
||||
|
||||
/////// ClientApp methods
|
||||
|
||||
public synchronized void startup() {
|
||||
changeState(STARTING);
|
||||
try {
|
||||
start(null);
|
||||
changeState(RUNNING);
|
||||
} catch (Exception e) {
|
||||
changeState(START_FAILED, "Failed to start", e);
|
||||
_log.error("Unable to start jetty server", e);
|
||||
stop();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void shutdown(String[] args) {
|
||||
if (_state == STOPPED)
|
||||
return;
|
||||
changeState(STOPPING);
|
||||
stop();
|
||||
changeState(STOPPED);
|
||||
}
|
||||
|
||||
public synchronized ClientAppState getState() {
|
||||
return _state;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return "I2PControl";
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return "I2PControl";
|
||||
}
|
||||
|
||||
/////// end ClientApp methods
|
||||
|
||||
private void changeState(ClientAppState state) {
|
||||
changeState(state, null, null);
|
||||
}
|
||||
|
||||
private synchronized void changeState(ClientAppState state, String msg, Exception e) {
|
||||
_state = state;
|
||||
if (_mgr != null)
|
||||
_mgr.notify(this, state, msg, e);
|
||||
if (_context == null) {
|
||||
if (msg != null)
|
||||
System.out.println(state + ": " + msg);
|
||||
if (e != null)
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deprecated, use constructor
|
||||
*/
|
||||
public static void main(String args[]) {
|
||||
if (args.length != 3 || (!"-d".equals(args[0])))
|
||||
throw new IllegalArgumentException("Usage: PluginController -d $PLUGINDIR [start|stop]");
|
||||
|
||||
if ("start".equals(args[2])) {
|
||||
File pluginDir = new File(args[1]);
|
||||
if (!pluginDir.exists())
|
||||
throw new IllegalArgumentException("Plugin directory " + pluginDir.getAbsolutePath() + " does not exist");
|
||||
synchronized(I2PControlController.class) {
|
||||
if (_instance != null)
|
||||
throw new IllegalStateException();
|
||||
I2PControlController i2pcc = new I2PControlController(pluginDir);
|
||||
try {
|
||||
i2pcc.startup();
|
||||
_instance = i2pcc;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} else if ("stop".equals(args[2])) {
|
||||
synchronized(I2PControlController.class) {
|
||||
if (_instance != null) {
|
||||
_instance.shutdown(null);
|
||||
_instance = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Usage: PluginController -d $PLUGINDIR [start|stop]");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private synchronized void start(String args[]) throws Exception {
|
||||
_appContext.logManager().getLog(JSONRPC2Servlet.class).setMinimumPriority(Log.DEBUG);
|
||||
_server.start();
|
||||
_context.portMapper().register(SVC_HTTPS_I2PCONTROL,
|
||||
_conf.getConf("i2pcontrol.listen.address", "127.0.0.1"),
|
||||
_conf.getConf("i2pcontrol.listen.port", 7650));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Builds a new server. Used for changing ports during operation and such.
|
||||
* @return Server - A new server built from current configuration.
|
||||
*/
|
||||
private Connector buildDefaultListener(Server server) {
|
||||
Connector ssl = buildSslListener(server, _conf.getConf("i2pcontrol.listen.address", "127.0.0.1"),
|
||||
_conf.getConf("i2pcontrol.listen.port", 7650));
|
||||
return ssl;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a new server. Used for changing ports during operation and such.
|
||||
*
|
||||
* Does NOT start the server. Must call start() on the returned server.
|
||||
*
|
||||
* @return Server - A new server built from current configuration.
|
||||
*/
|
||||
public Server buildServer() {
|
||||
Server server = new Server();
|
||||
Connector ssl = buildDefaultListener(server);
|
||||
server.addConnector(ssl);
|
||||
|
||||
ServletHandler sh = new ServletHandler();
|
||||
sh.addServletWithMapping(new ServletHolder(new JSONRPC2Servlet(_context, _secMan)), "/");
|
||||
HostCheckHandler hch = new HostCheckHandler(_appContext);
|
||||
Set<String> listenHosts = new HashSet<String>(8);
|
||||
// fix up the allowed hosts set (see HostCheckHandler)
|
||||
// empty set says all are valid
|
||||
String address = _conf.getConf("i2pcontrol.listen.address", "127.0.0.1");
|
||||
if (!(address.equals("0.0.0.0") ||
|
||||
address.equals("::") ||
|
||||
address.equals("0:0:0:0:0:0:0:0"))) {
|
||||
listenHosts.add("localhost");
|
||||
listenHosts.add("127.0.0.1");
|
||||
listenHosts.add("::1");
|
||||
listenHosts.add("0:0:0:0:0:0:0:1");
|
||||
String allowed = _conf.getConf(PROP_ALLOWED_HOSTS, "");
|
||||
if (!allowed.equals("")) {
|
||||
StringTokenizer tok = new StringTokenizer(allowed, " ,");
|
||||
while (tok.hasMoreTokens()) {
|
||||
listenHosts.add(tok.nextToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
hch.setListenHosts(listenHosts);
|
||||
hch.setHandler(sh);
|
||||
server.getServer().setHandler(hch);
|
||||
|
||||
_conf.writeConfFile();
|
||||
return server;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a SSLListener with all the default options. The listener will use all the default options.
|
||||
* @param address - The address the listener will listen to.
|
||||
* @param port - The port the listener will listen to.
|
||||
* @return - Newly created listener
|
||||
*/
|
||||
private Connector buildSslListener(Server server, String address, int port) {
|
||||
int listeners = 0;
|
||||
if (server != null) {
|
||||
listeners = server.getConnectors().length;
|
||||
}
|
||||
|
||||
// the keystore path and password
|
||||
SslContextFactory sslFactory = new SslContextFactory(_ksp.getKeyStoreLocation());
|
||||
sslFactory.setKeyStorePassword(KeyStoreProvider.DEFAULT_KEYSTORE_PASSWORD);
|
||||
// the X.509 cert password (if not present, verifyKeyStore() returned false)
|
||||
sslFactory.setKeyManagerPassword(KeyStoreProvider.DEFAULT_CERTIFICATE_PASSWORD);
|
||||
sslFactory.addExcludeProtocols(I2PSSLSocketFactory.EXCLUDE_PROTOCOLS.toArray(
|
||||
new String[I2PSSLSocketFactory.EXCLUDE_PROTOCOLS.size()]));
|
||||
sslFactory.addExcludeCipherSuites(I2PSSLSocketFactory.EXCLUDE_CIPHERS.toArray(
|
||||
new String[I2PSSLSocketFactory.EXCLUDE_CIPHERS.size()]));
|
||||
|
||||
HttpConfiguration httpConfig = new HttpConfiguration();
|
||||
httpConfig.setSecureScheme("https");
|
||||
httpConfig.setSecurePort(port);
|
||||
httpConfig.addCustomizer(new SecureRequestCustomizer());
|
||||
// number of acceptors, (default) number of selectors
|
||||
ServerConnector ssl = new ServerConnector(server, 1, 0,
|
||||
new SslConnectionFactory(sslFactory, "http/1.1"),
|
||||
new HttpConnectionFactory(httpConfig));
|
||||
ssl.setHost(address);
|
||||
ssl.setPort(port);
|
||||
ssl.setIdleTimeout(90*1000); // default 10 sec
|
||||
// all with same name will use the same thread pool
|
||||
ssl.setName("I2PControl");
|
||||
|
||||
ssl.setName("SSL Listener-" + ++listeners);
|
||||
|
||||
return ssl;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add a listener to the server
|
||||
* If a listener listening to the same port as the provided listener
|
||||
* uses already exists within the server, replace the one already used by
|
||||
* the server with the provided listener.
|
||||
* @param listener
|
||||
* @throws Exception
|
||||
*/
|
||||
/****
|
||||
public synchronized void replaceListener(Connector listener) throws Exception {
|
||||
if (_server != null) {
|
||||
stopServer();
|
||||
}
|
||||
_server = buildServer(listener);
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* Get all listeners of the server.
|
||||
* @return
|
||||
*/
|
||||
/****
|
||||
public synchronized Connector[] getListeners() {
|
||||
if (_server != null) {
|
||||
return _server.getConnectors();
|
||||
}
|
||||
return new Connector[0];
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* Removes all listeners
|
||||
*/
|
||||
/****
|
||||
public synchronized void clearListeners() {
|
||||
if (_server != null) {
|
||||
for (Connector listen : getListeners()) {
|
||||
_server.removeConnector(listen);
|
||||
}
|
||||
}
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* Stop it
|
||||
*/
|
||||
private synchronized void stopServer()
|
||||
{
|
||||
try {
|
||||
if (_server != null) {
|
||||
_appContext.portMapper().unregister(SVC_HTTPS_I2PCONTROL);
|
||||
_server.stop();
|
||||
for (Connector listener : _server.getConnectors()) {
|
||||
listener.stop();
|
||||
}
|
||||
_server.destroy();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
_log.error("Stopping server", e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stop() {
|
||||
_conf.writeConfFile();
|
||||
_secMan.stopTimedEvents();
|
||||
stopServer();
|
||||
|
||||
/****
|
||||
// Get and stop all running threads
|
||||
ThreadGroup threadgroup = Thread.currentThread().getThreadGroup();
|
||||
Thread[] threads = new Thread[threadgroup.activeCount() + 3];
|
||||
threadgroup.enumerate(threads, true);
|
||||
for (Thread thread : threads) {
|
||||
if (thread != null) {//&& thread.isAlive()){
|
||||
thread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
for (Thread thread : threads) {
|
||||
if (thread != null) {
|
||||
System.out.println("Active thread: " + thread.getName());
|
||||
}
|
||||
}
|
||||
threadgroup.interrupt();
|
||||
|
||||
//Thread.currentThread().getThreadGroup().destroy();
|
||||
****/
|
||||
}
|
||||
|
||||
public String getPluginDir() {
|
||||
return _pluginDir;
|
||||
}
|
||||
}
|
20
src/java/net/i2p/i2pcontrol/I2PControlVersion.java
Normal file
20
src/java/net/i2p/i2pcontrol/I2PControlVersion.java
Normal file
@ -0,0 +1,20 @@
|
||||
package net.i2p.i2pcontrol;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class I2PControlVersion {
|
||||
/** The current version of I2PControl */
|
||||
public final static String VERSION = "0.12.0";
|
||||
|
||||
/** The current version of the I2PControl API being primarily being implemented */
|
||||
public final static int API_VERSION = 1;
|
||||
|
||||
/** The supported versions of the I2PControl API */
|
||||
public final static Set<Integer> SUPPORTED_API_VERSIONS;
|
||||
|
||||
static {
|
||||
SUPPORTED_API_VERSIONS = new HashSet<Integer>();
|
||||
SUPPORTED_API_VERSIONS.add(1);
|
||||
}
|
||||
}
|
50
src/java/net/i2p/i2pcontrol/security/AuthToken.java
Normal file
50
src/java/net/i2p/i2pcontrol/security/AuthToken.java
Normal file
@ -0,0 +1,50 @@
|
||||
package net.i2p.i2pcontrol.security;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
public class AuthToken {
|
||||
static final int VALIDITY_TIME = 1; // Measured in days
|
||||
private final SecurityManager _secMan;
|
||||
private final String id;
|
||||
private final Date expiry;
|
||||
|
||||
public AuthToken(SecurityManager secMan, String password) {
|
||||
_secMan = secMan;
|
||||
String hash = _secMan.getPasswdHash(password);
|
||||
this.id = _secMan.getHash(hash + Calendar.getInstance().getTimeInMillis());
|
||||
Calendar expiry = Calendar.getInstance();
|
||||
expiry.add(Calendar.DAY_OF_YEAR, VALIDITY_TIME);
|
||||
this.expiry = expiry.getTime();
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the AuthToken has expired.
|
||||
* @return True if AuthToken hasn't expired. False in any other case.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return Calendar.getInstance().getTime().before(expiry);
|
||||
}
|
||||
|
||||
public String getExpiryTime() {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat();
|
||||
sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
|
||||
return sdf.format(expiry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.i2p.i2pcontrol.security;
|
||||
|
||||
public class ExpiredAuthTokenException extends Exception {
|
||||
private static final long serialVersionUID = 2279019346592900289L;
|
||||
|
||||
private String expiryTime;
|
||||
|
||||
public ExpiredAuthTokenException(String str, String expiryTime) {
|
||||
super(str);
|
||||
this.expiryTime = expiryTime;
|
||||
}
|
||||
|
||||
public String getExpirytime() {
|
||||
return expiryTime;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package net.i2p.i2pcontrol.security;
|
||||
|
||||
public class InvalidAuthTokenException extends Exception {
|
||||
private static final long serialVersionUID = 7605321329341235577L;
|
||||
|
||||
public InvalidAuthTokenException(String str) {
|
||||
super(str);
|
||||
}
|
||||
}
|
218
src/java/net/i2p/i2pcontrol/security/KeyStoreProvider.java
Normal file
218
src/java/net/i2p/i2pcontrol/security/KeyStoreProvider.java
Normal file
@ -0,0 +1,218 @@
|
||||
package net.i2p.i2pcontrol.security;
|
||||
|
||||
import net.i2p.crypto.KeyStoreUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class KeyStoreProvider {
|
||||
public static final String DEFAULT_CERTIFICATE_ALGORITHM_STRING = "RSA";
|
||||
public static final int DEFAULT_CERTIFICATE_KEY_LENGTH = 4096;
|
||||
public static final int DEFAULT_CERTIFICATE_VALIDITY = 365 * 10;
|
||||
public final static String DEFAULT_CERTIFICATE_DOMAIN = "localhost";
|
||||
public final static String DEFAULT_CERTIFICATE_ALIAS = "I2PControl CA";
|
||||
public static final String DEFAULT_KEYSTORE_NAME = "i2pcontrol.ks";
|
||||
public static final String DEFAULT_KEYSTORE_PASSWORD = KeyStoreUtil.DEFAULT_KEYSTORE_PASSWORD;
|
||||
public static final String DEFAULT_CERTIFICATE_PASSWORD = "nut'nfancy";
|
||||
private final String _pluginDir;
|
||||
private KeyStore _keystore;
|
||||
|
||||
public KeyStoreProvider(String pluginDir) {
|
||||
_pluginDir = pluginDir;
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
KeyStoreUtil.createKeys(new File(getKeyStoreLocation()),
|
||||
DEFAULT_KEYSTORE_PASSWORD,
|
||||
DEFAULT_CERTIFICATE_ALIAS,
|
||||
DEFAULT_CERTIFICATE_DOMAIN,
|
||||
"i2pcontrol",
|
||||
DEFAULT_CERTIFICATE_VALIDITY,
|
||||
DEFAULT_CERTIFICATE_ALGORITHM_STRING,
|
||||
DEFAULT_CERTIFICATE_KEY_LENGTH,
|
||||
DEFAULT_CERTIFICATE_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password unused
|
||||
* @return null on failure
|
||||
*/
|
||||
public static X509Certificate readCert(KeyStore ks, String certAlias, String password) {
|
||||
try {
|
||||
X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias);
|
||||
|
||||
if (cert == null) {
|
||||
throw new RuntimeException("Got null cert from keystore!");
|
||||
}
|
||||
|
||||
try {
|
||||
cert.verify(cert.getPublicKey());
|
||||
return cert;
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to verify caCert certificate against caCert");
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param password for the keystore
|
||||
* @return null on failure
|
||||
*/
|
||||
/****
|
||||
public static X509Certificate readCert(File keyStoreFile, String certAlias, String password) {
|
||||
try {
|
||||
KeyStore ks = getDefaultKeyStore();
|
||||
ks.load(new FileInputStream(keyStoreFile), password.toCharArray());
|
||||
X509Certificate cert = (X509Certificate) ks.getCertificate(certAlias);
|
||||
|
||||
if (cert == null) {
|
||||
throw new RuntimeException("Got null cert from keystore!");
|
||||
}
|
||||
|
||||
try {
|
||||
cert.verify(cert.getPublicKey());
|
||||
return cert;
|
||||
} catch (Exception e) {
|
||||
System.err.println("Failed to verify caCert certificate against caCert");
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("Couldn't read keystore from: " + keyStoreFile.toString());
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
} catch (KeyStoreException e) {
|
||||
System.err.println("No certificate with alias: " + certAlias + " found.");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* @param password for the key
|
||||
* @return null on failure, or throws RuntimeException...
|
||||
*/
|
||||
/****
|
||||
public static PrivateKey readPrivateKey(KeyStore ks, String alias, String password) {
|
||||
try {
|
||||
// load the key entry from the keystore
|
||||
Key key = ks.getKey(alias, password.toCharArray());
|
||||
|
||||
if (key == null) {
|
||||
throw new RuntimeException("Got null key from keystore!");
|
||||
}
|
||||
|
||||
PrivateKey privKey = (PrivateKey) key;
|
||||
return privKey;
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* @return null on failure
|
||||
*/
|
||||
/****
|
||||
public static PrivateKey readPrivateKey(String alias, File keyStoreFile, String keyStorePassword, String keyPassword) {
|
||||
try {
|
||||
KeyStore ks = getDefaultKeyStore();
|
||||
ks.load(new FileInputStream(keyStoreFile), keyStorePassword.toCharArray());
|
||||
return readPrivateKey(ks, alias, keyStorePassword);
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
System.err.println("Couldn't read keystore from: " + keyStoreFile.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* @return null on failure
|
||||
*/
|
||||
/****
|
||||
public static KeyStore writeCACertToKeyStore(KeyStore keyStore, String keyPassword, String alias, PrivateKey caPrivKey, X509Certificate caCert) {
|
||||
try {
|
||||
X509Certificate[] chain = new X509Certificate[1];
|
||||
chain[0] = caCert;
|
||||
|
||||
keyStore.setKeyEntry(alias, caPrivKey, keyPassword.toCharArray(), chain);
|
||||
File keyStoreFile = new File(I2PControlController.getPluginDir() + File.separator + DEFAULT_KEYSTORE_NAME);
|
||||
keyStore.store(new FileOutputStream(keyStoreFile), DEFAULT_KEYSTORE_PASSWORD.toCharArray());
|
||||
return keyStore;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (CertificateException e) {
|
||||
e.printStackTrace();
|
||||
} catch (KeyStoreException e) {
|
||||
e.printStackTrace();
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* @return null on failure
|
||||
*/
|
||||
public synchronized KeyStore getDefaultKeyStore() {
|
||||
if (_keystore == null) {
|
||||
File keyStoreFile = new File(getKeyStoreLocation());
|
||||
|
||||
try {
|
||||
_keystore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
if (keyStoreFile.exists()) {
|
||||
InputStream is = new FileInputStream(keyStoreFile);
|
||||
_keystore.load(is, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
|
||||
return _keystore;
|
||||
}
|
||||
|
||||
initialize();
|
||||
if (keyStoreFile.exists()) {
|
||||
InputStream is = new FileInputStream(keyStoreFile);
|
||||
_keystore.load(is, DEFAULT_KEYSTORE_PASSWORD.toCharArray());
|
||||
return _keystore;
|
||||
} else {
|
||||
throw new IOException("KeyStore file " + keyStoreFile.getAbsolutePath() + " wasn't readable");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ignore. Not an issue. Let's just create a new keystore instead.
|
||||
}
|
||||
return null;
|
||||
} else {
|
||||
return _keystore;
|
||||
}
|
||||
}
|
||||
|
||||
public String getKeyStoreLocation() {
|
||||
File keyStoreFile = new File(_pluginDir, DEFAULT_KEYSTORE_NAME);
|
||||
return keyStoreFile.getAbsolutePath();
|
||||
}
|
||||
}
|
252
src/java/net/i2p/i2pcontrol/security/SecurityManager.java
Normal file
252
src/java/net/i2p/i2pcontrol/security/SecurityManager.java
Normal file
@ -0,0 +1,252 @@
|
||||
package net.i2p.i2pcontrol.security;
|
||||
/*
|
||||
* Copyright 2011 hottuna (dev@robertfoss.se)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.crypto.SHA256Generator;
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer2;
|
||||
|
||||
import org.mindrot.jbcrypt.BCrypt;
|
||||
|
||||
import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager;
|
||||
|
||||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.security.KeyStore;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* Manage the password storing for I2PControl.
|
||||
*/
|
||||
public class SecurityManager {
|
||||
public final static String DEFAULT_AUTH_PASSWORD = "itoopie";
|
||||
private final HashMap<String, AuthToken> authTokens;
|
||||
private final SimpleTimer2.TimedEvent timer;
|
||||
private final KeyStore _ks;
|
||||
private final Log _log;
|
||||
private final ConfigurationManager _conf;
|
||||
private final I2PAppContext _context;
|
||||
|
||||
/**
|
||||
* @param ksp may be null (if webapp)
|
||||
*/
|
||||
public SecurityManager(I2PAppContext ctx, KeyStoreProvider ksp, ConfigurationManager conf) {
|
||||
_context = ctx;
|
||||
_conf = conf;
|
||||
_log = ctx.logManager().getLog(SecurityManager.class);
|
||||
authTokens = new HashMap<String, AuthToken>();
|
||||
|
||||
timer = new Sweeper();
|
||||
|
||||
_ks = ksp != null ? ksp.getDefaultKeyStore() : null;
|
||||
}
|
||||
|
||||
public void stopTimedEvents() {
|
||||
timer.cancel();
|
||||
synchronized (authTokens) {
|
||||
authTokens.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the X509Certificate of the server as a Base64 encoded string.
|
||||
* @return base64 encode of X509Certificate
|
||||
*/
|
||||
/**** unused and incorrectly uses I2P Base64. Switch to CertUtil.exportCert() if needed.
|
||||
public String getBase64Cert() {
|
||||
X509Certificate caCert = KeyStoreProvider.readCert(_ks,
|
||||
KeyStoreProvider.DEFAULT_CERTIFICATE_ALIAS,
|
||||
KeyStoreProvider.DEFAULT_KEYSTORE_PASSWORD);
|
||||
return getBase64FromCert(caCert);
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* Return the X509Certificate as a base64 encoded string.
|
||||
* @param cert
|
||||
* @return base64 encode of X509Certificate
|
||||
*/
|
||||
/**** unused and incorrectly uses I2P Base64. Switch to CertUtil.exportCert() if needed.
|
||||
private static String getBase64FromCert(X509Certificate cert) {
|
||||
try {
|
||||
return Base64.encode(cert.getEncoded());
|
||||
} catch (CertificateEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
****/
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Hash pwd with using BCrypt with the default salt.
|
||||
* @param pwd
|
||||
* @return BCrypt hash of salt and input string
|
||||
*/
|
||||
public String getPasswdHash(String pwd) {
|
||||
String salt;
|
||||
synchronized(_conf) {
|
||||
salt = _conf.getConf("auth.salt", "");
|
||||
if (salt.equals("")) {
|
||||
salt = BCrypt.gensalt(10, _context.random());
|
||||
_conf.setConf("auth.salt", salt);
|
||||
_conf.writeConfFile();
|
||||
}
|
||||
}
|
||||
return BCrypt.hashpw(pwd, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved password hash. Stores if not previously set.
|
||||
* @return BCrypt hash of salt and password
|
||||
* @since 0.12
|
||||
*/
|
||||
private String getSavedPasswdHash() {
|
||||
String pw;
|
||||
synchronized(_conf) {
|
||||
pw = _conf.getConf("auth.password", "");
|
||||
if (pw.equals("")) {
|
||||
pw = getPasswdHash(DEFAULT_AUTH_PASSWORD);
|
||||
_conf.setConf("auth.password", pw);
|
||||
_conf.writeConfFile();
|
||||
}
|
||||
}
|
||||
return pw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash input one time with SHA-256, return Base64 encdoded string.
|
||||
* @param string
|
||||
* @return Base64 encoded string
|
||||
*/
|
||||
public String getHash(String string) {
|
||||
SHA256Generator hashGen = _context.sha();
|
||||
byte[] bytes = string.getBytes();
|
||||
bytes = hashGen.calculateHash(bytes).toByteArray();
|
||||
return Base64.encode(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this password correct?
|
||||
* @return true if password is valid.
|
||||
* @since 0.12
|
||||
*/
|
||||
public boolean isValid(String pwd) {
|
||||
String storedPass = getSavedPasswdHash();
|
||||
byte[] p1 = DataHelper.getASCII(getPasswdHash(pwd));
|
||||
byte[] p2 = DataHelper.getASCII(storedPass);
|
||||
return p1.length == p2.length && DataHelper.eqCT(p1, 0, p2, 0, p1.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this password correct?
|
||||
* @return true if password is valid.
|
||||
* @since 0.12
|
||||
*/
|
||||
public boolean isDefaultPasswordValid() {
|
||||
return isValid(DEFAULT_AUTH_PASSWORD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Authentication Token if the provided password is valid.
|
||||
* The token will be valid for one day.
|
||||
* @return AuthToken if password is valid. If password is invalid null will be returned.
|
||||
*/
|
||||
public AuthToken validatePasswd(String pwd) {
|
||||
if (isValid(pwd)) {
|
||||
AuthToken token = new AuthToken(this, pwd);
|
||||
synchronized (authTokens) {
|
||||
authTokens.put(token.getId(), token);
|
||||
}
|
||||
return token;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new password. Old tokens will NOT remain valid, to encourage the new password being tested.
|
||||
* @param newPasswd
|
||||
* @return Returns true if a new password was set.
|
||||
*/
|
||||
public boolean setPasswd(String newPasswd) {
|
||||
String newHash = getPasswdHash(newPasswd);
|
||||
String oldHash = getSavedPasswdHash();
|
||||
|
||||
if (!newHash.equals(oldHash)) {
|
||||
_conf.setConf("auth.password", newHash);
|
||||
_conf.writeConfFile();
|
||||
synchronized (authTokens) {
|
||||
authTokens.clear();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the AuthToken with the given ID exists and if it does whether is has expired.
|
||||
* @param tokenID - The token to validate
|
||||
* @throws InvalidAuthTokenException
|
||||
* @throws ExpiredAuthTokenException
|
||||
*/
|
||||
public void verifyToken(String tokenID) throws InvalidAuthTokenException, ExpiredAuthTokenException {
|
||||
synchronized (authTokens) {
|
||||
AuthToken token = authTokens.get(tokenID);
|
||||
if (token == null)
|
||||
throw new InvalidAuthTokenException("AuthToken with ID: " + tokenID + " couldn't be found.");
|
||||
if (!token.isValid()) {
|
||||
authTokens.remove(tokenID);
|
||||
throw new ExpiredAuthTokenException("AuthToken with ID: " + tokenID + " expired " + token.getExpiryTime(), token.getExpiryTime());
|
||||
}
|
||||
}
|
||||
// Everything is fine. :)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up old authorization tokens to keep the token store slim and fit.
|
||||
* @author hottuna
|
||||
*
|
||||
*/
|
||||
private class Sweeper extends SimpleTimer2.TimedEvent {
|
||||
// Start running periodic task after 1 day, run periodically every 30 minutes.
|
||||
public Sweeper() {
|
||||
super(_context.simpleTimer2(), AuthToken.VALIDITY_TIME * 24*60*60*1000L);
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
_log.debug("Starting cleanup job..");
|
||||
synchronized (authTokens) {
|
||||
for (Iterator<AuthToken> iter = authTokens.values().iterator(); iter.hasNext(); ) {
|
||||
AuthToken token = iter.next();
|
||||
if (!token.isValid())
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
_log.debug("Cleanup job done.");
|
||||
schedule(30*60*1000L);
|
||||
}
|
||||
}
|
||||
}
|
245
src/java/net/i2p/i2pcontrol/servlets/JSONRPC2Servlet.java
Normal file
245
src/java/net/i2p/i2pcontrol/servlets/JSONRPC2Servlet.java
Normal file
@ -0,0 +1,245 @@
|
||||
package net.i2p.i2pcontrol.servlets;
|
||||
/*
|
||||
* Copyright 2011 hottuna (dev@robertfoss.se)
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
import com.thetransactioncompany.jsonrpc2.*;
|
||||
import com.thetransactioncompany.jsonrpc2.server.Dispatcher;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.router.RouterContext;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.PortMapper;
|
||||
|
||||
import net.i2p.i2pcontrol.I2PControlVersion;
|
||||
import net.i2p.i2pcontrol.security.KeyStoreProvider;
|
||||
import net.i2p.i2pcontrol.security.SecurityManager;
|
||||
import net.i2p.i2pcontrol.servlets.jsonrpc2handlers.*;
|
||||
import net.i2p.i2pcontrol.servlets.configuration.ConfigurationManager;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
|
||||
|
||||
/**
|
||||
* Provide an JSON-RPC 2.0 API for remote controlling of I2P
|
||||
*/
|
||||
public class JSONRPC2Servlet extends HttpServlet {
|
||||
|
||||
private static final long serialVersionUID = -45075606818515212L;
|
||||
private static final int BUFFER_LENGTH = 2048;
|
||||
private static final String SVC_HTTP_I2PCONTROL = "http_i2pcontrol";
|
||||
private static final String SVC_HTTPS_I2PCONTROL = "https_i2pcontrol";
|
||||
private Dispatcher disp;
|
||||
private Log _log;
|
||||
private final SecurityManager _secMan;
|
||||
private final ConfigurationManager _conf;
|
||||
private final JSONRPC2Helper _helper;
|
||||
private final RouterContext _context;
|
||||
private final boolean _isWebapp;
|
||||
private boolean _isHTTP, _isHTTPS;
|
||||
|
||||
/**
|
||||
* Webapp
|
||||
*/
|
||||
public JSONRPC2Servlet() {
|
||||
I2PAppContext ctx = I2PAppContext.getGlobalContext();
|
||||
if (!ctx.isRouterContext())
|
||||
throw new IllegalStateException();
|
||||
_context = (RouterContext) ctx;
|
||||
File appDir = ctx.getAppDir();
|
||||
_conf = new ConfigurationManager(ctx, appDir, false);
|
||||
// we don't really need a keystore
|
||||
//File ksDir = new File(ctx.getConfigDir(), "keystore");
|
||||
//ksDir.mkDir();
|
||||
//KeyStoreProvider ksp = new KeyStoreProvider(ksDir.getAbsolutePath());
|
||||
//_secMan = new SecurityManager(ctx, ksp, _conf);
|
||||
_secMan = new SecurityManager(ctx, null, _conf);
|
||||
_helper = new JSONRPC2Helper(_secMan);
|
||||
_log = ctx.logManager().getLog(JSONRPC2Servlet.class);
|
||||
_conf.writeConfFile();
|
||||
_isWebapp = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin
|
||||
*/
|
||||
public JSONRPC2Servlet(RouterContext ctx, SecurityManager secMan) {
|
||||
_context = ctx;
|
||||
_secMan = secMan;
|
||||
_helper = new JSONRPC2Helper(_secMan);
|
||||
if (ctx != null)
|
||||
_log = ctx.logManager().getLog(JSONRPC2Servlet.class);
|
||||
else
|
||||
_log = I2PAppContext.getGlobalContext().logManager().getLog(JSONRPC2Servlet.class);
|
||||
_conf = null;
|
||||
_isWebapp = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() throws ServletException {
|
||||
super.init();
|
||||
disp = new Dispatcher();
|
||||
disp.register(new EchoHandler(_helper));
|
||||
disp.register(new GetRateHandler(_helper));
|
||||
disp.register(new AuthenticateHandler(_helper, _secMan));
|
||||
disp.register(new NetworkSettingHandler(_context, _helper));
|
||||
disp.register(new RouterInfoHandler(_context, _helper));
|
||||
disp.register(new RouterManagerHandler(_context, _helper));
|
||||
disp.register(new I2PControlHandler(_context, _helper, _secMan));
|
||||
disp.register(new AdvancedSettingsHandler(_context, _helper));
|
||||
if (_isWebapp) {
|
||||
PortMapper pm = _context.portMapper();
|
||||
int port = pm.getPort(PortMapper.SVC_CONSOLE);
|
||||
if (port > 0) {
|
||||
String host = pm.getHost(PortMapper.SVC_CONSOLE, "127.0.0.1");
|
||||
pm.register(SVC_HTTP_I2PCONTROL, host, port);
|
||||
_isHTTP = true;
|
||||
}
|
||||
port = pm.getPort(PortMapper.SVC_HTTPS_CONSOLE);
|
||||
if (port > 0) {
|
||||
String host = pm.getHost(PortMapper.SVC_HTTPS_CONSOLE, "127.0.0.1");
|
||||
pm.register(SVC_HTTPS_I2PCONTROL, host, port);
|
||||
_isHTTPS = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
if (_isWebapp) {
|
||||
PortMapper pm = _context.portMapper();
|
||||
if (_isHTTP)
|
||||
pm.unregister(SVC_HTTP_I2PCONTROL);
|
||||
if (_isHTTPS)
|
||||
pm.unregister(SVC_HTTPS_I2PCONTROL);
|
||||
_secMan.stopTimedEvents();
|
||||
_conf.writeConfFile();
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
|
||||
httpServletResponse.setContentType("text/html");
|
||||
PrintWriter out = httpServletResponse.getWriter();
|
||||
out.println("<p>I2PControl RPC Service version " + I2PControlVersion.VERSION + " : Running");
|
||||
if ("/password".equals(httpServletRequest.getServletPath())) {
|
||||
out.println("<form method=\"POST\" action=\"password\">");
|
||||
if (_secMan.isDefaultPasswordValid()) {
|
||||
out.println("<p>The current API password is the default, \"" + _secMan.DEFAULT_AUTH_PASSWORD + "\". You should change it.");
|
||||
} else {
|
||||
out.println("<p>Current API password:<input name=\"password\" type=\"password\">");
|
||||
}
|
||||
out.println("<p>New API password (twice):<input name=\"password2\" type=\"password\">" +
|
||||
"<input name=\"password3\" type=\"password\">" +
|
||||
"<input name=\"save\" type=\"submit\" value=\"Change API Password\">" +
|
||||
"<p>If you forget the API password, stop i2pcontrol, delete the file <tt>" + _conf.getConfFile() +
|
||||
"</tt>, and restart i2pcontrol.");
|
||||
} else {
|
||||
out.println("<p><a href=\"password\">Change API Password</a>");
|
||||
}
|
||||
out.close();
|
||||
}
|
||||
|
||||
/** @since 0.12 */
|
||||
private void doPasswordChange(HttpServletRequest req, HttpServletResponse httpServletResponse) throws ServletException, IOException {
|
||||
httpServletResponse.setContentType("text/html");
|
||||
PrintWriter out = httpServletResponse.getWriter();
|
||||
String pw = req.getParameter("password");
|
||||
if (pw == null)
|
||||
pw = _secMan.DEFAULT_AUTH_PASSWORD;
|
||||
else
|
||||
pw = pw.trim();
|
||||
String pw2 = req.getParameter("password2");
|
||||
String pw3 = req.getParameter("password3");
|
||||
if (pw2 == null || pw3 == null) {
|
||||
out.println("<p>Enter new password twice!");
|
||||
} else {
|
||||
pw2 = pw2.trim();
|
||||
pw3 = pw3.trim();
|
||||
if (!pw2.equals(pw3)) {
|
||||
out.println("<p>New passwords don't match!");
|
||||
} else if (pw2.length() <= 0) {
|
||||
out.println("<p>Enter new password twice!");
|
||||
} else if (_secMan.isValid(pw)) {
|
||||
_secMan.setPasswd(pw2);
|
||||
out.println("<p>API Password changed");
|
||||
} else {
|
||||
out.println("<p>Incorrect old password, not changed");
|
||||
}
|
||||
}
|
||||
out.println("<p><a href=\"password\">Change API Password</a>");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException {
|
||||
if ("/password".equals(httpServletRequest.getServletPath())) {
|
||||
doPasswordChange(httpServletRequest, httpServletResponse);
|
||||
return;
|
||||
}
|
||||
String req = getRequest(httpServletRequest.getInputStream());
|
||||
httpServletResponse.setContentType("application/json");
|
||||
PrintWriter out = httpServletResponse.getWriter();
|
||||
JSONRPC2Message msg = null;
|
||||
JSONRPC2Response jsonResp = null;
|
||||
try {
|
||||
msg = JSONRPC2Message.parse(req);
|
||||
|
||||
if (msg instanceof JSONRPC2Request) {
|
||||
jsonResp = disp.process((JSONRPC2Request)msg, null);
|
||||
jsonResp.toJSONObject().put("API", I2PControlVersion.API_VERSION);
|
||||
if (_log.shouldDebug()) {
|
||||
_log.debug("Request: " + msg);
|
||||
_log.debug("Response: " + jsonResp);
|
||||
}
|
||||
}
|
||||
else if (msg instanceof JSONRPC2Notification) {
|
||||
disp.process((JSONRPC2Notification)msg, null);
|
||||
if (_log.shouldDebug())
|
||||
_log.debug("Notification: " + msg);
|
||||
}
|
||||
|
||||
out.println(jsonResp);
|
||||
out.close();
|
||||
} catch (JSONRPC2ParseException e) {
|
||||
_log.error("Unable to parse JSONRPC2Message: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String getRequest(ServletInputStream sis) throws IOException {
|
||||
Writer writer = new StringWriter();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(sis, "UTF-8"));
|
||||
char[] readBuffer = new char[BUFFER_LENGTH];
|
||||
int n;
|
||||
while ((n = reader.read(readBuffer)) != -1) {
|
||||
writer.write(readBuffer, 0, n);
|
||||
}
|
||||
return writer.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
package net.i2p.i2pcontrol.servlets.configuration;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.OrderedProperties;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* Manage the configuration of I2PControl.
|
||||
* @author mathias
|
||||
* modified: hottuna
|
||||
*
|
||||
*/
|
||||
public class ConfigurationManager {
|
||||
private final String CONFIG_FILE = "I2PControl.conf";
|
||||
private final String WEBAPP_CONFIG_FILE = "i2pcontrol.config";
|
||||
private final File configLocation;
|
||||
private final Log _log;
|
||||
private boolean _changed;
|
||||
|
||||
//Configurations with a String as value
|
||||
private final Map<String, String> stringConfigurations = new HashMap<String, String>();
|
||||
//Configurations with a Boolean as value
|
||||
private final Map<String, Boolean> booleanConfigurations = new HashMap<String, Boolean>();
|
||||
//Configurations with an Integer as value
|
||||
private final Map<String, Integer> integerConfigurations = new HashMap<String, Integer>();
|
||||
|
||||
|
||||
|
||||
public ConfigurationManager(I2PAppContext ctx, File dir, boolean isPlugin) {
|
||||
_log = ctx.logManager().getLog(ConfigurationManager.class);
|
||||
if (isPlugin) {
|
||||
configLocation = new File(dir, CONFIG_FILE);
|
||||
} else {
|
||||
configLocation = new File(dir, WEBAPP_CONFIG_FILE);
|
||||
}
|
||||
readConfFile();
|
||||
}
|
||||
|
||||
/** @since 0.12 */
|
||||
public File getConfFile() {
|
||||
return configLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects arguments of the form --word, --word=otherword and -blah
|
||||
* to determine user parameters.
|
||||
* @param settingNames Command line arguments to the application
|
||||
*/
|
||||
/****
|
||||
public void loadArguments(String[] settingNames) {
|
||||
for (int i = 0; i < settingNames.length; i++) {
|
||||
String settingName = settingNames[i];
|
||||
if (settingName.startsWith("--")) {
|
||||
parseConfigStr(settingName.substring(2));
|
||||
}
|
||||
}
|
||||
}
|
||||
****/
|
||||
|
||||
/**
|
||||
* Reads configuration from file, every line is parsed as key=value.
|
||||
*/
|
||||
public synchronized void readConfFile() {
|
||||
try {
|
||||
Properties input = new Properties();
|
||||
// true: map to lower case
|
||||
DataHelper.loadProps(input, configLocation, true);
|
||||
parseConfigStr(input);
|
||||
_changed = false;
|
||||
} catch (FileNotFoundException e) {
|
||||
if (_log.shouldInfo())
|
||||
_log.info("Unable to find config file, " + configLocation);
|
||||
} catch (IOException e) {
|
||||
_log.error("Unable to read from config file, " + configLocation, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write configuration into default config file.
|
||||
* As of 0.12, doesn't actually write unless something changed.
|
||||
*/
|
||||
public synchronized void writeConfFile() {
|
||||
if (!_changed)
|
||||
return;
|
||||
Properties tree = new OrderedProperties();
|
||||
tree.putAll(stringConfigurations);
|
||||
for (Entry<String, Integer> e : integerConfigurations.entrySet()) {
|
||||
tree.put(e.getKey(), e.getValue().toString());
|
||||
}
|
||||
for (Entry<String, Boolean> e : booleanConfigurations.entrySet()) {
|
||||
tree.put(e.getKey(), e.getValue().toString());
|
||||
}
|
||||
try {
|
||||
DataHelper.storeProps(tree, configLocation);
|
||||
_changed = false;
|
||||
} catch (IOException e1) {
|
||||
_log.error("Couldn't open file, " + configLocation + " for writing config.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the input as 'key=value',
|
||||
* where value will (in order) be parsed as integer/boolean/string.
|
||||
* @param str
|
||||
*/
|
||||
private void parseConfigStr(Properties input) {
|
||||
for (Entry< |