tor-android/tor-android-binary/src/main/java/net/freehaven/tor/control/TorControlConnection.java

733 lines
30 KiB
Java

// Copyright 2005 Nick Mathewson, Roger Dingledine
// See LICENSE file for copying information
package org.torproject.android.control;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.Socket;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.CancellationException;
/** A connection to a running Tor process as specified in control-spec.txt. */
public class TorControlConnection implements TorControlCommands {
private final LinkedList<Waiter> waiters;
private final BufferedReader input;
private final Writer output;
private ControlParseThread thread; // Locking: this
private volatile EventHandler handler;
private volatile PrintWriter debugOutput;
private volatile IOException parseThreadException;
static class Waiter {
List<ReplyLine> response; // Locking: this
synchronized List<ReplyLine> getResponse() throws InterruptedException {
while (response == null) {
wait();
}
return response;
}
synchronized void setResponse(List<ReplyLine> response) {
this.response = response;
notifyAll();
}
}
static class ReplyLine {
final String status;
final String msg;
final String rest;
ReplyLine(String status, String msg, String rest) {
this.status = status; this.msg = msg; this.rest = rest;
}
}
/** Create a new TorControlConnection to communicate with Tor over
* a given socket. After calling this constructor, it is typical to
* call launchThread and authenticate. */
public TorControlConnection(Socket connection) throws IOException {
this(connection.getInputStream(), connection.getOutputStream());
}
/** Create a new TorControlConnection to communicate with Tor over
* an arbitrary pair of data streams.
*/
public TorControlConnection(InputStream i, OutputStream o) {
this(new InputStreamReader(i), new OutputStreamWriter(o));
}
public TorControlConnection(Reader i, Writer o) {
this.output = o;
if (i instanceof BufferedReader)
this.input = (BufferedReader) i;
else
this.input = new BufferedReader(i);
this.waiters = new LinkedList<Waiter>();
}
protected final void writeEscaped(String s) throws IOException {
StringTokenizer st = new StringTokenizer(s, "\n");
while (st.hasMoreTokens()) {
String line = st.nextToken();
if (line.startsWith("."))
line = "."+line;
if (line.endsWith("\r"))
line += "\n";
else
line += "\r\n";
if (debugOutput != null)
debugOutput.print(">> "+line);
output.write(line);
}
output.write(".\r\n");
if (debugOutput != null)
debugOutput.print(">> .\n");
}
protected static final String quote(String s) {
StringBuffer sb = new StringBuffer("\"");
for (int i = 0; i < s.length(); ++i) {
char c = s.charAt(i);
switch (c)
{
case '\r':
case '\n':
case '\\':
case '\"':
sb.append('\\');
}
sb.append(c);
}
sb.append('\"');
return sb.toString();
}
protected final ArrayList<ReplyLine> readReply() throws IOException {
ArrayList<ReplyLine> reply = new ArrayList<ReplyLine>();
char c;
do {
String line = input.readLine();
if (line == null) {
// if line is null, the end of the stream has been reached, i.e.
// the connection to Tor has been closed!
if (reply.isEmpty()) {
// nothing received so far, can exit cleanly
return reply;
}
// received half of a reply before the connection broke down
throw new TorControlSyntaxError("Connection to Tor " +
" broke down while receiving reply!");
}
if (debugOutput != null)
debugOutput.println("<< "+line);
if (line.length() < 4)
throw new TorControlSyntaxError("Line (\""+line+"\") too short");
String status = line.substring(0,3);
c = line.charAt(3);
String msg = line.substring(4);
String rest = null;
if (c == '+') {
StringBuffer data = new StringBuffer();
while (true) {
line = input.readLine();
if (debugOutput != null)
debugOutput.print("<< "+line);
if (line.equals("."))
break;
else if (line.startsWith("."))
line = line.substring(1);
data.append(line).append('\n');
}
rest = data.toString();
}
reply.add(new ReplyLine(status, msg, rest));
} while (c != ' ');
return reply;
}
protected synchronized List<ReplyLine> sendAndWaitForResponse(String s,
String rest) throws IOException {
if(parseThreadException != null) throw parseThreadException;
checkThread();
Waiter w = new Waiter();
if (debugOutput != null)
debugOutput.print(">> "+s);
synchronized (waiters) {
output.write(s);
if (rest != null)
writeEscaped(rest);
output.flush();
waiters.addLast(w);
}
List<ReplyLine> lst;
try {
lst = w.getResponse();
} catch (InterruptedException ex) {
throw new IOException("Interrupted");
}
for (Iterator<ReplyLine> i = lst.iterator(); i.hasNext(); ) {
ReplyLine c = i.next();
if (! c.status.startsWith("2"))
throw new TorControlError("Error reply: "+c.msg);
}
return lst;
}
/** Helper: decode a CMD_EVENT command and dispatch it to our
* EventHandler (if any). */
protected void handleEvent(ArrayList<ReplyLine> events) {
if (handler == null)
return;
for (Iterator<ReplyLine> i = events.iterator(); i.hasNext(); ) {
ReplyLine line = i.next();
int idx = line.msg.indexOf(' ');
String tp = line.msg.substring(0, idx).toUpperCase();
String rest = line.msg.substring(idx+1);
if (tp.equals("CIRC")) {
List<String> lst = Bytes.splitStr(null, rest);
handler.circuitStatus(lst.get(1),
lst.get(0),
lst.get(1).equals("LAUNCHED")
|| lst.size() < 3 ? ""
: lst.get(2));
} else if (tp.equals("STREAM")) {
List<String> lst = Bytes.splitStr(null, rest);
handler.streamStatus(lst.get(1),
lst.get(0),
lst.get(3));
// XXXX circID.
} else if (tp.equals("ORCONN")) {
List<String> lst = Bytes.splitStr(null, rest);
handler.orConnStatus(lst.get(1), lst.get(0));
} else if (tp.equals("BW")) {
List<String> lst = Bytes.splitStr(null, rest);
handler.bandwidthUsed(Integer.parseInt(lst.get(0)),
Integer.parseInt(lst.get(1)));
} else if (tp.equals("NEWDESC")) {
List<String> lst = Bytes.splitStr(null, rest);
handler.newDescriptors(lst);
} else if (tp.equals("DEBUG") ||
tp.equals("INFO") ||
tp.equals("NOTICE") ||
tp.equals("WARN") ||
tp.equals("ERR")) {
handler.message(tp, rest);
} else {
handler.unrecognized(tp, rest);
}
}
}
/** Sets <b>w</b> as the PrintWriter for debugging output,
* which writes out all messages passed between Tor and the controller.
* Outgoing messages are preceded by "\>\>" and incoming messages are preceded
* by "\<\<"
*/
public void setDebugging(PrintWriter w) {
debugOutput = w;
}
/** Sets <b>s</b> as the PrintStream for debugging output,
* which writes out all messages passed between Tor and the controller.
* Outgoing messages are preceded by "\>\>" and incoming messages are preceded
* by "\<\<"
*/
public void setDebugging(PrintStream s) {
debugOutput = new PrintWriter(s, true);
}
/** Set the EventHandler object that will be notified of any
* events Tor delivers to this connection. To make Tor send us
* events, call setEvents(). */
public void setEventHandler(EventHandler handler) {
this.handler = handler;
}
/**
* Start a thread to react to Tor's responses in the background.
* This is necessary to handle asynchronous events and synchronous
* responses that arrive independantly over the same socket.
*/
public synchronized Thread launchThread(boolean daemon) {
ControlParseThread th = new ControlParseThread();
if (daemon)
th.setDaemon(true);
th.start();
this.thread = th;
return th;
}
protected class ControlParseThread extends Thread {
@Override
public void run() {
try {
react();
} catch (IOException ex) {
parseThreadException = ex;
}
}
}
protected synchronized void checkThread() {
if (thread == null)
launchThread(true);
}
/** helper: implement the main background loop. */
protected void react() throws IOException {
while (true) {
ArrayList<ReplyLine> lst = readReply();
if (lst.isEmpty()) {
// connection has been closed remotely! end the loop!
return;
}
if ((lst.get(0)).status.startsWith("6"))
handleEvent(lst);
else {
synchronized (waiters) {
if (!waiters.isEmpty())
{
Waiter w;
w = waiters.removeFirst();
w.setResponse(lst);
}
}
}
}
}
/** Change the value of the configuration option 'key' to 'val'.
*/
public void setConf(String key, String value) throws IOException {
List<String> lst = new ArrayList<String>();
lst.add(key+" "+value);
setConf(lst);
}
/** Change the values of the configuration options stored in kvMap. */
public void setConf(Map<String, String> kvMap) throws IOException {
List<String> lst = new ArrayList<String>();
for (Iterator<Map.Entry<String,String>> it = kvMap.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String,String> ent = it.next();
lst.add(ent.getKey()+" "+ent.getValue()+"\n");
}
setConf(lst);
}
/** Changes the values of the configuration options stored in
* <b>kvList</b>. Each list element in <b>kvList</b> is expected to be
* String of the format "key value".
*
* Tor behaves as though it had just read each of the key-value pairs
* from its configuration file. Keywords with no corresponding values have
* their configuration values reset to their defaults. setConf is
* all-or-nothing: if there is an error in any of the configuration settings,
* Tor sets none of them.
*
* When a configuration option takes multiple values, or when multiple
* configuration keys form a context-sensitive group (see getConf below), then
* setting any of the options in a setConf command is taken to reset all of
* the others. For example, if two ORBindAddress values are configured, and a
* command arrives containing a single ORBindAddress value, the new
* command's value replaces the two old values.
*
* To remove all settings for a given option entirely (and go back to its
* default value), include a String in <b>kvList</b> containing the key and no value.
*/
public void setConf(Collection<String> kvList) throws IOException {
if (kvList.size() == 0)
return;
StringBuffer b = new StringBuffer("SETCONF");
for (Iterator<String> it = kvList.iterator(); it.hasNext(); ) {
String kv = it.next();
int i = kv.indexOf(' ');
if (i == -1)
b.append(" ").append(kv);
b.append(" ").append(kv.substring(0,i)).append("=")
.append(quote(kv.substring(i+1)));
}
b.append("\r\n");
sendAndWaitForResponse(b.toString(), null);
}
/** Try to reset the values listed in the collection 'keys' to their
* default values.
**/
public void resetConf(Collection<String> keys) throws IOException {
if (keys.size() == 0)
return;
StringBuffer b = new StringBuffer("RESETCONF");
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
String key = it.next();
b.append(" ").append(key);
}
b.append("\r\n");
sendAndWaitForResponse(b.toString(), null);
}
/** Return the value of the configuration option 'key' */
public List<ConfigEntry> getConf(String key) throws IOException {
List<String> lst = new ArrayList<String>();
lst.add(key);
return getConf(lst);
}
/** Requests the values of the configuration variables listed in <b>keys</b>.
* Results are returned as a list of ConfigEntry objects.
*
* If an option appears multiple times in the configuration, all of its
* key-value pairs are returned in order.
*
* Some options are context-sensitive, and depend on other options with
* different keywords. These cannot be fetched directly. Currently there
* is only one such option: clients should use the "HiddenServiceOptions"
* virtual keyword to get all HiddenServiceDir, HiddenServicePort,
* HiddenServiceNodes, and HiddenServiceExcludeNodes option settings.
*/
public List<ConfigEntry> getConf(Collection<String> keys) throws IOException {
StringBuffer sb = new StringBuffer("GETCONF");
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
String key = it.next();
sb.append(" ").append(key);
}
sb.append("\r\n");
List<ReplyLine> lst = sendAndWaitForResponse(sb.toString(), null);
List<ConfigEntry> result = new ArrayList<ConfigEntry>();
for (Iterator<ReplyLine> it = lst.iterator(); it.hasNext(); ) {
String kv = (it.next()).msg;
int idx = kv.indexOf('=');
if (idx >= 0)
result.add(new ConfigEntry(kv.substring(0, idx),
kv.substring(idx+1)));
else
result.add(new ConfigEntry(kv));
}
return result;
}
/** Request that the server inform the client about interesting events.
* Each element of <b>events</b> is one of the following Strings:
* ["CIRC" | "STREAM" | "ORCONN" | "BW" | "DEBUG" |
* "INFO" | "NOTICE" | "WARN" | "ERR" | "NEWDESC" | "ADDRMAP"] .
*
* Any events not listed in the <b>events</b> are turned off; thus, calling
* setEvents with an empty <b>events</b> argument turns off all event reporting.
*/
public void setEvents(List<String> events) throws IOException {
StringBuffer sb = new StringBuffer("SETEVENTS");
for (Iterator<String> it = events.iterator(); it.hasNext(); ) {
sb.append(" ").append(it.next());
}
sb.append("\r\n");
sendAndWaitForResponse(sb.toString(), null);
}
/** Authenticates the controller to the Tor server.
*
* By default, the current Tor implementation trusts all local users, and
* the controller can authenticate itself by calling authenticate(new byte[0]).
*
* If the 'CookieAuthentication' option is true, Tor writes a "magic cookie"
* file named "control_auth_cookie" into its data directory. To authenticate,
* the controller must send the contents of this file in <b>auth</b>.
*
* If the 'HashedControlPassword' option is set, <b>auth</b> must contain the salted
* hash of a secret password. The salted hash is computed according to the
* S2K algorithm in RFC 2440 (OpenPGP), and prefixed with the s2k specifier.
* This is then encoded in hexadecimal, prefixed by the indicator sequence
* "16:".
*
* You can generate the salt of a password by calling
* 'tor --hash-password <password>'
* or by using the provided PasswordDigest class.
* To authenticate under this scheme, the controller sends Tor the original
* secret that was used to generate the password.
*/
public void authenticate(byte[] auth) throws IOException {
String cmd = "AUTHENTICATE " + Bytes.hex(auth) + "\r\n";
sendAndWaitForResponse(cmd, null);
}
/** Instructs the server to write out its configuration options into its torrc.
*/
public void saveConf() throws IOException {
sendAndWaitForResponse("SAVECONF\r\n", null);
}
/** Sends a signal from the controller to the Tor server.
* <b>signal</b> is one of the following Strings:
* <ul>
* <li>"RELOAD" or "HUP" : Reload config items, refetch directory</li>
* <li>"SHUTDOWN" or "INT" : Controlled shutdown: if server is an OP, exit immediately.
* If it's an OR, close listeners and exit after 30 seconds</li>
* <li>"DUMP" or "USR1" : Dump stats: log information about open connections and circuits</li>
* <li>"DEBUG" or "USR2" : Debug: switch all open logs to loglevel debug</li>
* <li>"HALT" or "TERM" : Immediate shutdown: clean up and exit now</li>
* </ul>
*/
public void signal(String signal) throws IOException {
String cmd = "SIGNAL " + signal + "\r\n";
sendAndWaitForResponse(cmd, null);
}
/** Send a signal to the Tor process to shut it down or halt it.
* Does not wait for a response. */
public void shutdownTor(String signal) throws IOException {
String s = "SIGNAL " + signal + "\r\n";
Waiter w = new Waiter();
if (debugOutput != null)
debugOutput.print(">> "+s);
synchronized (waiters) {
output.write(s);
output.flush();
}
}
/** Tells the Tor server that future SOCKS requests for connections to a set of original
* addresses should be replaced with connections to the specified replacement
* addresses. Each element of <b>kvLines</b> is a String of the form
* "old-address new-address". This function returns the new address mapping.
*
* The client may decline to provide a body for the original address, and
* instead send a special null address ("0.0.0.0" for IPv4, "::0" for IPv6, or
* "." for hostname), signifying that the server should choose the original
* address itself, and return that address in the reply. The server
* should ensure that it returns an element of address space that is unlikely
* to be in actual use. If there is already an address mapped to the
* destination address, the server may reuse that mapping.
*
* If the original address is already mapped to a different address, the old
* mapping is removed. If the original address and the destination address
* are the same, the server removes any mapping in place for the original
* address.
*
* Mappings set by the controller last until the Tor process exits:
* they never expire. If the controller wants the mapping to last only
* a certain time, then it must explicitly un-map the address when that
* time has elapsed.
*/
public Map<String,String> mapAddresses(Collection<String> kvLines) throws IOException {
StringBuffer sb = new StringBuffer("MAPADDRESS");
for (Iterator<String> it = kvLines.iterator(); it.hasNext(); ) {
String kv = it.next();
int i = kv.indexOf(' ');
sb.append(" ").append(kv.substring(0,i)).append("=")
.append(quote(kv.substring(i+1)));
}
sb.append("\r\n");
List<ReplyLine> lst = sendAndWaitForResponse(sb.toString(), null);
Map<String,String> result = new HashMap<String,String>();
for (Iterator<ReplyLine> it = lst.iterator(); it.hasNext(); ) {
String kv = (it.next()).msg;
int idx = kv.indexOf('=');
result.put(kv.substring(0, idx),
kv.substring(idx+1));
}
return result;
}
public Map<String,String> mapAddresses(Map<String,String> addresses) throws IOException {
List<String> kvList = new ArrayList<String>();
for (Iterator<Map.Entry<String, String>> it = addresses.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String,String> e = it.next();
kvList.add(e.getKey()+" "+e.getValue());
}
return mapAddresses(kvList);
}
public String mapAddress(String fromAddr, String toAddr) throws IOException {
List<String> lst = new ArrayList<String>();
lst.add(fromAddr+" "+toAddr+"\n");
Map<String,String> m = mapAddresses(lst);
return m.get(fromAddr);
}
/** Queries the Tor server for keyed values that are not stored in the torrc
* configuration file. Returns a map of keys to values.
*
* Recognized keys include:
* <ul>
* <li>"version" : The version of the server's software, including the name
* of the software. (example: "Tor 0.0.9.4")</li>
* <li>"desc/id/<OR identity>" or "desc/name/<OR nickname>" : the latest server
* descriptor for a given OR, NUL-terminated. If no such OR is known, the
* corresponding value is an empty string.</li>
* <li>"network-status" : a space-separated list of all known OR identities.
* This is in the same format as the router-status line in directories;
* see tor-spec.txt for details.</li>
* <li>"addr-mappings/all"</li>
* <li>"addr-mappings/config"</li>
* <li>"addr-mappings/cache"</li>
* <li>"addr-mappings/control" : a space-separated list of address mappings, each
* in the form of "from-address=to-address". The 'config' key
* returns those address mappings set in the configuration; the 'cache'
* key returns the mappings in the client-side DNS cache; the 'control'
* key returns the mappings set via the control interface; the 'all'
* target returns the mappings set through any mechanism.</li>
* <li>"circuit-status" : A series of lines as for a circuit status event. Each line is of the form:
* "CircuitID CircStatus Path"</li>
* <li>"stream-status" : A series of lines as for a stream status event. Each is of the form:
* "StreamID StreamStatus CircID Target"</li>
* <li>"orconn-status" : A series of lines as for an OR connection status event. Each is of the
* form: "ServerID ORStatus"</li>
* </ul>
*/
public Map<String,String> getInfo(Collection<String> keys) throws IOException {
StringBuffer sb = new StringBuffer("GETINFO");
for (Iterator<String> it = keys.iterator(); it.hasNext(); ) {
sb.append(" ").append(it.next());
}
sb.append("\r\n");
List<ReplyLine> lst = sendAndWaitForResponse(sb.toString(), null);
Map<String,String> m = new HashMap<String,String>();
for (Iterator<ReplyLine> it = lst.iterator(); it.hasNext(); ) {
ReplyLine line = it.next();
int idx = line.msg.indexOf('=');
if (idx<0)
break;
String k = line.msg.substring(0,idx);
String v;
if (line.rest != null) {
v = line.rest;
} else {
v = line.msg.substring(idx+1);
}
m.put(k, v);
}
return m;
}
/** Return the value of the information field 'key' */
public String getInfo(String key) throws IOException {
List<String> lst = new ArrayList<String>();
lst.add(key);
Map<String,String> m = getInfo(lst);
return m.get(key);
}
/** An extendCircuit request takes one of two forms: either the <b>circID</b> is zero, in
* which case it is a request for the server to build a new circuit according
* to the specified path, or the <b>circID</b> is nonzero, in which case it is a
* request for the server to extend an existing circuit with that ID according
* to the specified <b>path</b>.
*
* If successful, returns the Circuit ID of the (maybe newly created) circuit.
*/
public String extendCircuit(String circID, String path) throws IOException {
List<ReplyLine> lst = sendAndWaitForResponse(
"EXTENDCIRCUIT "+circID+" "+path+"\r\n", null);
return (lst.get(0)).msg;
}
/** Informs the Tor server that the stream specified by <b>streamID</b> should be
* associated with the circuit specified by <b>circID</b>.
*
* Each stream may be associated with
* at most one circuit, and multiple streams may share the same circuit.
* Streams can only be attached to completed circuits (that is, circuits that
* have sent a circuit status "BUILT" event or are listed as built in a
* getInfo circuit-status request).
*
* If <b>circID</b> is 0, responsibility for attaching the given stream is
* returned to Tor.
*
* By default, Tor automatically attaches streams to
* circuits itself, unless the configuration variable
* "__LeaveStreamsUnattached" is set to "1". Attempting to attach streams
* via TC when "__LeaveStreamsUnattached" is false may cause a race between
* Tor and the controller, as both attempt to attach streams to circuits.
*/
public void attachStream(String streamID, String circID)
throws IOException {
sendAndWaitForResponse("ATTACHSTREAM "+streamID+" "+circID+"\r\n", null);
}
/** Tells Tor about the server descriptor in <b>desc</b>.
*
* The descriptor, when parsed, must contain a number of well-specified
* fields, including fields for its nickname and identity.
*/
// More documentation here on format of desc?
// No need for return value? control-spec.txt says reply is merely "250 OK" on success...
public String postDescriptor(String desc) throws IOException {
List<ReplyLine> lst = sendAndWaitForResponse("+POSTDESCRIPTOR\r\n", desc);
return (lst.get(0)).msg;
}
/** Tells Tor to change the exit address of the stream identified by <b>streamID</b>
* to <b>address</b>. No remapping is performed on the new provided address.
*
* To be sure that the modified address will be used, this event must be sent
* after a new stream event is received, and before attaching this stream to
* a circuit.
*/
public void redirectStream(String streamID, String address) throws IOException {
sendAndWaitForResponse("REDIRECTSTREAM "+streamID+" "+address+"\r\n",
null);
}
/** Tells Tor to close the stream identified by <b>streamID</b>.
* <b>reason</b> should be one of the Tor RELAY_END reasons given in tor-spec.txt, as a decimal:
* <ul>
* <li>1 -- REASON_MISC (catch-all for unlisted reasons)</li>
* <li>2 -- REASON_RESOLVEFAILED (couldn't look up hostname)</li>
* <li>3 -- REASON_CONNECTREFUSED (remote host refused connection)</li>
* <li>4 -- REASON_EXITPOLICY (OR refuses to connect to host or port)</li>
* <li>5 -- REASON_DESTROY (Circuit is being destroyed)</li>
* <li>6 -- REASON_DONE (Anonymized TCP connection was closed)</li>
* <li>7 -- REASON_TIMEOUT (Connection timed out, or OR timed out while connecting)</li>
* <li>8 -- (unallocated)</li>
* <li>9 -- REASON_HIBERNATING (OR is temporarily hibernating)</li>
* <li>10 -- REASON_INTERNAL (Internal error at the OR)</li>
* <li>11 -- REASON_RESOURCELIMIT (OR has no resources to fulfill request)</li>
* <li>12 -- REASON_CONNRESET (Connection was unexpectedly reset)</li>
* <li>13 -- REASON_TORPROTOCOL (Sent when closing connection because of Tor protocol violations)</li>
* </ul>
*
* Tor may hold the stream open for a while to flush any data that is pending.
*/
public void closeStream(String streamID, byte reason)
throws IOException {
sendAndWaitForResponse("CLOSESTREAM "+streamID+" "+reason+"\r\n",null);
}
/** Tells Tor to close the circuit identified by <b>circID</b>.
* If <b>ifUnused</b> is true, do not close the circuit unless it is unused.
*/
public void closeCircuit(String circID, boolean ifUnused) throws IOException {
sendAndWaitForResponse("CLOSECIRCUIT "+circID+
(ifUnused?" IFUNUSED":"")+"\r\n", null);
}
}