diff --git a/apps/q/doc/client.jpg b/apps/q/doc/client.jpg new file mode 100644 index 000000000..1d3702b50 Binary files /dev/null and b/apps/q/doc/client.jpg differ diff --git a/apps/q/doc/diagrams.html b/apps/q/doc/diagrams.html new file mode 100644 index 000000000..633dda014 --- /dev/null +++ b/apps/q/doc/diagrams.html @@ -0,0 +1,26 @@ + + +
+
+
+
+ + Q is a distributed Peer2Peer file storage/retrieval network that aims to deliver optimal + performance by respecting the properties of the I2P network.+ + + +
+
+ This manual serves as a 'walkthrough' guide, to take you through the steps from initial + download, to everyday usage. It also provides information for the benefit of higher-level + client application authors. +
+ OK, we assume here that you've already cracked the tarball, and are looking at + the distribution files.+ + + +
+
+ In order to get Q set up and running, you'll need: ++
+- An I2P router installed, set up and (permanently or transiently) running
+- Your system shell set up with at the environment variables: +
+
+- CLASSPATH - this should include: +
++
+- The regular I2P jar files and 3rd party support jar files (eg i2p.jar, + i2ptunnel.jar, streaming.jar, + mstreaming.jar, jbigi.jar)
+- Apache's XML-RPC support jarfile - included in this Q distro as + xmlrpc.jar
+- Aum's jarfile aum.jar, which includes Q and all needed support code
+- PATH - your execution search path must include the directory + in which your main java VM execution program (java, or on windows systems, + java.exe) resides.
+
+ NOTE - if java[.exe] is not on your PATH, then Q will + not run.
+ Nearly everyone will want to run a Q Client Node.+ + + +
+
+ It is only client nodes which provide users with full access to the Q network.
+
+ However, if you have a (near-) permanently running I2P Router, and you're a kind and + generous soul, you might also be willing to run a Q Server Node in addition + to your Q Client Node.
+
+ If you do choose to run a server node, you'll be expected to keep it running as near as + possible to 24/7. While transience of client nodes - frequent entering and leaving the + Q network - causes little or no disruption, transience of server nodes can significantly + impair Q's usability for everyone, particularly if this transience occurs frequently amongst + more than the smallest percentage of the server node pool.
+
+ Until you're feeling well "settled in" with Q, your best approach is to just run a + client node for now, and add a server node later when you feel ready.
+
+ This chapter discusses the deployment and usage of a Q Client Node, and will take you + through the steps of: ++ ++
+ Setup and running of Q Server Nodes will be discussed in a later chapter. +- Double-checking that you've met the installation requirements
+- Launching a Q Client Node
+- Verifying that your Q Client Node is running
+- If your node fails to launch, figuring out why
+- Importing one or more noderefs into your node
+- Observing that your node is discovering other nodes on the network
+- Observing that your node is discovering content on the network
+- Searching for items of content that match chosen criteria
+- Retrieving stuff from the network
+- Inserting stuff to the network
+- Shutting down your client node
+
+ Ensure that all the needed I2P jarfiles, as well as xmlrpc.jar and + Q's very own aum.jar are correctly listed in your CLASSPATH environment + varaible, and your main java launcher is correctly listed in your PATH environment + variable.+ +
+
+ Typically, you will likely copy the jarfiles aum.jar and xmlrpc.jar + into the lib/ subdirectory of your I2P router installation, along with all + the other I2P jar files. Wherever you choose to put these files, make sure they're + correctly listed in your CLASSPATH. +
+ Also, you'll want to add execute permission to your qmgr (or qmgr.bat) + wrapper script, and copy it into one of the directories listed in your PATH + environment variable.
+
+ qmgr (or qmgr.bat) is a convenience wrapper script to save your + sore fingers from needless typing. It's just a wrapper which passes arguments + to the java command+ +java net.i2p.aum.q.QMgr
+
+ You can verify you've set up qmgr correctly with the command: ++ This displays a brief summary of qmgr commands. On the other hand, the command: ++qmgr help+ floods your terminal window with a detailed explanation of all the qmgr commands + and their arguments.+qmgr help verbose
+
+ Provided you've successfully completed the preliminaries, you can launch your + Q Client Node with the command: ++ ++ + All going well, you should have a Q Client Node now running in background. ++qmgr start
+ After typed the qmgr start command, you will see little or no + evidence that Q is actually running.+ +
+
+ You can test if the node is actually up by typing the command: ++ If your Q Client Node is running, this status command should produce + something like: ++qmgr status+ + If you see something like this, then smile, because Q is now up on your system.+Pinging node at '/home/myusername/.quartermaster_client'... +Node Ping: + status=ok + numPeers=0 + dest=-3LQaE215uIYwl-DsirnzXGQBI31EZQj9u~xx45E823WqjN5i2Umi37GPFTWc8KyislDjF37J7jy5newLUp-qrDpY7BZum3bRyTXo3Udl8a3sUjuu4qR5oBEWFfoghQiqDGYDQyJV9Rtz7DEGaKHGlhtoGsAYRXGXEa8a43T2llqZx2fqaXs~836g8t6sLZjryA5A9fpq98nE5lT0hcTalPieFpluJVairZREXpUiAUmGHG7wAIjF6iszXLEHSZ8Qc622Xgwy0d1yrPojL2yhZ64o05aueYcr~xNCiFxYoHyEJO3XYmkx~q-W-mzS3nn6pRevRda74MnX1~3fFDZ0u~OG6cLZoFkWgnxrwrWGFUUVMR87Yz251xMCKJAX6zErcoGjGFpqGZsWxl4~yq7yfkjPnq3GuTxp2cB75bRAOZRIAieqBOVJDEodFYW5amCinu4AxYE7G1ezz4ghqHFe~0yaAdO74Q1XoUny138YT6P33oNOOlISO1cAAAA + uptime=4952 + load=0.0 + id=6LVZ9-~GgJJ52WUF1fLHt3UnH50TnXSoPQXy7WZ4GA= + numLocalItems=47 + numRemoteItems=2173
+
+ If the node launch failed, you might see something like: ++ This indicates that your Q client node has either crashed, or failed to launch in the + first place.+Pinging node at '/home/myusername/.quartermaster_client'... +java.io.IOException: Connection refused + at org.apache.xmlrpc.XmlRpcClient$Worker.execute(Unknown Source) + at org.apache.xmlrpc.XmlRpcClient.execute(Unknown Source) + at net.i2p.aum.q.QMgr.doStatus(QMgr.java:310) + at net.i2p.aum.q.QMgr.execute(QMgr.java:813) + at net.i2p.aum.q.QMgr.main(QMgr.java:869) +Failed to ping node
+
+ If you're having trouble like this, you might like to try running your Q client node + in foreground, instead of spawning it off into background.
+
+ The command to run a Q client node in foreground is: ++ You should see some meaningless startup messages, and no return to your shell prompt.+qmgr foreground
+ +
+ By default, when you run a Q Client Node, it creates a datastore directory tree + at ~/.quartermaster_client. (Windows users note - you'll find this directory + wherever your user home directory is - this depends on what version of Windows + you have installed).+ +
+
+ Within this directory tree, you should see a file called node.log, which + will contain various debug log messages, and can help you to rectify any problems + with your Q installation. If you hit a wall and can't rectify the problems + yourself, you should send this file to the Q author (aum).
+
+ It's possible to run your Q node from another directory, by passing that directory + as a -dir <path> argument to the + qmgr start, foreground and stop + commands. See qmgr help verbose for more information. +
+ Note from the prior qmgr status command the line: ++ ++ This means that your Q client node is running standalone, and doesn't have any contact + with any Q network. As such, your node is effectively useless. We need to hook up + your node with other nodes in the Q network.+numPeers=0
+
+ Q doesn't ship with any means for new client nodes to automatically connect to any Q + server nodes. This is deliberate.
+
+ In all likelihood, there will be one 'main' Q network running within I2P, largely + based around the author's own Q server node, and most people will likely want to + use this Q network. But the author doesn't want to stop other people running their + own private Q networks, for whatever purpose has meaning for them. + +++ + Ok, getting back on topic - your brand new virgin Q client node is useless and lonely, + and desperately needs some Q server nodes to talk to. So let's hook up your node to + the mainstream Q network.
+ This is especially relevant for Q as opposed to Freenet. With Freenet, there's + no way for a user to know of the existence of any item of content without + first being given its 'key'. However, since Q works with published catalogs, + any user can know everything that's available on a Q network, which might + not be desirable to those wishing to share content in a private situation.
+
+ The Q author anticipates, and warmly supports, people running their own + private Q networks within I2P, in addition to accessing the mainstream + 'official' Q network.
+
+ The way Q is designed and implemented, there is no way for anyone, including + Q's author, to know of the existence of anyone else's private Q network. + It is beyond the author's control, (and thus arguably the author's + legal responsibility), what private Q networks people set up, and what + kind of content is trafficked on these networks. This claim of plausible + deniability on the part of Q's author parallels that of a hardware retailer + denying responsibility for what people do with tools that they purchase. +
+ +
+
+ You'll need to get one or more 'noderefs' for Q server nodes.
+
+ There's nothing fancy about a Q noderef. It's just a regular I2P 'destination', with + which your Q Client Node can connect with a Q Server Node.
+
+ A 'semi-official' list of noderefs for the mainstream Q network can be downloaded + from the url: http://aum.i2p/q/qnoderefs.txt.
+
+ Download this file, save it as (say) qnoderefs.txt. (Alternatively, if you're + wanting to subscribe into a private Q network, then get a noderef for at least one + of that network's server nodes from someone on that network who trusts you).
+
+ Import these noderefs into your Q client node via the command: ++ If all goes well, you should see no output from this command, or (possibly) a brief + line or two suggesting success.+qmgr addref qnoderefs.txt
+
+ Your client node is now subscribed into the Q network of your choice. Verify this + with the command: ++ In the output from that command, you should see the numPeers= line showing at least + 1 peer.+qmgr status
+
+ If there is more than one Q Server Node on the Q network you've just subscribed to, + then your local node should sooner or later discover all these server nodes, and + the numPeers value should increase over time.
+
+++ + When your client node gets its noderefs to a Q network, it will periodically, + from then on, retrieve differential peer list and catalog updates from servers + it knows about.
+ While Q is in its early development and testing stages, the author may abdicate + the mainstream Q network, and publish nodrefs for a whole new mainstream Q network. + This will especially happen if the author makes any substantial changes to the + inter-node protocol, and/or releases incompatible new versions of Q client/server + nodes. Remember that + http://aum.i2p/q/qnoderefs.txt will + serve as the authoritative source for noderefs for the mainstream Q network within + the mainstream I2P network. +
+
+
+ Even if you only feed your client just one ref for a single server node, it will + in time discover all other operating server nodes on that Q network, and will + build up a full local catalog of everything that's available on that Q network.
+
+ Provided that your client is running ok, and has been fed with at least one + ref for a live Q network that contains content, then over time, successive: ++ commands should report increasing values in the fields: ++qmgr status+
+ +- numPeers - number of peers this client node knows about
+- numLocalItems - number of locally stored content items, ie items + which you have either inserted to, or retrieved from, your client node
+- numRemoteItems - number of unique data items which are available + on remote server nodes in the Q network, and which can be retrieved through + your local client node.
+++
+ +4.7.1. One Big Warning
+ + If you are participating in more than one distinct Q network, then do not + insert noderefs for different networks into the same running instance of a + local Q client, unless you don't plan on inserting content via that client.
+
+ For instance, let's say you are participating in two different Q networks: ++
+ If you get a noderef for both these networks, and insert both of these into the + same running Q client node, then this local client node will be transparently + connected to both networks.- The 'mainstream' Q netowrk
+- A secret Q network - "My friends' teen angst diaries"
+
+
+ If you only ever plan on retrieving content, and never inserting content, this + won't be a problem, except that you won't be able to tell which content + resides on the mainstream Q network, and which resides in the secret Q network.
+
+ The big problem arises from inserting content. Whenever you insert data through this + 'contaminated' + Q client node, this node picks 3 different servers to which upload a copy of this + data. You won't have any control over whether the data gets inserted to the mainstream + Q network, or your secret Q network. You might insert something sensitive, intending it + to go only into the secret Q network, where in fact it also ends up in the mainstream + network, with consequences you might not want. +
+ Whenever content gets stored on Q, it is actually stored as two separate items: ++ ++
+ Metadata consists of a set of category=value pairs.- The raw data - whether a text file, or the raw bytes of image files, + audio files etc
+- The metadata, which contains human-readable and machine-readable + descriptions of the data
+
+
+ Confused yet? Don't worry, I'm confused as well. Let's illustrate this with an + example of metadata for an MP3 audio recording: ++
+ All metadata categories are optional. In fact, you can insert content with no metadata + at all.- title=Fight_Last_Thursday.mp3
+- type=audio
+- mimetype=audio/mpeg
+- abstract=upcoming single recorded in our garage last April
+- keywords=grunge,country,indie
+- artist=Ring of Fire
+- size=4379443
+- contact=ring-of-fire@mail.i2p
+- key=blah37blah24-yada23hfhyada
+
+
+ If you fail to provide metadata when inserting an item, a blank set of metadata will + be created with at least the following categories: ++
+ Within Q, there is a convention to supply a minimal amount of metadata. While this + is not expected or enforced, including all these categories is most strongly + recommended. These core categories are: +- key - the derived key, under which the item will later be retrievable + by yourself and others
+- title - if not provided at insert time, this will be set to the key
+- size - size of the item's raw data, in bytes
++
+ Note that you can supply extra metadata categories in addition to the above, and that + people searching for content can search on these extra categories if they know about + them. +- title - a meaningful title for the data item, consisting only of characters + which are legal in filenames on all platforms, and which ends with a file extension.
+- type - one of a superset of eMule classifiers, such as: +
++
+- text - plain text
+- html - HTML content
+- image - content is in an image format, such as .png, .jpg, .gif etc
+- audio - content is an audio sample, such as .ogg, .mp3, .wav etc
+- video - due to the sheer size of video files, and Q's present design, + it's unlikely people will be inserting video content anytime soon (unless it's + very short)
+- archive - packed file collections, such as .tar.gz, .zip, .rar etc
+- misc - content does not fit into any of the above categories
+- mimetype - not as important as the type category, but providing + this category in your metadata is still strongly encouraged. Value for this category + should be one of the standard mimetypes, eg text/html, audio/ogg etc.
+- abstract - a short description (<255 characters), intended for human reading
+- keywords - a comma-separated list of keywords, intended for + machine-readability, should be all lowercase, no spaces
+
+ As mentioned earlier - in constrast with Freenet, local Q nodes build up a complete + catalog of all available content on whatever Q network they are connected to.+ +
+
+ This is a design decision, based on the choice to eliminate query traffic.
+
+ The author hopes that this will result in a distributed storage network with a + high retrievability guarantee, in contrast with freenet which offers no such + guarantee.
+
+ With Freenet, you only ever know of the existence of something if someone tells + you about it.
+
+ But with Q, your local client node builds up a global catalog of everything that's + available within the whole network.
+
+ The QMgr client has a command for searching your Q client node: ++ For example: ++qmgr search -m category1=pattern1 category2=pattern2 ...+ or: ++qmgr search -m type=audio artist=Mozart keywords=symphony+ As implied in the latter example, search patterns are regular expressions. This example will + locate all text items, whose title metadata category contains one of bible, biblical or Nag Hammadi, and whose keywords category contains either + or both the words apocrypha or Magdalene.+qmgr search -m type=text title="bible|biblical|(Nag Hammadi)" keywords="apocrypha|Magdalene"
+
+ Please use the search function carefully, otherwise (if and when Q usage grows) you + could be inundated with thousands or even millions of entries.
+
+ If a search turns up nothing, qmgr will simply exit. But if it turns up one or more items, + it will the items out one at a time, with the key first, then each metadata entry + on an indented line following. +
+ Now, we're actually going to retrieve something.+ +
+
+ Presumably, after following the previous section, you will have seen one or more search + results come up, with the 'keys' under which the items can be accessed.
+
+ Now, choose one of the keys, preferably for a short text item. Try either of the following + commands: ++or: ++qmgr get <keystring> something.txt+ (both have the same effect - the first one explicitly writes to the named file, the second + one dumps the raw data to stdout, which we shell-redirect into the file.+qmgr get <keystring> > something.txt
+
+ Note - redirection of fetched data to a file via shell is not working at present. Use only + the first form till we fix the bug. + +
+ Our last example in this walkthrough relates to inserting content.+ +
+
+ Firstly, create a small text file with 2-3 lines of text, and save it as (say) + myqinsert.txt.
+
+ Now, think of some metadata to insert along with the file. Or, you can just use + the set: ++ + Now, let's insert the file. Ensure your Q client node is running, then type: ++type=text +keywords=test +abstract=My simple test of inserting into Q +title=myqinsert.txt+ If all went well, this command should produce half a line of gibberish, followed + immediately by your shell prompt, eg: ++qmgr put myqinsert.txt -m type=text keywords=test title="myqinsert.txt" \ + abstract="My simple test of inserting into Q"+ The '$' at the end is your shell prompt, and all the characters before it are the 'key' + which was derived from the content you just inserted.+aRoFC~9MU~pM2C-uCTDBp5B7j79spFD8gUeu~BNkUf0=$ +
+
+ To avoid the hassle of copying/pasting the key, you could just add output redirection + to the above command, eg: ++ This will cause the generated key to be written safe and sound into the file + myqinsert.key.+qmgr put myqinsert.txt -m type=text keywords=test title="myqinsert.txt" \ + abstract="My simple test of inserting into Q" \ + > myqinsert.key
+
+ You can verify that this insert worked by a 'get' command, as in: ++ (Note that this won't work on windows because the DOS shell is irredeemably brain-damaged. If + you're using Windows, you will have to cut/paste the key. ++qmgr get `cat myqinsert.key` somefilename.ext
+ If you've worked through to here, then congratulations! You've got your Q Client Node set up + and working, and ready to meet all your distributed file storage and retrieval needs.+ + + +
+
+ You can leave your client node running 24/7 if you want. In fact, we recommend you keep your + client node running as much of the time as possible, so that you get prompt catalog updates, + and can more quickly stay in touch with new content.
+
+ However, if you need to shut down your node, the command for doing this is: ++ This command will take a while to complete (since the node has to wait for the I2P + java shutdown hooks to complete before it can rest in peace). But once your node is + shut down, you can start it up again at any time and pick up where you left off. ++qmgr stop
+ This section describes the requirements for, and procedures involved with, running + a Q Server Node.+ +
+
+ We'll use a similar 'walkthrough' style to that which we used in the previous section + on client nodes. +
+ Running a Q server is a generous thing to do, and helps substantially with making + Q work at its best for everyone. However, please do make sure you can meet some + basic requirements: ++ ++
+ Also, please decide whether you want your server node to contribute to the mainstream + Q network, or whether you want to create your own private Q network, or join someone + else's private network. Your contribution will be most appreciated, though, if you + can run a server within the mainstream Q network. +- You are running a permanent (24/7) I2P Router, on a box with at least (say) + 98% uptime.
+- You have a little bandwidth to spare, and don't mind the extra memory, disk and + CPU-usage footprint of running a fulltime Q server node
+- You have already been able to successfully run a Q client node.
+
+ Starting up a Q Server node is very similar to starting up a Q client node, except + that with the qmgr command line, you must put the keyword arg server before the + command word. So the command is: ++ ++ Similar to Q client nodes, you can check the status of a running Q server node with + the command: ++qmgr server start+ (Note that this command will take longer to complete than with client nodes, because + the communication passes through a multi-hop I2P tunnel, rather than just through + localhost TCP).+qmgr server status
+
+ If the status command succeeds, then you'll know your new Q Server Node is happily + running in background. +
+ When a Q Server node starts up for the first time, it is in a private network + all by itself.+ +
+
+ If you want to link your server into an existing Q network, you'll have to add a + noderef for at least one other server on that network. The command to do this + is similar to that for subscribing a client node to a network: ++ where <noderef-file> is a file into which you've saved the noderef for + the network you want to join. ++qmgr server addref <noderef-file>++ After you've added the noderef, subsequent qmgr server status commands + should show numPeers having a value of at least 1 (and growing, as more + server nodes come online in the mainstream Q network.) + +
+ Recall from the section on client nodes that the authoritative noderefs + for the mainstream Q network can be downloaded from + http://aum.i2p/q/qnoderefs.txt. +
+
+ If you're planning to start your own private Q network, and want to include other + server operators in this network, then you'll have to export your server's noderef + and make it available to the others you want to invite into your network.+ + + +
+
+ The command to export your Q Server noderef is: ++ This will extract the I2P Destination of your running server node, and + write it into <noderef-file>. You can then privately share this file with + others who you want to invite into your private network. Each recipient of + this file will do a qmgr server addref <noderef-file> command + to import your ref into their servers.+qmgr server getref <noderef-file>
+
+ Don't forget that if you're running, or participating in, a private Q network, then + you'll need to run a separate client for accessing this network, separate from any + mainstream Q network client you may already be running.
+
+ To start this extra client, you'll have to choose a directory where you want this + client to reside, a port number you want your client to listen on locally for + user commands, and run the command: ++ You need the -port <portnum> command, because otherwise it'll fail + to launch (if you already have a client node running off the mainstream Q network).+qmgr -dir /path/to/my/new/client -port <portnum> start
+
+ This will create, and launch, a new instance of a Q client, accessing your private + Q network. Don't forget to import your server's noderef into this client. Also, + note that you'll have to use this same -port <portnum> argument when + doing any operation on this client instance, such as get, put, status, search. + +
+qmgr help
+ for a quick help summary, or:
+
+qmgr help verbose
+ for the 'War and Peace' treatise.+ +
+ One crucial concept to remember with qmgr is that client and server node instances + are uniquely identified by the directories at which they reside. If you are running + multiple server and/or client instances, you can specify an instance with the + -dir <dirpath> option - see the help for details. +
| Term | +Definition | +
key |
+ A metadata category name, technically a key as the word is used with
+ Java Hashtable and Python dict objects. |
+
uri |
+ A Uniform Resource Indicator for an item of content stored within the Q network. + Q URIs have the form: Q:<basename>[,<cryptoKey>][<path>]
+ + + Some examples: +
|
+
basename |
+ The basic element of a Q uri. This will be a base64-encoded hash - refer below to + URI calculation procedures | +
cryptoKey |
+ An optional session encryption key for the stored data, encoded as base64. + This affords some protection to server node operators, and gives them a level + of plausible deniability for whatever gets stored in their server's + datastore without their direct human awareness. | +
path |
+ Whever an item of content is inserted in secure space mode, this path
+ serves as a pseudo-pathname, and is conceptually similar to the path
+ component in (for example) standard HTTP URLs
+ http://<domainname>[:<port>][<path>], such as
+ http://slashdot.org/faq/editorial.shtml (whose path
+ is /faq/editorial.shtml).+ + Paths, if not empty, should contain a leading slash ("/"). + If an application specifies a non-empty path that doesn't begin with a
+ leading '/', a '/' will be automatically prepended by the receiving node.
+ |
+
plain hash |
+ A mode of inserting items, whereby the security of the resulting URI comes from
+ computing the URI from a hash of the item's data and metadata (and imposing a
+ mathematical barrier against spoofing content under a given URI). Corresponds to
+ Freenet's CHK@ keys. |
+
secure space |
+ A mode of inserting items where the security of the URI is based not on a hash of the
+ item's data and metadata (as with plain hash mode),
+ but on the privateKey provided by the
+ application, and a content signature created from that private key.
+ Corresponds to Freenet's SSK@ keys. Within a secure space, you
+ can insert any number of items under different pseudo-pathnames (as is the case
+ with Freenet SSK keys).
+
+ |
putItem RPCsputItem RPC on the local Q client node.| Key | +Data Type | +Description | +
title |
+ String | +Optional but strongly recommended. A free-text short description of the item, + should be less than 80 characters. The idea is that applications should + support a 'view' of catalogue data that shows item titles. (Prior Q convention of + titles expressed as valid filename syntax has been abandoned). + | +
path |
+ String | +Optional but strongly recommended.
+ A virtual 'pathname' for the item, which should be in valid *nix
+ absolute pathname syntax (beginning with '/', containing no '//', consisting
+ only of alphanumerics, '-', '_', '.' and '/'. + + In Q web interfaces, the filename component of this path will
+ serve as the recommended filename when downloading/saving the item.+ + If the application also provides a + privateKey key, the path
+ is used in conjunction with the private key to generate publicKey
+ and signature keys (see below), and ultimately the final uri
+ under which the item can be retrieved by others.+ + Refer also to mimetype below.
+ |
+
encrypt |
+ String | +Optional. If this key is present, and has a value "1", "yes" or "true",
+ this indicates that the application wishes the data to be stored on servers in
+ encrypted form. + + If this key is present and set to a positive value, the Q node, on receiving the + putItem RPC, will:
+
|
+
type |
+ String | +Optional but strongly recommended. A standard ed2k specifier, one of text html image
+ audio video archive other |
+
mimetype |
+ String | +Optional but moderately recommended. Mimetype designation of data, eg text/html,
+ image/jpeg etc. If not specified, an attempt will be made to guess
+ a mometype from the value of the path key. If this attempt fails, then
+ this key will be set to application/x-octet-stream by the node receiving
+ the putItem RPC. |
+
keywords |
+ String | +Optional but moderately recommended. + A set of keywords, under which the inserting app would like this item to be + discoverable. Keywords should be entirely lower case and comma-separated. Content + inserts should consider keywords carefully, and only use space characters inside + keywords when necessary (eg, for flagging a distinctive phrase containing very + common words). | +
privateKey |
+ String | +Optional. A Base64-encoded signing private key, in cases where the application wishes
+ to insert an item in signed space mode. This can be accompanied by another key,
+ path, indicating a 'path' within the signed space. If 'path'
+ is not given, it will default to '/'.+ + Either way, when a node receives a + putItem RPC containing a privateKey in its metadata,
+ it removes this key and replaces it with publicKey and
+ signature.
+ |
+
path |
+ String | +Optional. The virtual pathname, within signed space, under which to store the item.
+ This gets ignored and deleted unless the application also provides a
+ privateKey as well. But if the private key is given, the path
+ is used in conjunction with the private key to generate publicKey
+ and signature keys (see below).+ path should be a 'unix-style pathname', ie, containing only slashes
+ as (pseudo) directory delimiters, and alphanumeric, '-', '_' and '.' characters,
+ and preferably ending in a meaningful file extension such as .html
+ |
+
expiry |
+ int | +Unixtime at which the inserted item should expire. When this expiry time
+ is reached, the item won't necessarily be deleted straight away, but may
+ be deleted whenever a node's data store is full. + + If this is not provided, it will default to a given duration according to + the client node's configuration. + + If it is provided, by an application, then the client node will transparently + generate the required 'rent payment' before caching the data item and uploading + it to servers. + |
+
putItem RPC| Key | +Data Type | +Description | +
size |
+ Integer | +Size of the data to be inserted, in bytes. | +
dataHash |
+ String | +base64-encoded SHA256 hash of data. | +
uri |
+ String | +This depends on whether the item is being inserted in plain or
+ signed space mode. + + If inserting in plain mode, then the uri is in the form + Q:somebase64hash, where the hash is computed according to
+ the plain hash calculation procedure.+ + If inserting in signed space mode, then the uri will be in the form + Q:somebase64hash/path.ext, where the hash is computed as per
+ the signed space hash calculation procedure, and
+ the /path.ext is the verbatim value of the app-supplied
+ path key.
+ |
+
publicKey |
+ String | +Base64-encoded signing public key. In cases where app provides
+ privateKey,
+ a node will derive the signing public key from the private key,
+ delete the private key from the metadata, and replace it with its corresponding
+ public key
+ key. |
+
signature |
+ String | +Base64-encoded signature of path+dataHash, created using
+ the app-provided privateKey. |
+
rent |
+ String | +A rent payment for the data's accommodation on the server. + Intention is to support a variety of payment tokens. Initially, the + only acceptable form of payment will be a hashcash-like token, + in the form hashcash:base64string. The hashcash:
+ prefix indicates that this payment is in hashcash currency, in which case
+ the base64String should decode to a 16-byte string whose
+ SHA256 hash partially collides with dataHash.
+ The greater the number of bits in the collision,
+ the longer the data's accommodation will be 'paid up for'.+ + If this key is already present, a Q node will verify the hashcash, + and adjust the expiry key value to the time the item's accommodation
+ is paid up till.+ + If the key is not present: +
|
+
plain mode, the final URI is determined from
+ a hash of the data and metadata. Security of the item is based on the mathematical difficulty
+ of creating an arbitrary data+metadata set whose hash collides with the target URI.size is missing, set this to the size of the data,
+ in bytesdataHash is missing, set this to the base64-encoded
+ SHA256(data)title is missing, set this to the value of dataHashkey=value,
+ where each line contains a metadata key and its value, and
+ is terminated by an ASCII linefeed (\n, 0x10).uri is omittedQ: to thisprivateKey
+ given by the application.path (recall that path,
+ if not empty, must begin with a '/')Q: to thisQ:pubkeyHash/path.ext
+
+ + + For example, with the getItem primitive, server nodes will only look in + their local content store for the item, returning either that item's data and + metadata, or a failure reply. On the other hand, client nodes will try their + local content store first, and if the item is not found, will look in their + peer catalogues. If the item is found in a peer catalogue, the client node will + then on-send getItem calls to all server nodes believed to hold that item, + until or unless it retrieves a verifiable copy of that item + ++ +
++ + XML-RPC supports a canonical set of data types, which are seamlessly integrated into + all its high level language implementations. A quick overview of the XML-RPC data types + used in Q appears below.+ + It's possibly a good idea here to get a hold of the XML-RPC library for + your favourite programming language, as well as the manual, and look up + the description of data types. Also, if you're especially keen, + you might like to read up on XML-RPC in general: + + ++
| XML-RPC Data Type | +Description | +
| int | +Plain 32-bit integer | +
| string | +Sequence of ASCII bytes, viewed as java.lang.String objects in java, and str + objects (strings) in Python. + Note that ASCII control chars, and high-bit-set chars, are highly illegal and will + cause failure. | +
| binary data | +Raw binary data, viewed as byte [] in java, and xmlrpclib.Binary objects + in Python. This is the format used for raw content data. | +
| list | +Sequence of objects, viewed as java.util.Vector in java, and list objects in Python. + | +
| struct | +An unordered set of (key, value) pairs. + Represented as java.util.Hashtable objects in java, and + dict objects in Python, (associative array in perl, ...) | +
+ In certain cases, XML-RPC calls to Q nodes may return an exception.+ +
+
+ For instance, any attempt to invoke any primitive other than those listed below + will most definitely cause an exception, because in the Q XML-RPC implementation, + no provision is made for default handlers.
+
+ Apart from this, it's possible that calls to known legal methods may trigger an + exception. This is not supposed to happen, and the author will be working over + time to intercept all such exceptions and wrap them in appropriate response + structures. But in the meantime, client app developers should catch any exceptions + resulting from their XML-RPC calls and recover appropriately. +
+ ++ +Overview
+ ++ + The i2p.q.ping primitive is used to test if a given server or client node + is presently online. It can be sent by server nodes, client nodes and client apps. + ++ +Arguments
+ ++ This primitive accepts no arguments, and will fail if any arguments are given. ++ +Server Behaviour
+ ++ No action on the part of the receiving server is required, apart from sending back: +++
+ ++ Key Type Description + +status +string +"ok" ++ +id +string +The node's nodeId, as a base64 string ++ +dest +string +Node's destination, represented as base64 string ++ +uptime +int +The number of seconds that this node has been running for ++ +load +float +Current load this node is experiencing, as a float between + 0.0 (no load) to 1.0 (impossibly flatlined) +Client Behaviour
+ ++ Same as server. ++ +
+ ++ +Overview
+ ++ + The i2p.q.hello primitive is sent by new server nodes to advise other existing + server nodes of their existence. It is only sent by server nodes to other server + nodes. It is considered an abuse for a client node to send this command. + ++ +Arguments
+ ++
+ +- destination (string) - the base64 representation of the calling node's + I2P destination (on which the calling node's in-I2P XML-RPC server may be + subsequently reached). Same format as the I2P hosts.txt listing. +
Server Behaviour
+ ++ If the destination is valid, the receiving server will reply with: ++ ++
+ ++ Key Type Description + +status +string +"ok" ++ If the destination is invalid, the receiving server will send back: ++ ++
+ ++ Key Type Description + +status +string +"error" ++ +error +string +"baddest" +Client Behaviour
+ ++ i2p.q.hello calls to clients are illegal. Client nodes receiving such + calls will respond with: ++ ++
+ ++ Key Type Description + +status +string +"error" ++ +error +string +"unimplemented" +
+ ++ +Overview
+ ++ + The i2p.q.getItem primitive is used to attempt retrieval of an item of content + from a client or server node. + ++ +Arguments
+ ++
+ +- key (string) - the base64 key under which the item in question is + stored
+Server Behaviour
+ ++ Servers receiving this command will only search their own datastore for the item. + They will never attempt to on-request this item from other servers.+ +
+
+ If the server possesses the requested item in its datastore, it will respond with: ++
+ ++ Key Type Description + +status +string +"ok" ++ +metadata +struct +A nested struct, containing the metadata for the key. (Refer section on + metadata). ++ +data +binary data +The raw data. ++ If the server doesn't possess the data, it will respond with: ++ ++
+ + ++ Key Type Description + +status +string +"error" ++ +error +string +"notfound" +Client Behaviour
+ ++ If the client possesses the key in its own local datastore, it will send back + the full data immediately: ++ ++
+ ++ Key Type Description + +status +string +"ok" ++ +metadata +struct +A nested struct, containing the metadata for the key. (Refer section on + metadata). ++ +data +binary data +The raw data. ++ If the client doesn't possess the key, it will search its internal catalogues + for a server which does have the key.+ +
+
+ If one or more servers possessing the key are found, the client will on-send + an i2p.q.getItem command to each of those servers in turn, until it + either successfully retrieves the data, or fails.
+
+ If the client successfully retrieves the data from one or more of its servers, + it will add the data to its internal cache, and reply with the above success + response.
+
+ If the client was unable to source the complete data from any of its servers, + it will reply with: ++
+ ++ Key Type Description + +status +string +"error" ++ +error +string +"notfound" +
+ ++ +Overview
+ ++ + The i2p.q.putItem primitive is used by client nodes to insert a new item + of content onto a server node.+ +
+
+ It is also used by client apps to insert a new item onto their + client node.
+
+ Also, if a server node is receiving a high traffic of requests for a given item, + it may at its discretion send i2p.q.putItem commands to peer servers + to mirror the item on those servers, and spread the load. +Arguments
+ ++
+ +- data - (binary) - the raw data to insert. Refer earlier - the compatible + Java datatype is byte[], and Python datatype is xmlrpclib.Binary.
+- metadata - (struct) - optional - a struct of metadata to + insert alongside the data. If this is not given, a minimal metadata set will + be automatically created by the recipient. See the section on + metadata. +
Server Behaviour
+ ++ If the server successfully received and stored the data (and optionally provided + metadata), it will reply with: ++ ++
+ ++ Key Type Description + +status +string +"ok" ++ +key +string +The base64 key under which this item has been stored, and which should + be used for any subsequent i2p.q.getItem requests for that item + within the Q network. ++ However, if the server's datastore is full, the server will not be able to store + this item, in which case it will respond with: ++ ++
+ ++ Key Type Description + +status +string +"error" ++ +error +string +"storefull" +Client Behaviour
+ ++ Client nodes receiving this command will attempt to store the item in their own + datastore, and respond immediately with one of the above server responses.+ +
+
+ In addition, client nodes will enqueue a background job to upload this item to + one or more selected server nodes. +
+ +Overview
+ ++ + The i2p.q.getUpdate primitive is used to request a differential peers list + update (which optionally can include a catalog update as well).+ +
+
+ Client apps invoke this primitive on client nodes to get up-to-date + listings of items available in the network. Note that client apps will not + hand over any peers list.
+
+ Client nodes periodically schedule a background job to invoke this primitive + on their known servers, such that they keep the most recent possible view of + available data and other servers.
+Arguments
+ ++
+ +- since - (int) - unix time in seconds to update from. The recipient + will send back a list of all content it has become aware of since this + time.
+- includePeers - (int) - set to 1 to include peer list update in the return + data, 0 to omit.
+- includeCatalog - (int) - set to 1 to include catalog update in the return + data, 0 to omit.
+Server Behaviour
+ ++ On receiving this command, a server node will send back lists of metadata records + for all new content (and/or all new peers) it has become aware of since the given + date. The full response is formatted as follows: ++ ++
+ ++ Key Type Description + +status +string +"ok" ++ +items +list +A list of metadata records for new items. Refer to the section on + metadata for more information. If the server + has not become aware of any new data since the given date (or if the + includeCatalog argument was 0), this list will be empty. ++ peers +list +A list of destinations of new peers. If the server has not discovered + any new peers since the given date (or if the includePeers argument + was 0), this list will be empty. + + +timeUpdateEnds +int +unixtime in secs that this update ends. The peer receiving this + response should note this time, and quote it as the since argument + in the next getUpdate request ++ +timeNextContact +int +Advised time (unixtime in sec) for sending the next getUpdate command. The sending + peer should not issue any getCatalog commands before this time, but is + welcome to issue them after this time. The actual time value is guesstimated + by the server node, depending on its current load. +4.11. i2p.q.search
+ ++ ++ + +Overview
+ ++ + The i2p.q.search primitive is invoked by client apps to search a client node + for data items matching a set of criteria. ++ +
+ Only client nodes support this primitive. Server nodes will return an empty + result set and an error response. +Arguments
+ ++
+ +- criteria - (hashtable) - a set of metadata criteria to match. Each key in + this hashtable is a metadata key (eg title, type etc), and the + corresponding value is a regular expression string to match. Regular expression + syntax is documented in the java API in the + section + on class 'Pattern'.
+
+
+ The search criteria work 'AND-style', in that if more than one metadata key + match pattern is given, then only items matching all of the given criteria + will be returned.
+
+ Python example (using XML-RPC proxy - see code samples below): ++ Java Example (using XML-RPC proxy - see code examples below): ++result = mynode.i2p.q.search({"type":"text", "summary":"^War.*"}) +metaRecs = result['items'] ++ Note that if the criteria argument is empty (no keys/values), then the + client node will send back metadata for every item of content it knows of, which + (depending on the size of the Q network), could be quite a resource-hungry operation. ++Hashtable criteria = new Hashtable(); +criteria.put("type", "text"); +criteria.put("summary", "^War.*"); +Vector args = new Vector(); +args.addElement(criteria); +Hashtable result = (Hashtable)mynode.execute("i2p.q.search", args); +Vector metaRecs = (Vector)result.get("items"); +Server Behaviour
+ ++ Servers receiving this command will send back an error response: ++ ++
+ ++ Key Type Description + +status +string +"error" ++ +error +string +"unimplemented" +Client Behaviour
+ ++ Client nodes receiving this command will send back the following response: ++ ++
+ ++ Key Type Description + +status +string +"ok" ++ +items +vector +A list of metadata records (Hashtables) for items which match the given + search criteria, and are retrievable through this client + node (ie, the client node either possesses the item, or knows one or more + servers which possess the item).
+
+ +5. Client Program Examples
+ +5.1. Overview
+ + This section provides a couple of simple examples of client app programming.
+
+ At present, only Python and Java examples are given.
+
+ (If you don't know either of these languages, you should be + able to get the general drift by studying the examples, sufficient to map the concepts to the + XML-RPC API available to your preferred language.)
+
+ The examples below communicate with a client node XML-RPC server (running on the + local machine and listening on its default port of 7651), and perform simple + operations of data insertion, catalog fetching and data retrieval. + +
+ +5.2. Java Example
+ + To run this example, you'll need: ++
+ Now for the code (heavily annotated, so you don't necessarily need to know or understand Java), which + should be written to a source file called QDemo.java. Note that this client would be a + significantly shorter if it instantiated a QClientNode class directly and invoked its methods, + but that is not what we're showing here - we're demonstrating the use of the client node's XML-RPC + interface. +- A running I2P installation, with an instance of a Q client node. +
- The I2P standard jarfiles declared in your java CLASSPATH
+- The standard Apache XML-RPC library jarfile in your CLASSPATH (which you will + already have on your CLASSPATH, because this is part of installing Q). Recall that you + can get a copy of Apache java XML-RPC lib jarfile from + http://ws.apache.org/xmlrpc).
+++ +++// QDemo.java +// +// A simple demo example of a Q client application, which +// communicates with a running Q client node on the local +// machine via its TCP XML-RPC interface +// +// If your client node is not running on localhost, or +// if it's listening on a port other than the default +// 7651, you'll need to change the code below. +// +// Note that this demo is bloated by the fact we're using +// raw XML-RPC. +// +// The following exercises are left to the reader: +// 1. Modify this app so that instead of using the XML-RPC +// interface, it instantiates a QClientNode, and +// invokes its methods directly. +// 2. Write a thin wrapper class which instantiates an XML-RPC +// client, and offers simpler access methods (thus avoiding +// the need to create and populate Vectors of args before +// calling, and pick through a reply Hashtable after the call), +// and create a version of this demo which uses the wrapper. + +// pull in some standard java stuff +import java.*; +import java.lang.*; +import java.util.*; +import java.net.*; +import java.io.*; + +// pull in some xml-rpc stuff +import org.apache.xmlrpc.*; + +// since we're talking to the node via xmlrpc, and talking to +// it in a separate VM, we don't need to import any Q packages + +// Define a minimal demo class, which kust defines a +// main method enabling us to run the demo from a shell. +// +// For the purposes of this demo, we're assuming that your Q client node is +// running on your local machine, and that you haven't altered the +// listening port (default 7651) for the client's XML-RPC interface. + +public class QDemo { + + // just define a main so we can run this from a shell + static public void main(String [] args) + throws MalformedURLException, XmlRpcException, IOException + { + // for getting and analysing replies from node + Hashtable result; + String status; + + // Create a new client app object + XmlRpcClient myClient = new XmlRpcClient("http://127.0.0.1:7651"); + + // ------------------------------------- + // First action - execute a 'ping' on this peer + // ------------------------------------- + + Vector noArgs = new Vector(); + result = (Hashtable)myClient.execute("i2p.q.ping", noArgs); + print("ping: result=" + result); + + // ------------------------------------- + // Second action - insert an item of data + // ------------------------------------- + + // mark the current time, we'll use this later + Integer then = new Integer((int)(new Date().getTime() / 1000)); + + // create metadata + // (note from previous chapter that metadata is optional) + Hashtable meta = new Hashtable(); + meta.put("type", "text"); + meta.put("abstract", "a simple piece of demo data"); + meta.put("mimetype", "text/plain"); + + // create some data + String data = "Hello, world"; + + // set up the arguments list + Vector insertArgs = new Vector(); + insertArgs.addElement(meta); + insertArgs.addElement(data.getBytes()); // must insert data as byte[] + + // and do the insert + result = (Hashtable)myClient.execute("i2p.q.putItem", insertArgs); + print("putItem: result=" + result); + + // check what happened + status = (String)result.get("status"); + String key; + if (status.equals("ok")) { + // insert succeeded + key = (String)result.get("key"); + print("Insert successful"); + } else { + // insert failed, bail + print("Insert failed: error=" + (String)result.get("error")); + return; + } + + // ------------------------------------- + // Third action - check for catalog updates + // (which should include what we've just inserted) + // ------------------------------------- + + // create an args list, with just the date we noted before the insert + Vector updateArgs = new Vector(); + updateArgs.addElement(then); + // add the flags + updateArgs.addElement(new Integer(0)); // 'includePeers' + updateArgs.addElement(new Integer(1)); // 'includeCatalog' + + // execute the 'getCatalog' + result = (Hashtable)myClient.execute("i2p.q.getUpdate", updateArgs); + print("getUpdate: result="+result); + + // pick out the results, and search for what we just inserted + int i; + Vector items = (Vector)result.get("items"); + int nitems = items.size(); + boolean foundit = false; + for (i = 0; i < nitems; i++) { + // get the nth item + Hashtable metaRec = (Hashtable)items.get(i); + String thisKey = (String)metaRec.get("key"); + if (thisKey.equals(key)) { + // yay, got it! + foundit = true; + break; + } + } + + // did we get it? + if (!foundit) { + print("wtf? we inserted it but it's not in the catalog!"); + return; + } + + // yep, we got it, so try to retrieve it back + Vector getArgs = new Vector(); + getArgs.addElement(key); + result = (Hashtable)myClient.execute("i2p.q.getItem", getArgs); + print("getItem: result=" + result); + + // did we get it? + status = (String)result.get("status"); + if (!status.equals("ok")) { + print("getItem failed: " + (String)result.get("error")); + return; + } + + // yep, got it + byte [] binData = (byte [])result.get("data"); + String strData = new String(binData); + print("getItem: success, data='"+strData+"'"); + + print("--- END OF Q CLIENT DEMO ---"); + } + + // a convenient shorthand method for printing stuff to stdout + static void print(String msg) { + System.out.println(msg); + } +} +
+ +5.3. Python Example
+ + To run this example, you will need a running I2P installation, including a running instance + of a Q client node.
+
+ Note that, in contrast to Java, Python 2.3 and later have all the necessary XML-RPC libraries built in. +
+ Now for some code (again, heavily annotated). This, together with the previous example, present an + interesting comparison between some of Java and Python's ways of doing things. +++ + + +++#!/usr/bin/env python +""" +QDemo.py + +A simple demo example of a Q client application, which +communicates with a running Q client node on the local +machine via its TCP XML-RPC interface + +If your client node is not running on localhost, or +if it's listening on a port other than the default +7651, you'll need to change the code below. + +Note that this demo is bloated by the fact we're using +raw XML-RPC. + +The following exercise is left to the reader: + * Write a thin wrapper class which instantiates an XML-RPC + client, and offers simpler access methods (thus avoiding + the need to pick through a reply dict after the call), + and create a version of this demo which uses the wrapper. +""" + +# a coupla needed imports +from time import time +from xmlrpclib import ServerProxy, Binary + +# For the purposes of this demo, we're assuming that your Q client node is +# running on your local machine, and that you haven't altered the +# listening port (default 7651) for the client's XML-RPC interface. + +def qdemo(): + # Create a new client app object + myClient = ServerProxy("http://127.0.0.1:7651") + + # ------------------------------------- + # First action - execute a 'ping' on this peer + # ------------------------------------- + + result = myClient.i2p.q.ping() + print "ping: result=%s" % result + + # ------------------------------------- + # Second action - insert an item of data + # ------------------------------------- + + # mark the current time, we'll use this later + then = int(time()) + + # create metadata + # (note from previous chapter that metadata is optional) + meta = { + "type" : "text", + "abstract" : "a simple piece of demo data", + "mimetype" : "text/plain", + } + + # create some data, and binary-wrap it + data = "Hello, world" + binData = Binary(data) + + # and do the insert + result = myClient.i2p.q.putItem(meta, binData) + print "putItem: result=%s" % result + + # check what happened + if result["status"] == "ok": + # insert succeeded + key = result["key"] + print "Insert successful" + else: + # insert failed, bail + print "Insert failed: error=%s" % result['error'] + return; + + # ------------------------------------- + # Third action - check for catalog updates + # (which should include what we've just inserted) + # ------------------------------------- + + # execute the 'getUpdate' + result = myClient.i2p.q.getUpdate(then, 0, 1) + print "getUpdate: result=%s" % result + + # pick out the results, and search for what we just inserted + foundit = False + for metaRec in result['items']: + if metaRec['key'] == key: + # yay, got it! + foundit = True + break + + # did we get it? + if not foundit: + print "wtf? we inserted it but it's not in the catalog!" + return; + + # yep, we got it, so try to retrieve it back + print "getCatalog: found the item we just inserted" + result = myClient.i2p.q.getItem(key) + print "getItem: result=%s" % result + + # did we get it? + if result["status"] != "ok": + print "getItem failed: %s" + result["error"] + return; + + # yep, got it (note that data is an xmlrpclib.Binary object, + # and the raw data we want is in its .data attribute) + print "getItem: success, data='%s'" % result['data'].data + + print "--- END OF Q CLIENT DEMO ---" + +# run the demo func if this script is executed directly +if __name__ == '__main__': + qdemo() +
+ +6. Keys and Metadata
+ +6.1. Overview
+ Like Freenet, content is stored in Q as (data, metadata) pairs.
+
+ However, there's a difference. On Freenet, metadata is stored as a string of up to + 32k length, and must be parsed (and sometimes executed) by client code. On the other + hand, metadata is exposed in Q as an XML-RPC struct (Java Hashtable or + Properties object, or Python dict, or Perl associative array etc).
+
+ If a content item gets inserted to the Q network without metadata, a minimal metadata set + will be transparently generated, and is guaranteed to contain at least the following + elements:
+
++
++ Key Type Description + +size +int +Size of the stored data item, in bytes ++ +dataHash +string +a base64 representation of the SHA256 hash of the full raw data, using the I2P + base64 alphabet +
+ +
+ +6.2. Node IDs
+ + When Q nodes are first created, they generate themselves a random + I2P privKey/dest keypair using the in-I2P services.
+
+ The I2P destination gets converted to what we call a Q Node ID, as follows: ++
+ +- Start with binary destination (not base64)
+- Determine the SHA256 binary digest of this dest
+- Encode the resulting binary string via I2P's base64 alphabet
+
+ +6.3. Keys
+ + Here, 'key' means the unique short string, by which items of content can be + retrieved, and which is returned from an i2p.q.putItem command.
+
+ Like Freenet's CHK@ keytype, Q keys are hashes of the key's content and + metadata.
+
+ The recipe for calculating the 'key' of a particular item of metadata+data is: ++
+ +- If no metadata is submitted with the data, create a minimal metadata as per above
+- Serialise out the metadata into a string representation, with the fieldnames in + alphanumeric order. The format of such string is one line per metadata field/value + pair per line, in the format: +
+++ metadatakeyname=metadatakeyvalue\n +- Calculate the binary SHA1 digest of this serialised metadata string
+- Base64-encode this binary digest via the I2P Base64 alphabet
+
+ +6.4. Q Metadata Conventions
+ + Additional to the core metadata defined above, there is a convention in Q that the + following optional extra metadata + keys be provided on insert, and recognised and honoured on retrieve.
+
+ It is highly recommended that these keys be included + in metadata when content is inserted:
+
++
++ Key Type Description + +title +string +A short and descriptive title for the item, preferably formatted as + a filename which is legal and convenient on all main operating systems, ie, + containing only alphanumerics, '-', '_' and '.'. +
+
+ It is highly advisable that an appropriate file extension appear at the + end of the title. Refer to the Security Considerations + section below. +
+ It is expected that client applications will use this title field when + displaying available content lists to users. ++ +type +string +Generic type of material, using the following superset of the eMule/Donkey + classifications: + ++
+- text
+- html
+- image
+- audio
+- video
+- software
+- archive
+- misc
++ +mimetype +string +A recognised mime-type, as per RFC1341, RFC1521, RFC1522, such as + audio/mpeg, text/plain etc. +
+
+ This will help client app developers devise ways of disposing with data items + they request from client nodes.
+
+ For instance, client apps with http front ends + may send back this mimetype as the value of the Content-type: header, + (and possibly take preventative action with potentially hazardous mimetypes, such + as those which some browsers such as IE might trust and execute blindly as + binary code).
+
+ Alternatively, gui-based or cli-based client apps may convert this mimetype to + an appropriate benign file extension (such as .txt, + .ogg, .jpg etc). See Security + Considerations below. ++ +keywords +string +A set of space-separated keywords describing this item, intended for + human reading, as well as automatic parsing by client apps. ++ +abstract +string +A short descriptive summary of the nature of the data, intended for + human reading, as well as automatic pattern matching searches by client + apps. +
+
+ +
+ +6.5. One Data Item, Many Metadata Sets?
+ + It is perfectly possible, and legal, for one item of data to be referenced by two + completely different items of metadata.
+
+ Since content keys are a hash of metadata, which in turn contains a hash of the data, + then two pieces of metadata referencing the same data item, but containing different + metadata values, will end up with different keys.
+
+ So as far as key addresses go, there will be a many-to-1 relationship between raw + content keys, and the data returned under these keys.
+
+ + + +
+ +7. Security Considerations
+ + All Peer2Peer software (as with all networked software in general) carries with it a set of + devastating security risks which should be respected to the utmost.
+
+ This applies in no small part to Q.
+
+ So this brief sermon is addressed to anyone writing any client applications or + APIs talking to the Q network.
+
+ Any material which involves the execution of code on a client machine is risky. + However, much of the risk can be managed if the code is open source and peer-reviewed.
+
+ Perhaps the biggest issue as far as Q is concerned is this: + ++ Client app developers should never, NEVER implicitly + trust incoming content, and should always assume that malicious remote users + will insert content which attempts to compromise other users' systems. ++ + If a Q client app wants to offer filetype-specific support, then perhaps a good + strategy is for the client app to use a whitelist of + known low-risk file extensions, such as .txt, or (possibly) + .ogg, .png etc. Recall that in some Windows configurations, even + .jpg can carry an arbitrary code execution attack!
+
+ Note that .html (text/html) is especially dangerous, and + should be respected accordingly.
+
+ Support for .html could be a real boon. For instance, it could allow + I2P users to publish an I2P equivalent of freenet's freesites - static + HTML websites which are accessible even when the author goes offline.
+
+ However, if a client app chooses to recognise .html, it should either + use a code-screening mechanism like freenet's fproxy and keep it + up to date with all the latest advisories, or use a mandatory-proxy + mechanism like I2P's eepProxy.
+
+ One possiblility is to serve up such content via a totally in-I2P http interface, + such that Joe can view the content via his regular eeproxy-configured browser.
+
+ This is a typical case where security and ease/convenience can end up in + direct conflict. Automatic handling of content according to data type + is great from a Joe Sixpack Windows User point of view, but it is a snake-pit + of risks that can potentially result in any of the following (or worse): ++
+ + The crux of this lecture is that client app writers have a huge responsibility to + ensure their apps are safe against malicious content.- Set up Joe's computer as a spambot
+- Get Joe's personal credit card and other info, and use this criminally
+- Download child pornography or terrorist information onto Joe's PC, use an + exploit to get Joe's IP address and/or identity details, and report this to + authorities, thus framing Joe and sending him off undeservedly to Club Fed or + Her Majesty's
+- Mount a DDoS, anonymity or other attack on the I2P network
+- Further spread additional content for achieving more of the above on + other unsuspecting users.
+
+
+ Perhaps the best and most + practical solution is to just store downloaded material into a directory + known to and owned by the user, and make it the user's task and responsibility to + manually copy materials out of this directory and take responsibility for how s/he uses + this content thereafter.
+
+ + + +
+ +8. Contacting the Author
+ + I am aum, and can be reached as aum on in-I2P IRC networks, and also + at the in-I2P email address of aum@mail.i2p.
+
+ +
++ + Introduction | + XML-RPC | + Architecture | + Commands | + Example Code | + Metadata | + Security | + Contact us + + +
+
+ + + +Last modified: Sat Apr 2 13:31:08 NZST 2005 + + + diff --git a/apps/q/java/build.xml b/apps/q/java/build.xml new file mode 100644 index 000000000..761fefc7f --- /dev/null +++ b/apps/q/java/build.xml @@ -0,0 +1,68 @@ + ++ + + + + + + + + + + + diff --git a/apps/q/java/src/HTML/Template.java b/apps/q/java/src/HTML/Template.java new file mode 100644 index 000000000..6c50e36e5 --- /dev/null +++ b/apps/q/java/src/HTML/Template.java @@ -0,0 +1,1115 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +* +* Modified by David McNab (david@rebirthing.co.nz) to allow nesting of +* templates (ie, passing a child Template object as a value argument +* to a .setParam() invocation on a parent Template object). +* +*/ + +package HTML; +import java.util.*; +import java.io.*; +import HTML.Tmpl.Element.*; +import HTML.Tmpl.Parsers.*; +import HTML.Tmpl.Util; +import HTML.Tmpl.Filter; + +/** + * Use HTML Templates with java. + *+ + ++ + + + + + + + ++ + + + + + ++ + + + + ++ + ++ + + ++ + + + + + ++ + ++ ++ + ++ + + ++ + + + * The HTML.Template class allows you to use HTML Templates from within + * your java programs. It makes it possible to change the look of your + * servlets without having to recompile them. Use HTML.Template to + * separate code from presentation in your servlets. + *
+ *
+ * Hashtable args = new Hashtable(); + * args.put("filename", "my_template.tmpl"); + * + * Template t = new Template(args); + * + * t.setParam("title", "The HTML Template package"); + * t.printTo(response.getWriter()); + *+ *+ * HTML.Template is based on the perl module HTML::Template by Sam Tregar + *
+ * Modified by David McNab (david@rebirthing.co.nz) to allow nesting of + * templates (ie, passing a child Template object as a value argument + * to a .setParam() invocation on a parent Template object). + *
+ * @author Philip S Tellis + * @version 0.1.2 + */ +public class Template +{ + private If __template__ = new If("__template__"); + private Hashtable params = new Hashtable(); + + private boolean dirty = true; + + private boolean strict = true; + private boolean die_on_bad_params = false; + private boolean global_vars = false; + private boolean case_sensitive = false; + private boolean loop_context_vars = false; + private boolean debug = false; + private boolean no_includes = false; + private boolean search_path_on_include = false; + private int max_includes = 11; + private String filename = null; + private String scalarref = null; + private String [] arrayref = null; + private String [] path = null; + private Reader filehandle = null; + private Filter [] filters = null; + + private Stack elements = new Stack(); + private Parser parser; + + /** + * Initialises a new HTML.Template object with the contents of + * the given file. + * + * @param filename a string containing the name of + * the file to be used as a + * template. This may be an + * absolute or relative path to a + * template file. + * + * @throws FileNotFoundException If the file specified does not + * exist. + * @throws IllegalStateException If <tmpl_include> is + * used when no_includes is in + * effect. + * @throws IOException If an input or output Exception + * occurred while reading the + * template. + * + * @deprecated No replacement. You should use either + * {@link #Template(Object [])} or + * {@link #Template(Hashtable)} + */ + public Template(String filename) + throws FileNotFoundException, + IllegalStateException, + IOException + { + this.filename = filename; + init(); + } + + + /** + * Initialises a new Template object, using the name/value + * pairs passed as default values. + *+ * The parameters passed may be any combination of filename, + * scalarref, arrayref, path, case_sensitive, loop_context_vars, + * strict, die_on_bad_params, global_vars, max_includes, + * no_includes, search_path_on_include and debug. + * Each with its own value. Any one of filename, scalarref or + * arrayref must be passed. + *
+ * Eg: + *
+ * String [] template_init = { + * "filename", "my_template.tmpl", + * "case_sensitive", "true", + * "max_includes", "5" + * }; + * + * Template t = new Template(template_init); + *+ *+ * The above code creates a new Template object, initialising + * its input file to my_template.tmpl, turning on case_sensitive + * parameter matching, and restricting maximum depth of includes + * to five. + *
+ * Parameter values that take boolean values may either be a String + * containing the words true/false, or the Boolean values Boolean.TRUE + * and Boolean.FALSE. Numeric values may be Strings, or Integers. + * + * @since 0.0.8 + * + * @param args an array of name/value pairs to initialise + * this template with. Valid values for + * each element may be: + * @param filename [Required] a String containing the path to a + * template file + * @param scalarref [Required] a String containing the entire + * template as its contents + * @param arrayref [Required] an array of lines that make up + * the template + * @param path [Optional] an array of Strings specifying + * the directories in which to look for the + * template file. If not specified, the current + * working directory is used. If specified, + * only the directories in this array are used. + * If you want the current directory searched, + * include "." in the path. + *
+ * If you have only a single path, it can be a + * plain String instead of a String array. + *
+ * This is effective only for the template file, + * and not for included files, but see + * search_path_on_include for how to change that. + * @param case_sensitive [Optional] specifies whether parameter + * matching is case sensitive or not. A value + * of "false", "0" or "" is considered false. + * All other values are true. + *
+ * Default: false + * @param loop_context_vars [Optional] when set to true four loop + * context variables are made available inside a + * loop:
__FIRST__, __LAST__, __INNER__, __ODD__, __COUNTER__. + * They can be used with<TMPL_IF>, + *<TMPL_UNLESS>and<TMPL_ELSE>to + * control how a loop is output. Example: + *+ * <TMPL_LOOP NAME="FOO"> + * <TMPL_IF NAME="__FIRST__"> + * This only outputs on the first pass. + * </TMPL_IF> + * + * <TMPL_IF NAME="__ODD__"> + * This outputs on the odd passes. + * </TMPL_IF> + * + * <TMPL_UNLESS NAME="__ODD__"> + * This outputs on the even passes. + * </TMPL_IF> + * + * <TMPL_IF NAME="__INNER__"> + * This outputs on passes that are + * neither first nor last. + * </TMPL_IF> + * + * <TMPL_IF NAME="__LAST__"> + * This only outputs on the last pass. + * <TMPL_IF> + * </TMPL_LOOP> + *+ *+ * NOTE: A loop with only a single pass will get + * both
__FIRST__and__LAST__+ * set to true, but not__INNER__. + *+ * Default: false + * @param strict [Optional] if set to false the module will + * allow things that look like they might be + * TMPL_* tags to get by without throwing + * an exception. Example: + *
+ * <TMPL_HUH NAME=ZUH> + *+ *+ * Would normally cause an error, but if you + * create the Template with strict == 0, + * HTML.Template will ignore it. + *
+ * Default: true + * @param die_on_bad_params [Optional] if set to true + * the module will complain if you try to set + * tmpl.setParam("param_name", "value") and + * param_name doesn't exist in the template. + *
+ * This effect doesn't descend into loops. + *
+ * Default: false (may change in later versions) + * @param global_vars [Optional] normally variables declared outside + * a loop are not available inside a loop. This + * option makes TMPL_VARs global throughout + * the template. It also affects TMPL_IF and TMPL_UNLESS. + *
+ * <p>This is a normal variable: <TMPL_VAR NORMAL>.</p> + * + * <TMPL_LOOP NAME="FROOT_LOOP> + * Here it is inside the loop: <TMPL_VAR NORMAL> + * </TMPL_LOOP> + *+ *+ * Normally this wouldn't work as expected, since + * <TMPL_VAR NORMAL>'s value outside the loop + * isn't available inside the loop. + *
+ * Default: false (may change in later versions) + * @param max_includes [Optional] specifies the maximum depth that + * includes can reach. Including files to a + * depth greater than this value causes an error + * message to be displayed. Set to 0 to disable + * this protection. + *
+ * Default: 10 + * @param no_includes [Optional] If set to true, disallows the + * <TMPL_INCLUDE> tag in the template + * file. This can be used to make opening + * untrusted templates slightly less dangerous. + *
+ * Default: false + * @param search_path_on_include [Optional] if set, then the + * path is searched for included files as well + * as the template file. See the path parameter + * for more information. + *
+ * Default: false + * @param debug [Optional] setting this option to true causes + * HTML.Template to print random error messages + * to STDERR. + * + * @throws ArrayIndexOutOfBoundsException If an odd number of + * parameters is passed. + * @throws FileNotFoundException If the file specified does not + * exist or no filename is passed. + * @throws IllegalArgumentException If an unknown parameter is + * passed. + * @throws IllegalStateException If <tmpl_include> is + * used when no_includes is in + * effect. + * @throws IOException If an input or output Exception + * occurred while reading the + * template. + */ + public Template(Object [] args) + throws ArrayIndexOutOfBoundsException, + FileNotFoundException, + IllegalArgumentException, + IllegalStateException, + IOException + + { + if(args.length%2 != 0) + throw new ArrayIndexOutOfBoundsException("odd number " + + "of arguments passed"); + + for(int i=0; i
+ * + */ + public void doUploadItem() throws QException { + QDataItem item = (QDataItem)job.get("item"); + String uri = (String)item.get("uri"); + String desc = "uploadItem:uri="+uri; + byte [] data = item._data; + + Hashtable peersUploaded = (Hashtable)job.get("peersUploaded"); + Hashtable peersPending = (Hashtable)job.get("peersPending"); + Hashtable peersFailed = (Hashtable)job.get("peersFailed"); + Hashtable peersNumTries = (Hashtable)job.get("peersNumTries"); + + String itemHash = item.getStoreFilename(); + QPeer peerRec; + + // get current list of up to 100 closest peers to item's URI + Vector cPeers = node.peersClosestTo(uri, 100); + + // loop on this list, try to upload item to n of them + for (Enumeration en = cPeers.elements(); en.hasMoreElements();) { + QPeer peer = (QPeer)en.nextElement(); + String peerId = peer.getId(); + + // skip this peer if we've already succeeded or failed with it + if (peersFailed.containsKey(peerId) || peersUploaded.containsKey(peerId)) { + continue; + } + + // if there are less than 3 or more pending peers, add this peer to + // pending list, otherwise skip it + if (!peersPending.containsKey(peerId)) { + if (peersPending.size() < 3) { + peersPending.put(peerId, ""); + } else { + continue; + } + } + + // try to insert item to this peer + boolean uploadedOk; + try { + Hashtable res = node.peerPutItem(peerId, item, item._data); + if (res.containsKey("status") && ((String)res.get("status")).equals("ok")) { + // successful upload + uploadedOk = true; + } else { + // upload failed for some reason + uploadedOk = false; + System.out.println("upload failure:"+res); + } + } catch (Exception e) { + // possibly because peer is offline or presently unreachable + uploadedOk = false; + e.printStackTrace(); + System.out.println("upload failure"); + } + + // how'd the upload go? + if (uploadedOk) { + // successful - remove from pending list, add to success list + peersPending.remove(peerId); + peersNumTries.remove(peerId); + peersUploaded.put(peerId, ""); + + // have we successfully uploaded to 3 or more peers yet? + if (peersUploaded.size() >= 3) { + // yep, this job has now run its course and can expire + return; + } else { + // bust out so we don't hog a scheduler slot + node.runAfter(5000, job, desc); + return; + } + + } else { + // insert failed + // increment retry count, fail this peer if retries maxed out + int numTries = ((Integer)peersNumTries.get(peerId)).intValue() + 1; + if (numTries > 4) { + // move peer from pending list to failed list + peersPending.remove(peerId); + peersNumTries.remove(peerId); + peersFailed.put(peerId, ""); + } + + // bust out so we don't hog a scheduler slot + node.runAfter(30000, job, desc); + return; + } + } + + // we'return out of peers, reschedule this job to retry in an hour's time + node.runAfter(3600000, job, desc); + } + + public void doHello() { + QPeer peerRec = (QPeer)node.peers.get(peerId); + + node.log.debug("doHello: "+node.id+" -> "+peerId); + + try { + // execute peers list req on peer + Hashtable result = node.peerHello(peerId, node.destStr); + + // see what happened + String status = (String)result.get("status"); + if (status.equals("ok")) { + peerRec.markAsGreeted(); + + // and, schedule in regular peersList updates + node.schedulePeerUpdateJob(peerRec); + } + } catch (Exception e) { + node.log.warn("Got an xmlrpc client failure, trying again in 1 hour", e); + + // schedule another attempt in 2 hours + Hashtable job = new Hashtable(); + job.put("cmd", "hello"); + job.put("peerId", peerId); + node.runAfter(3600000, job, "hello:peerId="+peerId); + } + } + + public void doGetUpdate() { + QPeer peerRec = (QPeer)node.peers.get(peerId); + int timeLastPeersUpdate = peerRec.getTimeLastUpdate(); + int timeNextContact; + int doCatalog = ((Integer)(job.get("includeCatalog"))).intValue(); + int doPeers = ((Integer)(job.get("includePeers"))).intValue(); + Vector peers; + Vector items; + + node.log.info("doGetUpdate: "+node.id+" -> "+peerId); + + try { + // execute peers list req on peer + Hashtable result = node.peerGetUpdate( + peerId, timeLastPeersUpdate, doPeers, doCatalog); + + // see what happened + String status = (String)result.get("status"); + if (status.equals("ok")) { + + node.log.debug("doGetUpdate: successful, result="+result); + + int i; + + // success - add all new peers + peers = (Vector)result.get("peers"); + int npeers = peers.size(); + for (i=0; i+ * The parameters passed are the same as in the Template(Object []) + * constructor. Each with its own value. Any one of filename, + * scalarref or arrayref must be passed. + * + * Eg: + *
+ * Hashtable args = new Hashtable(); + * args.put("filename", "my_template.tmpl"); + * args.put("case_sensitive", "true"); + * args.put("loop_context_vars", Boolean.TRUE); + * // args.put("max_includes", "5"); + * args.put("max_includes", new Integer(5)); + * + * Template t = new Template(args); + *+ *+ * The above code creates a new Template object, initialising + * its input file to my_template.tmpl, turning on case_sensitive + * parameter matching, and the loop context variables __FIRST__, + * __LAST__, __ODD__ and __INNER__, and restricting maximum depth of + * includes to five. + *
+ * Parameter values that take boolean values may either be a String + * containing the words true/false, or the Boolean values Boolean.TRUE + * and Boolean.FALSE. Numeric values may be Strings, or Integers. + * + * @since 0.0.10 + * + * @param args a Hashtable of name/value pairs to initialise + * this template with. Valid values are the same + * as in the Template(Object []) constructor. + * + * @throws FileNotFoundException If the file specified does not + * exist or no filename is passed. + * @throws IllegalArgumentException If an unknown parameter is + * passed. + * @throws IllegalStateException If <tmpl_include> is + * used when no_includes is in + * effect. + * @throws IOException If an input or output Exception + * occurred while reading the + * template. + * + * @see #Template(Object []) + */ + public Template(Hashtable args) + throws FileNotFoundException, + IllegalArgumentException, + IllegalStateException, + IOException + + { + Enumeration e = args.keys(); + while(e.hasMoreElements()) { + String key = (String)e.nextElement(); + Object value = args.get(key); + + parseParam(key, value); + } + + init(); + } + + /** + * Prints the parsed template to the provided PrintWriter. + * + * @param out the PrintWriter that this template will be printed + * to + */ + public void printTo(PrintWriter out) + { + out.print(output()); + } + + /** + * Returns the parsed template as a String. + * + * @return a string containing the parsed template + */ + public String output() + { + return __template__.parse(params); + } + + /** + * Sets the values of parameters in this template from a Hashtable. + * + * @param params a Hashtable containing name/value pairs for + * this template. Keys in this hashtable must + * be Strings and values may be either Strings + * or Vectors. + *
+ * Parameter names are currently not case + * sensitive. + *
+ * Parameter names can contain only letters, + * digits, ., /, +, - and _ characters. + *
+ * Parameter names starting and ending with + * a double underscore are not permitted. + * eg:
+ *__myparam__is illegal. + * + * @return the number of parameters actually set. + * Illegal parameters will not be set, but + * no error/exception will be thrown. + */ + public int setParams(Hashtable params) + { + if(params == null || params.isEmpty()) + return 0; + int count=0; + for(Enumeration e = params.keys(); e.hasMoreElements();) { + Object key = e.nextElement(); + if(key.getClass().getName().endsWith(".String")) { + Object value = params.get(key); + try { + setParam((String)key, value); + count++; + } catch (Exception pe) { + // key was not a String or Vector + // or key was null + // don't increment count + } + } + } + if(count>0) { + dirty=true; + Util.debug_print("Now dirty: set params"); + } + + return count; + } + + /** + * Sets a single scalar parameter in this template. + * + * @param name a String containing the name of this parameter. + * Parameter names are currently not case sensitive. + * @param value a String containing the value of this parameter + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + * + * @see #setParams(Hashtable) + */ + public String setParam(String name, String value) + throws IllegalArgumentException, NullPointerException + { + try { + return (String)setParam(name, (Object)value); + } catch(ClassCastException iae) { + return null; + } + } + + /** + * Sets a single Integer parameter in this template. + * + * @param name a String containing the name of this parameter. + * Parameter names are currently not case sensitive. + * @param value an Integer containing the value of this parameter + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + * + * @see #setParams(Hashtable) + */ + public Integer setParam(String name, Integer value) + throws IllegalArgumentException, NullPointerException + { + try { + return (Integer)setParam(name, (Object)value); + } catch(ClassCastException iae) { + return null; + } + } + + /** + * Sets a single int parameter in this template. + * + * @param name a String containing the name of this parameter. + * Parameter names are currently not case sensitive. + * @param value an int containing the value of this parameter + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + * + * @see #setParams(Hashtable) + */ + public int setParam(String name, int value) + throws IllegalArgumentException, NullPointerException + { + return setParam(name, new Integer(value)).intValue(); + } + + /** + * Sets a single boolean parameter in this template. + * + * @param name a String containing the name of this parameter. + * Parameter names are currently not case sensitive. + * @param value a boolean containing the value of this parameter + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + * + * @see #setParams(Hashtable) + */ + public boolean setParam(String name, boolean value) + throws IllegalArgumentException, NullPointerException + { + return setParam(name, new Boolean(value)).booleanValue(); + } + + /** + * Sets a single Boolean parameter in this template. + * + * @param name a String containing the name of this parameter. + * Parameter names are currently not case sensitive. + * @param value a Boolean containing the value of this parameter + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + * + * @see #setParams(Hashtable) + */ + public Boolean setParam(String name, Boolean value) + throws IllegalArgumentException, NullPointerException + { + try { + return (Boolean)setParam(name, (Object)value); + } catch(ClassCastException iae) { + return null; + } + } + + + /** + * Sets a single parameter in this template to a nested Template + * + * @param name a String containing the name of this parameter. + * Parameter names are currently not case sensitive. + * @param value a Template object to be nested in + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + */ + public Template setParam(String name, Template value) + throws IllegalArgumentException, NullPointerException + { + try { + return (Template)setParam(name, (Object)value); + } catch(ClassCastException iae) { + return null; + } + } + + + /** + * Sets a single list parameter in this template. + * + * @param name a String containing the name of this parameter. + * Parameter names are not currently case sensitive. + * @param value a Vector containing a list of Hashtables of parameters + * + * @return the value of the parameter set + * @throws IllegalArgumentException if the parameter name contains + * illegal characters + * @throws NullPointerException if the parameter name is null + * + * @see #setParams(Hashtable) + */ + public Vector setParam(String name, Vector value) + throws IllegalArgumentException, NullPointerException + { + try { + return (Vector)setParam(name, (Object)value); + } catch(ClassCastException iae) { + return null; + } + } + + /** + * Returns a parameter from this template identified by the given name. + * + * @param name a String containing the name of the parameter to be + * returned. Parameter names are not currently case + * sensitive. + * + * @return the value of the requested parameter. If the parameter + * is a scalar, the return value is a String, if the + * parameter is a list, the return value is a Vector. + * + * @throws NoSuchElementException if the parameter does not exist + * in the template + * @throws NullPointerException if the parameter name is null + */ + public Object getParam(String name) + throws NoSuchElementException, NullPointerException + { + if(name == null) + throw new NullPointerException("name cannot be null"); + if(!params.containsKey(name)) + throw new NoSuchElementException(name + + " is not a parameter in this template"); + + if(case_sensitive) + return params.get(name); + else + return params.get(name.toLowerCase()); + } + + + private void parseParam(String key, Object value) + throws IllegalStateException + { + if(key.equals("case_sensitive")) + { + this.case_sensitive=boolify(value); + Util.debug_print("case_sensitive: "+value); + } + else if(key.equals("strict")) + { + this.strict=boolify(value); + Util.debug_print("strict: "+value); + } + else if(key.equals("global_vars")) + { + this.global_vars=boolify(value); + Util.debug_print("global_vars: "+value); + } + else if(key.equals("die_on_bad_params")) + { + this.die_on_bad_params=boolify(value); + Util.debug_print("die_obp: "+value); + } + else if(key.equals("max_includes")) + { + this.max_includes=intify(value)+1; + Util.debug_print("max_includes: "+value); + } + else if(key.equals("no_includes")) + { + this.no_includes=boolify(value); + Util.debug_print("no_includes: "+value); + } + else if(key.equals("search_path_on_include")) + { + this.search_path_on_include=boolify(value); + Util.debug_print("path_includes: "+value); + } + else if(key.equals("loop_context_vars")) + { + this.loop_context_vars=boolify(value); + Util.debug_print("loop_c_v: "+value); + } + else if(key.equals("debug")) + { + this.debug=boolify(value); + Util.debug=this.debug; + Util.debug_print("debug: "+value); + } + else if(key.equals("filename")) + { + this.filename = (String)value; + Util.debug_print("filename: "+value); + } + else if(key.equals("scalarref")) + { + this.scalarref = (String)value; + Util.debug_print("scalarref"); + } + else if(key.equals("arrayref")) + { + this.arrayref = (String [])value; + Util.debug_print("arrayref"); + } + else if(key.equals("path")) + { + if(value.getClass().getName().startsWith("[")) + this.path = (String [])value; + else { + this.path = new String[1]; + this.path[0] = (String)value; + } + Util.debug_print("path"); + for(int j=0; jnot " + + "allowed when " + + "no_includes in effect" + ); + if(max_includes == 0) { + throw new IndexOutOfBoundsException( + "include too deep"); + } else { + // come here if positive + // or negative + elements.push(e); + read_file(p.getProperty("name")); + } + } + else if(type.equals("var")) + { + String name = p.getProperty("name"); + String escape = p.getProperty("escape"); + String def = p.getProperty("default"); + Util.debug_print("name: " + name); + Util.debug_print("escape: " + escape); + Util.debug_print("default: " + def); + e.add(new Var(name, escape, def)); + } + else if(type.equals("else")) + { + Util.debug_print("adding branch"); + ((Conditional)e).addBranch(); + } + else if(p.getProperty("close").equals("true")) + { + Util.debug_print("closing tag"); + if(!type.equals(e.Type())) + throw new EmptyStackException(); + + e = (Element)elements.pop(); + } + else + { + Element t = parser.getElement(p); + e.add(t); + elements.push(e); + e=t; + } + } + return e; + } + + private void read_file(String filename) + throws FileNotFoundException, + IllegalStateException, + IOException, + EmptyStackException + { + BufferedReader br=openFile(filename); + + String line; + + Element e = null; + if(elements.empty()) + e = __template__; + else + e = (Element)elements.pop(); + + max_includes--; + while((line=br.readLine()) != null) { + Util.debug_print("Line: " + line); + e = parseLine(line+"\n", e); + } + max_includes++; + + br.close(); + br=null; + + } + + private void read_line_array(String [] lines) + throws FileNotFoundException, + IllegalStateException, + IOException, + EmptyStackException + { + + Element e = __template__; + + max_includes--; + for(int i=0; i 0) + type = type.substring(type.lastIndexOf(".")+1); + + String valid_types = ",String,Vector,Boolean,Integer,Template"; + + if(valid_types.indexOf(type) < 0) + throw new ClassCastException( + "value is neither scalar nor list nor Template"); + + name=case_sensitive?name:name.toLowerCase(); + + if(!case_sensitive && type.equals("Vector")) { + value = lowerCaseAll((Vector)value); + } + + Util.debug_print("setting: " + name); + params.put(name, value); + + dirty=true; + return value; + } + + private static Vector lowerCaseAll(Vector v) + { + Vector v2 = new Vector(); + for(Enumeration e = v.elements(); e.hasMoreElements(); ) { + Hashtable h = (Hashtable)e.nextElement(); + if(h == null) { + v2.addElement(h); + continue; + } + Hashtable h2 = new Hashtable(); + for(Enumeration e2 = h.keys(); e2.hasMoreElements(); ) { + String key = (String)e2.nextElement(); + Object value = h.get(key); + String value_type = value.getClass().getName(); + Util.debug_print("to lower case: " + key + "(" + value_type + ")"); + if(value_type.endsWith(".Vector")) + value = lowerCaseAll((Vector)value); + h2.put(key.toLowerCase(), value); + } + v2.addElement(h2); + } + return v2; + } + + private static boolean boolify(Object o) + { + String s; + if(o.getClass().getName().endsWith(".Boolean")) + return ((Boolean)o).booleanValue(); + else if(o.getClass().getName().endsWith(".String")) + s = (String)o; + else + s = o.toString(); + + if(s.equals("0") || s.equals("") || s.equals("false")) + return false; + return true; + } + + private static int intify(Object o) + { + String s; + if(o.getClass().getName().endsWith(".Integer")) + return ((Integer)o).intValue(); + else if(o.getClass().getName().endsWith(".String")) + s = (String)o; + else + s = o.toString(); + + try { + return Integer.parseInt(s); + } catch(NumberFormatException nfe) { + return 0; + } + } + + private static String stringify(boolean b) + { + if(b) + return "1"; + else + return ""; + } + + private BufferedReader openFile(String filename) + throws FileNotFoundException + { + boolean add_path=true; + + if(!elements.empty() && !search_path_on_include) + add_path=false; + + if(filename.startsWith("/")) + add_path=false; + + if(this.path == null) + add_path=false; + + Util.debug_print("open " + filename); + if(!add_path) + return new BufferedReader(new FileReader(filename)); + + BufferedReader br=null; + + for(int i=0; i 0) + control_class = control_class.substring( + control_class.lastIndexOf(".")+1); + + if(control_class.equals("String")) { + return !(((String)control_val).equals("") || + ((String)control_val).equals("0")); + } else if(control_class.equals("Vector")) { + return !((Vector)control_val).isEmpty(); + } else if(control_class.equals("Boolean")) { + return ((Boolean)control_val).booleanValue(); + } else if(control_class.equals("Integer")) { + return (((Integer)control_val).intValue() != 0); + } else { + throw new IllegalArgumentException("Unrecognised type"); + } + } +} + diff --git a/apps/q/java/src/HTML/Tmpl/Element/Element.java b/apps/q/java/src/HTML/Tmpl/Element/Element.java new file mode 100644 index 000000000..2dea977fa --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Element/Element.java @@ -0,0 +1,66 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + + +package HTML.Tmpl.Element; +import java.util.Hashtable; +import java.util.NoSuchElementException; + +public abstract class Element +{ + protected String type; + protected String name=""; + + public abstract String parse(Hashtable params); + public abstract String typeOfParam(String param) + throws NoSuchElementException; + + public void add(String data){} + public void add(Element node){} + + public boolean contains(String param) + { + try { + return (typeOfParam(param) != null?true:false); + } catch(NoSuchElementException nse) { + return false; + } + } + + public final String Type() + { + return type; + } + + public final String Name() + { + return name; + } +} diff --git a/apps/q/java/src/HTML/Tmpl/Element/If.java b/apps/q/java/src/HTML/Tmpl/Element/If.java new file mode 100644 index 000000000..4384e8fbd --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Element/If.java @@ -0,0 +1,39 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + +package HTML.Tmpl.Element; + +public class If extends Conditional +{ + public If(String control_var) throws IllegalArgumentException + { + super("if", control_var); + } +} diff --git a/apps/q/java/src/HTML/Tmpl/Element/Loop.java b/apps/q/java/src/HTML/Tmpl/Element/Loop.java new file mode 100644 index 000000000..ffd13dafd --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Element/Loop.java @@ -0,0 +1,183 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + +package HTML.Tmpl.Element; +import java.util.Vector; +import java.util.Hashtable; +import java.util.Enumeration; +import java.util.NoSuchElementException; + +public class Loop extends Element +{ + private boolean loop_context_vars=false; + private boolean global_vars=false; + + private Vector control_val = null; + private Vector data; + + public Loop(String name) + { + this.type = "loop"; + this.name = name; + this.data = new Vector(); + } + + public Loop(String name, boolean loop_context_vars) + { + this(name); + this.loop_context_vars=loop_context_vars; + } + + public Loop(String name, boolean loop_context_vars, boolean global_vars) + { + this(name); + this.loop_context_vars=loop_context_vars; + this.global_vars=global_vars; + } + + public void add(String text) + { + data.addElement(text); + } + + public void add(Element node) + { + data.addElement(node); + } + + public void setControlValue(Vector control_val) + throws IllegalArgumentException + { + this.control_val = process_var(control_val); + } + + public String parse(Hashtable p) + { + if(!p.containsKey(this.name)) + this.control_val = null; + else { + Object o = p.get(this.name); + if(!o.getClass().getName().endsWith(".Vector") && + !o.getClass().getName().endsWith(".List")) + throw new ClassCastException( + "Attempt to set with a non-list. tmpl_loop=" + this.name); + setControlValue((Vector)p.get(this.name)); + } + + if(control_val == null) + return ""; + + StringBuffer output = new StringBuffer(); + Enumeration iterator = control_val.elements(); + + boolean first=true; + boolean last=false; + boolean inner=false; + boolean odd=true; + int counter=1; + + while(iterator.hasMoreElements()) { + Hashtable params = (Hashtable)iterator.nextElement(); + + if(params==null) + params = new Hashtable(); + + if(global_vars) { + for(Enumeration e = p.keys(); e.hasMoreElements();) { + Object key = e.nextElement(); + if(!params.containsKey(key)) + params.put(key, p.get(key)); + } + } + + if(loop_context_vars) { + if(!iterator.hasMoreElements()) + last=true; + inner = !first && !last; + + params.put("__FIRST__", first?"1":""); + params.put("__LAST__", last?"1":""); + params.put("__ODD__", odd?"1":""); + params.put("__INNER__", inner?"1":""); + params.put("__COUNTER__", "" + (counter++)); + } + + Enumeration de = data.elements(); + while(de.hasMoreElements()) { + + Object e = de.nextElement(); + if(e.getClass().getName().indexOf("String")>-1) + output.append((String)e); + else + output.append(((Element)e).parse(params)); + } + first = false; + odd = !odd; + } + + return output.toString(); + } + + public String typeOfParam(String param) + throws NoSuchElementException + { + for(Enumeration e = data.elements(); e.hasMoreElements();) + { + Object o = e.nextElement(); + if(o.getClass().getName().endsWith(".String")) + continue; + if(((Element)o).Name().equals(param)) + return ((Element)o).Type(); + } + throw new NoSuchElementException(param); + } + + private Vector process_var(Vector control_val) + throws IllegalArgumentException + { + String control_class = ""; + + if(control_val == null) + return null; + + control_class=control_val.getClass().getName(); + + if(control_class.indexOf("Vector") > -1) { + if(control_val.isEmpty()) + return null; + } else { + throw new IllegalArgumentException("Unrecognised type"); + } + + return control_val; + } + +} + diff --git a/apps/q/java/src/HTML/Tmpl/Element/Unless.java b/apps/q/java/src/HTML/Tmpl/Element/Unless.java new file mode 100644 index 000000000..8caca00c6 --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Element/Unless.java @@ -0,0 +1,39 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + +package HTML.Tmpl.Element; + +public class Unless extends Conditional +{ + public Unless(String control_var) throws IllegalArgumentException + { + super("unless", control_var); + } +} diff --git a/apps/q/java/src/HTML/Tmpl/Element/Var.java b/apps/q/java/src/HTML/Tmpl/Element/Var.java new file mode 100644 index 000000000..7aee9b165 --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Element/Var.java @@ -0,0 +1,144 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +* +* Modified by David McNab (david@rebirthing.co.nz) to allow nesting of +* templates (ie, passing a child Template object as a value argument +* to a .setParam() invocation on a parent Template object). +*/ + +package HTML.Tmpl.Element; +import java.util.Hashtable; +import java.util.NoSuchElementException; +import HTML.Tmpl.Util; +import HTML.Template; + +public class Var extends Element +{ + public static final int ESCAPE_NONE = 0; + public static final int ESCAPE_URL = 1; + public static final int ESCAPE_HTML = 2; + public static final int ESCAPE_QUOTE = 4; + + public Var(String name, int escape, Object default_value) + throws IllegalArgumentException + { + this(name, escape); + this.default_value = stringify(default_value); + } + + public Var(String name, int escape) + throws IllegalArgumentException + { + if(name == null) + throw new IllegalArgumentException("tmpl_var must have a name"); + this.type = "var"; + this.name = name; + this.escape = escape; + } + + public Var(String name, String escape) + throws IllegalArgumentException + { + this(name, escape, null); + } + + public Var(String name, String escape, Object default_value) + throws IllegalArgumentException + { + this(name, ESCAPE_NONE, default_value); + + if(escape.equalsIgnoreCase("html")) + this.escape = ESCAPE_HTML; + else if(escape.equalsIgnoreCase("url")) + this.escape = ESCAPE_URL; + else if(escape.equalsIgnoreCase("quote")) + this.escape = ESCAPE_QUOTE; + } + + public Var(String name, boolean escape) + throws IllegalArgumentException + { + this(name, escape?ESCAPE_HTML:ESCAPE_NONE); + } + + public String parse(Hashtable params) + { + String value = null; + + if(params.containsKey(this.name)) + value = stringify(params.get(this.name)); + else + value = this.default_value; + + if(value == null) + return ""; + + if(this.escape == ESCAPE_HTML) + return Util.escapeHTML(value); + else if(this.escape == ESCAPE_URL) + return Util.escapeURL(value); + else if(this.escape == ESCAPE_QUOTE) + return Util.escapeQuote(value); + else + return value; + } + + public String typeOfParam(String param) + throws NoSuchElementException + { + throw new NoSuchElementException(param); + } + + private String stringify(Object o) + { + if(o == null) + return null; + + String cname = o.getClass().getName(); + if(cname.endsWith(".String")) + return (String)o; + else if(cname.endsWith(".Integer")) + return ((Integer)o).toString(); + else if(cname.endsWith(".Boolean")) + return ((Boolean)o).toString(); + else if(cname.endsWith(".Date")) + return ((java.util.Date)o).toString(); + else if(cname.endsWith(".Vector")) + throw new ClassCastException("Attempt to set with a non-scalar. Var name=" + this.name); + else if(cname.endsWith(".Template")) + return ((Template)o).output(); + else + throw new ClassCastException("Unknown object type: " + cname); + } + + // Private data starts here + private int escape=ESCAPE_NONE; + private String default_value=null; + +} diff --git a/apps/q/java/src/HTML/Tmpl/Filter.java b/apps/q/java/src/HTML/Tmpl/Filter.java new file mode 100644 index 000000000..5d5f82112 --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Filter.java @@ -0,0 +1,145 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + + +package HTML.Tmpl; + +/** + * Pre-parse filters for HTML.Template templates. + * + * The HTML.Tmpl.Filter interface allows you to write Filters + * for your templates. The filter is called after the template + * is read and before it is parsed. + *
+ * You can use a filter to make changes in the template file before + * it is parsed by HTML.Template, so for example, use it to replace + * constants, or to translate your own tags to HTML.Template tags. + *
+ * A common usage would be to do what you think you're doing when you + * do
<TMPL_INCLUDE file="<TMPL_VAR name="the_file">">: + *+ * myTemplate.tmpl: + *
+ * <TMPL_INCLUDE file="<%the_file%>"> + *+ *+ * myFilter.java: + *
+ * class myFilter implements HTML.Tmpl.Filter + * { + * private String myFile; + * private int type=SCALAR + * + * public myFilter(String myFile) { + * this.myFile = myFile; + * } + * + * public int format() { + * return this.type; + * } + * + * public String parse(String t) { + * // replace all <%the_file%> with myFile + * return t; + * } + * + * public String [] parse(String [] t) { + * throw new UnsupportedOperationException(); + * } + * } + *+ *+ * myClass.java: + *
+ * Hashtable params = new Hashtable(); + * params.put("filename", "myTemplate.tmpl"); + * params.put("filter", new myFilter("myFile.tmpl")); + * Template t = new Template(params); + *+ * + * @author Philip S Tellis + * @version 0.0.1 + */ +public interface Filter +{ + /** + * Tells HTML.Template to call the parse(String) method of this filter. + */ + public final static int SCALAR=1; + + /** + * Tells HTML.Template to call the parse(String []) method of this + * filter. + */ + public final static int ARRAY=2; + + /** + * Tells HTML.Template what kind of filter this is. + * Should return either SCALAR or ARRAY to indicate which parse method + * must be called. + * + * @return the values SCALAR or ARRAY indicating which parse method + * is to be called + */ + public int format(); + + /** + * parses the template as a single string, and returns the parsed + * template as a single string. + *+ * Should throw an UnsupportedOperationException if it isn't implemented + * + * @param t a string containing the entire template + * + * @return a string containing the template after you've parsed it + * + * @throws UnsupportedOperationException if this method isn't + * implemented + */ + public String parse(String t); + + /** + * parses the template as an array of strings, and returns the parsed + * template as an array of strings. + *
+ * Should throw an UnsupportedOperationException if it isn't implemented + * + * @param t an array of strings containing the template - one line + * at a time + * + * @return an array of strings containing the parsed template - + * one line at a time + * + * @throws UnsupportedOperationException if this method isn't + * implemented + */ + public String [] parse(String [] t); +} + diff --git a/apps/q/java/src/HTML/Tmpl/Parsers/Parser.java b/apps/q/java/src/HTML/Tmpl/Parsers/Parser.java new file mode 100644 index 000000000..dd6fd439c --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Parsers/Parser.java @@ -0,0 +1,385 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + + +package HTML.Tmpl.Parsers; +import java.util.*; +import HTML.Tmpl.Element.*; +import HTML.Tmpl.Util; + +public class Parser +{ + private boolean case_sensitive=false; + private boolean strict=true; + private boolean loop_context_vars=false; + private boolean global_vars=false; + + public Parser() + { + } + + public Parser(String [] args) + throws ArrayIndexOutOfBoundsException, + IllegalArgumentException + { + if(args.length%2 != 0) + throw new ArrayIndexOutOfBoundsException("odd number of arguments passed"); + + for(int i=0; i
+ */ +public class QIndexFile { + + public String path; + File fileObj; + RandomAccessFile file; + public long rawLength; + public int numRecs; + FileReader reader; + FileWriter writer; + + /** length of base64 representation of sha256 hash */ + static public int hashLen = 43; + + /** length of unixtime milliseconds in decimal format */ + static public int timeLen = 13; + + /** + * length of records, allowing for time field, delimter (,), + * hash field and terminating newline + */ + static public int recordLen = hashLen + timeLen + 2; + + /** + * Create a new index file + * @param path absolute pathname on filesystem + */ + public QIndexFile(String path) throws IOException { + this.path = path; + fileObj = new File(path); + + // if file doesn't exist, ensure parent dir exists, so subsequent + // file creation will (hopefully) succeed + if (!fileObj.exists()) + { + // create parent directory if not already existing + String parentDir = fileObj.getParent(); + File parentFile = new File(parentDir); + if (!parentFile.isDirectory()) + { + parentFile.mkdirs(); + } + } + + // get a random access object, creating file if not yet existing + file = new RandomAccessFile(fileObj, "rws"); + + // barf if file's length is not a multiple of record length + rawLength = file.length(); + if (rawLength % recordLen != 0) { + throw new IOException("File size not a multiple of record length ("+recordLen+")"); + } + + // note record count + numRecs = (int)(rawLength / recordLen); + } + + /** + * fetch an iterator for items after a given time + */ + public synchronized Iterator getItemsSince(int time) throws IOException + { + //System.out.println("getItemsSince: time="+time); + + // if no records, return an empty iterator + if (numRecs == 0) + { + return new QIndexFileIterator(this, 0); + } + + // otherwise, binary search till we find an item time-stamped + // after given time + long mtime = ((long)time) * 1000; + int lo = 0; + int hi = numRecs; + int lastguess = -1; + while (hi - lo > 0) + { + int guess = (hi + lo) / 2; + //System.out.println("getItemsSince: lo="+lo+" guess="+guess+" hi="+hi); + if (guess == lastguess) // && hi - lo == 1) + { + break; + } + lastguess = guess; + + Object [] rec = getRecord(guess); + long t = ((Long)rec[0]).longValue(); + if (t <= mtime) + { + // guess too low, go for upper range + lo = guess; + continue; + } + else + { + // guess too high, pick lower range + hi = guess; + continue; + } + } + + // found + return new QIndexFileIterator(this, hi); + } + + /** + * adds a new base64 hash value record, saving it with current time + */ + public synchronized void add(String h) throws IOException + { + // barf if hash is incorrect length + if (h.length() != hashLen) + { + System.out.println("hash="+h); + throw new IOException("Incorrect hash length ("+h.length()+"), should be "+hashLen); + } + + // format current date/time as decimal string, pad with leading zeroes + Date d = new Date(); + String ds = String.valueOf(d.getTime()); + while (ds.length() < timeLen) + { + ds = "0" + ds; + } + + // now can construct record + String rec = ds + "," + h + "\n"; + + // append it to file + file.seek(numRecs * recordLen); + file.writeBytes(rec); + + // and update count + numRecs += 1; + rawLength += recordLen; + } + + public long getRecordTime(int n) throws IOException + { + Object [] rec = getRecord(n); + + return ((Long)rec[0]).longValue(); + } + + /** return number of records currently within file */ + public int length() + { + return numRecs; + } + + /** + * returns the hash field of record n + */ + public String getRecordHash(int n) throws IOException + { + Object [] rec = getRecord(n); + return (String)rec[1]; + } + + public synchronized Object [] getRecord(int n) throws IOException + { + Object [] rec = new Object[2]; + + String recStr = getRecordStr(n); + String [] flds = recStr.split(","); + Long t = new Long(flds[0]); + String h = flds[1]; + rec[0] = t; + rec[1] = h; + return rec; + } + + protected synchronized String getRecordStr(int n) throws IOException + { + // barf if over or under-reaching + if (n < 0 || n > numRecs - 1) + { + throw new IOException("Record number ("+n+") out of range"); + } + + // position to location of the record + file.seek(n * recordLen); + + // read, trim and return + return file.readLine().trim(); + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + try { + QIndexFile q = new QIndexFile("/home/david/.quartermaster_client/content/index.dat"); + Iterator i = q.getItemsSince((int)(new Date().getTime() / 1000)); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java b/apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java new file mode 100644 index 000000000..d96676c7d --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QIndexFileIterator.java @@ -0,0 +1,56 @@ +/* + * QIndexFileIterator.java + * + * Created on March 24, 2005, 1:49 PM + */ + +package net.i2p.aum.q; + +import java.*; +import java.util.*; + +/** + * Implements an Iterator for index files + */ +public class QIndexFileIterator implements Iterator +{ + public QIndexFile file; + int recNum; + + /** Creates an iterator starting from beginning of index file */ + public QIndexFileIterator(QIndexFile qif) + { + this(qif, 0); + } + + /** Creates a new instance of QIndexFileIterator */ + public QIndexFileIterator(QIndexFile qif, int recNum) + { + file = qif; + this.recNum = recNum; + } + + public boolean hasNext() + { + return recNum < file.length(); + } + + public Object next() throws NoSuchElementException + { + String rec; + try { + rec = file.getRecordHash(recNum); + } + catch (Exception e) { + throw new NoSuchElementException("Reached end of index"); + } + recNum += 1; + return rec; + } + + public void remove() + { + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java b/apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java new file mode 100644 index 000000000..6b3955248 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QKademliaComparator.java @@ -0,0 +1,57 @@ +/* + * QKademliaComparator.java + * + * Created on March 30, 2005, 12:30 PM + */ + +package net.i2p.aum.q; + +import java.util.*; +import java.math.*; + +/** + * implements a Comparator class which compares two QPeerRec objects + * for kademlia-closeness to a given base64 sha hash value + */ +public class QKademliaComparator implements Comparator { + + QNode node; + BigInteger hashed; + + /** + * Creates a kademlia comparator, which given a base64 sha256 hash + * of something, can compare two nodes for their kademlia-closeness to + * that hash + * @param node a QNode object - needed for access to its base64 routines + * @param base64hash - string - a base64 representation of the sha256 hash + * of anything + */ + public QKademliaComparator(QNode node, String base64hash) { + + this.node = node; + hashed = new BigInteger(node.base64Dec(base64hash).getBytes()); + } + + /** + * compares two given QPeerRec objects for how close each one's ID + * is to the stored hash + */ + public int compare(Object o1, Object o2) { + + QPeer peer1 = (QPeer)o1; + QPeer peer2 = (QPeer)o2; + + String id1 = peer1.getId(); + String id2 = peer2.getId(); + + BigInteger i1 = new BigInteger(id1.getBytes()); + BigInteger i2 = new BigInteger(id2.getBytes()); + + BigInteger xor1 = i1.xor(hashed); + BigInteger xor2 = i2.xor(hashed); + + return xor1.compareTo(xor2); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QMgr.java b/apps/q/java/src/net/i2p/aum/q/QMgr.java new file mode 100644 index 000000000..62554a17d --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QMgr.java @@ -0,0 +1,921 @@ +/* + * QLaunch.java + * + * Created on March 30, 2005, 10:09 PM + */ + +package net.i2p.aum.q; + +import java.*; +import java.lang.*; +import java.io.*; +import java.util.*; + +import org.apache.xmlrpc.*; + +import net.i2p.data.*; + +import net.i2p.aum.*; + +/** + *'; i++) { + tag.append(c[i]); + } + // > is not allowed inside a template tag + // so we can be sure that if this is a + // template tag, it ends with a > + + // add the closing > as well + if(i -1) + { + do { + temp.append(tag.charAt(0)); + tag=new StringBuffer( + tag.toString().substring(1)); + } while(tag.charAt(0) != '<'); + } + + Util.debug_print("tag: " + tag); + + String test_tag = tag.toString().toLowerCase(); + // if it doesn't contain tmpl_ it is not + // a template tag + if(test_tag.indexOf("tmpl_") < 0) { + temp.append(tag); + continue; + } + + // may be a template tag + // check if it starts with tmpl_ + + test_tag = cleanTag(test_tag); + + Util.debug_print("clean: " + test_tag); + + // check if it is a closing tag + if(test_tag.startsWith("/")) + test_tag = test_tag.substring(1); + + // if it still doesn't start with tmpl_ + // then it is not a template tag + if(!test_tag.startsWith("tmpl_")) { + temp.append(tag); + continue; + } + + // now it must be a template tag + String tag_type=getTagType(test_tag); + + if(tag_type == null) { + if(strict) + throw new + IllegalArgumentException( + tag.toString()); + else + temp.append(tag); + } + + Util.debug_print("type: " + tag_type); + + // if this was an invalid key and we've + // reached so far, then next iteration + if(tag_type == null) + continue; + + // now, push the previous stuff + // into the Vector + if(temp.length()>0) { + parts.addElement(temp.toString()); + temp = new StringBuffer(); + } + + // it is a valid template tag + // get its properties + + Util.debug_print("Checking: " + tag); + Properties tag_props = + getTagProps(tag.toString()); + + if(tag_props.containsKey("name")) + Util.debug_print("name: " + + tag_props.getProperty("name")); + else + Util.debug_print("no name"); + + parts.addElement(tag_props); + } + } + + if(temp.length()>0) + parts.addElement(temp.toString()); + + return parts; + } + + private String cleanTag(String tag) + throws IllegalArgumentException + { + String test_tag = new String(tag); + // first remove < and > + if(test_tag.startsWith("<")) + test_tag = test_tag.substring(1); + if(test_tag.endsWith(">")) + test_tag = test_tag.substring(0, test_tag.length()-1); + else + throw new IllegalArgumentException("Tags must start " + + "and end on the same line"); + + // remove any leading !-- and trailing + // -- in case of comment style tags + if(test_tag.startsWith("!--")) { + test_tag=test_tag.substring(3); + } + if(test_tag.endsWith("--")) { + test_tag=test_tag.substring(0, test_tag.length()-2); + } + // then leading and trailing spaces + test_tag = test_tag.trim(); + + return test_tag; + } + + private String getTagType(String tag) + { + int sp = tag.indexOf(" "); + String tag_type=""; + if(sp < 0) { + tag_type = tag.toLowerCase(); + } else { + tag_type = tag.substring(0, sp).toLowerCase(); + } + if(tag_type.startsWith("tmpl_")) + tag_type=tag_type.substring(5); + + Util.debug_print("tag_type: " + tag_type); + + if(tag_type.equals("var") || + tag_type.equals("if") || + tag_type.equals("unless") || + tag_type.equals("loop") || + tag_type.equals("include") || + tag_type.equals("else")) { + return tag_type; + } else { + return null; + } + } + + private Properties getTagProps(String tag) + throws IllegalArgumentException, + NullPointerException + { + Properties p = new Properties(); + + tag = cleanTag(tag); + + Util.debug_print("clean: " + tag); + + if(tag.startsWith("/")) { + p.put("close", "true"); + tag=tag.substring(1); + } else { + p.put("close", ""); + } + + Util.debug_print("close: " + p.getProperty("close")); + + p.put("type", getTagType(tag)); + + Util.debug_print("type: " + p.getProperty("type")); + + if(p.getProperty("type").equals("else") || + p.getProperty("close").equals("true")) + return p; + + if(p.getProperty("type").equals("var")) + p.put("escape", ""); + + int sp = tag.indexOf(" "); + // if we've got so far, this must succeed + + tag = tag.substring(sp).trim(); + Util.debug_print("checking params: " + tag); + + // now, we should have either name=value pairs + // or name space escape in case of old style vars + + if(tag.indexOf("=") < 0) { + // no = means old style + // first will be var name + // second if any will be escape + + sp = tag.toLowerCase().indexOf(" escape"); + if(sp < 0) { + // no escape + p.put("name", tag); + p.put("escape", "0"); + } else { + tag = tag.substring(0, sp); + p.put("name", tag); + p.put("escape", "html"); + } + } else { + // = means name=value pairs. + // use a StringTokenizer + StringTokenizer st = new StringTokenizer(tag, " ="); + while(st.hasMoreTokens()) { + String key, value; + key = st.nextToken().toLowerCase(); + if(st.hasMoreTokens()) + value = st.nextToken(); + else if(key.equals("escape")) + value = "html"; + else + throw new NullPointerException( + "parameter " + key + " has no value"); + + if(value.startsWith("\"") && + value.endsWith("\"")) + value = value.substring(1, + value.length()-1); + else if(value.startsWith("'") && + value.endsWith("'")) + value = value.substring(1, + value.length()-1); + + if(value.length()==0) + throw new NullPointerException( + "parameter " + key + " has no value"); + + if(key.equals("escape")) + value=value.toLowerCase(); + + p.put(key, value); + } + } + + String name = p.getProperty("name"); + // if not case sensitive, and not special variable, flatten case + // never flatten case for includes + if(!case_sensitive && !p.getProperty("type").equals("include") + && !( name.startsWith("__") && name.endsWith("__") )) + { + p.put("name", name.toLowerCase()); + } + + if(!Util.isNameChar(name)) + throw new IllegalArgumentException( + "parameter name may only contain " + + "letters, digits, ., /, +, -, _"); + // __var__ is allowed in the template, but not in the + // code. this is so that people can reference __FIRST__, + // etc + + return p; + } +} diff --git a/apps/q/java/src/HTML/Tmpl/Util.java b/apps/q/java/src/HTML/Tmpl/Util.java new file mode 100644 index 000000000..46ad2568b --- /dev/null +++ b/apps/q/java/src/HTML/Tmpl/Util.java @@ -0,0 +1,130 @@ +/* +* HTML.Template: A module for using HTML Templates with java +* +* Copyright (c) 2002 Philip S Tellis (philip.tellis@iname.com) +* +* This module is free software; you can redistribute it +* and/or modify it under the terms of either: +* +* a) the GNU General Public License as published by the Free +* Software Foundation; either version 1, or (at your option) +* any later version, or +* +* b) the "Artistic License" which comes with this module. +* +* This program is distributed in the hope that it will be +* useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +* PURPOSE. See either the GNU General Public License or the +* Artistic License for more details. +* +* You should have received a copy of the Artistic License +* with this module, in the file ARTISTIC. If not, I'll be +* glad to provide one. +* +* You should have received a copy of the GNU General Public +* License along with this program; if not, write to the Free +* Software Foundation, Inc., 59 Temple Place, Suite 330, +* Boston, MA 02111-1307 USA +*/ + + +package HTML.Tmpl; + +public class Util +{ + public static boolean debug=false; + + public static String escapeHTML(String element) + { + String s = new String(element); // don't change the original + String [] metas = {"&", "<", ">", "\""}; + String [] repls = {"&", "<", ">", """}; + for(int i = 0; i < metas.length; i++) { + int pos=0; + do { + pos = s.indexOf(metas[i], pos); + if(pos<0) + break; + + s = s.substring(0, pos) + repls[i] + s.substring(pos+1); + pos++; + } while(pos >= 0); + } + + return s; + } + + public static String escapeURL(String url) + { + StringBuffer s = new StringBuffer(); + String no_escape = "./-_"; + + for(int i=0; i = 0); + } + + return s; + } + + public static boolean isNameChar(char c) + { + return true; + } + + public static boolean isNameChar(String s) + { + String alt_valid = "./+-_"; + + for(int i=0; i "+dest.toBase64()); + + start(); + + } + + /** + * run this EchoServer + */ + public void run() + { + System.out.println("Server: listening on dest:"); + + /** + try { + System.out.println(key.toDestinationBase64()); + } catch (DataFormatException e) { + e.printStackTrace(); + } + */ + + System.out.println(dest.toBase64()); + + while (true) + { + try { + I2PSocket sessSocket = serverSocket.accept(); + + System.out.println("Server: Got connection from client"); + + InputStream socketIn = sessSocket.getInputStream(); + OutputStreamWriter socketOut = new OutputStreamWriter(sessSocket.getOutputStream()); + + System.out.println("Server: created streams"); + + // read a line from input, and echo it back + String line = DataHelper.readLine(socketIn); + + System.out.println("Server: got '" + line + "'"); + + String reply = "EchoServer: got '" + line + "'\n"; + socketOut.write(reply); + socketOut.flush(); + + System.out.println("Server: sent trply"); + + sessSocket.close(); + + System.out.println("Server: closed socket"); + + } catch (ConnectException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (I2PException e) { + e.printStackTrace(); + } + + } + + } + + public Destination getDest() throws DataFormatException + { + // return key.toDestination(); + return dest; + } + + public String getDestBase64() throws DataFormatException + { + // return key.toDestinationBase64(); + return dest.toBase64(); + } + + /** + * runs EchoServer from the command shell + */ + public static void main(String [] args) + { + System.out.println("Constructing an EchoServer"); + + try { + EchoServer myServer = new EchoServer(); + System.out.println("Got an EchoServer"); + System.out.println("Here's the dest:"); + System.out.println(myServer.getDestBase64()); + + myServer.run(); + + } catch (I2PException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + + } +} + diff --git a/apps/q/java/src/net/i2p/aum/EchoTest.java b/apps/q/java/src/net/i2p/aum/EchoTest.java new file mode 100644 index 000000000..e3e009b0e --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/EchoTest.java @@ -0,0 +1,56 @@ +// runs EchoServer and EchoClient as threads + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.*; + +/** + * A simple program which runs the EchoServer and EchoClient + * demos as threads + */ + +public class EchoTest +{ + /** + * create one instance each of EchoServer and EchoClient, + * run the server as a thread, run the client in foreground, + * display detailed results + */ + public static void main(String [] args) + { + EchoServer server; + EchoClient client; + + try { + server = new EchoServer(); + Destination serverDest = server.getDest(); + + System.out.println("EchoTest: serverDest=" + serverDest.toBase64()); + + client = new EchoClient(serverDest); + + } catch (I2PException e) { + e.printStackTrace(); return; + } catch (IOException e) { + e.printStackTrace(); return; + } + + System.out.println("Starting server..."); + //server.start(); + + System.out.println("Starting client..."); + client.run(); + + } + +} + + diff --git a/apps/q/java/src/net/i2p/aum/EmbargoedQueue.java b/apps/q/java/src/net/i2p/aum/EmbargoedQueue.java new file mode 100644 index 000000000..39ca901f3 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/EmbargoedQueue.java @@ -0,0 +1,322 @@ +/* + * SimpleScheduler.java + * + * Created on March 24, 2005, 11:14 PM + */ + +package net.i2p.aum; + +import java.*; +import java.lang.*; +import java.util.*; + +/** + * Implements a queue of objects, where each object is 'embargoed' + * against release until a given time. Threads which attempt to .get + * items from this queue will block if the queue is empty, or if the + * first item of the queue has a 'release time' which has not yet passed.
+ * + *Think of it like a news desk which receives media releases which are + * 'embargoed' till a certain time. These releases sit in a queue, and when + * their embargo expires, they are actioned and go to print or broadcast. + * The reporters at this news desk are the 'threads', which get blocked + * until the next item's embargo expires.
+ * + *Purpose of implementing this is to provide a mechanism for scheduling + * background jobs to be executed at precise times
. + */ +public class EmbargoedQueue extends Thread { + + /** + * items which are waiting for dispatch - stored as 2-element vectors, + * where elem 0 is Integer dispatch time, and elem 1 is the object; + * note that this list is kept in strict ascending order of time. + * Whenever an object becomes ready, it is removed from this queue + * and appended to readyItems + */ + public Vector waitingItems; + + /** + * items which are ready for dispatch (their time has come). + */ + public SimpleQueue readyItems; + + /** set this true to enable verbose debug messages */ + public boolean debug = false; + + /** Creates a new embargoed queue */ + public EmbargoedQueue() { + waitingItems = new Vector(); + readyItems = new SimpleQueue(); + + // fire up scheduler thread + start(); + } + + /** + * fetches the item at head of queue, blocking if queue is empty + */ + public Object get() + { + return readyItems.get(); + } + + /** + * adds a new object to queue without any embargo (or, an embargo that expires + * immediately) + * @param item the object to be added + */ + public synchronized void putNow(Object item) + { + putAfter(0, item); + } + + /** + * adds a new object to queue, embargoed until given number of milliseconds + * have elapsed + * @param delay number of milliseconds from now when embargo expires + * @param item the object to be added + */ + public synchronized void putAfter(long delay, Object item) + { + long now = new Date().getTime(); + putAt(now+delay, item); + } + + /** + * adds a new object to the queue, embargoed until given time + * @param time the unixtime in milliseconds when the object's embargo expires, + * and the object is to be made available + * @param item the object to be added + */ + public synchronized void putAt(long time, Object item) + { + Vector elem = new Vector(); + elem.addElement(new Long(time)); + elem.addElement(item); + + long now = new Date().getTime(); + long future = time - now; + //System.out.println("putAt: time="+time+" ("+future+"ms from now), job="+item); + + // find where to insert + int i; + int nitems = waitingItems.size(); + for (i = 0; i < nitems; i++) + { + // get item i + Vector itemI = (Vector)waitingItems.get(i); + long timeI = ((Long)(itemI.get(0))).longValue(); + if (time < timeI) + { + // new item earlier than item i, insert here and bust out + waitingItems.insertElementAt(elem, i); + break; + } + } + + // did we insert? + if (i == nitems) + { + // no - gotta append + waitingItems.addElement(elem); + } + + // debugging + if (debug) { + printWaiting(); + } + + // awaken this scheduler object's thread, so it can + // see if any jobs are ready + //notify(); + interrupt(); + } + + /** + * for debugging - prints out a list of waiting items + */ + public synchronized void printWaiting() + { + int i; + long now = new Date().getTime(); + + System.out.println("EmbargoedQueue dump:"); + + System.out.println(" Waiting items:"); + int nwaiting = waitingItems.size(); + for (i = 0; i < nwaiting; i++) + { + Vector item = (Vector)waitingItems.get(i); + long when = ((Long)item.get(0)).longValue(); + Object job = item.get(1); + int delay = (int)(when - now)/1000; + System.out.println(" "+delay+"s, t="+when+", job="+job); + } + + System.out.println(" Ready items:"); + int nready = readyItems.items.size(); + for (i = 0; i < nready; i++) + { + //Vector item = (Vector)readyItems.items.get(i); + Object item = readyItems.items.get(i); + System.out.println(" job="+item); + } + + } + + /** + * scheduling thread, which wakes up every time a new job is queued, and + * if any jobs are ready, transfers them to the readyQueue and notifies + * any waiting client threads + */ + public void run() + { + // monitor the waiting queue, waiting till one becomes ready + while (true) + { + try { + if (waitingItems.size() > 0) + { + // at least 1 waiting item + Vector item = (Vector)(waitingItems.get(0)); + long now = new Date().getTime(); + long then = ((Long)item.get(0)).longValue(); + long delay = then - now; + + // ready? + if (delay <= 0) + { + // yep, ready, remove job and stick on waiting queue + waitingItems.remove(0); // ditch from waiting + Object elem = item.get(1); + readyItems.put(elem); // and add to ready + + if (debug) + { + System.out.println("embargo expired on "+elem); + printWaiting(); + } + } + else + { + // not ready, hang about till we get woken, or the + // job becomes ready + if (debug) + { + System.out.println("waiting for "+delay+"ms"); + } + Thread.sleep(delay); + } + } + else + { + // no items yet, hang out for an interrupt + if (debug) + { + System.out.println("queue is empty"); + } + synchronized (this) { + wait(); + } + } + } catch (Exception e) { + //System.out.println("exception"); + if (debug) + { + System.out.println("exception ("+e.getClass().getName()+") "+e.getMessage()); + } + } + } + } + + private static class TestThread extends Thread { + + String id; + + EmbargoedQueue q; + + public TestThread(String id, EmbargoedQueue q) { + this.id = id; + this.q = q; + } + + public void run() { + try { + print("waiting for queue"); + + Object item = q.get(); + + print("got item: '"+item+"'"); + + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + public void print(String msg) { + System.out.println("thread '"+id+"': "+msg); + } + + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + + int i; + int nthreads = 7; + + Thread [] threads = new Thread[nthreads]; + + EmbargoedQueue q = new EmbargoedQueue(); + SimpleSemaphore threadPool = new SimpleSemaphore(nthreads); + + // populate the queue with some stuff + q.putAfter(10000, "red"); + q.putAfter(3000, "orange"); + q.putAfter(6000, "yellow"); + + // populate threads array + for (i = 0; i < nthreads; i++) { + threads[i] = new TestThread("thread"+i, q); + } + + // and launch the threads + for (i = 0; i < nthreads; i++) { + threads[i].start(); + } + + // wait, presumably till all these elements are actioned + try { + Thread.sleep(12000); + } catch (Exception e) { + e.printStackTrace(); + return; + } + + // add some more shit to the queue, randomly scheduled + Random r = new Random(); + String [] items = {"green", "blue", "indigo", "violet", "black", "white", "brown"}; + for (i = 0; i < items.length; i++) { + String item = items[i]; + int delay = 2000 + r.nextInt(8000); + System.out.println("main: adding '"+item+"' after "+delay+"ms ..."); + q.putAfter(delay, item); + } + + // wait, presumably for all jobs to finish + try { + Thread.sleep(12000); + } catch (Exception e) { + e.printStackTrace(); + return; + } + + System.out.println("main: terminating"); + + } + +} diff --git a/apps/q/java/src/net/i2p/aum/I2PCat.java b/apps/q/java/src/net/i2p/aum/I2PCat.java new file mode 100644 index 000000000..ad76e546f --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PCat.java @@ -0,0 +1,452 @@ + +// I2P equivalent of 'netcat' + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.naming.*; +import net.i2p.client.streaming.*; +import net.i2p.data.*; + +import net.i2p.util.*; + +/** + * A I2P equivalent of the much-beloved 'netcat' utility. + * This command-line utility can either connect to a remote + * destination, or listen on a private destination for incoming + * connections. Once a connection is established, input on stdin + * is sent to the remote peer, and anything received from the + * remote peer is printed to stdout + */ + +public class I2PCat extends Thread +{ + public I2PSocketManager socketManager; + public I2PServerSocket serverSocket; + public I2PSocket sessSocket; + + public PrivDestination key; + public Destination dest; + + public InputStream socketIn; + public OutputStream socketOutStream; + public OutputStreamWriter socketOut; + + public SockInput rxThread; + + protected static Log _log; + + public static String defaultHost = "127.0.0.1"; + public static int defaultPort = 7654; + + /** + * a thread for reading from socket and displaying on stdout + */ + private class SockInput extends Thread { + + InputStream _in; + + protected Log _log; + public SockInput(InputStream i) { + + _in = i; + } + + public void run() + { + // the thread portion, receives incoming bytes on + // the socket input stream and spits them to stdout + + byte [] ch = new byte[1]; + + print("Receiver thread listening..."); + + try { + while (true) { + + //String line = DataHelper.readLine(socketIn); + if (_in.read(ch) != 1) { + print("failed to receive from socket"); + break; + } + + //System.out.println(line); + System.out.write(ch, 0, 1); + System.out.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + print("Receiver thread crashed, terminating!!"); + System.exit(1); + } + + } + + + void print(String msg) + { + System.out.println("-=- I2PCat: "+msg); + + if (_log != null) { + _log.debug(msg); + } + + } + + + } + + + public I2PCat() + { + _log = new Log("I2PCat"); + + } + + /** + * Runs I2PCat in server mode, listening on the given destination + * for one incoming connection. Once connection is established, + * copyies data between the remote peer and + * the local terminal console. + */ + public void runServer(String keyStr) throws IOException, DataFormatException + { + Properties props = new Properties(); + props.setProperty("inbound.length", "0"); + props.setProperty("outbound.length", "0"); + props.setProperty("inbound.lengthVariance", "0"); + props.setProperty("outbound.lengthVariance", "0"); + + // generate new key if needed + if (keyStr.equals("new")) { + + try { + key = PrivDestination.newKey(); + } catch (I2PException e) { + e.printStackTrace(); + return; + } catch (IOException e) { + e.printStackTrace(); + return; + } + + print("Creating new server dest..."); + + socketManager = I2PSocketManagerFactory.createManager(key.getInputStream(), props); + + print("Getting server socket..."); + + serverSocket = socketManager.getServerSocket(); + + print("Server socket created, ready to run..."); + + dest = socketManager.getSession().getMyDestination(); + + print("private key follows:"); + System.out.println(key.toBase64()); + + print("dest follows:"); + System.out.println(dest.toBase64()); + + } + + else { + + key = PrivDestination.fromBase64String(keyStr); + + String dest64Abbrev = key.toBase64().substring(0, 16); + + print("Creating server socket manager on dest "+dest64Abbrev+"..."); + + socketManager = I2PSocketManagerFactory.createManager(key.getInputStream(), props); + + serverSocket = socketManager.getServerSocket(); + + print("Server socket created, ready to run..."); + } + + print("Awaiting client connection..."); + + I2PSocket sessSocket; + + try { + sessSocket = serverSocket.accept(); + } catch (I2PException e) { + e.printStackTrace(); + return; + } catch (ConnectException e) { + e.printStackTrace(); + return; + } + + print("Got connection from client"); + + chat(sessSocket); + + } + + public void runClient(String destStr) + throws DataFormatException, IOException + { + runClient(destStr, defaultHost, defaultPort); + } + + /** + * runs I2PCat in client mode, connecting to a remote + * destination then copying data between the remote peer and + * the local terminal console + */ + public void runClient(String destStr, String host, int port) + throws DataFormatException, IOException + { + // accept 'file:' prefix + if (destStr.startsWith("file:", 0)) + { + String path = destStr.substring(5); + destStr = new SimpleFile(path, "r").read(); + } + + else if (destStr.length() < 255) { + // attempt hosts file lookup + I2PAppContext ctx = new I2PAppContext(); + HostsTxtNamingService h = new HostsTxtNamingService(ctx); + Destination dest1 = h.lookup(destStr); + if (dest1 == null) { + usage("Cannot resolve hostname: '"+destStr+"'"); + } + + // successful lookup + runClient(dest1, host, port); + } + + else { + // otherwise, bigger strings are assumed to be base64 dests + + Destination dest = new Destination(); + dest.fromBase64(destStr); + runClient(dest, host, port); + } + } + + public void runClient(Destination dest) { + runClient(dest, "127.0.0.1", 7654); + } + + /** + * An alternative constructor which accepts an I2P Destination object + */ + public void runClient(Destination dest, String host, int port) + { + this.dest = dest; + + String destAbbrev = dest.toBase64().substring(0, 16)+"..."; + + print("Connecting via i2cp "+host+":"+port+" to destination "+destAbbrev+"..."); + System.out.flush(); + + try { + // get a socket manager + socketManager = I2PSocketManagerFactory.createManager(host, port); + + // get a client socket + print("socketManager="+socketManager); + + sessSocket = socketManager.connect(dest); + + } catch (I2PException e) { + e.printStackTrace(); + return; + } catch (ConnectException e) { + e.printStackTrace(); + return; + } catch (NoRouteToHostException e) { + e.printStackTrace(); + return; + } catch (InterruptedIOException e) { + e.printStackTrace(); + return; + } + + print("Successfully connected!"); + print("(Press Control-C to quit)"); + + // Perform console interaction + chat(sessSocket); + + try { + sessSocket.close(); + + } catch (IOException e) { + e.printStackTrace(); + return; + } + } + + /** + * Launch the background thread to copy incoming data to stdout, then + * loop in foreground copying lines from stdin and sending them to remote peer + */ + public void chat(I2PSocket sessSocket) { + + try { + socketIn = sessSocket.getInputStream(); + socketOutStream = sessSocket.getOutputStream(); + socketOut = new OutputStreamWriter(socketOutStream); + + // launch receiver thread + start(); + //launchRx(); + + while (true) { + + String line = DataHelper.readLine(System.in); + print("sent: '"+line+"'"); + + socketOut.write(line+"\n"); + socketOut.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + return; + } + + } + + /** + * executes in a thread, receiving incoming bytes on + * the socket input stream and spitting them to stdout + */ + public void run() + { + + byte [] ch = new byte[1]; + + print("Receiver thread listening..."); + + try { + while (true) { + + //String line = DataHelper.readLine(socketIn); + if (socketIn.read(ch) != 1) { + print("failed to receive from socket"); + break; + } + + //System.out.println(line); + System.out.write(ch, 0, 1); + System.out.flush(); + } + } catch (IOException e) { + e.printStackTrace(); + print("Receiver thread crashed, terminating!!"); + System.exit(1); + } + + } + + + public void launchRx() { + + rxThread = new SockInput(socketIn); + rxThread.start(); + + } + + static void print(String msg) + { + System.out.println("-=- I2PCat: "+msg); + + if (_log != null) { + _log.debug(msg); + } + + } + + public static void usage(String msg) + { + usage(msg, 1); + } + + public static void usage(String msg, int ret) + { + System.out.println(msg); + usage(ret); + } + + public static void usage(int ret) + { + System.out.print( + "This utility is an I2P equivalent of the standard *nix 'netcat' utility\n"+ + "usage:\n"+ + " net.i2p.aum.I2PCat [-h]\n"+ + " - display this help\n"+ + " net.i2p.aum.I2PCat dest [host [port]]\n"+ + " - run in client mode, 'dest' should be one of:\n"+ + " hostname.i2p - an I2P hostname listed in hosts.txt\n"+ + " (only works with a hosts.txt in current directory)\n"+ + " base64dest - a full base64 destination string\n"+ + " file:b64filename - filename of a file containing base64 dest\n"+ + " net.i2p.aum.I2PCat -l privkey\n"+ + " - run in server mode, 'key' should be one of:\n"+ + " base64privkey - a full base64 private key string\n"+ + " file:b64filename - filename of a file containing base64 privkey\n"+ + "\n" + ); + System.exit(ret); + } + + public static void main(String [] args) throws IOException, DataFormatException + { + int argc = args.length; + + // barf if no args + if (argc == 0) { + usage("Missing argument"); + } + + // show help on request + if (args[0].equals("-h") || args[0].equals("--help")) { + usage(0); + } + + // server or client? + if (args[0].equals("-l")) { + if (argc != 2) { + usage("Bad argument count"); + } + + new I2PCat().runServer(args[1]); + } + else { + // client mode - barf if not 1-3 args + if (argc < 1 || argc > 3) { + usage("Bad argument count"); + } + + try { + int port = defaultPort; + String host = defaultHost; + if (args.length > 1) { + host = args[1]; + if (args.length > 2) { + port = new Integer(args[2]).intValue(); + } + } + new I2PCat().runClient(args[0], host, port); + + } catch (DataFormatException e) { + e.printStackTrace(); + } + } + } + +} + + + diff --git a/apps/q/java/src/net/i2p/aum/I2PSocketHelper.java b/apps/q/java/src/net/i2p/aum/I2PSocketHelper.java new file mode 100644 index 000000000..77b7b0e5b --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PSocketHelper.java @@ -0,0 +1,26 @@ + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + +/** + * Class which wraps an I2PSocket object with convenient methods. + * Nothing presently implemented here. + */ + +public class I2PSocketHelper +{ + +} + + diff --git a/apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java b/apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java new file mode 100644 index 000000000..7998f731a --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PTunnelXMLObject.java @@ -0,0 +1,147 @@ +package net.i2p.aum; + +import org.apache.xmlrpc.*; +import java.lang.*; +import java.io.*; +import java.util.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + + +/** + * Defines the I2P tunnel management methods which will be + * exposed to XML-RPC clients + * Methods in this class are forwarded to an I2PTunnelXMLWrapper object + */ +public class I2PTunnelXMLObject +{ + protected I2PTunnelXMLWrapper tunmgr; + + /** + * Builds the interface object. You normally shouldn't have to + * instantiate this directly - leave it to I2PTunnelXMLServer + */ + public I2PTunnelXMLObject() + { + tunmgr = new I2PTunnelXMLWrapper(); + } + + /** + * Generates an I2P keypair, returning a dict with keys 'result' (usually 'ok'), + * priv' (private key as base64) and 'dest' (destination as base64) + */ + public Hashtable genkeys() + { + return tunmgr.xmlrpcGenkeys(); + } + + /** + * Get a list of active TCP tunnels currently being managed by this + * tunnel manager. + * @return a dict with keys 'status' (usually 'ok'), + * 'jobs' (a list of dicts representing each job, each with keys 'job' (int, job + * number), 'type' (string, 'server' or 'client'), port' (int, the port number). + * Also for server, keys 'host' (hostname, string) and 'ip' (IP address, string). + * For clients, key 'dest' (string, remote destination as base64). + */ + public Hashtable list() + { + return tunmgr.xmlrpcList(); + } + + /** + * Attempts to find I2P hostname in hosts.txt. + * @param hostname string, I2P hostname + * @return dict with keys 'status' ('ok' or 'fail'), + * and if successful lookup, 'dest' (base64 destination). + */ + public Hashtable lookup(String hostname) + { + return tunmgr.xmlrpcLookup(hostname); + } + + /** + * Attempt to open client tunnel + * @param port local port to listen on, int + * @param dest remote dest to tunnel to, base64 string + * @return dict with keys 'status' (string - 'ok' or 'fail'). + * If 'ok', also key 'result' with text output from tunnelmgr + */ + public Hashtable client(int port, String dest) + { + return tunmgr.xmlrpcClient(port, dest); + } + + /** + * Attempts to open server tunnel + * @param host TCP hostname of TCP server to tunnel to + * @param port number of TCP server + * @param key - base64 private key to receive I2P connections on + * @return dict with keys 'status' (string, 'ok' or 'fail'). + * if 'fail', also a key 'error' with explanatory text. + */ + public Hashtable server(String host, int port, String key) + { + return tunmgr.xmlrpcServer(host, port, key); + } + + /** + * Close an existing tunnel + * @param jobnum (int) job number of connection to close + * @return dict with keys 'status' (string, 'ok' or 'fail') + */ + public Hashtable close(int jobnum) + { + return tunmgr.xmlrpcClose(jobnum); + } + + /** + * Close an existing tunnel + * @param jobnum (string) job number of connection to close as string, + * 'all' to close all jobs. + * @return dict with keys 'status' (string, 'ok' or 'fail') + */ + public Hashtable close(String job) + { + return tunmgr.xmlrpcClose(job); + } + + /** + * Close zero or more tunnels matching given criteria + * @param criteria A dict containing zero or more of the keys: + * 'job' (job number), 'type' (string, 'server' or 'client'), + * 'host' (hostname), 'port' (port number), + * 'ip' (IP address), 'dest' (string, remote dest) + */ + public Hashtable close(Hashtable criteria) + { + return tunmgr.xmlrpcClose(criteria); + } + + /** + * simple method to help with debugging your client prog + * @param x an int + * @return x + 1 + */ + public int bar(int x) + { + System.out.println("foo invoked"); + return x + 1; + } + + /** + * as for bar(int), but returns zero if no arg given + */ + public int bar() + { + return bar(0); + } + +} + + diff --git a/apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java b/apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java new file mode 100644 index 000000000..e489b130a --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PTunnelXMLServer.java @@ -0,0 +1,72 @@ + +package net.i2p.aum; + +import org.apache.xmlrpc.*; +import java.lang.*; +import java.io.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + +/** + * Provides a means for programs in any language to dynamically manage + * their own I2P <-> TCP tunnels, via simple TCP XML-RPC function calls. + * This server is presently hardwired to listen on port 22322. + */ + +public class I2PTunnelXMLServer +{ + protected WebServer ws; + protected I2PTunnelXMLObject tunobj; + + public int port = 22322; + + // constructor + + public void _init() + { + ws = new WebServer(port); + tunobj = new I2PTunnelXMLObject(); + ws.addHandler("i2p.tunnel", tunobj); + + } + + + // default constructor + public I2PTunnelXMLServer() + { + super(); + _init(); + } + + // constructor which takes shell args + public I2PTunnelXMLServer(String args[]) + { + super(); + _init(); + } + + // run the server + public void run() + { + ws.start(); + System.out.println("I2PTunnel XML-RPC server listening on port "+port); + ws.run(); + + } + + public static void main(String args[]) + { + I2PTunnelXMLServer tun; + + tun = new I2PTunnelXMLServer(); + tun.run(); + } + +} + + diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java new file mode 100644 index 000000000..ca59ea42c --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PXmlRpcClient.java @@ -0,0 +1,72 @@ + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.Base64; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + + +/** + * an object which is used to invoke methods on remote I2P XML-RPC + * servers. You should not instantiate these objects directly, but + * create them through + * {@link net.i2p.aum.I2PXmlRpcClientFactory#newClient(Destination) I2PXmlRpcClientFactory.newClient()} + * Note that this is really just a thin wrapper around XmlRpcClient, mostly for reasons + * of consistency with I2PXmlRpcServer[Factory]. + */ + +public class I2PXmlRpcClient extends XmlRpcClient +{ + public static boolean debug = false; + + protected static Log _log; + + /** + * Construct an I2P XML-RPC client with this URL. + * Note that you should not + * use this constructor directly - use I2PXmlRpcClientFactory.newClient() instead + */ + public I2PXmlRpcClient(URL url) + { + super(url); + _log = new Log("I2PXmlRpcClient"); + + } + + /** + * Construct a XML-RPC client for the URL represented by this String. + * Note that you should not + * use this constructor directly - use I2PXmlRpcClientFactory.newClient() instead + */ + public I2PXmlRpcClient(String url) throws MalformedURLException + { + super(url); + _log = new Log("I2PXmlRpcClientFactory"); + + } + + /** + * Construct a XML-RPC client for the specified hostname and port. + * Note that you should not + * use this constructor directly - use I2PXmlRpcClientFactory.newClient() instead + */ + public I2PXmlRpcClient(String hostname, int port) throws MalformedURLException + { + super(hostname, port); + _log = new Log("I2PXmlRpcClient"); + + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java new file mode 100644 index 000000000..d925889b9 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PXmlRpcClientFactory.java @@ -0,0 +1,230 @@ + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.Base64; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + + +/** + * Creates I2P XML-RPC client objects, which you can use + * to issue XML-RPC function calls over I2P. + * Instantiating this class causes the vm-wide http proxy system + * properties to be set to the address of the I2P eepProxy host/port. + * I2PXmlRpcClient objects need to communicate with the I2P + * eepProxy. If your eepProxy is at the standard localhost:4444 address, + * you can use the default constructor. Otherwise, you can set this + * eepProxy address by either (1) passing eepProxy hostname/port to the + * constructor, or (2) running the jvm with 'eepproxy.tcp.host' and + * 'eepproxy.tcp.port' system properties set. Note that (1) takes precedence. + * Failure to set up EepProxy host/port correctly will result in an IOException + * when you invoke .execute() on your client objects. + * Invoke this class from your shell to see a demo + */ + +public class I2PXmlRpcClientFactory +{ + public static boolean debug = false; + + public static String _defaultEepHost = "127.0.0.1"; + public static int _defaultEepPort = 4444; + + protected static Log _log; + + /** + * Create an I2P XML-RPC client factory, and set it to create + * clients of a given class. + * @param clientClass a class to use when creating new clients + */ + public I2PXmlRpcClientFactory() + { + this(null, 0); + } + + /** + * Create an I2P XML-RPC client factory, and set it to create + * clients of a given class, and dispatch calls through a non-standard + * eepProxy. + * @param eepHost the eepProxy TCP hostname + * @param eepPort the eepProxy TCP port number + */ + public I2PXmlRpcClientFactory(String eepHost, int eepPort) + { + String eepPortStr; + + _log = new Log("I2PXmlRpcClientFactory"); + _log.shouldLog(Log.DEBUG); + + Properties p = System.getProperties(); + + // determine what actual eepproxy host/port we're using + if (eepHost == null) { + eepHost = p.getProperty("eepproxy.tcp.host", _defaultEepHost); + } + if (eepPort > 0) { + eepPortStr = String.valueOf(eepPort); + } + else { + eepPortStr = p.getProperty("eepproxy.tcp.port"); + if (eepPortStr == null) { + eepPortStr = String.valueOf(_defaultEepPort); + } + } + + p.put("proxySet", "true"); + p.put("http.proxyHost", eepHost); + p.put("http.proxyPort", eepPortStr); + } + + /** + * Create an I2P XML-RPC client object, which is subsequently used for + * dispatching XML-RPC requests. + * @param dest - an I2P destination object, comprising the + * destination of the remote + * I2P XML-RPC server. + * @return a new XmlRpcClient object (refer org.apache.xmlrpc.XmlRpcClient). + */ + public I2PXmlRpcClient newClient(Destination dest) throws MalformedURLException { + + return newClient(new URL("http", "i2p/"+dest.toBase64(), "/")); + } + + /** + * Create an I2P XML-RPC client object, which is subsequently used for + * dispatching XML-RPC requests. + * @param hostOrDest - an I2P hostname (listed in hosts.txt) or a + * destination base64 string, for the remote I2P XML-RPC server + * @return a new XmlRpcClient object (refer org.apache.xmlrpc.XmlRpcClient). + */ + public I2PXmlRpcClient newClient(String hostOrDest) + throws DataFormatException, MalformedURLException + { + String hostname; + URL u; + + try { + // try to make a dest out of the string + Destination dest = new Destination(); + dest.fromBase64(hostOrDest); + + // converted ok, treat as valid dest, form i2p/blahblah url from it + I2PXmlRpcClient client = newClient(new URL("http", "i2p/"+hostOrDest, "/")); + client.debug = debug; + return client; + + } catch (DataFormatException e) { + + if (debug) { + e.printStackTrace(); + print("hostOrDest length="+hostOrDest.length()); + } + + // failed to load up a dest, test length + if (hostOrDest.length() < 255) { + // short-ish, assume a hostname + u = new URL("http", hostOrDest, "/"); + I2PXmlRpcClient client = newClient(u); + client.debug = debug; + return client; + } + else { + // too long for a host, barf + throw new DataFormatException("Bad I2P hostname/dest:\n"+hostOrDest); + } + } + } + + /** + * Create an I2P XML-RPC client object, which is subsequently used for + * dispatching XML-RPC requests. This method is not recommended. + * @param u - a URL object, containing the URL of the remote + * I2P XML-RPC server, for example, "http://xmlrpc.aum.i2p" (assuming + * there's a hosts.txt entry for 'xmlrpc.aum.i2p'), or + * "http://i2p/base64destblahblah...". Note that if you use this method + * directly, the created XML-RPC client object will ONLY work if you + * instantiate the URL object as 'new URL("http", "i2p/"+host-or-dest, "/")'. + */ + protected I2PXmlRpcClient newClient(URL u) + { + Object [] args = { u }; + //return new I2PXmlRpcClient(u); + + // construct and return a client object of required class + return new I2PXmlRpcClient(u); + } + + /** + * Runs a demo of an I2P XML-RPC client. Assumes you have already + * launched an I2PXmlRpcServerFactory demo, because it gets its + * dest from the file 'demo.dest64' created by I2PXmlRpcServerFactory demo. + * + * Ensure you have first launched net.i2p.aum.I2PXmlRpcServerFactory + * from your command line. + */ + public static void main(String [] args) { + + String destStr; + + debug = true; + + try { + print("Creating client factory..."); + + I2PXmlRpcClientFactory f = new I2PXmlRpcClientFactory(); + + print("Creating new client..."); + + if (args.length == 0) { + print("Reading dest from demo.dest64"); + destStr = new SimpleFile("demo.dest64", "r").read(); + } + else { + destStr = args[0]; + } + + XmlRpcClient c = f.newClient(destStr); + + print("Invoking foo..."); + + Vector v = new Vector(); + v.add("one"); + v.add("two"); + + Object res = c.execute("foo.bar", v); + + print("Got back object: " + res); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + /** + * Used for internal debugging + */ + protected static void print(String msg) + { + if (debug) { + System.out.println("I2PXmlRpcClient: " + msg); + + if (_log != null) { + System.out.println("LOGGING SOME SHIT"); + _log.debug(msg); + } + } + } +} + + + diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java new file mode 100644 index 000000000..d62b3ae39 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PXmlRpcDemoClass.java @@ -0,0 +1,35 @@ + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.Base64; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + +/** + * A simple class providing callable xmlrpc server methods, gets linked in to + * the server demo. + */ +public class I2PXmlRpcDemoClass +{ + public int add1(int n) { + return n + 1; + } + + public String bar(String arg1, String arg2) { + System.out.println("Demo: got hit to bar: arg1='"+arg1+"', arg2='"+arg2+"'"); + return "I2P demo xmlrpc server(foo.bar): arg1='"+arg1+"', arg2='"+arg2+"'"; + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java b/apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java new file mode 100644 index 000000000..a64878706 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/I2PXmlRpcServer.java @@ -0,0 +1,429 @@ +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.Base64; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + + +/** + * An XML-RPC server which works completely within I2P, listening + * on a dest for requests. + * You should not instantiate this class directly, but instead create + * an I2PXmlRpcServerFactory object, and use its .newServer() method + * to create a server object. + */ +public class I2PXmlRpcServer extends XmlRpcServer implements Runnable +{ + public class I2PXmlRpcServerWorkerThread extends Thread { + + I2PSocket _sock; + + public I2PXmlRpcServerWorkerThread(I2PSocket sock) { + _sock = sock; + } + + public void run() { + + try { + System.out.println("I2PXmlRpcServer.run: got inbound XML-RPC I2P conn"); + + log.info("run: Got client connection, creating streams"); + + InputStream socketIn = _sock.getInputStream(); + OutputStreamWriter socketOut = new OutputStreamWriter(_sock.getOutputStream()); + + log.info("run: reading http headers"); + + // read headers, determine size of req + int size = readHttpHeaders(socketIn); + + if (size <= 0) { + // bad news + log.info("read req failed, terminating session"); + _sock.close(); + return; + } + + log.info("run: reading request body of "+size+" bytes"); + + // get raw request body + byte [] reqBody = new byte[size]; + for (int i=0; imimetypes + */ + +public class Mimetypes +{ + public static String [][] _map = { + + { ".bz2", "application/x-bzip2" }, + { ".csm", "application/cu-seeme" }, + { ".cu", "application/cu-seeme" }, + { ".tsp", "application/dsptype" }, + { ".xls", "application/excel" }, + { ".spl", "application/futuresplash" }, + { ".hqx", "application/mac-binhex40" }, + { ".doc", "application/msword" }, + { ".dot", "application/msword" }, + { ".bin", "application/octet-stream" }, + { ".oda", "application/oda" }, + { ".pdf", "application/pdf" }, + { ".asc", "application/pgp-keys" }, + { ".pgp", "application/pgp-signature" }, + { ".ps", "application/postscript" }, + { ".ai", "application/postscript" }, + { ".eps", "application/postscript" }, + { ".ppt", "application/powerpoint" }, + { ".rtf", "application/rtf" }, + { ".wp5", "application/wordperfect5.1" }, + { ".zip", "application/zip" }, + { ".wk", "application/x-123" }, + { ".bcpio", "application/x-bcpio" }, + { ".pgn", "application/x-chess-pgn" }, + { ".cpio", "application/x-cpio" }, + { ".deb", "application/x-debian-package" }, + { ".dcr", "application/x-director" }, + { ".dir", "application/x-director" }, + { ".dxr", "application/x-director" }, + { ".dvi", "application/x-dvi" }, + { ".pfa", "application/x-font" }, + { ".pfb", "application/x-font" }, + { ".gsf", "application/x-font" }, + { ".pcf", "application/x-font" }, + { ".pcf.Z", "application/x-font" }, + { ".gtar", "application/x-gtar" }, + { ".tgz", "application/x-gtar" }, + { ".hdf", "application/x-hdf" }, + { ".phtml", "application/x-httpd-php" }, + { ".pht", "application/x-httpd-php" }, + { ".php", "application/x-httpd-php" }, + { ".php3", "application/x-httpd-php3" }, + { ".phps", "application/x-httpd-php3-source" }, + { ".php3p", "application/x-httpd-php3-preprocessed" }, + { ".class", "application/x-java" }, + { ".latex", "application/x-latex" }, + { ".frm", "application/x-maker" }, + { ".maker", "application/x-maker" }, + { ".frame", "application/x-maker" }, + { ".fm", "application/x-maker" }, + { ".fb", "application/x-maker" }, + { ".book", "application/x-maker" }, + { ".fbdoc", "application/x-maker" }, + { ".mif", "application/x-mif" }, + { ".nc", "application/x-netcdf" }, + { ".cdf", "application/x-netcdf" }, + { ".pac", "application/x-ns-proxy-autoconfig" }, + { ".o", "application/x-object" }, + { ".pl", "application/x-perl" }, + { ".pm", "application/x-perl" }, + { ".shar", "application/x-shar" }, + { ".swf", "application/x-shockwave-flash" }, + { ".swfl", "application/x-shockwave-flash" }, + { ".sit", "application/x-stuffit" }, + { ".sv4cpio", "application/x-sv4cpio" }, + { ".sv4crc", "application/x-sv4crc" }, + { ".tar", "application/x-tar" }, + { ".gf", "application/x-tex-gf" }, + { ".pk", "application/x-tex-pk" }, + { ".PK", "application/x-tex-pk" }, + { ".texinfo", "application/x-texinfo" }, + { ".texi", "application/x-texinfo" }, + { ".~", "application/x-trash" }, + { ".%", "application/x-trash" }, + { ".bak", "application/x-trash" }, + { ".old", "application/x-trash" }, + { ".sik", "application/x-trash" }, + { ".t", "application/x-troff" }, + { ".tr", "application/x-troff" }, + { ".roff", "application/x-troff" }, + { ".man", "application/x-troff-man" }, + { ".me", "application/x-troff-me" }, + { ".ms", "application/x-troff-ms" }, + { ".ustar", "application/x-ustar" }, + { ".src", "application/x-wais-source" }, + { ".wz", "application/x-wingz" }, + { ".au", "audio/basic" }, + { ".snd", "audio/basic" }, + { ".mid", "audio/midi" }, + { ".midi", "audio/midi" }, + { ".mpga", "audio/mpeg" }, + { ".mpega", "audio/mpeg" }, + { ".mp2", "audio/mpeg" }, + { ".mp3", "audio/mpeg" }, + { ".m3u", "audio/mpegurl" }, + { ".aif", "audio/x-aiff" }, + { ".aiff", "audio/x-aiff" }, + { ".aifc", "audio/x-aiff" }, + { ".gsm", "audio/x-gsm" }, + { ".ra", "audio/x-pn-realaudio" }, + { ".rm", "audio/x-pn-realaudio" }, + { ".ram", "audio/x-pn-realaudio" }, + { ".rpm", "audio/x-pn-realaudio-plugin" }, + { ".wav", "audio/x-wav" }, + { ".gif", "image/gif" }, + { ".ief", "image/ief" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".jpe", "image/jpeg" }, + { ".png", "image/png" }, + { ".tiff", "image/tiff" }, + { ".tif", "image/tiff" }, + { ".ras", "image/x-cmu-raster" }, + { ".bmp", "image/x-ms-bmp" }, + { ".pnm", "image/x-portable-anymap" }, + { ".pbm", "image/x-portable-bitmap" }, + { ".pgm", "image/x-portable-graymap" }, + { ".ppm", "image/x-portable-pixmap" }, + { ".rgb", "image/x-rgb" }, + { ".xbm", "image/x-xbitmap" }, + { ".xpm", "image/x-xpixmap" }, + { ".xwd", "image/x-xwindowdump" }, + { ".csv", "text/comma-separated-values" }, + { ".html", "text/html" }, + { ".htm", "text/html" }, + { ".mml", "text/mathml" }, + { ".txt", "text/plain" }, + { ".rtx", "text/richtext" }, + { ".tsv", "text/tab-separated-values" }, + { ".h++", "text/x-c++hdr" }, + { ".hpp", "text/x-c++hdr" }, + { ".hxx", "text/x-c++hdr" }, + { ".hh", "text/x-c++hdr" }, + { ".c++", "text/x-c++src" }, + { ".cpp", "text/x-c++src" }, + { ".cxx", "text/x-c++src" }, + { ".cc", "text/x-c++src" }, + { ".h", "text/x-chdr" }, + { ".csh", "text/x-csh" }, + { ".c", "text/x-csrc" }, + { ".java", "text/x-java" }, + { ".moc", "text/x-moc" }, + { ".p", "text/x-pascal" }, + { ".pas", "text/x-pascal" }, + { ".etx", "text/x-setext" }, + { ".sh", "text/x-sh" }, + { ".tcl", "text/x-tcl" }, + { ".tk", "text/x-tcl" }, + { ".tex", "text/x-tex" }, + { ".ltx", "text/x-tex" }, + { ".sty", "text/x-tex" }, + { ".cls", "text/x-tex" }, + { ".vcs", "text/x-vCalendar" }, + { ".vcf", "text/x-vCard" }, + { ".dl", "video/dl" }, + { ".fli", "video/fli" }, + { ".gl", "video/gl" }, + { ".mpeg", "video/mpeg" }, + { ".mpg", "video/mpeg" }, + { ".mpe", "video/mpeg" }, + { ".qt", "video/quicktime" }, + { ".mov", "video/quicktime" }, + { ".asf", "video/x-ms-asf" }, + { ".asx", "video/x-ms-asf" }, + { ".avi", "video/x-msvideo" }, + { ".movie", "video/x-sgi-movie" }, + { ".vrm", "x-world/x-vrml" }, + { ".vrml", "x-world/x-vrml" }, + { ".wrl", "x-world/x-vrml" }, + + }; + + /** + * Attempts to determine a mimetype + * @param path - either a file extension string (containing the + * leading '.') or a full file pathname (in which case, the extension + * will be extracted). + * @return the mimetype that corresponds to the file extension, if the + * file extension is known, or "application/octet-stream" if the + * file extension is not known. + */ + public static String guessType(String path) { + // rip the file extension from the path + // first - split 'directories', and get last part + String [] dirs = path.split("/"); + String filename = dirs[dirs.length-1]; + String [] bits = filename.split("\\."); + String extension = "." + bits[bits.length-1]; + + // default mimetype applied to unknown file extensions + String type = "application/octet-stream"; + + for (int i=0; i<_map.length; i++) { + String [] rec = _map[i]; + if (rec[0].equals(extension)) { + type = rec[1]; + break; + } + } + return type; + } + + /** + * Attempts to guess the file extension corresponding to a given + * mimetype. + * @param type a mimetype string + * @return a file extension commonly used for storing files of this type, + * or defaults to ".bin" if mimetype not known + */ + public static String guessExtension(String type) { + // default extension applied to unknown mimetype + String extension = ".bin"; + for (int i=0; i<_map.length; i++) { + String [] rec = _map[i]; + if (rec[1].equals(type)) { + extension = rec[0]; + break; + } + } + return extension; + } + +} + +/** + +suffix_map = { + '.tgz': '.tar.gz', + '.taz': '.tar.gz', + '.tz': '.tar.gz', + } + +encodings_map = { + '.gz': 'gzip', + '.Z': 'compress', + } + +# Before adding new types, make sure they are either registered with IANA, at +# http://www.isi.edu/in-notes/iana/assignments/media-types +# or extensions, i.e. using the x- prefix + +# If you add to these, please keep them sorted! +types_map = { + '.a' : 'application/octet-stream', + '.ai' : 'application/postscript', + '.aif' : 'audio/x-aiff', + '.aifc' : 'audio/x-aiff', + '.aiff' : 'audio/x-aiff', + '.au' : 'audio/basic', + '.avi' : 'video/x-msvideo', + '.bat' : 'text/plain', + '.bcpio' : 'application/x-bcpio', + '.bin' : 'application/octet-stream', + '.bmp' : 'image/x-ms-bmp', + '.c' : 'text/plain', + # Duplicates :( + '.cdf' : 'application/x-cdf', + '.cdf' : 'application/x-netcdf', + '.cpio' : 'application/x-cpio', + '.csh' : 'application/x-csh', + '.css' : 'text/css', + '.dll' : 'application/octet-stream', + '.doc' : 'application/msword', + '.dot' : 'application/msword', + '.dvi' : 'application/x-dvi', + '.eml' : 'message/rfc822', + '.eps' : 'application/postscript', + '.etx' : 'text/x-setext', + '.exe' : 'application/octet-stream', + '.gif' : 'image/gif', + '.gtar' : 'application/x-gtar', + '.h' : 'text/plain', + '.hdf' : 'application/x-hdf', + '.htm' : 'text/html', + '.html' : 'text/html', + '.ief' : 'image/ief', + '.jpe' : 'image/jpeg', + '.jpeg' : 'image/jpeg', + '.jpg' : 'image/jpeg', + '.js' : 'application/x-javascript', + '.ksh' : 'text/plain', + '.latex' : 'application/x-latex', + '.m1v' : 'video/mpeg', + '.man' : 'application/x-troff-man', + '.me' : 'application/x-troff-me', + '.mht' : 'message/rfc822', + '.mhtml' : 'message/rfc822', + '.mif' : 'application/x-mif', + '.mov' : 'video/quicktime', + '.movie' : 'video/x-sgi-movie', + '.mp2' : 'audio/mpeg', + '.mp3' : 'audio/mpeg', + '.mpa' : 'video/mpeg', + '.mpe' : 'video/mpeg', + '.mpeg' : 'video/mpeg', + '.mpg' : 'video/mpeg', + '.ms' : 'application/x-troff-ms', + '.nc' : 'application/x-netcdf', + '.nws' : 'message/rfc822', + '.o' : 'application/octet-stream', + '.obj' : 'application/octet-stream', + '.oda' : 'application/oda', + '.p12' : 'application/x-pkcs12', + '.p7c' : 'application/pkcs7-mime', + '.pbm' : 'image/x-portable-bitmap', + '.pdf' : 'application/pdf', + '.pfx' : 'application/x-pkcs12', + '.pgm' : 'image/x-portable-graymap', + '.pl' : 'text/plain', + '.png' : 'image/png', + '.pnm' : 'image/x-portable-anymap', + '.pot' : 'application/vnd.ms-powerpoint', + '.ppa' : 'application/vnd.ms-powerpoint', + '.ppm' : 'image/x-portable-pixmap', + '.pps' : 'application/vnd.ms-powerpoint', + '.ppt' : 'application/vnd.ms-powerpoint', + '.ps' : 'application/postscript', + '.pwz' : 'application/vnd.ms-powerpoint', + '.py' : 'text/x-python', + '.pyc' : 'application/x-python-code', + '.pyo' : 'application/x-python-code', + '.qt' : 'video/quicktime', + '.ra' : 'audio/x-pn-realaudio', + '.ram' : 'application/x-pn-realaudio', + '.ras' : 'image/x-cmu-raster', + '.rdf' : 'application/xml', + '.rgb' : 'image/x-rgb', + '.roff' : 'application/x-troff', + '.rtx' : 'text/richtext', + '.sgm' : 'text/x-sgml', + '.sgml' : 'text/x-sgml', + '.sh' : 'application/x-sh', + '.shar' : 'application/x-shar', + '.snd' : 'audio/basic', + '.so' : 'application/octet-stream', + '.src' : 'application/x-wais-source', + '.sv4cpio': 'application/x-sv4cpio', + '.sv4crc' : 'application/x-sv4crc', + '.swf' : 'application/x-shockwave-flash', + '.t' : 'application/x-troff', + '.tar' : 'application/x-tar', + '.tcl' : 'application/x-tcl', + '.tex' : 'application/x-tex', + '.texi' : 'application/x-texinfo', + '.texinfo': 'application/x-texinfo', + '.tif' : 'image/tiff', + '.tiff' : 'image/tiff', + '.tr' : 'application/x-troff', + '.tsv' : 'text/tab-separated-values', + '.txt' : 'text/plain', + '.ustar' : 'application/x-ustar', + '.vcf' : 'text/x-vcard', + '.wav' : 'audio/x-wav', + '.wiz' : 'application/msword', + '.xbm' : 'image/x-xbitmap', + '.xlb' : 'application/vnd.ms-excel', + # Duplicates :( + '.xls' : 'application/excel', + '.xls' : 'application/vnd.ms-excel', + '.xml' : 'text/xml', + '.xpm' : 'image/x-xpixmap', + '.xsl' : 'application/xml', + '.xwd' : 'image/x-xwindowdump', + '.zip' : 'application/zip', + } + +# These are non-standard types, commonly found in the wild. They will only +# match if strict=0 flag is given to the API methods. + +# Please sort these too +common_types = { + '.jpg' : 'image/jpg', + '.mid' : 'audio/midi', + '.midi': 'audio/midi', + '.pct' : 'image/pict', + '.pic' : 'image/pict', + '.pict': 'image/pict', + '.rtf' : 'application/rtf', + '.xul' : 'text/xul' + } +**/ + diff --git a/apps/q/java/src/net/i2p/aum/OOTest.java b/apps/q/java/src/net/i2p/aum/OOTest.java new file mode 100644 index 000000000..f7a6fffed --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/OOTest.java @@ -0,0 +1,18 @@ +package net.i2p.aum; + + +public class OOTest +{ + public int add(int a, int b) + { + return (a + b); + } + + public static void main(String[] args) + { + OOTest mytest = new OOTest(); + System.out.println(mytest.add(3,3)); + } +} + + diff --git a/apps/q/java/src/net/i2p/aum/PrivDestination.java b/apps/q/java/src/net/i2p/aum/PrivDestination.java new file mode 100644 index 000000000..8c1386925 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/PrivDestination.java @@ -0,0 +1,234 @@ + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.data.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.Base64; +import net.i2p.util.*; +import net.i2p.data.*; +import net.i2p.i2ptunnel.*; + + +/** + * A convenience class for encapsulating and manipulating I2P private keys + */ + +public class PrivDestination + //extends ByteArrayInputStream + extends DataStructureImpl +{ + protected byte [] _bytes; + + protected Destination _dest; + protected PrivateKey _privKey; + protected SigningPrivateKey _signingPrivKey; + + protected static Log _log; + + /** + * Create a PrivDestination object. + * In most cases, you'll probably want to skip this constructor, + * and create PrivDestination objects by invoking the desired static methods + * of this class. + * @param raw an array of bytes containing the raw binary private key + */ + public PrivDestination(byte [] raw) throws DataFormatException, IOException + { + //super(raw); + _log = new Log("PrivDestination"); + + _bytes = raw; + readBytes(getInputStream()); + } + + /** + * reconstitutes a PrivDestination from previously exported Base64 + */ + public PrivDestination(String b64) throws DataFormatException, IOException { + this(Base64.decode(b64)); + } + + /** + * generates a new PrivDestination with random keys + */ + public PrivDestination() throws I2PException, IOException + { + I2PClient client = I2PClientFactory.createClient(); + + ByteArrayOutputStream streamOut = new ByteArrayOutputStream(); + + // create a dest + client.createDestination(streamOut); + + _bytes = streamOut.toByteArray(); + readBytes(getInputStream()); + + // construct from the stream + //return new PrivDestination(streamOut.toByteArray()); + } + + /** return the public Destination object for this private dest */ + public Destination getDestination() { + return _dest; + } + + /** return a PublicKey (encryption public key) object for this priv dest */ + public PublicKey getPublicKey() { + return getDestination().getPublicKey(); + } + + /** return a PrivateKey (encryption private key) object for this priv dest */ + public PrivateKey getPrivateKey() { + return _privKey; + } + + /** return a SigningPublicKey object for this priv dest */ + public SigningPublicKey getSigningPublicKey() { + return getDestination().getSigningPublicKey(); + } + + /** return a SigningPrivateKey object for this priv dest */ + public SigningPrivateKey getSigningPrivateKey() { + return _signingPrivKey; + } + + // static methods returning an instance + + /** + * Creates a PrivDestination object + * @param base64 a string containing the base64 private key data + * @return a PrivDestination object encapsulating that key + */ + public static PrivDestination fromBase64String(String base64) + throws DataFormatException, IOException + { + return new PrivDestination(Base64.decode(base64)); + } + + /** + * Creates a PrivDestination object, from the base64 key data + * stored in a file. + * @param path the pathname of the file from which to read the base64 private key data + * @return a PrivDestination object encapsulating that key + */ + public static PrivDestination fromBase64File(String path) + throws FileNotFoundException, IOException, DataFormatException + { + return fromBase64String(new SimpleFile(path, "r").read()); + /* + File f = new File(path); + char [] rawchars = new char[(int)(f.length())]; + byte [] rawbytes = new byte[(int)(f.length())]; + FileReader fr = new FileReader(f); + fr.read(rawchars); + String raw64 = new String(rawchars); + return PrivDestination.fromBase64String(raw64); + */ + } + + /** + * Creates a PrivDestination object, from the binary key data + * stored in a file. + * @param path the pathname of the file from which to read the binary private key data + * @return a PrivDestination object encapsulating that key + */ + public static PrivDestination fromBinFile(String path) + throws FileNotFoundException, IOException, DataFormatException + { + byte [] raw = new SimpleFile(path, "r").readBytes(); + return new PrivDestination(raw); + } + + /** + * Generate a new random I2P private key + * @return a PrivDestination object encapsulating that key + */ + public static PrivDestination newKey() throws I2PException, IOException + { + return new PrivDestination(); + } + + public ByteArrayInputStream getInputStream() + { + return new ByteArrayInputStream(_bytes); + } + + /** + * Exports the key's full contents to a string + * @return A base64-format string containing the full contents + * of this private key. The string can be used in any subsequent + * call to the .fromBase64String static constructor method. + */ +/* + public String toBase64() + { + return Base64.encode(_bytes); + } +*/ + + /** + * Exports the key's full contents to a byte array + * @return A byte array containing the full contents + * of this private key. + */ +/* + public byte [] toBytes() + { + return _bytes; + } +*/ + + /** + * Converts this key to a public destination. + * @return a standard I2P Destination object containing the + * public portion of this private key. + */ + /* + public Destination toDestination() throws DataFormatException + { + Destination dest = new Destination(); + dest.readBytes(_bytes, 0); + return dest; + } + */ + + /** + * Converts this key to a base64 string representing a public destination + * @return a string containing a base64 representation of the destination + * corresponding to this private key. + */ + public String getDestinationBase64() throws DataFormatException + { + return getDestination().toBase64(); + } + + public void readBytes(java.io.InputStream strm) + throws net.i2p.data.DataFormatException, java.io.IOException + { + _dest = new Destination(); + _privKey = new PrivateKey(); + _signingPrivKey = new SigningPrivateKey(); + + _dest.readBytes(strm); + _privKey.readBytes(strm); + _signingPrivKey.readBytes(strm); + } + + public void writeBytes(java.io.OutputStream outputStream) + throws net.i2p.data.DataFormatException, java.io.IOException + { + _dest.writeBytes(outputStream); + _privKey.writeBytes(outputStream); + _signingPrivKey.writeBytes(outputStream); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/PropertiesFile.java b/apps/q/java/src/net/i2p/aum/PropertiesFile.java new file mode 100644 index 000000000..77c7c2c41 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/PropertiesFile.java @@ -0,0 +1,209 @@ +/* + * PropertiesFile.java + * + * Created on 20 March 2005, 19:30 + */ + +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; + +/** + * builds on Properties with methods to load/save directly to/from file + */ +public class PropertiesFile extends Properties { + + public String _path; + public File _file; + public boolean _fileExists; + + /** + * Creates a new instance of PropertiesFile + * @param path Absolute pathname of file where properties are to be stored + */ + public PropertiesFile(String path) throws IOException { + super(); + _path = path; + _file = new File(path); + _fileExists = _file.isFile(); + + if (_file.canRead()) { + loadFromFile(); + } + } + + /** + * Creates new PropertiesFile, updating its content with the + * keys/values in given hashtable + * @param path absolute pathname where properties file is located in filesystem + * @param h instance of Hashtable (or subclass). its content + * will be written to this object (note that string representations of keys/vals + * will be used) + */ + public PropertiesFile(String path, Hashtable h) throws IOException + { + this(path); + Enumeration keys = h.keys(); + Object key; + while (true) + { + try { + key = keys.nextElement(); + } catch (NoSuchElementException e) { + break; + } + setProperty(key.toString(), h.get(key).toString()); + } + } + + /** + * Loads this object from the file + */ + public void loadFromFile() throws IOException, FileNotFoundException { + if (_file.canRead()) { + InputStream fis = new FileInputStream(_file); + load(fis); + } + } + + /** + * Saves this object to the file + */ + public void saveToFile() throws IOException, FileNotFoundException { + + if (!_fileExists) { + _file.createNewFile(); + _fileExists = true; + } + OutputStream fos = new FileOutputStream(_file); + store(fos, null); + } + + /** + * Stores attribute + */ + public Object setProperty(String key, String value) { + Object o = super.setProperty(key, value); + try { + saveToFile(); + } catch (Exception e) { + e.printStackTrace(); + } + return o; + } + + /** + * return a property as an int, fall back on default if not found or invalid + */ + public int getIntProperty(String key, int dflt) { + try { + return new Integer((String)getProperty(key)).intValue(); + } catch (Exception e) { + setIntProperty(key, dflt); + return dflt; + } + } + + /** + * return a property as an int + */ + public int getIntProperty(String key) { + return new Integer((String)getProperty(key)).intValue(); + } + + /** + * set a property as an int + */ + public void setIntProperty(String key, int value) { + setProperty(key, String.valueOf(value)); + } + + /** + * return a property as a long, fall back on default if not found or invalid + */ + public long getIntProperty(String key, long dflt) { + try { + return new Long((String)getProperty(key)).longValue(); + } catch (Exception e) { + setLongProperty(key, dflt); + return dflt; + } + } + + /** + * return a property as an int + */ + public long getLongProperty(String key) { + return new Long((String)getProperty(key)).longValue(); + } + + /** + * set a property as an int + */ + public void setLongProperty(String key, long value) { + setProperty(key, String.valueOf(value)); + } + + /** + * return a property as a float + */ + public double getFloatProperty(String key) { + return new Float((String)getProperty(key)).floatValue(); + } + + /** + * return a property as a float, fall back on default if not found or invalid + */ + public double getFloatProperty(String key, float dflt) { + try { + return new Float((String)getProperty(key)).floatValue(); + } catch (Exception e) { + setFloatProperty(key, dflt); + return dflt; + } + } + + /** + * set a property as a float + */ + public void setFloatProperty(String key, float value) { + setProperty(key, String.valueOf(value)); + } + + /** + * return a property as a double + */ + public double getDoubleProperty(String key) { + return new Double((String)getProperty(key)).doubleValue(); + } + + /** + * return a property as a double, fall back on default if not found + */ + public double getDoubleProperty(String key, double dflt) { + try { + return new Double((String)getProperty(key)).doubleValue(); + } catch (Exception e) { + setDoubleProperty(key, dflt); + return dflt; + } + } + + /** + * set a property as a double + */ + public void setDoubleProperty(String key, double value) { + setProperty(key, String.valueOf(value)); + } + + /** + * increment an integer property value + */ + public void incrementIntProperty(String key) { + setIntProperty(key, getIntProperty(key)+1); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/SimpleFile.java b/apps/q/java/src/net/i2p/aum/SimpleFile.java new file mode 100644 index 000000000..281948aa3 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/SimpleFile.java @@ -0,0 +1,120 @@ +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import net.i2p.data.*; + +/** + * SimpleFile - subclass of File which adds some python-like + * methods. Cuts out a lot of the red tape involved with reading + * from and writing to files + */ +public class SimpleFile { + + public RandomAccessFile _file; + public String _path; + + public SimpleFile(String path, String mode) throws FileNotFoundException { + + _path = path; + _file = new RandomAccessFile(path, mode); + } + + public byte [] readBytes() throws IOException { + return readBytes((int)_file.length()); + } + + public byte[] readBytes(int n) throws IOException { + byte [] buf = new byte[n]; + _file.readFully(buf); + return buf; + } + + public char [] readChars() throws IOException { + return readChars((int)_file.length()); + } + + public char[] readChars(int n) throws IOException { + char [] buf = new char[n]; + //_file.readFully(buf); + return buf; + } + + /** + * Reads all remaining content from the file + * @return the content as a String + * @throws IOException + */ + public String read() throws IOException { + + return read((int)_file.length()); + } + + /** + * Reads one or more bytes of data from the file + * @return the content as a String + * @throws IOException + */ + public String read(int nbytes) throws IOException { + + return new String(readBytes(nbytes)); + } + + /** + * Writes one or more bytes of data to a file + * @param buf a String containing the data to write + * @return the number of bytes written, as an int + * @throws IOException + */ + public int write(String buf) throws IOException { + + return write(buf.getBytes()); + } + + public int write(byte [] buf) throws IOException { + + _file.write(buf); + return buf.length; + } + + /** + * convenient one-hit write + * @param path pathname of file to write to + * @param buf data to write + */ + public static int write(String path, String buf) throws IOException { + return new SimpleFile(path, "rws").write(buf); + } + + /** + * tests if argument refers to an actual file + * @param path pathname to test + * @return true if a file, false if not + */ + public boolean isFile() { + return new File(_path).isFile(); + } + + /** + * tests if argument refers to a directory + * @param path pathname to test + * @return true if a directory, false if not + */ + public boolean isDir() { + return new File(_path).isDirectory(); + } + + /** + * tests if a file or directory exists + * @param path pathname to test + * @return true if exists, or false + */ + public boolean exists() { + return new File(_path).exists(); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/SimpleFile_old.java b/apps/q/java/src/net/i2p/aum/SimpleFile_old.java new file mode 100644 index 000000000..4afa7f783 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/SimpleFile_old.java @@ -0,0 +1,123 @@ +package net.i2p.aum; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.net.*; + +import net.i2p.data.*; + +/** + * SimpleFile - subclass of File which adds some python-like + * methods. Cuts out a lot of the red tape involved with reading + * from and writing to files + */ +public class SimpleFile_old extends File { + + public FileReader _reader; + public FileWriter _writer; + + public SimpleFile_old(String path) { + + super(path); + + _reader = null; + _writer = null; + } + + /** + * Reads all remaining content from the file + * @return the content as a String + * @throws IOException + */ + public String read() throws IOException { + + return read((int)length()); + } + + /** + * Reads one or more bytes of data from the file + * @return the content as a String + * @throws IOException + */ + public String read(int nbytes) throws IOException { + + // get a reader, if we don't already have one + if (_reader == null) { + _reader = new FileReader(this); + } + + char [] cbuf = new char[nbytes]; + + int nread = _reader.read(cbuf); + + if (nread == 0) { + return ""; + } + + return new String(cbuf, 0, nread); + + } + + /** + * Writes one or more bytes of data to a file + * @param buf a String containing the data to write + * @return the number of bytes written, as an int + * @throws IOException + */ + public int write(String buf) throws IOException { + + // get a reader, if we don't already have one + if (_writer == null) { + _writer = new FileWriter(this); + } + + _writer.write(buf); + _writer.flush(); + return buf.length(); + } + + public int write(byte [] buf) throws IOException { + + return write(new String(buf)); + } + + /** + * convenient one-hit write + * @param path pathname of file to write to + * @param buf data to write + */ + public static int write(String path, String buf) throws IOException { + SimpleFile_old f = new SimpleFile_old(path); + return f.write(buf); + } + + /** + * tests if argument refers to an actual file + * @param path pathname to test + * @return true if a file, false if not + */ + public static boolean isFile(String path) { + return new File(path).isFile(); + } + + /** + * tests if argument refers to a directory + * @param path pathname to test + * @return true if a directory, false if not + */ + public static boolean isDir(String path) { + return new File(path).isDirectory(); + } + + /** + * tests if a file or directory exists + * @param path pathname to test + * @return true if exists, or false + */ + public static boolean exists(String path) { + return new File(path).exists(); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/SimpleQueue.java b/apps/q/java/src/net/i2p/aum/SimpleQueue.java new file mode 100644 index 000000000..835100ec0 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/SimpleQueue.java @@ -0,0 +1,138 @@ +/* + * SimpleQueue.java + * + * Created on March 24, 2005, 11:14 PM + */ + +package net.i2p.aum; + +import java.*; +import java.lang.*; +import java.util.*; + +/** + * Implements simething similar to python's 'Queue' class + */ +public class SimpleQueue { + + public Vector items; + + /** Creates a new instance of SimpleQueue */ + public SimpleQueue() { + items = new Vector(); + } + + /** + * fetches the item at head of queue, blocking if queue is empty + */ + public synchronized Object get() + { + while (true) + { + try { + if (items.size() == 0) + wait(); + + // someone has added + Object item = items.get(0); + items.remove(0); + return item; + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * adds a new object to the queue + */ + public synchronized void put(Object item) + { + items.addElement(item); + notify(); + } + + private static class TestThread extends Thread { + + String id; + + SimpleQueue q; + + public TestThread(String id, SimpleQueue q) { + this.id = id; + this.q = q; + } + + public void run() { + try { + print("waiting for queue"); + + Object item = q.get(); + + print("got item: '"+item+"'"); + + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + public void print(String msg) { + System.out.println("thread '"+id+"': "+msg); + } + + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + + int i; + int nthreads = 7; + + Thread [] threads = new Thread[nthreads]; + + SimpleQueue q = new SimpleQueue(); + + // populate the queue with some stuff + q.put("red"); + q.put("orange"); + q.put("yellow"); + + // populate threads array + for (i = 0; i < nthreads; i++) { + threads[i] = new TestThread("thread"+i, q); + } + + // and launch the threads + for (i = 0; i < nthreads; i++) { + threads[i].start(); + } + + try { + Thread.sleep(3000); + } catch (Exception e) { + e.printStackTrace(); + return; + } + + // wait a bit and see what happens + String [] items = {"green", "blue", "indigo", "violet", "black", "white", "brown"}; + for (i = 0; i < items.length; i++) { + String item = items[i]; + System.out.println("main: adding '"+item+"'..."); + q.put(item); + try { + Thread.sleep(3000); + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + System.out.println("main: terminating"); + + } + +} diff --git a/apps/q/java/src/net/i2p/aum/SimpleSemaphore.java b/apps/q/java/src/net/i2p/aum/SimpleSemaphore.java new file mode 100644 index 000000000..83a736acd --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/SimpleSemaphore.java @@ -0,0 +1,108 @@ +/* + * SimpleSemaphore.java + * + * Created on March 24, 2005, 11:51 PM + */ + +package net.i2p.aum; + +/** + * Simple implementation of semaphores + */ +public class SimpleSemaphore { + + protected int count; + + /** Creates a new instance of SimpleSemaphore */ + public SimpleSemaphore(int size) { + count = size; + } + + public synchronized void acquire() throws InterruptedException + { + if (count == 0) + { + wait(); + } + count -= 1; + } + + public synchronized void release() + { + count += 1; + notify(); + } + + private static class TestThread extends Thread + { + String id; + SimpleSemaphore sem; + + public TestThread(String id, SimpleSemaphore sem) + { + this.id = id; + this.sem = sem; + } + + public void run() + { + try { + print("waiting for semaphore"); + sem.acquire(); + + print("got semaphore"); + + Thread.sleep(1000); + + print("releasing semaphore"); + + sem.release(); + + print("terminating"); + + } catch (Exception e) { + e.printStackTrace(); + return; + } + } + + public void print(String msg) { + System.out.println("thread '"+id+"': "+msg); + } + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + + int i; + + Thread [] threads = new Thread[10]; + + SimpleSemaphore sem = new SimpleSemaphore(3); + + // populate threads array + for (i = 0; i < 10; i++) { + threads[i] = new TestThread("thread"+i, sem); + } + + // and launch the threads + for (i = 0; i < 10; i++) { + threads[i].start(); + } + + // wait a bit and see what happens + System.out.println("main: threads launched, waiting 20 secs"); + + try { + Thread.sleep(20000); + } catch (Exception e) { + e.printStackTrace(); + } + + System.out.println("main: terminating"); + + } + +} diff --git a/apps/q/java/src/net/i2p/aum/helloworld.java b/apps/q/java/src/net/i2p/aum/helloworld.java new file mode 100644 index 000000000..2a8ce30e9 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/helloworld.java @@ -0,0 +1,17 @@ + +public class helloworld +{ + public static void main(String [] args) + { + helloworld h = new helloworld(); + h.greet(); + } + + public void greet() + { + System.out.println("Hi, this is your greeting"); + } +} + + + diff --git a/apps/q/java/src/net/i2p/aum/http/HtmlPage.java b/apps/q/java/src/net/i2p/aum/http/HtmlPage.java new file mode 100644 index 000000000..5e9749e59 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/http/HtmlPage.java @@ -0,0 +1,72 @@ +/* + * HtmlPage.java + * + * Created on April 8, 2005, 8:22 PM + */ + +package net.i2p.aum.http; + +import java.util.*; + +import net.i2p.aum.*; + +/** + * Framework for building up a page of HTML by method calls alone, breaking + * every design rule by enmeshing content, presentation and logic + */ +public class HtmlPage { + + public String dtd = ""; + + public Tag page; + public Tag head; + public Tag body; + DupHashtable cssSettings; + + /** Creates a new HtmlPage object */ + public HtmlPage() { + page = new Tag("html"); + head = new Tag(page, "head"); + body = new Tag(page, "body"); + cssSettings = new DupHashtable(); + } + + /** renders out the whole page into a single string */ + public String toString() { + + // embed stylesheet, if non-empty + if (cssSettings.size() > 0) { + Tag t1 = head.nest("style type=\"text/css\""); + t1.raw("\n"); + Enumeration elems = cssSettings.keys(); + while (elems.hasMoreElements()) { + String name = (String)elems.nextElement(); + cssTag.raw(name + " { "); + Enumeration items = cssSettings.get(name).elements(); + while (items.hasMoreElements()) { + String item = (String)items.nextElement(); + cssTag.raw(item+";"); + } + cssTag.raw(" }\n"); + } + } + + // now render out the whole page + return dtd + "\n" + page; + } + + /** adds a setting to the page's embedded stylesheet */ + public HtmlPage css(String tag, String item, String val) { + return css(tag, item+":"+val); + } + + /** adds a setting to the page's embedded stylesheet */ + public HtmlPage css(String tag, String setting) { + cssSettings.put(tag, setting); + return this; + } +} diff --git a/apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java b/apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java new file mode 100644 index 000000000..f2671d762 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/http/I2PHttpRequestHandler.java @@ -0,0 +1,50 @@ +/* + * I2PHttpRequestHandler.java + * + * Created on April 8, 2005, 11:57 PM + */ + +package net.i2p.aum.http; + +import java.lang.*; +import java.lang.reflect.*; +import java.util.*; +import java.io.*; +import java.net.*; + +import net.i2p.data.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; + +/** + * + * @author david + */ +public abstract class I2PHttpRequestHandler extends MiniHttpRequestHandler +{ + /** Creates a new instance of I2PHttpRequestHandler */ + public I2PHttpRequestHandler(MiniHttpServer server, Object sock, Object arg) + throws Exception + { + super(server, sock, arg); + } + + /** Extracts a readable InputStream from own socket */ + public InputStream getInputStream() throws IOException { + try { + return ((I2PSocket)socket).getInputStream(); + } catch (Exception e) { + return ((Socket)socket).getInputStream(); + } + } + + /** Extracts a writeable OutputStream from own socket */ + public OutputStream getOutputStream() throws IOException { + try { + return ((I2PSocket)socket).getOutputStream(); + } catch (Exception e) { + return ((Socket)socket).getOutputStream(); + } + } + +} diff --git a/apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java b/apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java new file mode 100644 index 000000000..2027ec768 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/http/I2PHttpServer.java @@ -0,0 +1,119 @@ +/* + * I2PHttpServer.java + * + * Created on April 8, 2005, 11:39 PM + */ + +package net.i2p.aum.http; + +import java.io.*; +import java.util.*; + +import net.i2p.*; +import net.i2p.data.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; + +import net.i2p.aum.*; + +/** + * + * @author david + */ +public class I2PHttpServer extends MiniHttpServer { + + PrivDestination privKey; + I2PSocketManager socketMgr; + + public I2PHttpServer(PrivDestination key) + throws DataFormatException, IOException, I2PException + { + this(key, I2PHttpRequestHandler.class, null, null); + } + + public I2PHttpServer(PrivDestination key, Class hdlrClass) + throws DataFormatException, IOException, I2PException + { + this(key, hdlrClass, null, null); + } + + public I2PHttpServer(PrivDestination key, Class hdlrClass, Properties props) + throws DataFormatException, IOException, I2PException + { + this(key, hdlrClass, null, props); + } + + /** Creates a new instance of I2PHttpServer */ + public I2PHttpServer(PrivDestination key, Class hdlrClass, Object hdlrArg, Properties props) + throws DataFormatException, IOException, I2PException + { + super(hdlrClass, hdlrArg); + + if (key != null) { + privKey = key; + } else { + privKey = new PrivDestination(); + } + + // get a socket manager + // socketManager = I2PSocketManagerFactory.createManager(key); + if (props == null) { + socketMgr = I2PSocketManagerFactory.createManager(privKey.getInputStream()); + } else { + socketMgr = I2PSocketManagerFactory.createManager(privKey.getInputStream(), props); + } + + if (socketMgr == null) { + throw new I2PException("I2PHttpServer: Failed to create socketManager"); + } + + String d = privKey.getDestination().toBase64(); + System.out.println("Server: getting server socket for dest "+d); + + // get a server socket + //serverSocket = socketManager.getServerSocket(); + } + + public void getServerSocket() throws IOException { + + I2PServerSocket sock; + sock = socketMgr.getServerSocket(); + serverSocket = sock; + System.out.println("listening on dest: "+privKey.getDestination().toBase64()); + } + + /** + * Listens on our 'serverSocket' object for an incoming connection, + * and returns a connected socket object. You should override this + * if you're using non-standard socket objects + */ + public Object acceptConnection() throws IOException { + + I2PSocket sock; + + try { + sock = ((I2PServerSocket)serverSocket).accept(); + } catch (I2PException e) { + throw new IOException(e.toString()); + } + + System.out.println("Got connection from: "+sock.getPeerDestination().toBase64()); + + //System.out.println("New connection accepted" + + // sock.getInetAddress() + + // ":" + sock.getPort()); + return sock; + } + + public static void main(String [] args) { + try { + System.out.println("I2PHttpServer: starting up with new random key"); + I2PHttpServer server = new I2PHttpServer((PrivDestination)null); + System.out.println("I2PHttpServer: running server"); + server.run(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} + diff --git a/apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java b/apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java new file mode 100644 index 000000000..f75dfa94e --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/http/MiniDemoXmlRpcHandler.java @@ -0,0 +1,22 @@ +/* + * MiniDemoXmlRpcHandler.java + * + * Created on April 13, 2005, 3:20 PM + */ + +package net.i2p.aum.http; + + +public class MiniDemoXmlRpcHandler { + + MiniHttpServer server; + + public MiniDemoXmlRpcHandler(MiniHttpServer server) { + this.server = server; + } + + public String bar(String arg) { + return "bar: got '"+arg+"'"; + } +} + diff --git a/apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java b/apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java new file mode 100644 index 000000000..fe0786e64 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/http/MiniHttpRequestHandler.java @@ -0,0 +1,567 @@ +/* + * MiniHttpRequestHandler.java + * Adapted from pont.net's httpRequestHandler (httpServer.java) + * + * Created on April 8, 2005, 3:15 PM + */ + +package net.i2p.aum.http; + +import java.net.*; +import java.io.*; +import java.util.*; +import java.lang.*; + +import net.i2p.aum.*; + +public abstract class MiniHttpRequestHandler implements Runnable { + final static String CRLF = "\r\n"; + + /** server which created this handler */ + protected MiniHttpServer server; + + /** socket through which client is connected to us */ + protected Object socket; + + /** stored constructor arg */ + protected Object serverArg; + + /** input sent from client in request */ + protected InputStream input; + + /** we use this to read from client */ + protected BufferedReader br; + + /** output sent to client in reply */ + protected OutputStream output; + + /** http request type - GET, POST etc */ + protected String reqType; + + /** the request pathname */ + protected String reqFile; + + /** the request protocol (eg 'HTTP/1.0') */ + protected String reqProto; + + /** http headers */ + protected DupHashtable headerVars; + + /** variable settings from POST data */ + public DupHashtable postVars; + + /** variable settings from URL (?name1=val1&name2=val2...) */ + public DupHashtable urlVars; + + /** consolidated variable settings from URL or POST data */ + public DupHashtable allVars; + + /** first line of response we send back to client, set this + * with 'setStatus' + */ + private String status = "HTTP/1.0 200 OK"; + private String contentType = "text/plain"; + private String reqContentType = null; + protected String serverName = "aum's MiniHttpServer"; + + protected byte [] rawContentBytes = null; + + /** + * raw data sent by client in post req + */ + protected char [] postData; + + /** if a POST, this holds the full POST data as a string */ + public String postDataStr; + + // Constructors + public MiniHttpRequestHandler(MiniHttpServer server, Object socket) throws Exception { + this(server, socket, null); + } + + public MiniHttpRequestHandler(MiniHttpServer server, Object socket, Object arg) throws Exception { + this.server = server; + this.socket = socket; + this.serverArg = arg; + this.input = getInputStream(); + this.output = getOutputStream(); + this.br = new BufferedReader(new InputStreamReader(input)); + } + + // ------------------------------------------- + // START OF OVERRIDEABLES + // ------------------------------------------- + + // override these methods in subclass if your socket-type thang is not + // a genuine Socket objct + + /** Extracts a readable InputStream from own socket */ + public InputStream getInputStream() throws IOException { + return ((Socket)socket).getInputStream(); + } + + /** Extracts a writeable OutputStream from own socket */ + public OutputStream getOutputStream() throws IOException { + return ((Socket)socket).getOutputStream(); + } + + /** closes the socket (or our socket-ish object) */ + public void closeSocket() throws IOException { + ((Socket)socket).close(); + } + + /** method which gets called upon receipt of a GET. + * You should override this + */ + public abstract void on_GET() throws Exception; + + /** method which gets called upon receipt of a POST. + * You should override this + */ + public abstract void on_POST() throws Exception; + + // ------------------------------------------- + // END OF OVERRIDEABLES + // ------------------------------------------- + + /** Sets the HTTP status line (default 'HTTP/1.0 200 OK') */ + public void setStatus(String status) { + this.status = status; + } + + /** Sets the Content=Type header (default "text/plain") */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** Sets the 'Server' header (default "aum's MiniHttpServer") */ + public void setServer(String serverType) { + this.serverName = serverType; + } + + /** Sets the full body of raw output to be written, replacing + * the generated html tags + */ + public void setRawOutput(String raw) { + setRawOutput(raw.getBytes()); + } + + /** Sets the full body of raw output to be written, replacing + * the generated html tags + */ + public void setRawOutput(byte [] raw) { + rawContentBytes = raw; + } + + /** writes a String to output - normally you shouldn't need to call + * this directly + */ + public void write(String raw) { + write(raw.getBytes()); + } + + /** writes a byte array to output - normally you shouldn't need to call + * this directly + */ + public void write(byte [] raw) { + try { + output.write(raw); + } catch (Exception e) { + System.out.print(e); + } + } + + /** processes the request, sends back response */ + public void run() { + try { + processRequest(); + } + catch(Exception e) { + e.printStackTrace(); + System.out.println(e); + } + } + + /** does all the work of processing the request */ + protected void processRequest() throws Exception { + + headerVars = new DupHashtable(); + urlVars = new DupHashtable(); + postVars = new DupHashtable(); + allVars = new DupHashtable(); + + String line; + + // basic parsing of first req line + String reqLine = br.readLine(); + printReq(reqLine); + String [] reqBits = reqLine.split("\\s+", 3); + reqType = reqBits[0]; + String [] reqFileBits = reqBits[1].split("[?]", 2); + reqFile = reqFileBits[0]; + + // check for URL variables + if (reqFileBits.length > 1) { + urlVars = parseVars(reqFileBits[1]); + } + + // extract the 'request protocol', default to HTTP/1.0 + try { + reqProto = reqBits[2]; + } catch (Exception e) { + // workaround eepproxy bug + reqFile = "/"; + reqProto = "HTTP/1.0"; + } + + // suck the headers + while (true) { + line = br.readLine(); + //System.out.println("Got header line: "+line); + if (line.equals("")) { + break; + } + String [] lineBits = line.split(":\\s+", 2); + headerVars.put(lineBits[0], lineBits[1]); + } + //br.close(); + + // GET is simple, all the work is already done + if (reqType.equals("GET")) { + on_GET(); + } + + // POST is more involved - need to read POST data and + // break it up into fields + else if (reqType.equals("POST")) { + int postLen; + String postLenStr; + try { + reqContentType = headerVars.get("Content-Type", 0, ""); + + try { + postLenStr = headerVars.get("Content-Length", 0); + } catch (Exception e) { + // damn opera + postLenStr = headerVars.get("Content-length", 0); + } + + postLen = new Integer(postLenStr).intValue(); + postData = new char[postLen]; + + //System.out.println("postLen="+postLen); + for (int i=0; i 0) { + try { + port = new Integer(args[0]).intValue(); + } catch (NumberFormatException e) { + System.out.println("Invalid port: "+args[0]); + System.exit(1); + } + + server = new MiniHttpServer(DemoHandler.class, port); + } + else { + server = new MiniHttpServer(DemoHandler.class); + } + + MiniDemoXmlRpcHandler hdlr = new MiniDemoXmlRpcHandler(server); + server.addXmlRpcHandler("foo", hdlr); + + server.run(); + } +} + diff --git a/apps/q/java/src/net/i2p/aum/http/Tag.java b/apps/q/java/src/net/i2p/aum/http/Tag.java new file mode 100644 index 000000000..bb196f372 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/http/Tag.java @@ -0,0 +1,358 @@ +/* + * HtmlTag.java + * + * Created on April 8, 2005, 8:22 PM + */ + +package net.i2p.aum.http; + +import java.lang.*; +import java.util.*; +import java.io.*; + +/** + * Base class for building up quick-n-dirty HTML by code alone; + * Breaks every design rule by enmeshing content, presentation and logic together + * into java statements. + */ +public class Tag { + + static Vector nlOnOpen = new Vector(); + static { + nlOnOpen.addElement("html"); + nlOnOpen.addElement("html"); + nlOnOpen.addElement("head"); + nlOnOpen.addElement("body"); + nlOnOpen.addElement("frameset"); + nlOnOpen.addElement("frame"); + nlOnOpen.addElement("script"); + nlOnOpen.addElement("blockquote"); + nlOnOpen.addElement("div"); + nlOnOpen.addElement("hr"); + nlOnOpen.addElement("ul"); + nlOnOpen.addElement("ol"); + nlOnOpen.addElement("table"); + nlOnOpen.addElement("caption"); + nlOnOpen.addElement("col"); + nlOnOpen.addElement("thead"); + nlOnOpen.addElement("tfoot"); + nlOnOpen.addElement("tbody"); + nlOnOpen.addElement("tr"); + nlOnOpen.addElement("form"); + nlOnOpen.addElement("applet"); + nlOnOpen.addElement("br"); + nlOnOpen.addElement("style"); + }; + + static Vector nlOnClose = new Vector(); + static { + nlOnClose.addElement("h1"); + nlOnClose.addElement("h2"); + nlOnClose.addElement("h3"); + nlOnClose.addElement("h4"); + nlOnClose.addElement("h5"); + nlOnClose.addElement("h6"); + nlOnClose.addElement("p"); + nlOnClose.addElement("pre"); + nlOnClose.addElement("li"); + nlOnClose.addElement("td"); + nlOnClose.addElement("th"); + nlOnClose.addElement("button"); + nlOnClose.addElement("input"); + nlOnClose.addElement("label"); + nlOnClose.addElement("select"); + nlOnClose.addElement("option"); + nlOnClose.addElement("textarea"); + nlOnClose.addElement("font"); + nlOnClose.addElement("iframe"); + nlOnClose.addElement("img"); + nlOnClose.addElement("br"); + } + + String open; + String close; + Vector attribs; + Vector styles; + Vector content; + boolean breakBefore, breakAfter; + public Tag parent = null; + public Tag end = null; + + // ----------------------------------------------------- + // CONSTRUCTORS + // ----------------------------------------------------- + + /** Creates a new empty container tag */ + public Tag() { + this((String)null); + } + + /** Creates a new empty container tag, embedded in a parent tag */ + public Tag(Tag parent) { + this(parent, null); + } + + /** + * Creates a new HtmlTag instance, adds to a parent + */ + public Tag(Tag parent, String opentag) { + this(opentag); + parent.add(this); + this.end = this.parent = parent; + } + + /** Creates a new instance of HtmlTag */ + public Tag(String opentag) { + + content = new Vector(); + attribs = new Vector(); + styles = new Vector(); + + if (opentag == null) { + return; + } + + String [] tagBits = opentag.split("\\s+", 2); + open = tagBits[0]; + + if (open.endsWith("/")) { + open = open.substring(0, open.length()-1); + close = ""; + } + else { + close = ""+open+">"; + } + + if (tagBits.length > 1) { + attribs.addElement(tagBits[1]); + } + + breakBefore = nlOnOpen.contains(open); + breakAfter = breakBefore || nlOnClose.contains(open); + } + + // ----------------------------------------------------- + // METHODS FOR ADDING SPECIFIC HTML TAGS + // ----------------------------------------------------- + + /** insert a <br> on the fly */ + public Tag br() { + return add("br/"); + } + + /** insert a <hr> on the fly */ + public Tag hr() { + return add("hr/"); + } + + public Tag center() { + return nest("center"); + } + + public Tag center(String attr) { + return nest("center "+attr); + } + + public Tag big() { + return nest("big"); + } + + public Tag big(String attr) { + return nest("big "+attr); + } + + public Tag small() { + return nest("small"); + } + + public Tag small(String attr) { + return nest("small "+attr); + } + + public Tag i() { + return nest("i"); + } + + public Tag i(String attr) { + return nest("i "+attr); + } + + public Tag strong() { + return nest("strong"); + } + + public Tag strong(String attr) { + return nest("big "+attr); + } + + public Tag table() { + return nest("table"); + } + + public Tag table(String attr) { + return nest("table "+attr); + } + + public Tag tr() { + return nest("tr"); + } + + public Tag tr(String attr) { + return nest("tr "+attr); + } + + public Tag td() { + return nest("td"); + } + + public Tag td(String attr) { + return nest("td "+attr); + } + + public Tag form() { + return nest("form"); + } + + public Tag form(String attr) { + return nest("form "+attr); + } + + // ----------------------------------------------------- + // METHODS FOR ADDING GENERAL CONTENT + // ----------------------------------------------------- + + /** create a new tag, embed it into this one, return this tag */ + public Tag add(String s) { + Tag t = new Tag(s); + content.addElement(t); + return this; + } + + /** add a tag to this one, returning this tag */ + public Tag add(Tag t) { + content.addElement(t); + return this; + } + + /** create a new tag, nest it into this one, return the new tag */ + public Tag nest(String opentag) { + Tag t = new Tag(this, opentag); + t.parent = this; + return t; + } + public Tag nest() { + Tag t = new Tag(this); + t.parent = this; + return t; + } + + /** insert object into this tag, return this tag */ + public Tag raw(Object o) { + content.addElement(o); + return this; + } + + /** set an attribute of this tag, return this tag */ + public Tag set(String name, String val) { + return set(name + "=\"" + val + "\""); + } + + /** set an attribute of this tag, return this tag */ + public Tag set(String setting) { + attribs.addElement(setting); + return this; + } + + public Tag style(String name, String val) { + return style(name+":"+val); + } + + public Tag style(String setting) { + styles.addElement(setting); + return this; + } + + // ----------------------------------------------------- + // METHODS FOR RENDERING + // ----------------------------------------------------- + + public void render(OutputStream out) throws IOException { + + //System.out.print("{render:"+open+"}"); + //System.out.flush(); + + if (open != null) { + out.write("<".getBytes()); + out.write(open.getBytes()); + + // add in attributes, if any + for (int i=0; i 0) { + out.write((" style=\"").getBytes()); + Enumeration elems = styles.elements(); + while (elems.hasMoreElements()) { + String s = (String)elems.nextElement()+";"; + out.write(s.getBytes()); + } + out.write("\"".getBytes()); + } + + if (close.equals("")) { + out.write("/".getBytes()); + } + out.write(">".getBytes()); + + if (breakBefore) { + out.write("\n".getBytes()); + } + } + + for (int i=0; i < content.size(); i++) { + Object item = content.get(i); + if (item.getClass().isAssignableFrom(Tag.class)) { + ((Tag)item).render(out); + } else { + out.write(item.toString().getBytes()); + } + } + + if (open != null) { + out.write(close.getBytes()); + //buf.append(close); + + if (breakAfter) { + out.write("\n".getBytes()); + } + } + } + + public String render() { + ByteArrayOutputStream s = new ByteArrayOutputStream(); + try { + render(s); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + return s.toString(); + } + + public String toString() { + return render(); + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/Favicon.java b/apps/q/java/src/net/i2p/aum/q/Favicon.java new file mode 100644 index 000000000..f5f6ab6c9 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/Favicon.java @@ -0,0 +1,61 @@ +package net.i2p.aum.q; +public class Favicon { + public static byte [] image = { + 0, 0, 1, 0, 1, 0, 16, 16, 0, 0, 1, 0, 24, 0, 104, 3, + 0, 0, 22, 0, 0, 0, 40, 0, 0, 0, 16, 0, 0, 0, 32, 0, + 0, 0, 1, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 72, 0, + 0, 0, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, + 19, 19, -127, -127, -127, 12, 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2, -120, + -120, -120, -49, -49, -49, 116, 116, 116, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 90, 90, 90, -80, -80, -80, + -18, -18, -18, -55, -55, -55, -122, -122, -122, 68, 68, 68, 107, 107, 107, -62, + -62, -62, -20, -20, -20, -59, -59, -59, 4, 4, 4, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, -109, -109, -109, -30, -30, -30, -8, -8, -8, + -25, -25, -25, -2, -2, -2, -28, -28, -28, -49, -49, -49, -2, -2, -2, -14, + -14, -14, -36, -36, -36, 33, 33, 33, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 72, 72, 72, -1, -1, -1, -1, -1, -1, -28, -28, -28, + -34, -34, -34, 118, 118, 118, -124, -124, -124, -1, -1, -1, -1, -1, -1, -6, + -6, -6, 68, 68, 68, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -98, -98, -98, -1, -1, -1, -38, -38, -38, -80, -80, -80, + 13, 13, 13, 0, 0, 0, 100, 100, 100, -11, -11, -11, -9, -9, -9, -3, + -3, -3, -90, -90, -90, 10, 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -49, -49, -49, -4, -4, -4, -57, -57, -57, 63, 63, 63, + 0, 0, 0, 26, 26, 26, -74, -74, -74, -56, -56, -56, -35, -35, -35, -29, + -29, -29, -13, -13, -13, 104, 104, 104, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -28, -28, -28, -46, -46, -46, -22, -22, -22, 2, 2, 2, + 0, 0, 0, 2, 2, 2, 41, 41, 41, 108, 108, 108, 37, 37, 37, -32, + -32, -32, -29, -29, -29, -60, -60, -60, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -60, -60, -60, -60, -60, -60, -44, -44, -44, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 8, 8, -56, + -56, -56, -49, -49, -49, -43, -43, -43, 24, 24, 24, 0, 0, 0, 0, 0, + 0, 0, 0, 0, -117, -117, -117, -70, -70, -70, -48, -48, -48, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, -93, + -93, -93, -12, -12, -12, -47, -47, -47, 32, 32, 32, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 54, 54, 54, -42, -42, -42, -79, -79, -79, 28, 28, 28, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 7, 7, 110, 110, 110, -70, + -70, -70, -4, -4, -4, -64, -64, -64, 3, 3, 3, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 121, 121, 121, -51, -51, -51, -87, -87, -87, + 10, 10, 10, 0, 0, 0, 37, 37, 37, -119, -119, -119, -106, -106, -106, -20, + -20, -20, -33, -33, -33, 95, 95, 95, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, -124, -124, -124, -23, -23, -23, + -33, -33, -33, -107, -107, -107, -75, -75, -75, -68, -68, -68, -15, -15, -15, -16, + -16, -16, -111, -111, -111, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 55, 55, 55, + -97, -97, -97, -26, -26, -26, -29, -29, -29, -31, -31, -31, -61, -61, -61, 121, + 121, 121, 11, 11, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; +} diff --git a/apps/q/java/src/net/i2p/aum/q/QClientAPI.java b/apps/q/java/src/net/i2p/aum/q/QClientAPI.java new file mode 100644 index 000000000..1f7f1f86f --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QClientAPI.java @@ -0,0 +1,187 @@ +/* + * QClientAPI.java + * + * Created on March 31, 2005, 5:19 PM + */ + +package net.i2p.aum.q; + +import java.*; +import java.io.*; +import java.util.*; +import java.lang.*; +import java.net.*; + +import org.apache.xmlrpc.*; + +/** + * The official Java API for client applications wishing to access the Q + * network
+ *This API is just a thin wrapper that hides the XMLRPC details, and exposes + a simple set of methods.
+ *Note to app developers - I'm only implementing this API in Java + * and Python at present. If you've got some time and knowledge of other + * languages and their available XML-RPC client libs, we'd really appreciate + * it if you can port this API into other languages - such as Perl, C++, + * Ruby, OCaml, C#, etc. You can take this API implementation as the reference + * code for porting to your own language.
+ */ + +public class QClientAPI { + + XmlRpcClient node; + + /** + * Creates a new instance of QClientAPI talking on given xmlrpc port + */ + public QClientAPI(int port) throws MalformedURLException { + node = new XmlRpcClient("http://127.0.0.1:"+port); + } + + /** + * Creates a new instance of QClientAPI talking on default xmlrpc port + */ + public QClientAPI() throws MalformedURLException { + node = new XmlRpcClient("http://127.0.0.1:"+QClientNode.defaultXmlRpcServerPort); + } + + /** + * Pings a Q client node, gets back a bunch of useful stats + */ + public Hashtable ping() throws XmlRpcException, IOException { + return (Hashtable)node.execute("i2p.q.ping", new Vector()); + } + + /** + * Retrieves an update of content catalog + * @param since a unixtime in seconds. The content list returned will + * be a differential update since this time. + */ + public Hashtable getUpdate(int since) + throws XmlRpcException, IOException + { + Vector args = new Vector(); + args.addElement(new Integer(since)); + args.addElement(new Integer(1)); + args.addElement(new Integer(1)); + return (Hashtable)node.execute("i2p.q.getUpdate", args); + } + + /** + * Retrieves an item of content from the network, given its key + * @param key the key to retrieve + */ + public Hashtable getItem(String key) throws XmlRpcException, IOException { + Vector args = new Vector(); + args.addElement(key); + return (Hashtable)node.execute("i2p.q.getItem", args); + } + + /** + * Inserts a single item of data, without metadata. A default metadata set + * will be generated. + * @param data a byte[] of data to insert + * @return a Hashtable containing results, including: + *+ *
+ */ + public Hashtable putItem(byte [] data) throws XmlRpcException, IOException { + Vector args = new Vector(); + args.addElement(data); + return (Hashtable)node.execute("i2p.q.putItem", args); + } + + /** + * Inserts a single item of data, with metadata + * @param metadata a Hashtable of metadata to insert + * @param data a byte[] of data to insert + * @return a Hashtable containing results, including: + *- result - either "ok" or "error"
+ *- error - (only if result != "ok") - terse error label
+ *- key - the key under which this item has been inserted
+ *+ *
+ */ + public Hashtable putItem(Hashtable metadata, byte [] data) + throws XmlRpcException, IOException + { + Vector args = new Vector(); + args.addElement(metadata); + args.addElement(data); + return (Hashtable)node.execute("i2p.q.putItem", args); + } + + /** + * Generates a new keypair for inserting signed-space items + * @return a struct with the keys: + *- result - either "ok" or "error"
+ *- error - (only if result != "ok") - terse error label
+ *- key - the key under which this item has been inserted
+ *+ *
+ * When inserting an item using the privateKey, the resulting uri + * will be- status - "ok"
+ *- publicKey - base64-encoded signed space public key
+ *- privateKey - base64-encoded signed space private key
+ *Q:publicKey/path+ */ + public Hashtable newKeys() throws XmlRpcException, IOException + { + Vector args = new Vector(); + return (Hashtable)node.execute("i2p.q.newKeys", args); + } + + + /** + * Adds a new noderef to node + * @param dest - the base64 i2p destination for the remote peer + * @return a Hashtable containing results, including: + *+ *
+ */ + public Hashtable hello(String dest) throws XmlRpcException, IOException { + Vector args = new Vector(); + args.addElement(dest); + return (Hashtable)node.execute("i2p.q.hello", args); + } + + /** + * Shuts down a running node + * If the shutdown succeeds, then this call will fail with an exception. But + * if the call succeeds, then the shutdown has failed (sorry if this is a tad + * counter-intuitive). + * @param privKey - the base64 i2p private key for this node. + * @return a Hashtable containing results, including: + *- result - either "ok" or "error"
+ *- error - (only if result != "ok") - terse error label
+ *+ *
+ */ + public Hashtable shutdown(String privKey) throws XmlRpcException, IOException { + Vector args = new Vector(); + args.addElement(privKey); + return (Hashtable)node.execute("i2p.q.shutdown", args); + } + + /** + * Search the node for catalog entries matching a set of criteria + * @param criteria a Hashtable of metadata criteria to match, and whose + * values are regular expressions + * @return a Hashtable containing results, including: + *- result - "error"
+ *- error - terse error label
+ *+ *
+ */ + public Hashtable search(Hashtable criteria) throws XmlRpcException, IOException { + Vector args = new Vector(); + args.addElement(criteria); + return (Hashtable)node.execute("i2p.q.search", args); + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QClientNode.java b/apps/q/java/src/net/i2p/aum/q/QClientNode.java new file mode 100644 index 000000000..cd8bff090 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QClientNode.java @@ -0,0 +1,610 @@ +/* + * QClient.java + * + * Created on 20 March 2005, 23:22 + */ + +package net.i2p.aum.q; + +import java.*; +import java.io.*; +import java.util.*; +import java.lang.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.data.*; + +import net.i2p.aum.*; +import net.i2p.aum.http.*; + +import HTML.Template; + +/** + * Implements Q client nodes. + */ + +public class QClientNode extends QNode { + + static String defaultStoreDir = ".quartermaster_client"; + I2PHttpServer webServer; + MiniHttpServer webServerTcp; + Properties httpProps; + + public String nodeType = "Client"; + + // ------------------------------------------------------- + // CONSTRUCTORS + // ------------------------------------------------------- + + /** + * Creates a new instance of QClient, using default + * datastore location + * @throws IOException, DataFormatException, I2PException + */ + public QClientNode() throws IOException, DataFormatException, I2PException + { + super(System.getProperties().getProperty("user.home") + sep + defaultStoreDir); + log.debug("TEST CLIENT DEBUG MSG1"); + } + + /** + * Creates a new instance of QClient, using specified + * datastore location + * @param path of node's datastore directory + * @throws IOException, DataFormatException, I2PException + */ + public QClientNode(String dataDir) throws IOException, DataFormatException, I2PException + { + super(dataDir); + + log.error("TEST CLIENT DEBUG MSG"); + } + + // ------------------------------------------------------- + // METHODS - XML-RPC PRIMITIVE OVERRIDES + // ------------------------------------------------------- + + /** + * hello cmds to client nodes are illegal! + */ + /** + public Hashtable localHello(String destBase64) + { + Hashtable h = new Hashtable(); + h.put("status", "error"); + h.put("error", "unimplemented"); + return h; + } + **/ + + /** perform client-specific setup */ + public void setup() + { + updateCatalogFromPeers = 1; + isClient = true; + + // allow a port change for xmlrpc client app conns + String xmlPortStr = System.getProperty("q.xmlrpc.tcp.port"); + if (xmlPortStr != null) { + xmlRpcServerPort = new Integer(xmlPortStr).intValue(); + conf.setIntProperty("xmlRpcServerPort", xmlRpcServerPort); + } + + // ditto for listening host + String xmlHostStr = System.getProperty("q.xmlrpc.tcp.host"); + if (xmlHostStr != null) { + xmlRpcServerHost = xmlHostStr; + conf.setProperty("xmlRpcServerHost", xmlRpcServerHost); + } + + // --------------------------------------------------- + // now fire up the HTTP interface + // listening only within I2P on client node's dest + + // set up a properties object for short local tunnel + httpProps = new Properties(); + httpProps.setProperty("inbound.length", "0"); + httpProps.setProperty("outbound.length", "0"); + httpProps.setProperty("inbound.lengthVariance", "0"); + httpProps.setProperty("outbound.lengthVariance", "0"); + Properties sysProps = System.getProperties(); + String i2cpHost = sysProps.getProperty("i2cp.tcp.host", "127.0.0.1"); + String i2cpPort = sysProps.getProperty("i2cp.tcp.port", "7654"); + httpProps.setProperty("i2cp.tcp.host", i2cpHost); + httpProps.setProperty("i2cp.tcp.port", i2cpPort); + } + + public void run() { + + // then do all the parent stuff + super.run(); + } + + /** + *- result - "ok" or "error"
+ *- error - if result != "ok", a terse error label
+ *- items - a Vector of items found which match the given search + * criteria. If no available matching items were found, this vector + * will come back empty. + *
Sets up and launches an http server for servicing requests + * to this node.
+ *For server nodes, the xml-rpc server listens within I2P on the + * node's destination.
+ *For client nodes, the xml-rpc server listens on a local TCP + * port (according to attributes xmlRpcServerHost and xmlRpcServerPort)
+ */ + public void startExternalInterfaces(QServerMethods methods) throws Exception + { + System.out.println("Creating http interface..."); + try { + // create tcp http server for xmlrpc and browser access + webServerTcp = new MiniHttpServer(QClientWebInterface.class, xmlRpcServerPort, this); + webServerTcp.addXmlRpcHandler(baseXmlRpcServiceName, methods); + System.out.println("started in-i2p http/xmlrpc server listening on port:" + xmlRpcServerPort); + webServerTcp.start(); + + // create in-i2p http server for xmlrpc and browser access + webServer + = new I2PHttpServer(privKey, + QClientWebInterface.class, + this, + httpProps + ); + webServer.addXmlRpcHandler(baseXmlRpcServiceName, methods); + webServer.start(); + System.out.println("Started in-i2p http/xmlrpc server listening on dest:"); + String dest = privKey.getDestination().toBase64(); + System.out.println(dest); + + + System.out.println("web interfaces created"); + + } catch (Exception e) { + e.printStackTrace(); + System.out.println("Failed to create client web interfaces"); + System.exit(1); + } + +/** + WebServer serv = new WebServer(xmlRpcServerPort); + // if host is non-null, add as a listen host + if (xmlRpcServerHost.length() > 0) { + serv.setParanoid(true); + serv.acceptClient(xmlRpcServerHost); + } + serv.addHandler(baseXmlRpcServiceName, methods); + serv.start(); + log.info("Client XML-RPC server listening on port "+xmlRpcServerPort+" as service"+baseXmlRpcServiceName); +**/ + + } + + // ----------------------------------------------------- + // client-specific customisations of xmlRpc methods + // ----------------------------------------------------- + + /** + * Insert an item of content, with metadata. Then (since this is the client's + * override) schedules a job to insert this item to a selection of remote peers. + * @param metadata Hashtable of item's metadata + * @param data raw data to insert + */ + public Hashtable putItem(Hashtable metadata, byte [] data) throws QException + { + Hashtable resp = new Hashtable(); + QDataItem item; + String uri; + + // do the local insert first + try { + item = new QDataItem(metadata, data); + item.processAndValidate(true); + localPutItem(item); + uri = (String)item.get("uri"); + + } catch (QException e) { + resp.put("status", "error"); + resp.put("error", "qexception"); + resp.put("summary", e.getLocalizedMessage()); + return resp; + } + + // now schedule remote insertion + schedulePeerUploadJob(item); + + // and return success, rest will happen automatically in background + resp.put("status", "ok"); + resp.put("uri", uri); + return resp; + } + + /** + * Search datastore and catalog for a given item of content + * @param criteria Hashtable of criteria to match in metadata + */ + public Hashtable search(Hashtable criteria) + { + Hashtable result = new Hashtable(); + Vector matchingItems = new Vector(); + Iterator items; + Hashtable item; + Hashtable foundUris = new Hashtable(); + String uri; + + // get an iterator for all catalog items + try { + // test all local content + items = contentIdx.getItemsSince(0); + while (items.hasNext()) { + String uriHash = (String)items.next(); + item = getLocalMetadataForHash(uriHash); + uri = (String)item.get("uri"); + //System.out.println("search: testing "+metadata+" against "+criteria); + if (metadataMatchesCriteria(item, criteria)) { + matchingItems.addElement(item); + foundUris.put(uri, item); + } + } + + // now test remote catalog + items = catalogIdx.getItemsSince(0); + while (items.hasNext()) { + String uriHash = (String)items.next(); + item = getLocalCatalogMetadataForHash(uriHash); + uri = (String)item.get("uri"); + //System.out.println("search: testing "+metadata+" against "+criteria); + if (metadataMatchesCriteria(item, criteria)) { + if (!foundUris.containsKey("uri")) { + matchingItems.addElement(item); + } + } + } + + } catch (Exception e) { + e.printStackTrace(); + result.put("status", "error"); + result.put("error", e.getMessage()); + return result; + } + + result.put("status", "ok"); + result.put("items", matchingItems); + return result; + + } + + + /** + * retrieves a peers/catalog update - executes on base class, then + * adds in our catalog entries + */ + public Hashtable getUpdate(int since, int includePeers, int includeCatalog) + { + Hashtable h = localGetUpdate(since, includePeers, includeCatalog); + + if (includeCatalog != 0) { + + // must extend v with remote catalog entries + Vector vCat = (Vector)(h.get("items")); + Iterator items; + + // get an iterator for all new catalog items since given unixtime + try { + items = catalogIdx.getItemsSince(since); + + // pick through the iterator, and fetch metadata for each item + while (items.hasNext()) { + String key = (String)(items.next()); + Hashtable pf = getLocalCatalogMetadata(key); + log.error("getUpdate(client): key="+key+", pf="+pf); + System.out.println("getUpdate(client): key="+key+", pf="+pf); + if (pf != null) { + // clone this metadata, add in the key + Hashtable pf1 = (Hashtable)pf.clone(); + pf1.put("key", key); + vCat.addElement(pf1); + } + } + + + } catch (IOException e) { + e.printStackTrace(); + } + } + + return h; + } + + /** + *Retrieve an item of content.
+ *This client override tries the local datastore first, then + * attempts to get the data from remote servers believed to have the data
+ */ + public Hashtable getItem(String uri) throws IOException, QException + { + Hashtable res; + + log.info("getItem: uri='"+uri+"'"); + + if (localHasItem(uri)) { + + class Fred { + } + + Fred xxx = new Fred(); + + // got it locally, send it back + return localGetItem(uri); + } + + // ain't got it locally - try remote sources in turn till we + // either get it or fail + Vector sources = getItemLocation(uri); + + // send back an error if not in local catalog + if (sources == null || sources.size() == 0) { + Hashtable dnf = new Hashtable(); + dnf.put("status", "error"); + dnf.put("error", "notfound"); + dnf.put("comment", "uri not known locally or remotely"); + return dnf; + } + + // ok, got at least one remote source, go through them till + // we get data that checks out + int i; + int npeers = sources.size(); + int numCmdFail = 0; + int numDnf = 0; + int numBadData = 0; + for (i=0; i+ * status - String - either "ok" or "error" + *error - String - short summary of error, only present if + * status is "error" + *uri - the full Q URI for the top level of the site + * + */ + public Hashtable insertQSite(String privKey64, + String siteName, + String rootPath, + Hashtable metadata + ) + throws Exception + { + // for results + Hashtable result = new Hashtable(); + String uri = null; // uri under which this site will be reachable + String pubKey64; + + File dir = new File(rootPath); + + // barf if no such directory + if (!dir.isDirectory()) { + result.put("status", "error"); + result.put("error", "nosuchdir"); + result.put("detail", "Path '"+rootPath+"' is not a directory"); + return result; + } + + // barf if not readable + if (!dir.canRead()) { + result.put("status", "error"); + result.put("error", "cantread"); + result.put("detail", "Path '"+rootPath+"' is not readable"); + return result; + } + + // barf if missing or invalid site name + siteName = siteName.trim(); + if (!siteName.matches("[a-zA-Z0-9_-]+")) { + result.put("status", "error"); + result.put("error", "badsitename"); + result.put("detail", "QSite name should be only alphanumerics, '-' and '_'"); + return result; + } + + String defaultPath = rootPath + sep + "index.html"; + File defaultFile = new File(defaultPath); + + // barf if index.html not present and readable + if (!(defaultFile.isFile() && defaultFile.canRead())) { + result.put("status", "error"); + result.put("error", "noindex"); + result.put("detail", "Required file index.html missing or unreadable"); + return result; + } + + // derive public key and uri for site, barf if bad key + try { + pubKey64 = QUtil.privateToPubHash(privKey64); + } catch (Exception e) { + result.put("status", "error"); + result.put("error", "badprivkey"); + return result; + } + uri = "Q:" + pubKey64 + "/" + siteName + "/"; + + // now the fun recursive bit + insertQSiteDir(privKey64, siteName, rootPath, ""); + + // queue up an insert of default file + metadata.put("type", "qsite"); + metadata.put("path", siteName+"/"); + metadata.put("mimetype", "text/html"); + + //System.out.println("insertQSite: privKey='"+privKey64+"'"); + //System.out.println("insertQSite: siteName='"+siteName+"'"); + //System.out.println("insertQSite: rootDir='"+rootPath+"'"); + //System.out.println("insertQSite: metadata="+metadata); + //System.out.println("insertQSite: default="+defaultPath); + + insertQSiteFile(privKey64, siteName, defaultPath, "", metadata); + + result.put("status", "ok"); + result.put("uri", uri); + return result; + } + + /** + * recursively queues jobs for the insertion of a directory's contents, for + * a qsite. + * @param privKey64 - private 'signed space' key, base64 format + * @param siteName - short text name for the site + * @param absPath - physical pathname of the subdirectory to insert + * @param relPath - qsite-relative pathname of this item + */ + protected void insertQSiteDir(String privKey64, String siteName, String absPath, String relPath) + throws Exception + { + File dir = new File(absPath); + + // fail gracefully if not a readable directory + if (!(dir.isDirectory() && dir.canRead())) { + System.out.println("insertQSiteDir: not a readable directory "+absPath); + return; + } + + //System.out.println("insertQSiteDir: entry - abs='"+absPath+"' rel='"+relPath+"'"); + + // loop through the contents + String [] contents = dir.list(); + for (int i=0; i 0) { + node = new QClientNode(args[0]); + } + else { + node = new QClientNode(); + } + node.log.info("QClientNode: running node..."); + node.run(); + } + + public void foo1() { + System.out.println("QClientNode.foo: isClient="+isClient); + } + + +} diff --git a/apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java b/apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java new file mode 100644 index 000000000..4d02d46de --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QClientWebInterface.java @@ -0,0 +1,751 @@ +/* + * QClientWebInterface.java + * + * Created on April 9, 2005, 1:10 PM + */ + +package net.i2p.aum.q; + +import java.lang.*; +import java.lang.reflect.*; +import java.io.*; +import java.net.*; +import java.util.*; + +import HTML.Template; + +import net.i2p.aum.http.*; + + +/** + * Request handler for Q Client nodes that listens within I2P + * on the client node's destination. Intended for access via + * eepProxy, and by adding a hosts.txt entry for this dest + * under the hostname 'q'. + */ +public class QClientWebInterface extends I2PHttpRequestHandler { + + /** set this to true when debugging html layout */ + public static boolean loadTemplateWithEachHit = true; + + public QNode node = null; + + // refs to main page template, and components of main page + static Template tmplt; + static Vector tabRow; + static Vector pageItems; + + /** + * for security - disables direct-uri GETs of content if running directly over TCP; + * we need to coerce users to use their eepproxy browser instead + */ + public boolean isRunningOverTcp = true; + + /** Creates a new instance of QClientWebInterface */ + public QClientWebInterface(MiniHttpServer server, Object socket, Object node) + throws Exception + { + super(server, socket, node); + this.node = (QNode)node; + isRunningOverTcp = socket.getClass() == Socket.class; + } + + static String [] tabNames = { + "home", "search", "insert", "tools", "status", "jobs", "help", "about" + }; + + /** + * Loads a template of a given name. Invokes method on node + * to resolve this to an absolute pathname, so 'name' -> '/path/to/html/name.html' + */ + public Template loadTemplate(String name) throws Exception { + + String fullPath = node.getResourcePath("html"+node.sep+name)+".html"; + //System.out.println("fullPath='"+fullPath+"'"); + String [] args = new String [] { + "filename", fullPath, + "case_sensitive", "true", + "max_includes", "5" + }; + return new Template(args); + } + + // ---------------------------------------------------- + // FRONT-END METHODS + // ---------------------------------------------------- + + /** GET and POST both go through .safelyHandleReq() */ + public void on_GET() { + + safelyHandleReq(); + } + + /** GET and POST both go through .safelyHandleReq() */ + public void on_POST() { + + safelyHandleReq(); + } + + public void on_RPC() { + + } + + /** + * wrap .handleReq() - on exception, call dump_error() to + * generate a 400 error page with diagnostics + */ + public void safelyHandleReq() { + try { + handleReq(); + } catch (Exception e) { + dump_error(e); + } + } + + /** + * Forwards hits to either a path handler method, or generic get method.
+ * + *Detects hits to paths for which we have a handler (ie, methods + * of this class with name 'hdlr_<somepath>', (such as 'hdlr_help' + * for handling hits to '/help').
+ * + *If we have a handler, forward to it, otherwise forward to standard + * getItem() method
+ */ + public void handleReq() throws Exception { + + Class [] noArgs; + Method hdlrMethod; + + // strip useless leading slash from reqFile + reqFile = reqFile.substring(1); + + // default to 'home' + if (reqFile.equals("")) { + reqFile = "home"; + } + //print("handleReq: reqFile='"+reqFile+"'"); + + // Set up the main page template + try { + tmplt = loadTemplate("main"); + pageItems = new Vector(); + tmplt.setParam("items", pageItems); + tmplt.setParam("nodeType", node.nodeType); + + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + //print("handleReq: loaded template"); + + // execute if a command + if (allVars.containsKey("cmd")) { + do_cmd(); + } + + // -------------------------------------------------------- + // intercept magic paths for which we have a handler method + noArgs = new Class[0]; + try { + // extract top dir of path and make it method-name-safe + String methodName = "hdlr_"+reqFile.split("/")[0].replace('.','_'); + hdlrMethod = this.getClass().getMethod(methodName, null); + + // now dispatch the method + hdlrMethod.invoke(this, null); + + // spit out html, if no raw content inserted + sendPageIfNoContent(); + + // done + return; + + } catch (NoSuchMethodException e) { + // routinely fails if we dont' have handler, so assume it's + // a GET + } + + // if we get here, client is requesting a specific uri + allVars.put("uri", reqFile); + if (!cmd_get()) { + hdlr_home(); + } + sendPageIfNoContent(); + } + + /** + * as name implies, generates standard html page + * if setRawOutput hasnt' been called + */ + public void sendPageIfNoContent() { + + if (rawContentBytes == null) { + + // we're spitting out html + setContentType("text/html"); + + // set up tab row style vector + setupTabRow(); + + // finally, render out our filled-out template + setRawOutput(tmplt.output()); + } + } + + /** + * Inserts an item into main pane + */ + public Object addToMainPane(Object item) { + + Hashtable h = new Hashtable(); + h.put("item", item); + pageItems.addElement(h); + return item; + } + + /** + * Generates a set of tabs and adds these to the page, + * marking as active the tab whose name is in the current URL + */ + public void setupTabRow() + { + Hashtable h; + tabRow = new Vector(); + for (int i=0; i< tabNames.length; i++) { + String name = tabNames[i]; + h = new Hashtable(); + h.put("name", name); + h.put("label", name.substring(0,1).toUpperCase()+name.substring(1)); + if (name.equals(reqFile)) { + h.put("active", "1"); + } + tabRow.addElement(h); + tmplt.setParam("tabs", tabRow); + } + } + + // ----------------------------------------------------- + // METHODS FOR HANDLING MAGIC PATHS + // ---------------------------------------------------- + + /** Display home page */ + public void hdlr_home() throws Exception { + + // stick in 'getitem' form + addToMainPane(loadTemplate("getform")); + + } + + /** Display status page */ + public void hdlr_status() throws Exception { + + // ping the node, extract status items + Vector statusItems = new Vector(); + Hashtable h = node.ping(); + for (Enumeration e = h.keys(); e.hasMoreElements();) { + String key = (String)e.nextElement(); + String val = h.get(key).toString(); + if (val.length() > 60) { + // too big for table, stick into a readonly text field + val = ""; + } + Hashtable rec = new Hashtable(); + rec.put("key", key); + rec.put("value", val); + //print("key='"+key+"' val='"+val+"'"); + statusItems.addElement(rec); + } + + // get status form template insert the items, stick onto main pane + Template tmpltStatus = loadTemplate("status"); + tmpltStatus.setParam("items", statusItems); + addToMainPane(tmpltStatus); + + } + + /** display current node jobs list */ + public void hdlr_jobs() throws Exception { + + // get jobs list, add to jobs list template, add that to main pane + Template tmpltJobs = loadTemplate("jobs"); + tmpltJobs.setParam("items", node.getJobsList()); + addToMainPane(tmpltJobs); + } + + /** Display search form */ + public void hdlr_search() throws Exception { + addToMainPane(loadTemplate("searchform")); + } + + /** Display insert page */ + public void hdlr_insert() throws Exception { + + String formName = allVars.get("mode", 0, "file").equals("site") ? "putsiteform" : "putform"; + Template tmpltPut = loadTemplate(formName); + addToMainPane(tmpltPut); + } + + /** Display settings screen */ + public void hdlr_settings() throws Exception { + addToMainPane(loadTemplate("settings")); + } + + /** Display tools screen */ + public void hdlr_tools() throws Exception { + + addToMainPane(loadTemplate("tools")); + addToMainPane(loadTemplate("genkeysform")); + addToMainPane(loadTemplate("addrefform")); + } + + /** Display help screen */ + public void hdlr_help() throws Exception { + addToMainPane(loadTemplate("help")); + } + + /** Display about screen */ + public void hdlr_about() throws Exception { + addToMainPane(loadTemplate("about")); + } + + /** handle /favicon.ico hits */ + public void hdlr_favicon_ico() { + + System.out.println("Sending favicon image"); + setContentType("image/x-icon"); + setRawOutput(Favicon.image); + } + + /** dummy handler, causes an exception (for testing error dump pages */ + public void hdlr_shit() throws Exception { + throw new Exception("this method is shit"); + } + + // ---------------------------------------------------- + // METHODS FOR HANDLING COMMANDS + // ---------------------------------------------------- + + /** + * invoked if GET or POST vars contain 'cmd'. + * attempts to dispatch command handler method 'cmd_xxxx' + */ + public void do_cmd() throws Exception { + + // this whole method could be done in python with the statement: + // getattr(self, 'cmd_'+urlVars['cmd'], lambda:None)() + String cmd = allVars.get("cmd", 0); + try { + // extract top dir of path and make it method-name-safe + String methodName = "cmd_"+cmd; + Method hdlrMethod = this.getClass().getMethod(methodName, null); + + // now dispatch the method + hdlrMethod.invoke(this, null); + } catch (NoSuchMethodException e) {} + } + + + /** + * executes a 'get' cmd + */ + public boolean cmd_get() throws Exception { + + Hashtable result = null; + String status = null; + Hashtable metadata = null; + String mimetype = null; + + // bail if node offline + if (node == null) { + return false; + } + + // bail if no 'url' arg + if (!allVars.containsKey("uri")) { + return false; + } + + // get uri, prepend 'Q:' if needed + String uri = allVars.get("uri", 0); + if (!uri.startsWith("Q:")) { + uri = "Q:" + uri; + } + + // attempt the fetch + result = node.getItem(uri); + status = (String)result.get("status"); + + // how'd we go? + if (status.equals("ok")) { + // got it - send it back + metadata = (Hashtable)result.get("metadata"); + mimetype = (String)metadata.get("mimetype"); + + // forbid content retrieval via MSIE + boolean isIE = false; + for (Enumeration e = headerVars.get("User-Agent").elements(); e.hasMoreElements();) { + String val = ((String)e.nextElement()).toLowerCase(); + if (val.matches(".*(msie|windows|\\.net).*")) { + Template warning = loadTemplate("msiealert"); + addToMainPane(warning); + return false; + } + } + + // forbid direct delivery of text/* content via direct tcp + if (isRunningOverTcp) { + // security feature - set to application/octet-stream if req arrives via tcp. + // this prevents people surfing the q web interface directly over TCP and + // falling prey to anonymity attacks (eg gif bugs) + + // if user is trying to hit an html page, we can send back a warning + if (mimetype.startsWith("text")) { + Template warning = loadTemplate("anonalert"); + warning.setParam("dest", node.destStr); + addToMainPane(warning); + return false; + } + setContentType("application/octet-stream"); + } else { + // got this conn via I2P and eeproxy - safer to obey the mimetype + setContentType(mimetype); + } + + setRawOutput((byte [])result.get("data")); + return true; + } else { + // 404 + tmplt.setParam("show_404", "1"); + tmplt.setParam("404_uri", uri); + return false; + } + } + + /** executes genkeys command */ + public void cmd_genkeys() throws Exception { + + Hashtable res = node.newKeys(); + String pubKey = (String)res.get("publicKey"); + String privKey = (String)res.get("privateKey"); + Template keysWidget = loadTemplate("genkeysresult"); + keysWidget.setParam("publickey", pubKey); + keysWidget.setParam("privatekey", privKey); + addToMainPane(keysWidget); + } + + /** adds a noderef */ + public void cmd_addref() throws Exception { + + String ref = allVars.get("noderef", 0).trim(); + node.hello(ref); + } + + /** executes 'put' command */ + public void cmd_put() throws Exception { + + // barf if user posted both data and rawdata + if (allVars.containsKey("data") + && ((String)allVars.get("data", 0)).length() > 0 + && allVars.containsKey("rawdata") + && ((String)allVars.get("rawdata", 0)).length() > 0 + ) + { + Template t = loadTemplate("puterror"); + t.setParam("error", "you specified a file as well as 'rawdata'"); + addToMainPane(t); + addToMainPane(dumpVars().toString()); + return; + } + + Hashtable metadata = new Hashtable(); + byte [] data = new byte[0]; + + // stick in some defaults + String [] keys = { + "data", "rawdata", + "mimetype", "keywords", "privkey", "abstract", "type", "title", + "path" + }; + + //System.out.println("allVars='"+allVars+"'"); + + // extract all items from form, add to metadata ones that + // have non-zero length. Take 'data' or 'rawdata' and stick their + // bytes into data. + for (int i=0; i0) { + data = dataval; + } + } else if (key.equals("rawdata")) { + byte [] dataval = allVars.get("rawdata", 0).getBytes(); + if (dataval.length > 0) { + data = dataval; + } + } else if (key.equals("privkey")) { + String k = allVars.get("privkey", 0); + if (k.length() > 0) { + metadata.put("privateKey", k); + } + } else { + String val = allVars.get(key, 0); + //System.out.println("'"+key+"'='"+val+"'"); + if (val.length() > 0) { + metadata.put(key, allVars.get(key, 0)); + } + } + } + } + + //System.out.println("metadata="+metadata); + + if (metadata.size() == 0) { + Template err = loadTemplate("puterror"); + err.setParam("error", "No metadata!"); + addToMainPane(err); + addToMainPane(dumpVars().toString()); + return; + } + + if (data.length == 0) { + Template err = loadTemplate("puterror"); + err.setParam("error", "No data!"); + addToMainPane(err); + addToMainPane(dumpVars().toString()); + return; + } + + // phew! ready to put + System.out.println("WEB:cmd_put: inserting"); + + Hashtable result = node.putItem(metadata, data); + + System.out.println("WEB:cmd_put: got"+result); + + String status = (String)result.get("status"); + if (!status.equals("ok")) { + String errTxt = (String)result.get("error"); + if (result.containsKey("summary")) { + errTxt = errTxt + ":" + result.get("summary").toString(); + } + Template err = loadTemplate("puterror"); + err.setParam("error", (String)result.get("error")); + addToMainPane(err); + addToMainPane(dumpVars().toString()); + return; + } + + // success, yay! + Template success = loadTemplate("putok"); + success.setParam("uri", (String)result.get("uri")); + addToMainPane(success); + + //System.out.println("cmd_put: debug on page??"); + //addToMainPane(dumpVars().toString()); + } + + /** executes 'putsite' command */ + public void cmd_putsite() throws Exception { + + Hashtable metadata = new Hashtable(); + String privKey = allVars.get("privkey", 0, ""); + String name = allVars.get("name", 0, ""); + String dir = allVars.get("dir", 0, ""); + + // pick up optional metadata items + String [] keys = { + "title", "keywords", "abstract", + }; + + // extract all items from form, add to metadata ones that + // have non-zero length. + for (int i=0; i 0) { + metadata.put(key, allVars.get(key, 0)); + } + } + } + + //System.out.println("metadata="+metadata); + + if (metadata.size() == 0) { + cmd_putsite_error("No metadata!"); + return; + } + + // phew! ready to put + Hashtable result = node.insertQSite(privKey, name, dir, metadata); + String status = (String)result.get("status"); + if (!status.equals("ok")) { + cmd_putsite_error((String)result.get("error")); + return; + } + + // success, yay! + Template success = loadTemplate("putok"); + success.setParam("is_site", "1"); + success.setParam("uri", (String)result.get("uri")); + addToMainPane(success); + + //System.out.println("cmd_put: debug on page??"); + //addToMainPane(dumpVars().toString()); + } + + protected void cmd_putsite_error(String msg) throws Exception { + + Template err = loadTemplate("puterror"); + err.setParam("error", msg); + err.setParam("is_site", "1"); + addToMainPane(err); + addToMainPane(dumpVars().toString()); + } + + /** performs a search */ + public void cmd_search() throws Exception { + + Hashtable criteria = new Hashtable(); + String [] fields = { + "type", "title", "path", "mimetype", "keywords", + "summary", "searchmode" + }; + + for (int i=0; i 0) { + if (!_path.startsWith("/")) { + _path = "/" + _path; + put("path", _path); + } + } + + // determine file extension + String [] bits = _path.split("/"); + String name = bits[bits.length-1]; + bits = name.split("\\.", 2); + ext = "." + bits[bits.length-1]; + } + else { + // path is empty - set to '/ .ext' where 'ext' is the + // file extension guessed from present mimetype value, and dataHash + // is a shortened hash of the content + String mime = (String)get("mimetype"); + if (mime == null) { + mime = "application/octet-stream"; + put("mimetype", mime); + } + + // determine file extension + ext = Mimetypes.guessExtension(mime); + + // and determine final path + _path = "/" + ((String)get("dataHash")).substring(0, 10) + ext; + put("path", _path); + } + + // ----------------------------------------- + // default the mimetype + if (!containsKey("mimetype")) { + String mimetype = Mimetypes.guessType(ext); + put("mimetype", mimetype); + } + + // ------------------------------------------ + // barf if contains mutually-exclusive signed space keys + if (containsKey("privateKey") && (containsKey("publicKey") || containsKey("signature"))) { + throw new QException("Metadata must NOT contain privateKey and one of publicKey or signature"); + } + + // ------------------------------------------ + // barf if exactly one of publicKey and signature are present + if (containsKey("publicKey") ^ containsKey("signature")) { + throw new QException("Either both or neither of 'publicKey' and 'signature' must be present"); + } + + // ----------------------------------------- + // now discern between plain hash items and + // signed space items + if (containsKey("privateKey") || containsKey("publicKey")) { + + DSAEngine dsa = DSAEngine.getInstance(); + + // process/validate remaining data in signed space context + + if (containsKey("privateKey")) { + // only private key given - uplift, remove, replace with public key + _privKey = new SigningPrivateKey(); + String priv64 = get("privateKey").toString(); + try { + _privKey.fromBase64(priv64); + } catch (Exception e) { + throw new QException("Invalid privateKey", e); + } + + // ok, got valid privateKey + + // expunge privKey from metadata, replace with publicKey + this.remove("privateKey"); + _pubKey = _privKey.toPublic(); + put("publicKey", _pubKey.toBase64()); + + // create and insert a signature + QUtil.debug("before sig, asSortedString give:\n"+asSortedString()); + + Signature sig = dsa.sign(asSortedString().getBytes(), _privKey); + String sigBase64 = sig.toBase64(); + put("signature", sigBase64); + } + else { + // barf if not both signature and pubkey present + if (!(containsKey("publicKey") && containsKey("signature"))) { + throw new QException("need both publicKey and signature"); + } + _pubKey = new SigningPublicKey(); + String pub64 = get("publicKey").toString(); + try { + _pubKey.fromBase64(pub64); + } catch (Exception e) { + throw new QException("Invalid publicKey", e); + } + } + + // now, whether we just signed or not, validate the signature/pubkey + byte [] thisAsBytes = asSortedString().getBytes(); + + String sig64 = get("signature").toString(); + Signature sig1 = new Signature(); + try { + sig1.fromBase64(sig64); + } catch (DataFormatException e) { + throw new QException("Invalid signature string", e); + } + + if (!dsa.verifySignature(sig1, thisAsBytes, _pubKey)) { + throw new QException("Invalid signature"); + } + + // last step - determine the correct URI + String pubHash = QUtil.hashPubKey(_pubKey); + uri = "Q:"+pubHash+_path; + + } // end of 'signed space' mode processing + else { + // ----------------------------------------------------- + // process/validate remaining data in plain hash context + String thisHashed = QUtil.sha64(asSortedString()); + uri = "Q:"+ thisHashed + ext; + + } // end of plain hash mode processing + + + // ----------------------------------------------------- + // final step - add or validate uri + if (containsKey("uri")) { + if (!get("uri").toString().equals(uri)) { + throw new QException("Invalid URI"); + } + } else { + put("uri", uri); + } + + } + + /** + * returns a filename under which this item should be stored + */ + public String getStoreFilename() throws QException { + if (!containsKey("uri")) { + throw new QException("Missing URI"); + } + return QUtil.sha64((String)get("uri")); + } + + /** + * Hashes this set of metadata, excluding any 'signature' key + * @return Base64 hash of metadata + */ + public String hashThisAsBase64() { + + return QUtil.sha64(asSortedString()); + } + + public byte [] hashThis() { + + return QUtil.sha(asSortedString()); + } + + /** + * alphabetise thie metadata to a single string, containing one + * 'key=value' entry per line. Excludes keys 'uri' and 'signature' + */ + public String asSortedString() { + + TreeSet t = new TreeSet(keySet()); + Iterator keys = t.iterator(); + int nkeys = t.size(); + int i; + String metaStr = ""; + for (i = 0; i < nkeys; i++) + { + String metaKey = (String)keys.next(); + if (!(metaKey.equals("signature") || metaKey.equals("uri"))) { + metaStr += metaKey + "=" + get(metaKey) + "\n"; + } + } + return metaStr; + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QException.java b/apps/q/java/src/net/i2p/aum/q/QException.java new file mode 100644 index 000000000..fcb58aee0 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QException.java @@ -0,0 +1,48 @@ +/* + * QException.java + * + * Created on April 6, 2005, 2:05 PM + */ + +package net.i2p.aum.q; + +import java.io.PrintStream; +import java.io.PrintWriter; + +/** + * Base class of Q exceptions + * @author jrandom (shamelessly rebadged by aum) + */ + +public class QException extends Exception { + private Throwable _source; + + public QException() { + this(null, null); + } + + public QException(String msg) { + this(msg, null); + } + + public QException(String msg, Throwable source) { + super(msg); + _source = source; + } + + public void printStackTrace() { + if (_source != null) _source.printStackTrace(); + super.printStackTrace(); + } + + public void printStackTrace(PrintStream ps) { + if (_source != null) _source.printStackTrace(ps); + super.printStackTrace(ps); + } + + public void printStackTrace(PrintWriter pw) { + if (_source != null) _source.printStackTrace(pw); + super.printStackTrace(pw); + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QIndexFile.java b/apps/q/java/src/net/i2p/aum/q/QIndexFile.java new file mode 100644 index 000000000..c14e1192c --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QIndexFile.java @@ -0,0 +1,227 @@ +/* + * QIndexFile.java + * + * Created on March 24, 2005, 11:55 AM + */ + +package net.i2p.aum.q; + +import java.*; +import java.lang.*; +import java.io.*; +import java.util.*; + +/** + * Implements a binary-searchable file for storing (time, hash) records. + * This makes it faster for server nodes to determine which content entries, + * catalog entries and peer entries have appeared since time t.
+ * + *To ease inter-operation with other programs, as well as human troubleshooting, + * The file is implemented as a plain text file, with records in the + * following format: + *
+ *
+ *- time unixtime, as 10-byte decimal string
+ *- = single-char delimiter
+ *- hash - a 44-byte Base64 representation of an sha256 hash
+ *Command Line Interface (CLI) for starting/stopping Q nodes, + * and also, executing commands on Q nodes such as inserting, retrieving + * and searching for content.
+ * + *Commands include: + *
+ *
- Start a server or client Node
+ *- Stop a server or client Node
+ *- Get status of a server or client Node
+ *- Export a server node's dest
+ *- Import a foreign dest to a server or client node
+ *- Insert a file to a client node, with metadata
+ *- Retrieve data/metadata from a client node
+ *- Search a client node for content
+ */ +public class QMgr { + + public Runtime runtime; + public XmlRpcClient node; + public String nodePrivKey; + public String nodeDest; + public String nodeDirStr; + public File nodeDir; + public boolean isServer = false; + + public String [] args; + public String cmd; + public int cmdIdx; + public int argc; + public int argi; + + public static String [] commonI2PSystemPropertyKeys = { + "i2cp.tcp.host", + "i2cp.tcp.port", + "eepproxy.tcp.host", + "eepproxy.tcp.port", + "q.xmlrpc.tcp.host", + "q.xmlrpc.tcp.port", + "inbound.length", + "outbound.length", + "inbound.lengthVariance", + "outbound.lengthVariance", + }; + + /** Creates a new instance of QLaunch */ + public QMgr() { + } + + public void notimplemented() { + usage(1, "Command '"+cmd+"' not yet implemented, sorry"); + } + + /** procures an XML-RPC client for node interaction */ + public void getXmlRpcClient() { + + + } + + public int doHelp() { + if (argi == argc) { + // output short help + System.out.println( + "I2P QMgr - Brief command summary:\n" + +"Synopsis:" + +" java net.i2p.aum.q.QMgr [-dir] [server] [ [ ]]\n" + +"Commands:\n" + +" help - print this help summary\n" + +" help verbose - print detailed verbose usage info\n" + +" start - start a node in background\n" + +" foreground - run a node in foreground\n" + +" stop - terminate node\n" + +" status - display node status\n" + +" getref [ ] - output the node's noderef (its base64 dest)\n" + +" addref [ ] - add one or more node refs to node\n" + +" get key [ ] - get key to stdout (or to file)\n" + +" put [ ] [-m ] - insert content\n" + +" search item1=val1 item2=val2... - search for content\n" + ); + } + else if (args[argi].equals("verbose")) { + System.out.println( + "----------------------------\n" + +"Welcome to the I2P Q network\n" + +"----------------------------\n" + +"\n" + +"This program, QMgr, is a command-line interface to the Q network,\n" + +"(an in-I2P distributed file store)\n" + +"and allows you to perform basic operations, including:\n" + +"\n" + +" - create, startup and shutdown Q server and client nodes\n" + +" - determine status of running Q nodes\n" + +" - import and export noderefs to/from these nodes\n" + +" - search for, insert and retrieve content\n" + +"\n" + +"Command syntax:\n" + +" java net.i2p.aum.q.QMgr [-dir ] [-port ] [server] [ [ ]]\n" + +"\n" + +"Explanation of commands and arguments:" + +"\n" + +"* 'server'\n" + +" Specifies that we're operating on a server node (otherwise it's\n" + +" assumed we're operating on a client node)\n" + +"\n" + +"* '-dir= '\n" + +" Server nodes by default reside at ~/.quartermaster_server,\n" + +" and client nodes at ~/.quartermaster_client.\n" + +" Nodes are uniquely identified by the directory at which they\n" + +" reside. Specifying this argument allows you to operate on a\n" + +" server or client node which resides at a different location\n" + +"\n" + +"* '-port= '\n" + +" Applies to client nodes only. Valid only for startup command.\n" + +" Permanently changes the port on which a given client listens\n" + +" for cmmands.\n" + +"\n" + +"* Commands - the basic commands are:\n" + +"\n" + +" help\n" + +" - display a help summary\n" + +"\n" + +" help verbose\n" + +" - display this long-winded help\n" + +"\n" + +" start\n" + +" - start the node. If a nonexistent directory path is given,\n" + +" a whole new unique server or client node will be created\n" + +" at that path\n" + +"\n" + +" foreground\n" + +" - as for start, but run the server in foreground rather\n" + +" than as a background daemon\n" + +"\n" + +" stop\n" + +" - shutdown the node\n" + +"\n" + +" status\n" + +" - print a dump of node status and statistics to stdout\n" + +"\n" + +" newkeys\n" + +" - generate and print out a new keypair for signed-space\n" + +" data item inserts\n" + +"\n" + +" getref [ ]\n" + +" - print the node's noderef (its base64 destination) to\n" + +" stdout. If arg is given, writes the destination\n" + +" to this file instead.\n" + +"\n" + +" addref [ ]\n" + +" - add one or more noderefs to the node. If [ ] argument\n" + +" is given, the refs are read from this file, which is expected\n" + +" to contain one base64 destination per line\n" + +"\n" + +"The following commands are only valid for client nodes:\n" + +"\n" + +" get [ ]\n" + +" - Try to retrieve a content item, (identified by ), from the\n" + +" node. If the item is retrieved, its raw data will be printed\n" + +" to stdout, or to if given. NOTE - REDIRECTING TO STDOUT\n" + +" IS PRESENTLY UNRELIABLE, SO SPECIFY AN EXPLICIT FILENAME FOR NOW\n" + +"\n" + +" put [ ] [-m item=val ...]\n" + +" - Inserts an item of content to the node, and prints its key to\n" + +" stdout. Reads content data from if given, or from standard\n" + +" input if not. Metadata arguments may be given as '-m' followed by\n" + +" a space-separated sequence of 'item=value' specifiers.\n" + +" Typical metadata items include:\n" + +" - type (one of text/html/image/audio/video/archive)\n" + +" - title - a short (<80 char) descriptive title\n" + +" - filename - a recommended filename under which to store this\n" + +" item on retrieve.\n" + +" - abstract - a longer (<256 char) summary of content\n" + +" - keywords - a comma-separated list of keywords\n" + +"\n" + +" search -m item=val [ item=val ...]\n" + +" - searches node for content matching a set of metadata criteria\n" + +" each 'item=val' specifies an 'item' of metadata, to be matched\n" + +" against regular expression 'val'. For example:\n" + +" java net.i2p.aum.q.QMgr search -m title=\"^Madonna\" type=\"music\"\n" + ); + } + else { + System.out.println( + "Unrecognised help qualifier '"+args[argi]+"'\n" + +"type 'java net.i2p.aum.q.QMgr help' for more info" + ); + } + return 0; + } + + public int doStart() { + //notimplemented(); + + String [] startForegroundArgs; + int i; + + // Detect/add any '-D' settings + // search our list of known i2p-relevant sysprops, detect + // if they've been set in System properties, and if so, copy + // them to a customProps table + Hashtable customProps = new Hashtable(); + Properties sysprops = System.getProperties(); + for (i=0; i = argc || !args[argi].equals("-m")) { + usage("Bad put command syntax"); + } + + // now skip over the '-m' + argi++; + + metadata = readMetadataSpec(); + } + + byte [] data = null; + + if (path != null) { + // easy way - suck the file or barf + try { + data = new SimpleFile(path, "r").readBytes(); + } catch (IOException e) { + e.printStackTrace(); + usage("get: Failed to read input file '"+path+"'"); + } + } + else { + // the crap option - suck it from stdin + // read lines from stdin + ByteArrayOutputStream bo = new ByteArrayOutputStream(); + int c; + try { + while (true) { + c = System.in.read(); + if (c < 0) { + break; + } + bo.write(c); + } + } catch (Exception e) { + e.printStackTrace(); + usage("put: error reading from input stream"); + } + + data = bo.toByteArray(); + } + + // ok, got data (and possibly metadata too) + Vector putArgs = new Vector(); + Hashtable res; + putArgs.addElement(metadata); + putArgs.addElement(data); + + System.out.println("data length="+data.length); + + try { + res = (Hashtable)node.execute("i2p.q.putItem", putArgs); + } catch (Exception e) { + e.printStackTrace(System.err); + System.err.println("Failed to put"); + return 1; + } + + // got a res + String status = (String)res.get("status"); + if (!status.equals("ok")) { + String error = (String)res.get("error"); + usage("put: failure - "+error); + } + + // success + String key = (String)res.get("key"); + System.out.print(key); + System.out.flush(); + + return 0; + } + + public int doNewKeys() { + + System.err.println("Generating new signed-space keypair..."); + + String [] keys = QUtil.newKeys(); + System.out.println("Public: "+keys[0]); + System.out.println("Private: "+keys[1]); + + return 0; + } + + public int doSearch() { + + if (argi == argc) { + usage("Missing search metadata"); + } + + // expect -m, or error + if (argi >= argc || !args[argi].equals("-m")) { + usage("Bad search command syntax"); + } + + // now skip over the '-m' + argi++; + + if (argi == argc) { + usage("Missing search metadata"); + } + + Hashtable metadata = readMetadataSpec(); + + // ok, got data (and possibly metadata too) + Vector searchArgs = new Vector(); + Hashtable res; + searchArgs.addElement(metadata); + try { + res = (Hashtable)node.execute("i2p.q.search", searchArgs); + } catch (Exception e) { + e.printStackTrace(System.err); + System.err.println("Failed to search"); + return 1; + } + + // got a res + String status = (String)res.get("status"); + if (!status.equals("ok")) { + String error = (String)res.get("error"); + usage("search: failure - "+error); + } + + // success + Vector items = (Vector)res.get("items"); + + //System.out.println(items); + + for (int i=0; i ] [server] [cmd [args]]\n" + +"Type 'java net.i2p.aum.q.QMgr help' for help summary\n" + +"or 'java net.i2p.aum.q.QMgr help verbose' for long-winded help" + ); + System.exit(retval); + return 0; // stop silly compiler from whingeing + } + + /** + * Startup a Q server or client node, or send a command to a running node + * @param args the command line arguments + */ + public static void main(String[] args) { + QMgr mgr = new QMgr(); + int retval = mgr.execute(args); + System.exit(retval); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QNode.java b/apps/q/java/src/net/i2p/aum/q/QNode.java new file mode 100644 index 000000000..1cf3c7db7 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QNode.java @@ -0,0 +1,1940 @@ +/* + * QNode.java + * + * Created on 20 March 2005, 23:27 + */ + +package net.i2p.aum.q; + +import java.lang.*; +import java.io.*; +import java.util.*; +import java.util.jar.*; +import java.net.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.client.*; +import net.i2p.client.streaming.*; +import net.i2p.data.*; +import net.i2p.crypto.*; + +import net.i2p.aum.*; + +//import gnu.crypto.hash.*; + + +/** + * Base class for Quartermaster nodes. Contains mechanisms for local datastore + * and + * + */ +public abstract class QNode extends Thread +{ + + /** get an i2p context */ + public I2PAppContext i2p; + + // XML-RPC service name base + public static String baseXmlRpcServiceName = "i2p.q"; + + // generator of XML-RPC client objects + public I2PXmlRpcClientFactory peerInterfaceGen; + + // directory requirements + public static String [] coreSubDirs = { "peers", "content", "locations", "catalog", "jobs"}; + public static String [] extraSubDirs = {}; + + // thread pooling + public static int defaultMaxThreads = 3; + protected SimpleSemaphore threadPool; + protected EmbargoedQueue jobQueue; + + // directory paths of this node + + /** base path of our datastore directory */ + public String dataDir; + + /** subdirectory of peers records */ + public String peersDir; + + /** index file of peers */ + public QIndexFile peersIdx; + + /** subdirectory of catalog records */ + public String catalogDir; + + /** subdirectory of catalog location records */ + public String locationDir; + + /** index file of peers */ + public QIndexFile catalogIdx; + + /** subdirectory of content and metadata items */ + public String contentDir; + + /** directory where resources live */ + public String resourcesDir; + + /** index file of peers */ + public QIndexFile contentIdx; + + /** subdirectory of job records */ + public String jobsDir; + + /** index file of jobs */ + public QIndexFile jobsIdx; + + /** private key, as base64 string */ + public String privKeyStr; + + /** public dest, as base64 string */ + public String destStr; + + /** our own node ID - SHA1(dest) */ + public String id; + + /** our own node private key */ + public PrivDestination privKey; + + /** our own destination */ + public Destination dest; + + /** general node config properties */ + public PropertiesFile conf; + + /** path of node's config file */ + public String configPath; + + /** convenience */ + public static String sep = File.separator; + + public I2PXmlRpcServer xmlRpcServer; + + /** map of all known peers */ + public Hashtable peers; + + /** + * override in subclass + */ + public static String defaultStoreDir = ".quartermaster"; + + // status attributes + /** time node got online */ + public Date nodeStartTime; + + // logging file + public RandomAccessFile logFile; + public net.i2p.util.Log log; + + public static int updateCatalogFromPeers = 0; + + public boolean isClient = false; + + public double load_yPrev = 0.0; + public long load_tPrev = 0; + public double load_kRise = 10.0; + public double load_kFall = 800000.0; + + public int load_backoffMin = 180; + public int load_backoffBits = 13; + public double load_backoffBase = 3.0; + + // client only + public String xmlRpcServerHost = ""; + public int xmlRpcServerPort = 7651; + public static int defaultXmlRpcServerPort = 7651; + + /** Number of pending content uploads. You should never shut down a + * node while this value is non-zero. You can get the current value + * of this via a node 'ping' command + */ + public int numPendingUploads = 0; + + /** unixtime in millisecs of last incoming xml-rpc hit to this node, used + * in calculating node load + */ + + public String nodeType = "(base)"; + + // ---------------------------------------------------------- + // CONSTRUCTORS + // ---------------------------------------------------------- + + /** + * Creates a new QNode instance, with store tree located + * at default location + */ + public QNode() throws IOException, DataFormatException, I2PException + { + this(System.getProperties().getProperty("user.home") + sep + defaultStoreDir); + log.info("Constructor finished"); + } + + /** + * Creates a Q node, using specified datastore directory + * @param dataDir absolute pathname where this server's datastore tree is + * located. If tree doesn't exist, it will be created along with new keys + */ + public QNode(String dataDir) throws IOException, DataFormatException, I2PException + { + // establish ourself as a thread + super(); + + setupStoreTree(dataDir); + getConfig(); + peerInterfaceGen = new I2PXmlRpcClientFactory(); + + // determine threads limit + int maxThreads = defaultMaxThreads; + String maxThreadsStr = System.getProperty("qnode.maxthreads"); + if (maxThreadsStr != null) + { + try { + maxThreads = Integer.getInteger(maxThreadsStr).intValue(); + } catch (Exception e) { + e.printStackTrace(); + log.error("Invalid qnode.maxThreads setting '"+maxThreadsStr+"'"); + } + } + + // set up thread pool and job queue + threadPool = new SimpleSemaphore(maxThreads); + jobQueue = new EmbargoedQueue(); + + // load all known peers into peers table + loadPeers(); + + // for benefit of subclasses + //System.out.println("Invoking setup, isClient="+isClient); + setup(); + System.out.println("after setup, isClient="+isClient); + + // queue up the first lot of jobs + scheduleStartupJobs(); + + // now launch our background + //log.info("launching background engine"); + //start(); + + } + + public void loadPeers() throws IOException + { + // populate job queue with jobs for all known servers + // man, doesn't it feel good to eat up memory by the gigabyte!! :P + Iterator peerIds = peersIdx.getItemsSince(0); + QPeer peerRec; + peers = new Hashtable(); + while (peerIds.hasNext()) + { + String peerId = (String)peerIds.next(); + try { + peerRec = getPeerRecord(peerId); + } catch (Exception e) { + log.error("Failed to load peer '"+peerId+"'", e); + continue; + } + peers.put(peerId, peerRec); + } + } + + // -------------------------------------------- + // XML-RPC FRONT-END + // -------------------------------------------- + + /** + * Sets up and launches an xml-rpc server for servicing requests + * to this node.
+ *For server nodes, the xml-rpc server listens within I2P on the + * node's destination.
+ *For client nodes, the xml-rpc server listens on a local TCP + * port (according to attributes xmlRpcServerHost and xmlRpcServerPort)
+ */ + public abstract void startExternalInterfaces(QServerMethods methods) + throws Exception; + + + // -------------------------------------------- + // XML-RPC BACKEND + // -------------------------------------------- + + /** + * Dispatches a XML-RPC call to remote peer + */ + public Hashtable peerExecute(String peerId, String name, Vector args) + throws XmlRpcException, IOException, DataFormatException + { + // get peer record + QPeer peerRec = getPeerRecord(peerId); + + // need peer's dest + String dest64 = peerRec.destStr; + + // need xmlrpc client obj + log.debug("peerExecute: name="+name+", id="+peerId+", dest="+dest64); + + I2PXmlRpcClient client = peerInterfaceGen.newClient(dest64); + + // execute the request + Object result = client.execute(baseXmlRpcServiceName+"."+name, args); + + // ensure it's a hashtable + if (!result.getClass().isAssignableFrom(Hashtable.class)) { + throw new XmlRpcException(0, "Expected Hashtable in peer reply"); + } + + // all ok + return (Hashtable)result; + } + + // -------------------------------------- + // METHODS - initialisation + // -------------------------------------- + + /** perform mode-specific setup - overridden in subclasses */ + public void setup() throws DataFormatException, I2PException + { + } + + /** + * Checks the store directory tree, creating any missing + * directories + */ + public void setupStoreTree(String dataDir) throws IOException + { + this.dataDir = dataDir; + int i; + File rootDir = new File(dataDir); + + // ensure parent exists + if (!rootDir.isDirectory()) { + rootDir.mkdirs(); + } + String logPath = dataDir + sep + "node.log"; + + // set up node-specific logger + Properties envProps = new Properties(); + envProps.setProperty("loggerFilenameOverride", logPath); + + //i2p = new I2PAppContext(envProps); + i2p = I2PAppContext.getGlobalContext(); + + log = i2p.logManager().getLog(this.getClass()); + + //System.out.println("HASHTEST1: "+sha256Base64("hello, one, two three".getBytes())); + //System.out.println("BASE64TEST1: "+base64Enc("hello, one two three")); + //byte [] shit = {39,-20,54,-93,-19,-33,-61,65,-91,-85, + // -19,25,-31,-81,20,-125,26,92,-51,-100,83,43,38,58,77,72,3,40,-78,-62,79,0, + //}; + //System.out.println("BASE64TEST2: "+base64Enc(shit)); + + log.setMinimumPriority(log.DEBUG); + + log.info("creating server at directory "+dataDir); + + /** + if (!logFileObj.isFile()) { + logFileObj.createNewFile(); + } + System.out.println("Created logfile at "+logPath); + logFile = new RandomAccessFile(logFileObj, "rws"); + */ + + // create core subdirectories + for (i = 0; i < coreSubDirs.length; i++) + { + String subdir = dataDir + sep + coreSubDirs[i]; + File d = new File(subdir); + if (!d.isDirectory()) + { + log.info("Creating datastore subdirectory '"+subdir+"'"); + if (!d.mkdirs()) + { + throw new IOException("Failed to create directory "+subdir); + } + } + } + + // create supplementary subdirectories + for (i = 0; i < extraSubDirs.length; i++) + { + String subdir = dataDir + sep + extraSubDirs[i]; + File d = new File(subdir); + if (!d.isDirectory()) + { + log.info("Creating supplementary datastore subdir '"+subdir+"'"); + if (!d.mkdirs()) + { + throw new IOException("Failed to create directory "+subdir); + } + } + } + + // store pathnames of core subdirectories + peersDir = dataDir + sep + "peers"; + peersIdx = new QIndexFile(peersDir + sep + "index.dat"); + + catalogDir = dataDir + sep + "catalog"; + catalogIdx = new QIndexFile(catalogDir + sep + "index.dat"); + locationDir = dataDir + sep + "locations"; + + contentDir = dataDir + sep + "content"; + contentIdx = new QIndexFile(contentDir + sep + "index.dat"); + + jobsDir = dataDir + sep + "jobs"; + jobsIdx = new QIndexFile(jobsDir + sep + "index.dat"); + + // extract resources directory from jarfile (or wherever) + getResources(); + + } + + public void getConfig() throws IOException, DataFormatException, I2PException + { + // create a config object, and stick in any missing defaults + String confPath = dataDir + sep + "node.conf"; + conf = new PropertiesFile(confPath); + + // generate a new dest, if one doesn't already exist + privKeyStr = conf.getProperty("privKey"); + if (privKeyStr == null) + { + // need to generate whole new config + log.info("No private key found, generating new one"); + + ByteArrayOutputStream privBytes = new ByteArrayOutputStream(); + I2PClient client = I2PClientFactory.createClient(); + + // save attributes + dest = client.createDestination(privBytes); + privKey = new PrivDestination(privBytes.toByteArray()); + privKeyStr = privKey.toBase64(); + destStr = dest.toBase64(); + + // save out keys to files + String privKeyPath = dataDir + sep + "nodeKey.priv"; + SimpleFile.write(privKeyPath, privKey.toBase64()); + String destPath = dataDir + sep + "nodeKey.pub"; + SimpleFile.write(destPath, dest.toBase64()); + + // now we can figure out our own node ID + id = destToId(dest); + + // and populate our stored config + conf.setProperty("dest", dest.toBase64()); + conf.setProperty("privKey", privKey.toBase64()); + conf.setProperty("id", id); + conf.setProperty("numPeers", "0"); + conf.setDoubleProperty("loadDampRise", load_kRise); + conf.setDoubleProperty("loadDampFall", load_kFall); + conf.setIntProperty("loadBackoffMin", load_backoffMin); + conf.setIntProperty("loadBackoffBits", load_backoffBits); + + // these items only relevant to client nodes + conf.setIntProperty("xmlRpcServerPort", xmlRpcServerPort); + + log.info("Saved new keys, and nodeID " + id); + } + else + { + // already got a config, load it + //System.out.println("loading config"); + dest = new Destination(); + dest.fromBase64(conf.getProperty("dest")); + destStr = dest.toBase64(); + privKey = PrivDestination.fromBase64String(conf.getProperty("privKey")); + privKeyStr = privKey.toBase64(); + id = conf.getProperty("id"); + load_kRise = conf.getDoubleProperty("loadDampRise", load_kRise); + load_kFall = conf.getDoubleProperty("loadDampFall", load_kFall); + load_backoffMin = conf.getIntProperty("loadBackoffMin", load_backoffMin); + load_backoffBits = conf.getIntProperty("loadBackoffBits", load_backoffBits); + + // these items only relevant to client nodes + xmlRpcServerPort = conf.getIntProperty("xmlRpcServerPort", xmlRpcServerPort); + + //System.out.println("our privkey="+privKeyStr); + if (privKeyStr == null) { + privKeyStr = conf.getProperty("privKey"); + //System.out.println("our privkey="+privKeyStr); + } + } + } + + /** + * Copies resources from jarfile (or wherever) into datastore dir. + * Somwhat of a kludge which determines if the needed resources + * reside within a jarfile or on the host filesystem. + * If the resources live in a jarfile, we extract them and + * copy them into the 'resources' subdirectory of our datastore + * directory. If they live in a directory on the host filesystem, + * we configure the node to access the resources directly from that + * directory instead. + */ + public void getResources() throws IOException { + + String resPath = dataDir + sep + "resources"; + File resDir = new File(resPath); + ClassLoader cl = this.getClass().getClassLoader(); + String jarPath = cl.getResource("qresources").getPath(); + System.out.println("jarPath='"+jarPath+"'"); + if (jarPath.startsWith("jar:")) { + jarPath = jarPath.split("jar:")[1]; + } + + if (jarPath.startsWith("file:")) { + jarPath = jarPath.split("file:")[1]; + } + int bangIdx = jarPath.indexOf("!"); + //System.out.println("jarPath='"+jarPath+"' bangIdx="+bangIdx); + if (bangIdx > 0) { + jarPath = jarPath.substring(0, bangIdx); + } + + if (!jarPath.endsWith(".jar")) { + + // easy - found a directory with our resources + resourcesDir = jarPath; + System.out.println("Found physical resources dir: '"+resourcesDir+"'"); + return; + } + System.out.println("jarPath='"+jarPath+"'"); + + // harder case - create resources dir, copy across resources + if (!resDir.isDirectory()) { + resDir.mkdirs(); + } + resourcesDir = resDir.getPath(); + + JarFile jf = new JarFile(jarPath); + Enumeration jfe = jf.entries(); + Vector entlist = new Vector(); + while (jfe.hasMoreElements()) { + JarEntry ent = (JarEntry)jfe.nextElement(); + String name = ent.getName(); + if (name.startsWith("qresources") && !ent.isDirectory()) { + entlist.addElement(name); + System.out.println("Need to extract resource: "+name); + String absPath = resDir.getPath() + sep + name.split("qresources/")[1]; + File absFile = new File(absPath); + File parent = absFile.getParentFile(); + if (!parent.isDirectory()) { + parent.mkdirs(); + } + // finally, can create and copy the file + FileWriter fw = new FileWriter(absFile); + InputStream is = cl.getResourceAsStream(name); + int c; + while ((c = is.read()) >= 0) { + fw.write(c); + } + fw.close(); + } + } + } + + /** + * given a 'logical resource path', such as 'html/page.html', + * returns an absolute pathname on the host filesystem of + * the needed file + */ + public String getResourcePath(String name) { + return resourcesDir + sep + name; + } + + // -------------------------------------- + // METHODS - scheduling and traffic control + // + // Background processing depends on node type: + // - all nodes: + // - peer list synchronisation + // - client nodes + // - catalog synchronisation + // - content insertion, triggered by local + // insertion + // - server nodes + // - content insertion, triggered by above-threshold + // demand from clients + // + // All background jobs are scheduled on a queue of + // timed jobs (using an EmbargoedQueue), and picked off + // and passed to background threads. + // -------------------------------------- + + // -------------------------------------------- + // HIGH-LEVEL TASK-SPECIFIC JOB SCHEDULING METHODS + // -------------------------------------------- + + public void scheduleStartupJobs() + { + Iterator peerRecs = peers.values().iterator(); + while (peerRecs.hasNext()) { + QPeer peerRec = (QPeer)peerRecs.next(); + + // also, while we're here, schedule a 'getUpdate' update job + schedulePeerUpdateJob(peerRec); + } + + System.out.println("scheduleStartupJobs: cRetrieve an item of content.
+ *On server nodes this only retrieves from the local datastore.
+ *On client nodes, this tries the local datastore first, then + * attempts to get the data from remote servers believed to have the data
+ */ + public Hashtable getItem(String uri) throws IOException, QException + { + log.info("getItem: uri='"+uri+"'"); + return localGetItem(uri); + } + + /** + * retrieves an item of content from remote peer + */ + public Hashtable peerGetItem(String peerId, String uri) + throws XmlRpcException, IOException, DataFormatException + { + Vector v = new Vector(); + v.add(uri); + + return peerExecute(peerId, "getItem", v); + } + + + /** returns true if this node possesses given key, false if not */ + public boolean localHasItem(String uri) { + if (getLocalMetadata(uri) == null) { + return false; + } + else { + return true; + } + } + + /** returns true if this node possesses given key, false if not */ + public boolean localHasCatalogItem(String uri) { + if (getLocalCatalogMetadata(uri) == null) { + return false; + } + else { + return true; + } + } + + /** + * returns the data stored under given key + */ + public Hashtable localGetItem(String uri) throws IOException + { + log.info("localGetItem: uri='"+uri+"'"); + Hashtable h = new Hashtable(); + + QDataItem item = getLocalMetadata(uri); + if (item == null) + { + // Honest, officer, we don't have it, we were just + // holding it for a friend! + System.out.println("localGetItem: no metadata for uri "+uri); + h.put("status", "error"); + h.put("error", "notfound"); + return h; + } + + // locate the content + String dataHash = (String)item.get("dataHash"); + String dataPath = makeDataPath(dataHash); + SimpleFile dataFile = new SimpleFile(dataPath, "r"); + + // barf if content missing + if (!dataFile.isFile()) + { + System.out.println("localGetItem: no data for uri "+uri); + h.put("status", "error"); + h.put("error", "missingdata"); + return h; + } + + // get data, hand it back with metadata + byte [] dataImage = dataFile.readBytes(); + h.put("status", "ok"); + h.put("metadata", item); + h.put("data", dataImage); + System.out.println("localGetItem: successful get: uri "+uri); + System.out.println("localGetItem: data hash="+sha256Base64(dataImage)); + return h; + } + + // --------------------------------------- + // PRIMITIVE - putItem + // --------------------------------------- + + /** + * Insert an item of content, with no metadata + * @param raw data to insert + */ + public Hashtable putItem(byte [] data) throws IOException, QException + { + return putItem(new Hashtable(), data); + } + + /** + * Insert an item of content, with metadata + * overridden in client nodes + * @param metadata Hashtable of item's metadata + * @param data raw data to insert + */ + public Hashtable putItem(Hashtable metadata, byte [] data) throws QException + { + Hashtable resp = new Hashtable(); + QDataItem item; + try { + item = new QDataItem(metadata, data); + item.processAndValidate(false); + localPutItem(item); + } catch (QException e) { + resp.put("status", "error"); + resp.put("error", "qexception"); + resp.put("summary", e.getLocalizedMessage()); + return resp; + } + + // success, it seems + resp.put("status", "ok"); + resp.put("uri", (String)item.get("uri")); + return resp; + } + + /** + * inserts an item of content to remote peer + */ + public Hashtable peerPutItem(String peerId, byte [] data) + throws XmlRpcException, IOException, DataFormatException + { + Vector v = new Vector(); + v.add(data); + + return peerExecute(peerId, "putItem", v); + } + + /** + * inserts an item of content to remote peer + */ + public Hashtable peerPutItem(String peerId, Hashtable metadata, byte [] data) + throws XmlRpcException, IOException, DataFormatException + { + Vector v = new Vector(); + v.add(metadata); + v.add(data); + + return peerExecute(peerId, "putItem", v); + } + + /** + * adds a new item of content to our local store, with given metadata + */ + public void localPutItem(QDataItem item) throws QException + { + /** + // 1) hash the data, add to metadata + String dataHash = sha256Base64(data); + metadata.put("dataHash", dataHash); + System.out.println("localPutItem: dataHash="+dataHash); + + // 2) if metadata has no key 'title', use hash as data + if (!metadata.containsKey("title")) + { + metadata.put("title", dataHash); + } + + // 3) add size field to metadata + metadata.put("size", new Integer(data.length)); + + // 4) get deterministic hash of final metadata + TreeSet t = new TreeSet(metadata.keySet()); + Iterator keys = t.iterator(); + int nkeys = t.size(); + int i; + String metaStr = ""; + for (i = 0; i < nkeys; i++) + { + String metaKey = (String)keys.next(); + metaStr += metaKey + "=" + metadata.get(metaKey) + "\n"; + } + + // store the metadata and data + String metaPath = makeDataPath(metaHash+".meta"); + String dataPath = makeDataPath(dataHash+".data"); + new SimpleFile(dataPath, "rws").write(data); + + PropertiesFile pf = new PropertiesFile(metaPath, metadata); + + // update index + contentIdx.add(metaHash); + + Hashtable h = new Hashtable(); + h.put("status", "ok"); + h.put("key", metaHash); + return h; + + */ + + // work out where to store metadata and data + String metaFilename = item.getStoreFilename(); + String metaPath = makeDataPath(metaFilename); + String dataPath = makeDataPath((String)item.get("dataHash")); + + // store the data, if not already present + if (!(new File(dataPath).isFile())) { + byte [] data = item._data; + try { + new SimpleFile(dataPath, "rws").write(data); + } catch (Exception e) { + throw new QException("Error storing metadata", e); + } + } + + // store metadata and add to index, if not already present + if (!(new File(metaPath).isFile())) { + try { + // store the metadata + PropertiesFile pf = new PropertiesFile(metaPath, item); + } catch (Exception e) { + throw new QException("Error storing data", e); + } + + try { + // enter the metadata hash into our index + contentIdx.add(metaFilename); + } catch (Exception e) { + throw new QException("Error adding metadata to index", e); + } + } + } + + // --------------------------------------- + // PRIMITIVE - newKeys + // --------------------------------------- + + /** + * Generates a new keypair for signed-space insertions + * @return a struct with the keys: + *+ *
+ * When inserting an item using the privateKey, the resulting uri + * will be- status - "ok"
+ *- publicKey - base64-encoded signed space public key
+ *- privateKey - base64-encoded signed space private key
+ *Q:publicKey/path+ */ + public Hashtable newKeys() { + + String [] keys = QUtil.newKeys(); + Hashtable res = new Hashtable(); + res.put("status", "ok"); + res.put("publicKey", keys[0]); + res.put("privateKey", keys[1]); + return res; + } + + // --------------------------------------- + // PRIMITIVE - search + // --------------------------------------- + + /** + * Search datastore and catalog for a given item of content + * @param criteria + */ + public Hashtable search(Hashtable criteria) + { + return localSearch(criteria); + } + + public Hashtable localSearch(Hashtable criteria) + { + Hashtable result = new Hashtable(); + result.put("status", "error"); + result.put("error", "notimplemented"); + return result; + } + + public Hashtable insertQSite(String privKey64, + String siteName, + String rootPath, + Hashtable metadata + ) + throws Exception + { + Hashtable result = new Hashtable(); + result.put("status", "error"); + result.put("error", "notimplemented"); + return result; + } + + /** + * returns true if all values in a given metadata set match their respective + * regexps in criteria. + * @param metadata a Hashtable of metadata to test. Set the 'magic' key 'searchmode' + * to 'or' to make this an or-based test, otherwise defaults to and-based test. + * @param criteria a Hashbable containing zero or more matching criteria + */ + public boolean metadataMatchesCriteria(Hashtable metadata, Hashtable criteria) + { + boolean is_OrMode = false; + + // search mode defaults to AND unless explicitly set to OR + if (criteria.containsKey("searchmode")) { + if (((String)criteria.get("searchmode")).toLowerCase().equals("or")) { + is_OrMode = true; + } + } + + // test all keys and regexp values in criteria against metadata + Enumeration cKeys = criteria.keys(); + while (cKeys.hasMoreElements()) { + + String key = (String)cKeys.nextElement(); + if (key.equals("searchmode")) { + // this is a meta-key - skip + continue; + } + + String cval = (String)criteria.get(key); + String mval = (String)metadata.get(key); + if (mval == null) { + mval = ""; + } + + //System.out.println("metadataMatchesCriteria: key='"+key+"'" + // +" cval='"+cval+"'" + // +" mval='"+mval+"'"); + + // reduced xor-based comparison + if (!(mval.matches(cval) ^ is_OrMode)) { + return is_OrMode; + } + } + + // completed all + return !is_OrMode; + } + + // ---------------------------------------------------------- + // METHODS - datastore + // ---------------------------------------------------------- + + /** + * returns the number of known remote catalog entries + */ + public int remoteCatalogSize() + { + return this.catalogIdx.numRecs; + } + + /** + * returns the number of locally stored items + */ + public int localCatalogSize() + { + return this.contentIdx.numRecs; + } + + /** return a list of nodeIds containing a key, or null if none */ + public Vector getItemLocation(String key) throws IOException { + + String dir1 = key.substring(0, 1); + String dir2 = key.substring(0, 2); + String fullPath = locationDir + sep + dir1 + sep + dir2 + sep + key; + File fullFile = new File(fullPath); + File parent = fullFile.getParentFile(); + if (!parent.isDirectory()) { + parent.mkdirs(); + } + + if (!fullFile.exists()) { + return null; + } + + String p = new SimpleFile(fullPath, "r").read().trim(); + + String [] locs = p.split("\\s+"); + Vector v = new Vector(); + int i, nlocs=locs.length; + if (p.length() > 0) { + for (i=0; i0) { + for (i=0; i determines an absolute pathname for storing an item of a + * given name. Uses multi-level directories in sourceforge style For instance, if name is 'blah', and node's data dir lives + * at /home/qserver/content, then the path will be /home/qserver/content/b/bl/blah.
+ *Note that directories are created as needed
+ * @param name the filename to store + * @return the full pathname to write to + */ + public String makeDataPath(String name) + { + String dir1 = name.substring(0, 1); + String dir2 = name.substring(0, 2); + String fullPath = contentDir + sep + dir1 + sep + dir2 + sep + name; + File fullFile = new File(fullPath); + File parent = fullFile.getParentFile(); + if (!parent.isDirectory()) { + parent.mkdirs(); + } + + // all done, parent dir now exists + return fullPath; + } + + /** + *determines an absolute pathname for cataloging an item of a + * given name. Uses multi-level directories in sourceforge style
+ *For instance, if name is 'blah', and node's data dir lives + * at /home/qserver/content, then the path will be /home/qserver/content/b/bl/blah.
+ *Note that directories are created as needed
+ * @param name the filename to store + * @return the full pathname to write to + */ + public String makeCatalogPath(String name) + { + String dir1 = name.substring(0, 1); + String dir2 = name.substring(0, 2); + String fullPath = catalogDir + sep + dir1 + sep + dir2 + sep + name; + File fullFile = new File(fullPath); + File parent = fullFile.getParentFile(); + if (!parent.isDirectory()) { + parent.mkdirs(); + } + + // all done, parent dir now exists + return fullPath; + } + + + /** + * returns a PropertiesFile object for given peer + * @param peerId + * @return PropertiesFile object representing that peer's data + */ + public QPeer getPeerRecord(String peerId) throws IOException, DataFormatException + { + // return peer's property object + return new QPeer(this, peerId); + } + + /** + * Creates new peer record in our datastore + * @param dest64 String - destination in base64 format + */ + public void newPeer(String dest64) throws IOException, DataFormatException + { + Destination d = new Destination(); + d.fromBase64(dest64); + newPeer(d); + } + + /** + * Fetches/Creates new peer record in our datastore + */ + public void newPeer(Destination peerDest) throws IOException + { + String peerDest64 = peerDest.toBase64(); + + // bail if this new peer is self + if (peerDest64.equals(destStr)) { + return; + } + + // determine peerID + String peerId = destToId(peerDest); + + // bail if peer is already known + if (peers.containsKey(peerId)) { + log.debug("newPeer: already know peer "+peerId+" ("+peerDest64.substring(0, 12)+"...)"); + return; + } + + // where does the peer file live? + String peerPath = peersDir + sep + peerId; + + // get the record + QPeer peerRec = new QPeer(this, peerDest); + + // and write it into index + peersIdx.add(peerId); + + // and stick into our global peers map + peers.put(peerId, peerRec); + + // note that we've got a new peer + conf.incrementIntProperty("numPeers"); + + // and, finally, schedule in a greeting to this peer + if (isClient) { + schedulePeerUpdateJob(peerRec); + } else { + schedulePeerGreetingJob(peerRec); + } + } + + /** + * Get a list of peers, in order of their kademlia-closeness to + * a given uri + */ + public Vector peersClosestTo(String uri, int max) { + + String itemHash = sha256Base64(uri); + + // get our peer list as a vector + Vector allPeers = new Vector(); + Iterator peerRecs = peers.values().iterator(); + while (peerRecs.hasNext()) { + allPeers.addElement(peerRecs.next()); + } + + // create a comparator to find peers closest to URI + QKademliaComparator comp = new QKademliaComparator(this, itemHash); + + // sort the peerlist according to k-closeness of uri + Collections.sort(allPeers, comp); + + // get the closest (up to) n peers + int npeers = Math.min(max, allPeers.size()); + List closestPeers = allPeers.subList(0, npeers); + + return new Vector(closestPeers); + } + + // ---------------------------------------------------------- + // METHODS - node status indicators + // ---------------------------------------------------------- + + /** return uptime of this node, in seconds */ + public int nodeUptime() + { + Date now = new Date(); + return (int)((now.getTime() - nodeStartTime.getTime()) / 1000); + } + + /** return node load, as float */ + public float nodeLoad() + { + long now = new Date().getTime(); + long dt = now - load_tPrev; + load_tPrev = now; + + //System.out.println("nodeLoad: dt="+dt+" load_yPrev="+load_yPrev); + + load_yPrev = load_yPrev * Math.exp(-((double)dt) / load_kFall); + + //System.out.println("nodeLoad: y="+load_yPrev); + + return (float)load_yPrev; + } + + public float nodeLoadAfterHit() + { + //System.out.println("nodeLoadAfterHit: "+load_yPrev+" before recalc"); + // update decay phase + nodeLoad(); + + //System.out.println("nodeLoadAfterHit: "+load_yPrev+" after recalc"); + + // and add spike + load_yPrev += (1.0 - load_yPrev) / load_kRise; + + //System.out.println("nodeLoadAfterHit: "+load_yPrev+" after hit"); + //System.out.println("-----------------------------------------"); + + return (float)load_yPrev; + } + + /** + * Determine an advised time for next contact from a peer node. + * This is based on the node's current load + */ + public int getAdvisedNextContactTime() + { + //long now = new Date().getTime() / 1000; + // fudge 30 secs from now + //return (int)(now + 30); + + // formula here is to advise a backup delay of: + // loadBackoffMin + 2 ** (loadBackoffBits * currentLoad) + return nowSecs() + + load_backoffMin + + (int)(Math.pow(load_backoffBase, load_backoffBits * load_yPrev)); + } + + + // ---------------------------------------------------------- + // METHODS - general + // ---------------------------------------------------------- + + public String base64Enc(String raw) + { + return base64Enc(raw.getBytes()); + } + + public String base64Enc(byte[] raw) + { + return net.i2p.data.Base64.encode(raw); + } + + public String base64Dec(String enc) + { + return new String(net.i2p.data.Base64.decode(enc)); + } + + public String sha256Base64(String raw) + { + return sha256Base64(raw.getBytes()); + } + + public String sha256Base64(byte [] raw) + { + //return base64Enc(sha256(raw)); + return base64Enc(i2p.sha().calculateHash(raw).getData()).replaceAll("[=]+", ""); + } + + /** + * simple interface for sha256 hashing + * @param raw a String to be hashed + * @return the sha256 hash, as binary + */ + public String sha256(String raw) + { + return sha256(raw.getBytes()); + } + + public String sha256(byte [] raw) + { + return new String(i2p.sha().calculateHash(raw).getData()); + + //SHA256Generator shagen = new SHA256Generator(i2p); + //return new String(shagen.calculateHash(raw).getData()); + //Sha256 s = new Sha256(); + //s.update(raw, 0, raw.length); + //byte [] d = s.digest(); + //for (int i=0; i= 0 + * @param width minimum width of string, which will get padded + * with leading zeroes to make up the desired width + */ + public String intFmt(int n, int width) + { + String nS = String.valueOf(n); + while (nS.length() < width) + { + nS = "0" + nS; + } + return nS; + } + + public void log__(String msg) + { + System.out.println("QNode: " + msg); + + // bail if logFile not yet created, can help in avoiding npe + if (logFile == null) { + return; + } + + try { + Calendar now = Calendar.getInstance(); + String timestamp + = intFmt(now.YEAR, 4) + + "-" + + intFmt(now.MONTH, 2) + + "-" + + intFmt(now.DAY_OF_MONTH, 2) + + "-" + + intFmt(now.HOUR_OF_DAY, 2) + + ":" + + intFmt(now.MINUTE, 2) + + ":" + + intFmt(now.SECOND, 2) + + " "; + + synchronized (logFile) { + logFile.seek(logFile.length()); + logFile.write((timestamp + msg + "\n").getBytes()); + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + + public void dj() { + dumpjobs(); + } + + public void dumpjobs() { + + jobQueue.printWaiting(); + } + + public void foo() { + System.out.println("QNode.foo: isClient="+isClient); + } + +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QPeer.java b/apps/q/java/src/net/i2p/aum/q/QPeer.java new file mode 100644 index 000000000..4b03e9a67 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QPeer.java @@ -0,0 +1,105 @@ +/* + * QPeer.java + * + * Created on March 28, 2005, 2:13 PM + */ + +package net.i2p.aum.q; + +import java.io.*; + +import net.i2p.*; +import net.i2p.data.*; +import net.i2p.util.*; +import net.i2p.aum.*; + +/** + * Wrapper for a peer record file. + * Implements a bunch of accessor methods for getting/setting numerical attribs + */ +public class QPeer implements Serializable { + + QNode node; + protected Destination dest; + protected String peerId; + protected String destStr; + + public PropertiesFile file; + + /** Creates a whole new peer */ + public QPeer(QNode node, Destination dest) throws IOException { + + file = new PropertiesFile(node.peersDir + node.sep + node.destToId(dest)); + + this.dest = dest; + destStr = dest.toBase64(); + peerId = node.destToId(dest); + + file.setProperty("id", peerId); + file.setProperty("dest", destStr); + file.setProperty("timeLastUpdate", "0"); + file.setProperty("timeLastContact", "0"); + file.setProperty("timeNextContact", "0"); + } + + /** Loads an existing peer, barfs if nonexistent */ + public QPeer(QNode node, String destId) throws IOException, DataFormatException { + + file = new PropertiesFile(node.peersDir + node.sep + destId); + + // barf if file doesn't exist + if (!file._fileExists) { + throw new IOException("Missing peer record file"); + } + + destStr = file.getProperty("dest"); + dest = new Destination(); + dest.fromBase64(destStr); + peerId = destId; + } + + public Destination getDestination() { + return dest; + } + + public String getDestStr() { + return destStr; + } + + public String getId() { + return peerId; + } + + public int getTimeLastUpdate() { + return new Integer(file.getProperty("timeLastUpdate")).intValue(); + } + + public void setTimeLastUpdate(long when) { + file.setProperty("timeLastUpdate", String.valueOf(when)); + } + + public int getTimeLastContact() { + return new Integer(file.getProperty("timeLastContact")).intValue(); + } + + public void setTimeLastContact(int when) { + file.setProperty("timeLastContact", String.valueOf(when)); + } + + public int getTimeNextContact() { + return new Integer(file.getProperty("timeNextContact")).intValue(); + } + + public void setTimeNextContact(int when) { + file.setProperty("timeNextContact", String.valueOf(when)); + } + + public boolean hasBeenGreeted() { + return file.containsKey("sentHello"); + } + + public void markAsGreeted() { + file.setProperty("sentHello", "1"); + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QServerMethods.java b/apps/q/java/src/net/i2p/aum/q/QServerMethods.java new file mode 100644 index 000000000..84385d452 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QServerMethods.java @@ -0,0 +1,386 @@ +/* + * QServerMethods.java + * + * Created on 20 March 2005, 23:23 + */ + +package net.i2p.aum.q; + +import java.lang.*; +import java.util.*; +import java.io.*; + + +/** + * Defines the methods which will be exposed in the server's + * XML-RPC interface. On the xml-rpc client side, these methods are invoked + * through the 'peerXXXX' methods. + * This class is just a shim, which invokes methods of the same name on + * the QServerNode. It's separated off as a shim because the XML-RPC implementation + * we're using (org.apache.xmlrpc) can only add entire objects and all their + * methods as handlers, and doesn't support adding a-la-carte methods. + */ +public class QServerMethods { + + private QNode node; + + /** + * Creates a new instance of QServerMethods, + * with a ref to the server + */ + public QServerMethods(QNode node) { + this.node = node; + } + + /** + * pings this peer node + */ + public Hashtable ping() { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: ping"); + return node.ping(); + } + + /** + * pings this peer node + * @param args a Hashtable (dict, struct, assoc array) of args, all of which are + * completely ignored + */ + public Hashtable ping(Hashtable args) { + return ping(); + } + + /** + * introduces ourself to this remote peer. From then on, caller will be expected + * to maintain reasonable uptime + * @param destStr our own base64 destination + */ + public Hashtable hello(String destStr) { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: hello"); + return node.hello(destStr); + } + + /** + * introduces ourself to this remote peer. From then on, caller will be expected + * to maintain reasonable uptime + * @param args a Hashtable/dict/struct/assoc-array containing: + * + *
+ */ + public Hashtable hello(Hashtable args) { + String destStr; + System.out.println("XMLRPC: hello"); + try { + destStr = (String)args.get("dest"); + } catch (Exception e) { + destStr = null; + } + if (destStr == null) { + Hashtable res = new Hashtable(); + res.put("status", "error"); + res.put("error", "baddest"); + res.put("summary", "Bad or missing destination"); + node.nodeLoadAfterHit(); + return res; + } + return hello(destStr); + } + + /** + * Searches node for all data items whose metadata keys match the keys + * of the given mapping. + * @param criteria a Hashtable (or python dict, etc) of search criteria. Each + * 'key' is a metadata item to match, and corresponding value is a regular expression + * to match. + */ + public Hashtable search(Hashtable criteria) { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: search"); + System.out.println("XMLRPC: search: "+criteria); + return node.search(criteria); + } + + /** + * returns a list of new content and/or peers which have + * been stored on the server since a given time + * @param since (int) unixtime in seconds + * @param includePeers (int) set to 1 to include 'peers' list in update, 0 to omit + * @param includeCatalog (int) set to 1 to include 'items' (catalog) list in + * update, 0 to omit + */ + public Hashtable getUpdate(int since, int includePeers, int includeCatalog) { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: getUpdate: "+since+" "+includePeers+" "+includeCatalog); + return node.getUpdate(since, includePeers, includeCatalog); + } + + /** + * returns a list of new content and/or peers which have + * been stored on the server since a given time + * Wparam args a Hashtable/struct/dict/assoc-array of arguments, including: + *- dest - base64 destination (noderef) for the remote peer to add
+ *+ *
+ */ + public Hashtable getUpdate(Hashtable args) { + int since; + int includePeers = 0; + int includeCatalog = 0; + + // uplift 'since' key from args, or barf if invalid + try { + since = ((Integer)(args.get("since"))).intValue(); + } catch (Exception e) { + Hashtable res = new Hashtable(); + res.put("status", "error"); + res.put("error", "badargument"); + res.put("summary", "Invalid value for 'since'"); + node.nodeLoadAfterHit(); + return res; + } + + // uplift 'includePeers' key from args, silently fall back + // on default if invalid + if (args.containsKey("includePeers")) { + try { + includePeers = ((Integer)(args.get("includePeers"))).intValue(); + } catch (Exception e) {} + } + + // uplift 'includeCatalog' key from args, silently fall back + // on default if invalid + if (args.containsKey("includeCatalog")) { + try { + includeCatalog = ((Integer)(args.get("includeCatalog"))).intValue(); + } catch (Exception e) {} + } + return getUpdate(since, includePeers, includeCatalog); + } + + public Vector getJobsList() throws Exception { + return node.getJobsList(); + } + + /** + * attempt to retrieve a data item from remote peer + * @param key - the key under which the content item is assumedly stored in Q + */ + public Hashtable getItem(String uri) throws IOException, QException { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: getItem: "+uri); + return node.getItem(uri); + } + + /** + * attempt to retrieve a data item from remote peer + * @param args - a Hashtable/struct/dict/assoc-array, containing: + *- since - (int) unixtime in seconds
+ *- includePeers - (int) set to nonzero to include 'peers' list in update, 0 to omit, + * default 0
+ *- includeCatalog - (int) set to nonzero to include 'items' (catalog) list in + * update, 0 to omit (default 0)
+ *+ *
+ */ + public Hashtable getItem(Hashtable args) throws IOException, QException { + String key; + try { + key = (String)args.get("key"); + } catch (Exception e) { + Hashtable res = new Hashtable(); + res.put("status", "error"); + res.put("error", "badargs"); + node.nodeLoadAfterHit(); + return res; + } + + return getItem(key); + } + + /** + * puts an item of content to remote peer + * @param args - a Hashtable/struct/dict/assoc-array, containing at least: + *- key - (string) the key under which the content item is assumedly stored in Q
+ *+ *
+ * Any other key/value pairs in this struct will be taken as metadata, and + * inserted into the datastore as such. + * @return the assigned key for the item, under which the item + * can be subsequently retrieved. This key will be inserted into + * the metadata + */ + public Hashtable putItem(Hashtable args) + throws IOException, QException + { + byte [] data; + try { + data = (byte [])args.get("data"); + args.remove("data"); + } catch (Exception e) { + Hashtable res = new Hashtable(); + res.put("status", "error"); + res.put("error", "baddata"); + node.nodeLoadAfterHit(); + return res; + } + return putItem(args, data); + } + + /** + * alternative wrapper method which allows data to be a String. + * DO NOT USE if the string contains any control chars or bit-7-set chars + */ + public Hashtable putItem(Hashtable metadata, String data) + throws IOException, QException + { + return putItem(metadata, data.getBytes()); + } + + /** + * alternative wrapper method which allows data to be a String. + * DO NOT USE if the string contains any control chars or bit-7-set chars + */ + public Hashtable putItem(String data) + throws IOException, QException + { + return putItem(data.getBytes()); + } + + + /** + * puts an item of content to remote peer + * Wparam metadata a mapping object containing metadata + * @param data raw data to insert + * @return the assigned key for the item, under which the item + * can be subsequently retrieved. This key will be inserted into + * the metadata + */ + public Hashtable putItem(Hashtable metadata, byte [] data) + throws IOException, QException + { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: putItem: "+metadata); + return node.putItem(metadata, data); + } + + /** + * puts an item of data, without metadata, into the network + * @param data - binary - the raw data to insert + * @return the assigned key for the item + */ + public Hashtable putItem(byte [] data) + throws IOException, QException + { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: putItem (no metadata)"); + return node.putItem(data); + } + + /** + * Schedules the insertion of a qsite. Valid for client nodes only + * @param privKey64 base64 representation of a signed space private key + * @param siteName short text name of the qsite, whose URI will end up + * as 'Q:pubKey64/siteName/'. + * @param rootPath physical absolute pathname of the qsite's root directory + * on the host filesystem. + * Note that this directory must have a file called 'index.html' at its top + * level, which will be used as the qsite's default document. + * @param metadata A set of metadata to associate with the qsite + * @return Hashtable containing results, as the keys: + *- data - binary - the raw data to insert
+ *+ *
+ */ + public Hashtable insertQSite(String privKey64, + String siteName, + String rootPath, + Hashtable metadata + ) + throws Exception + { + node.nodeLoadAfterHit(); + System.out.println("XMLRPC: insertQSite("+privKey64+", "+siteName+", "+rootPath+", "+metadata+")"); + return node.insertQSite(privKey64, siteName, rootPath, metadata); + } + + /** + * Generates a new keypair for signed-space insertions + * @return a struct with the keys: + *- status - String - either "ok" or "error"
+ *- error - String - short summary of error, only present if + * status is "error"
+ *- uri - the full Q URI for the top level of the site + *
+ *
+ * When inserting an item using the privateKey, the resulting uri + * will be- status - "ok"
+ *- publicKey - base64-encoded signed space public key
+ *- privateKey - base64-encoded signed space private key
+ *Q:publicKey/path+ */ + public Hashtable newKeys() { + + return node.newKeys(); + } + + /** + * shuts down the node + * for the purpose of security, the caller must quote the node's full + * base64 private key + * @param nodePrivKey the node's full base64 I2P private key + * @return if shutdown succeeds, an XML-RPC error will result, because + * the node will fail to send a reply. If an invalid key is given, + * the reply Hashtable will contain {"status":"error", "error":"invalidkey"} + */ + public Hashtable shutdown(String nodePrivKey) { + + Hashtable res = new Hashtable(); + + // sekkret h4x - kill the VM if key is the node's I2P base64 privkey + //System.out.println("shutdown: our privkey="+node.privKeyStr); + //System.out.println("shutdown: nodePrivKey="+nodePrivKey); + if (nodePrivKey.equals(node.privKeyStr)) { + // get a runtime + System.out.println("Node at "+node.dataDir+" shutting down"); + Runtime r = Runtime.getRuntime(); + + // and terminate the vm + r.exit(0); + //r.halt(0); + } + else { + res.put("status", "error"); + res.put("error", "invalidkey"); + } + + return res; + } + + /** + * shuts down the node + * for the purpose of security, the caller must quote the node's full + * base64 private key + * @param args - a Hashtable/struct/dict/assoc-array, containing: + *+ *
+ * @return if shutdown succeeds, an XML-RPC error will result, because + * the node will fail to send a reply. If an invalid key is given, + * the reply Hashtable will contain {"status":"error", "error":"invalidkey"} + */ + public Hashtable shutdown(Hashtable args) { + String privKey; + try { + privKey = (String)args.get("privKey"); + } catch (Exception e) { + Hashtable res = new Hashtable(); + res.put("status", "error"); + res.put("error", "badkey"); + node.nodeLoadAfterHit(); + return res; + } + return shutdown(privKey); + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QServerNode.java b/apps/q/java/src/net/i2p/aum/q/QServerNode.java new file mode 100644 index 000000000..2729ba350 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QServerNode.java @@ -0,0 +1,149 @@ +/* + * QServer.java + * + * Created on 20 March 2005, 23:23 + */ + +package net.i2p.aum.q; + +import java.*; +import java.io.*; +import java.util.*; + +import org.apache.xmlrpc.*; + +import net.i2p.*; +import net.i2p.data.*; + +import net.i2p.aum.*; +import net.i2p.aum.http.*; + +/** + * + * Implements Q Server nodes. + */ +public class QServerNode extends QNode { + + /** + * default datastore directory + */ + public static String defaultStoreDir = ".quartermaster_server"; + + /** + * can set this to 0 before instantiating servers, to set tunnel length + * for debugging purposes + **/ + public static int tunLength = 2; + + public I2PXmlRpcServerFactory xmlRpcServerFactory; + + public String nodeType = "Server"; + + /** Creates a new instance of QServer */ + public QServerNode() throws IOException, DataFormatException, I2PException + { + super(System.getProperties().getProperty("user.home") + sep + defaultStoreDir); + } + + /** + * Creates a Q node in server mode, using specified datastore directory + * @param dataDir absolute pathname where this server's datastore tree is + * located. If tree doesn't exist, it will be created along with new keys + */ + public QServerNode(String dataDir) throws IOException, DataFormatException, I2PException + { + super(dataDir); + } + + /** + * performs mode-specific node setup + */ + public void setup() throws DataFormatException, I2PException + { + } + + /** + *- privKey - string - the node's full base64 I2P private key
+ *Sets up and launches an xml-rpc server for servicing requests + * to this node.
+ *For server nodes, the xml-rpc server listens within I2P on the + * node's destination.
+ *For client nodes, the xml-rpc server listens on a local TCP + * port (according to attributes xmlRpcServerHost and xmlRpcServerPort)
+ */ + public void startExternalInterfaces(QServerMethods methods) throws Exception { + /** + * // get a server factory if none already existing + * if (xmlRpcServerFactory == null) { + * getTunnelLength(); + * log.info("Creating an xml-rpc server factory with tunnel length "+tunLength); + * xmlRpcServerFactory = new I2PXmlRpcServerFactory( + * tunLength, tunLength, tunLength, tunLength, i2p); + * } + * + * log.info("Creating XML-RPC server listening within i2p"); + * xmlRpcServer = xmlRpcServerFactory.newServer(privKey); + * + * // bind in our interface class + * log.info("Binding XML-RPC interface object"); + * xmlRpcServer.addHandler(baseXmlRpcServiceName, methods); + * + * // and fire it up + * log.info("Launching XML-RPC server"); + * xmlRpcServer.start(); + **/ + + Properties httpProps = new Properties(); + + httpProps = new Properties(); + Properties sysProps = System.getProperties(); + String i2cpHost = sysProps.getProperty("i2cp.tcp.host", "127.0.0.1"); + String i2cpPort = sysProps.getProperty("i2cp.tcp.port", "7654"); + httpProps.setProperty("i2cp.tcp.host", i2cpHost); + httpProps.setProperty("i2cp.tcp.port", i2cpPort); + + // create in-i2p http server for xmlrpc and browser access + MiniHttpServer webServer = new I2PHttpServer(privKey, QClientWebInterface.class, this, httpProps); + webServer.addXmlRpcHandler(baseXmlRpcServiceName, methods); + webServer.start(); + System.out.println("Started in-i2p http/xmlrpc server listening on dest:"); + String dest = privKey.getDestination().toBase64(); + System.out.println(dest); + + } + + public void getTunnelLength() + { + String tunLenStr = System.getProperty("quartermaster.tunnelLength"); + if (tunLenStr == null) + { + return; + } + + tunLength = new Integer(tunLenStr).intValue(); + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + + QServerNode node; + + try { + if (args.length > 0) { + node = new QServerNode(args[0]); + } + else { + node = new QServerNode(); + } + node.log.info("QServerNode: entering endless loop..."); + while (true) { + Thread.sleep(1000); + } + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QTest.java b/apps/q/java/src/net/i2p/aum/q/QTest.java new file mode 100644 index 000000000..72bf3f577 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QTest.java @@ -0,0 +1,136 @@ +/* + * QTest.java + * + * Created on March 23, 2005, 11:34 PM + */ + +package net.i2p.aum.q; + +import java.*; +import java.lang.*; +import java.io.*; +import java.util.*; + +import net.i2p.*; +import net.i2p.data.*; + +import net.i2p.aum.*; + + +/** + * + * @author david + */ +public class QTest { + + QServerNode server; + + QClientNode client; + + /** Creates a new instance of QTest */ + public QTest() { + } + + /** + * performs a series of tests on client node + */ + public void testClientNode() + throws IOException, DataFormatException, I2PException, QException + { + print("Creating new client node"); + QClientNode node = new QClientNode(); + + print("Starting node background stuff"); + node.start(); + + print("Inserting new plain hash data item"); + byte [] data = "Hello, world".getBytes(); + Hashtable meta = new Hashtable(); + meta.put("title", "simple test"); + meta.put("type", "text"); + meta.put("path", "/test.txt"); + Hashtable res = node.putItem(meta, data); + print("putItem result="+res); + if (!res.get("status").equals("ok")) { + print("putItem fail: error="+res.get("error")); + node.interrupt(); + return; + } + + String uri = (String)res.get("uri"); + print("putItem successful: uri="+uri); + + print("now attempting to retrieve"); + Hashtable res1 = node.getItem(uri); + print("getItem: result="+res1); + if (!res1.get("status").equals("ok")) { + print("getItem fail: error="+res.get("error")); + node.interrupt(); + return; + } + byte [] data1 = (byte [])res1.get("data"); + String dataStr = new String(data1); + print("getItem: success, data="+dataStr); + + print("now searching for what we just inserted"); + Hashtable crit = new Hashtable(); + crit.put("type", "text"); + Hashtable res1a = node.search(crit); + print("After search: res="+res1a); + + print("now creating a keypair"); + Hashtable keys = node.newKeys(); + String pub = (String)keys.get("publicKey"); + String priv = (String)keys.get("privateKey"); + print("public="+pub); + print("private="+priv); + + print("Inserting new secure space data item"); + byte [] data2 = "The quick brown fox".getBytes(); + Hashtable meta2 = new Hashtable(); + meta2.put("title", "simple test 2"); + meta2.put("type", "text"); + meta2.put("path", "/test.txt"); + meta2.put("privateKey", priv); + Hashtable res2 = node.putItem(meta2, data2); + print("putItem result="+res2); + if (!res2.get("status").equals("ok")) { + print("putItem fail: error="+res2.get("error")); + node.interrupt(); + return; + } + + String uri2 = (String)res2.get("uri"); + print("putItem successful: uri="+uri2); + + print("now attempting to retrieve"); + Hashtable res2a = node.getItem(uri2); + print("getItem: result="+res2a); + if (!res2a.get("status").equals("ok")) { + print("getItem fail: error="+res.get("error")); + node.interrupt(); + return; + } + byte [] data2a = (byte [])res2a.get("data"); + String dataStr2a = new String(data2a); + print("getItem: success, data="+dataStr2a); + + } + + public void print(String msg) { + System.out.println(msg); + } + + /** + * @param args the command line arguments + */ + public static void main(String[] args) { + QTest test = new QTest(); + try { + test.testClientNode(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} + diff --git a/apps/q/java/src/net/i2p/aum/q/QUtil.java b/apps/q/java/src/net/i2p/aum/q/QUtil.java new file mode 100644 index 000000000..65ca76d24 --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QUtil.java @@ -0,0 +1,98 @@ +/* + * QUtil.java + * + * Created on April 6, 2005, 2:11 PM + */ + +package net.i2p.aum.q; + +import java.*; + +import net.i2p.*; +import net.i2p.data.*; + +/** + * A general collection of static utility methods + */ +public class QUtil { + + public static boolean debugEnabled = true; + + /** + * Generates a new secure space public/private keypair + * @return an array of 2 strings, first one is SSK Public Key, second one + * is SSK Private Key. + */ + public static String [] newKeys() { + Object [] keypair = I2PAppContext.getGlobalContext().keyGenerator().generateSigningKeypair(); + SigningPublicKey pub = (SigningPublicKey)keypair[0]; + SigningPrivateKey priv = (SigningPrivateKey)keypair[1]; + String [] sskKeypair = new String[2]; + sskKeypair[0] = hashPubKey(pub); + sskKeypair[1] = priv.toBase64(); + return sskKeypair; + } + + /** + * converts a signed space private key (in base64) + * to its base64 ssk public equivalent + * @param priv64 SSK private key string as base64 + * @return public key, base64-encoded + */ + public static String privateToPubHash(String priv) + throws DataFormatException + { + return hashPubKey(new SigningPrivateKey(priv).toPublic()); + } + + public static SigningPublicKey privateToPublic(String priv64) + throws DataFormatException + { + SigningPrivateKey priv = new SigningPrivateKey(priv64); + SigningPublicKey pub = priv.toPublic(); + return pub; + } + + public static String hashPubKey(String pub64) + throws DataFormatException + { + return hashPubKey(new SigningPublicKey(pub64)); + } + + /** + * hashes a public key for use in signed space keypairs + * possibly shorten this + */ + public static String hashPubKey(SigningPublicKey pub) { + String hashed = sha64(pub.toByteArray()); + String abbrev = hashed.substring(0, 24); + return abbrev; + } + + /** + * returns base64 of sha hash of a string + */ + public static String sha64(String raw) { + return sha64(raw.getBytes()); + } + + public static String sha64(byte [] raw) { + //return stripEquals(Base64.encode(sha(raw))); + return Base64.encode(sha(raw)).replaceAll("[=]", ""); + } + + public static byte [] sha(String raw) { + return sha(raw.getBytes()); + } + + public static byte [] sha(byte [] raw) { + return I2PAppContext.getGlobalContext().sha().calculateHash(raw).getData(); + } + + public static void debug(String s) { + if (debugEnabled) { + System.out.println("QSSL:"+s); + } + } + +} diff --git a/apps/q/java/src/net/i2p/aum/q/QWorkerThread.java b/apps/q/java/src/net/i2p/aum/q/QWorkerThread.java new file mode 100644 index 000000000..6340115dd --- /dev/null +++ b/apps/q/java/src/net/i2p/aum/q/QWorkerThread.java @@ -0,0 +1,327 @@ +/* + * QWorkerThread.java + * + * Created on April 17, 2005, 2:44 PM + */ + +package net.i2p.aum.q; + +import java.util.*; +import java.io.*; + +import net.i2p.aum.*; + +/** + * Thread which performs a single background job for a nod + */ + +class QWorkerThread extends Thread { + + QNode node; + Hashtable job; + String jobTime; + String peerId; + String jobDesc; + + /* + * Creates this thread for executing a background job for the node + * @param node the node for which this job is to run + * @param jobTime unixtime-milliseconds at which job is to run, + * represented as string because it denotes a file in the node's jobs dir + */ + public QWorkerThread(QNode node, String jobTime) { + this.node = node; + this.jobTime = jobTime; + } + + public void run() { + try { + node.log.info("worker: executing job: "+jobTime); + + // reconstitute the job from its serialisation in jobs directory + job = node.loadJob(jobTime); + jobDesc = node.loadJobDescription(jobTime); + + // a couple of details + String cmd = (String)job.get("cmd"); + peerId = (String)job.get("peerId"); + + // dispatch off to required handler routine + if (cmd.equals("getUpdate")) { + doGetUpdate(); + } + else if (cmd.equals("hello")) { + doHello(); + } + else if (cmd.equals("localPutItem")) { + doLocalPutItem(); + } + else if (cmd.equals("uploadItem")) { + doUploadItem(); + } + else if (cmd.equals("test")) { + doTest(); + } + else { + node.log.error("workerthread.run: unrecognised command '"+cmd+"'"); + System.out.println("workerthread.run: unrecognised command '"+cmd+"'"); + } + + } catch (Exception e) { + e.printStackTrace(); + node.log.warn("worker thread crashed"); + } + + // finished (or failed), so replenish the jobs pool + node.threadPool.release(); + + // and remove the job record and description + try { + new File(node.jobsDir + node.sep + jobTime).delete(); + new File(node.jobsDir + node.sep + jobTime + ".desc").delete(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void doTest() throws Exception { + + String msg = (String)job.get("msg"); + System.out.println("TESTJOB: msg='"+msg+"'"); + } + + public void doLocalPutItem() throws Exception { + Hashtable metadata = (Hashtable)job.get("metadata"); + String path = (String)job.get("localDataFilePath"); + SimpleFile f = new SimpleFile(path, "r"); + byte [] data = f.readBytes(); + + System.out.println("doLocalPutItem: path='"+path+"' dataLen="+data.length+" metadata="+metadata); + node.putItem(metadata, data); + } + + /** + *Upload a locally-inserted data item to n remote hubs.
+ *This is one intricate algorithm. The aim is to upload the content + * item to the 3 peers which are closest (Kademlia-wise) to the item's URI. + * Some requirements include: + *
+ *
+ *- If we discover new peers over time, we have to consider these peers + * as upload targets
+ *- If upload to an individual peer fails, we have to retry a few times
+ *- If there aren't enough viable peers yet, we need to keep rescheduling this + * job till enough peers come online
+ *- Don't hog a thread slot on the jobs queue, give other jobs a chance to run
+ *