/*
* Copyright (C) 2008, CHENG Yuk-Pong, Daniel <j16sdiz+freenet@gmail.com>
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Git Development Community nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.spearce.jgit.transport;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedHashMap;
import java.util.Map;
import org.spearce.jgit.lib.ProgressMonitor;
import org.spearce.jgit.util.TemporaryBuffer;
/**
* Freenet Client Protocol (FCP) 2.0 Client
* <p>
* See <a href="http://wiki.freenetproject.org/FreenetFCPSpec2Point0">Freenet
* Client Protocol 2.0 Specification</a> for detail on this protocol.
*/
public class FreenetFCP {
/** Default FCP port */
public static final int DEFAULT_FCP_PORT = 9481;
private InetAddress addr;
private int port;
private Socket socket;
private InputStream is;
private OutputStream os;
/**
* Create a new FCP Connection to default host and port (
* <code>localhost:9481</code>)
*
* @throws UnknownHostException
* if no IP address for the <code>localhost</code> could be
* found.
*/
public FreenetFCP() throws UnknownHostException {
this(InetAddress.getAllByName("127.0.0.1")[0], DEFAULT_FCP_PORT);
}
/**
* Create a new FCP Connection to the specified IP address and port
*
* @param address
* the node IP address
* @param port
* the port number
*/
public FreenetFCP(InetAddress address, int port) {
this.addr = address;
this.port = port;
}
/**
* Connect to the node
*
* @throws IOException
* if any I/O error occurred
*/
public void connect() throws IOException {
socket = new Socket(addr, port);
is = new BufferedInputStream(socket.getInputStream());
os = new BufferedOutputStream(socket.getOutputStream());
}
/**
* Hello Message
*
* Send handshake <code>ClientHello</code> message to node. Block until
* <code>NodeHello</code> is received.
*
* @param clientName
* Client name, must be unique in the freenet node
* @throws IOException
* if any I/O error occurred
*/
public void hello(String clientName) throws IOException {
Message msg = new Message();
msg.type = "ClientHello";
msg.field.put("ExpectedVersion", "2.0");
msg.field.put("Name", clientName);
send(msg);
while (true) {
Message reply = read(true);
if ("NodeHello".equals(reply.type))
return;
if ("ProtocolError".equals(reply.type))
throw new IOException("FCP error");
}
}
Message simplePut(String freenetURI, TemporaryBuffer data,
ProgressMonitor monitor, String monitorTask) throws IOException {
Message msg = new Message();
msg.type = "ClientPut";
msg.field.put("URI", freenetURI);
msg.field.put("Identifier", freenetURI);
msg.field.put("Verbosity", monitor == null ? "0" : "1");
msg.field.put("PriorityClass", "1");
msg.field.put("Global", "false");
msg.field.put("EarlyEncode", "true"); // for progress
msg.field.put("UploadFrom", "direct");
msg.field.put("DataLength", "" + data.length());
msg.extraData = data;
send(msg);
int totalBlocks = -1;
int completedBlocks = 0;
if (monitor != null)
monitor.beginTask(monitorTask, ProgressMonitor.UNKNOWN);
LinkedHashMap<String, String> allFields = new LinkedHashMap<String, String>();
try {
while (true) {
Message reply = read(true);
if ("ProtocolError".equals(reply.type))
throw new IOException("Protocol error");
if (!freenetURI.equals(reply.field.get("Identifier")))
continue; // identifier don't match, ignore it
if ("IdentifierCollision".equals(reply.type))
throw new IOException("IdentifierCollision");
if ("SimpleProgress".equals(reply.type) && monitor != null) {
if (totalBlocks == -1) {
totalBlocks = Integer
.parseInt(reply.field.get("Total"));
monitor.beginTask(monitorTask, totalBlocks);
}
int tmp = Integer.parseInt(reply.field.get("Succeeded"));
if (tmp < totalBlocks)
monitor.update(tmp - completedBlocks);
completedBlocks = tmp;
}
if (monitor != null && totalBlocks == -1)
monitor.update(1);
if ("URIGenerated".equals(reply.type))
allFields.putAll(reply.field);
if ("PutFailed".equals(reply.type)
|| "PutSuccessful".equals(reply.type)
|| "PutFetchable".equals(reply.type)) {
allFields.putAll(reply.field);
reply.field = allFields;
return reply;
}
}
} finally {
if (monitor != null)
monitor.endTask();
}
}
static class GetResult {
Map<String, String> field = new LinkedHashMap<String, String>();
String uri;
TemporaryBuffer data;
}
GetResult simpleGet(String freenetURI) throws IOException {
GET_LOOP: for (;;) {
final String rID = "GET-" + freenetURI;
Message msg = new Message();
msg.type = "ClientGet";
msg.field.put("URI", freenetURI);
msg.field.put("Identifier", rID);
msg.field.put("PriorityClass", "1");
msg.field.put("Verbosity", "1");
msg.field.put("MaxSize", Integer.toString(Integer.MAX_VALUE));
msg.field.put("Global", "false");
msg.field.put("ClientToken", rID);
msg.field.put("ReturnType", "direct");
send(msg);
GetResult ret = new GetResult();
ret.uri = freenetURI;
for (;;) {
Message reply = read(true);
if ("ProtocolError".equals(reply.type))
throw new IOException("Protocol error");
if (!rID.equals(reply.field.get("Identifier")))
continue; // identifier don't match, ignore it
if ("IdentifierCollision".equals(reply.type))
throw new IOException("IdentifierCollision");
if ("DataFound".equals(reply.type))
ret.field.putAll(reply.field);
if ("GetFailed".equals(reply.type)
|| "AllData".equals(reply.type)) {
final String rURI = reply.field.get("RedirectURI");
if (rURI != null) {
freenetURI = rURI;
continue GET_LOOP;
}
ret.field.putAll(reply.field);
ret.data = reply.extraData;
return ret;
}
}
}
}
/**
* Generate SSK Key Pair
*
* @return the generated SSK key pair. <code>key[0]</code> is the public
* key, <code>key[1]</code> is the private key.
* @throws IOException
* if any I/O error occurred
*/
String[] generateSSK() throws IOException {
Message msg = new Message();
msg.type = "GenerateSSK";
msg.field.put("Identifier", "GenerateSSK");
send(msg);
while (true) {
Message reply = read(true);
if ("ProtocolError".equals(reply.type))
throw new IOException("Protocol error");
if ("SSKKeypair".equals(reply.type)) {
String[] keys = new String[2];
keys[0] = reply.field.get("RequestURI");
keys[1] = reply.field.get("InsertURI");
return keys;
}
}
}
/**
* Get next FCP message
*
* @param blocking
* blocks when no data is available. NOTE: It may still block
* even if <code>blocking</code> is <code>false</code>
* @return the FCP Message, or
* <code>null<code> if no message available and <code>blocking</code>
* is false.
* @throws IOException
* if I/O error occur
*/
Message read(boolean blocking) throws IOException {
if (!blocking && is.available() == 0)
return null;
Message msg = Message.parse(is);
return msg;
}
void send(Message msg) throws IOException {
msg.writeTo(os);
}
/**
* Close the connection
*
* @throws IOException
* if any I/O error occurred
*/
public void close() throws IOException {
os.close();
is.close();
socket.close();
}
static class Message {
String type;
LinkedHashMap<String, String> field = new LinkedHashMap<String, String>();
TemporaryBuffer extraData;
Message() {
// default constructor
}
static Message parse(InputStream in) throws IOException {
Message ret = new Message();
String line = readLine(in);
ret.type = line;
while (true) {
line = readLine(in);
if (line == null)
throw new IOException("Malformed FCP message");
if ("EndMessage".equals(line))
break;
if ("Data".equals(line)) {
String strLen = ret.field.get("DataLength");
if (strLen == null)
throw new IOException("DataLength not found");
int len;
try {
len = Integer.parseInt(strLen);
} catch (NumberFormatException e) {
throw new IOException("DataLength malformed");
}
ret.extraData = readData(in, len);
break;
}
String[] v = line.split("=", 2);
if (v.length != 2)
throw new IOException("No '=' found in: " + line);
ret.field.put(v[0], v[1]);
}
return ret;
}
void writeTo(OutputStream os) throws IOException {
os.write(type.getBytes("UTF-8"));
os.write('\n');
for (Map.Entry<String, String> e : field.entrySet()) {
String l = e.getKey() + '=' + e.getValue();
os.write(l.getBytes("UTF-8"));
os.write('\n');
}
if (extraData == null)
os.write("EndMessage\n".getBytes("UTF-8"));
else {
os.write("Data\n".getBytes("UTF-8"));
extraData.writeTo(os, null);
}
os.flush();
}
@Override
public String toString() {
return type + ":" + field;
}
static String readLine(InputStream in) throws IOException {
byte[] buf = new byte[256];
int offset = 0;
while (true) {
int b = in.read();
if (b == -1)
return null;
if (b == '\n') {
if (offset == 0)
continue; // skip empty line
break;
}
if (offset == buf.length) {
if (offset >= 4096)
throw new IOException("line too long");
byte[] buf2 = new byte[buf.length * 2];
System.arraycopy(buf, 0, buf2, 0, buf.length);
buf = buf2;
}
buf[offset++] = (byte) b;
}
return new String(buf, 0, offset, "UTF-8");
}
static TemporaryBuffer readData(InputStream in, int len) throws IOException {
TemporaryBuffer buf = new TemporaryBuffer();
byte[] tmp = new byte[8192];
int read = 0;
while (read < len) {
int r = in.read(tmp, 0, Math.min(tmp.length, len - read));
if (r == -1)
throw new IOException("Not enough data");
buf.write(tmp, 0, r);
read += r;
}
buf.close();
return buf;
}
}
}