/* * Copyright (C) 2009, CHENG Yuk-Pong, Daniel <j16sdiz+freenet@gmail.com> * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> * * 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.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import org.spearce.jgit.errors.NotSupportedException; import org.spearce.jgit.errors.PackProtocolException; import org.spearce.jgit.errors.TransportException; import org.spearce.jgit.lib.Constants; import org.spearce.jgit.lib.ObjectId; import org.spearce.jgit.lib.ProgressMonitor; import org.spearce.jgit.lib.Ref; import org.spearce.jgit.lib.Repository; import org.spearce.jgit.lib.Ref.Storage; import org.spearce.jgit.transport.FreenetFCP.GetResult; import org.spearce.jgit.transport.FreenetFCP.Message; import org.spearce.jgit.util.FS; import org.spearce.jgit.util.TemporaryBuffer; /** * Transport over Freenet Client Protocol 2.0 *<p> * URI are in forms of <code>freenet://USK@PUBLIC_KEY/identifier</code> * (read-only) or <code>freenet://<config file name>/identifier</code>. The * identifier must not contain any slash. * <p> * Example configuration file (in <code>~/</code> or <code>./.git/</code>): * * <pre> * publicKey=USK@...............,....,AQACAAE/ * privateKey=USK@..............,....,AQECAAE/ * </pre> * * @see WalkFetchConnection */ class TransportFcp2 extends Transport implements WalkTransport { static boolean canHandle(final URIish uri) { if (!uri.isRemote()) return false; final String s = uri.getScheme(); return "freenet".equals(s); } private final String publicKey; private final String privateKey; private FreenetFCP fcp; TransportFcp2(final Repository local, final URIish uri) throws NotSupportedException { super(local, uri); File propsFile = new File(local.getDirectory(), uri.getHost()); if (!propsFile.isFile()) propsFile = new File(FS.userHome(), uri.getHost()); if (propsFile.isFile()) { Properties prop; try { final FileInputStream in = new FileInputStream(propsFile); prop = new Properties(); prop.load(in); } catch (final IOException e) { throw new NotSupportedException("cannot read " + propsFile, e); } String p = uri.getPath(); if (p.startsWith("/")) p = p.substring(1); publicKey = prop.getProperty("publicKey") + p; privateKey = prop.getProperty("privateKey") + p; } else { publicKey = uri.toString().substring(10); // Remove 'freenet://' prefix privateKey = null; } } @Override public FetchConnection openFetch() throws TransportException { try { fcp = new FreenetFCP(); fcp.connect(); fcp.hello("JGit-" + toString()); final FreenetDB c = new FreenetDB(fcp, publicKey, privateKey); final WalkFetchConnection r = new WalkFetchConnection(this, c); r.available(c.readAdvertisedRefs()); return r; } catch (final IOException e) { throw new TransportException("IO Error" + e, e); } } @Override public PushConnection openPush() throws TransportException { try { fcp = new FreenetFCP(); fcp.connect(); fcp.hello("JGit-" + toString()); final FreenetDB c = new FreenetDB(fcp, publicKey, privateKey); final WalkPushConnection r = new WalkPushConnection(this, c) { @Override public void push(final ProgressMonitor monitor, final Map<String, RemoteRefUpdate> refUpdates) throws TransportException { super.push(monitor, refUpdates); try { c.commit(monitor, "Commiting transection"); } catch (IOException e) { throw new TransportException("FCP Error", e); } } }; r.available(c.readAdvertisedRefs()); return r; } catch (final IOException e) { throw new TransportException("IO Error" + e, e); } } @Override public void close() { try { if (fcp != null) fcp.close(); } catch (final IOException e) { // Fall through. } fcp = null; } static class FreenetDB extends WalkRemoteObjectDatabase { private static final String URI_DELETED = "[DELETED]"; protected static final String NOT_IN_ARCHIVE = "10"; protected static final String DATA_NOT_FOUND = "13"; protected static final String FILELIST = ".JGIT-FREENET-FILELIST"; protected final FreenetFCP conn; /** Public key as specified by user */ protected final String publicKey; /** Public key after resolving the USK@ edition */ protected final String currentKey; protected final String privateKey; protected final SortedMap<String, String> fileList; protected final SortedMap<String, TemporaryBuffer> smallFile; protected final Set<TemporaryBuffer> tmpBuffers; protected String baseArchive; /** * Create a new freesite * * @param conn * freenet fcp connection * @param publicKey * public key * @param privateKey * private key, may be <code>null</code>. * @throws IOException */ public FreenetDB(final FreenetFCP conn, final String publicKey, final String privateKey) throws IOException { this.conn = conn; this.fileList = new TreeMap<String, String>(); this.smallFile = new TreeMap<String, TemporaryBuffer>(); this.tmpBuffers = new HashSet<TemporaryBuffer>(); /*- * Freenet URI Format: * USK@XXXXXXXXXX,XXXXX,XXXX/BLAR/100/HAHA/LALA * <--><--------------------><--> <-> <-------> * (1) (2) (3) (4) (5) * SSK@XXXXXXXXXX,XXXXX,XXXX/BLAR/HAHA/LALA * <--><--------------------><--> <-------> * (1) (2) (3) (5) * CHK@XXXXXXXXXX,XXXXX,XXXX/HAHA/LALA * <--><--------------------><-------> * (1) (2) (5) * * 1 - key type * 2 - key * 3 - docName * 4 - edition number (only for USK@) * 5 - metastring -- MUST NOT be specified */ if (publicKey != null) { if (!publicKey.startsWith("USK@") && !publicKey.startsWith("SSK@")) throw new IllegalArgumentException("Invalid public key: " + publicKey); if (!publicKey.endsWith("/")) throw new IllegalArgumentException( "Invalid p key, missing tailing slash: " + publicKey); if (privateKey != null && !privateKey.endsWith("/")) throw new IllegalArgumentException( "Invalid private key, missing tailing slash: " + privateKey); this.publicKey = publicKey; this.currentKey = getCurrentKey(publicKey); this.privateKey = privateKey; loadFileList(); } else { // new site final String s[] = conn.generateSSK(); this.publicKey = s[0].replace("SSK@", "USK@") + ".git/0"; this.currentKey = s[0] + ".git-0"; this.privateKey = s[1].replace("SSK@", "USK@") + ".git/0"; } } private String getCurrentKey(final String pubkey) throws IOException { if (pubkey.startsWith("SSK@") || pubkey.startsWith("CHK@") || pubkey.startsWith("KSK@")) return pubkey; if (!pubkey.startsWith("USK@")) throw new IOException("Unknown key type: " + pubkey); /*- * USK@XXXXXXXXXX,XXXXX,XXXX/BLARBLAR/100000/.... * <-----------p[0]--------> <-p[1]-> <p[2]> .... */ final String[] p = pubkey.split("\\/"); if (p[2].startsWith("-")) p[2] = p[2].substring(1); final GetResult m = conn.simpleGet(p[0] + "/" + p[1] + "/-" + p[2]); if (m.data != null) tmpBuffers.add(m.data); if (!m.uri.startsWith("USK@")) // ugh? throw new IOException("Redirected to non-USK@: " + m.uri); final String[] q = m.uri.split("\\/"); if (q[2].startsWith("-")) q[2] = q[2].substring(1); return "SSK@" + q[0].substring(4) + "/" + q[1] + "-" + q[2] + "/"; } private void loadFileList() throws IOException { final GetResult m = conn.simpleGet(currentKey + FILELIST); if (m.data == null) { if (NOT_IN_ARCHIVE.equals(m.field.get("Code"))) { // the docName exist, just not the file list baseArchive = currentKey; for (final String p : getPackNames()) { final String pa = "pack/" + p; final String ba = p.substring(0, p.length() - 5); final String pi = "pack/" + ba + ".idx"; fileList.put(pa, currentKey + "objects/" + pa); fileList.put(pi, currentKey + "objects/" + pi); } } else { if (DATA_NOT_FOUND.equals(m.field.get("Code"))) { // ignore this, maybe new site } else { throw new IOException(m.field.get("CodeDescription") + "(" + m.field.get("Code") + "): " + m.uri + " : " + m.field.get("ExtraDescription")); } } return; } tmpBuffers.add(m.data); final BufferedReader br = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(m.data.toByteArray()), "UTF-8")); try { for (;;) { final String line = br.readLine(); if (line == null) break; if (line.startsWith("#")) continue; if (line.startsWith("^") && !line.contains("\0")) { baseArchive = line.substring(1); continue; } final int idx = line.lastIndexOf('\t'); if (idx == -1) continue; final String k = line.substring(0, idx); final String v = line.substring(idx + 1); fileList.put(k, "*".equals(v) ? currentKey + k : v); } } finally { br.close(); } } /** * Commit the changes * * @param monitor * (optional) progress monitor to post write completion to * during the stream's close method. * @param monitorTask * (optional) task name to display during the close method. * @throws IOException */ public synchronized void commit(final ProgressMonitor monitor, final String monitorTask) throws IOException { if (privateKey == null) return; final TemporaryBuffer tmpBuf = new TemporaryBuffer(); long fileListSize; { final StringBuffer w = new StringBuffer(); w.append("# File List\n"); if (baseArchive != null) { w.append('^'); w.append(baseArchive); w.append('\n'); } for (final Map.Entry<String, String> e : fileList.entrySet()) { w.append(e.getKey()); w.append('\t'); w.append(e.getValue()); w.append('\n'); } for (final String f : smallFile.keySet()) { w.append(f); w.append("\t*\n"); } byte[] b = w.toString().getBytes("UTF-8"); fileListSize = b.length; tmpBuf.write(b); } for (final TemporaryBuffer b : smallFile.values()) b.writeTo(tmpBuf, null); final Message msg = new Message(); msg.type = "ClientPutComplexDir"; msg.field.put("Identifier", privateKey); msg.field.put("URI", privateKey); msg.field.put("Verbosity", monitor == null ? "0" : "1"); msg.field.put("PriorityClass", "1"); msg.field.put("EarlyEncode", "true"); // progress msg.field.put("Global", "false"); msg.field.put("ClientToken", privateKey); msg.field.put("Persistence", "connection"); msg.field.put("DefaultName", FILELIST); msg.field.put("Files.0.Name", FILELIST); msg.field.put("Files.0.UploadFrom", "direct"); msg.field.put("Files.0.DataLength", Long.toString(fileListSize)); msg.field.put("Files.0.Metadata.ContentType", "text/plain"); int idx = 1; for (final Map.Entry<String, TemporaryBuffer> e : smallFile .entrySet()) { msg.field.put("Files." + idx + ".Name", e.getKey()); msg.field.put("Files." + idx + ".UploadFrom", "direct"); msg.field.put("Files." + idx + ".DataLength", // Long.toString(e.getValue().length())); idx++; } for (final Map.Entry<String, String> e : fileList.entrySet()) { if (URI_DELETED.equals(e.getValue())) continue; msg.field.put("Files." + idx + ".Name", e.getKey()); msg.field.put("Files." + idx + ".UploadFrom", "redirect"); msg.field.put("Files." + idx + ".TargetURI", e.getValue()); idx++; } for (final TemporaryBuffer tmp2 : smallFile.values()) tmp2.destroy(); smallFile.clear(); tmpBuf.close(); msg.extraData = tmpBuf; conn.send(msg); tmpBuf.destroy(); int totalBlocks = -1; int completedBlocks = 0; if (monitor != null) monitor.beginTask(monitorTask, ProgressMonitor.UNKNOWN); try { while (true) { final Message r = conn.read(true); if ("SimpleProgress".equals(r.type) && monitor != null) { if (totalBlocks == -1) { totalBlocks = Integer .parseInt(r.field.get("Total")); monitor.beginTask(monitorTask, totalBlocks); } final int tmp = Integer.parseInt(r.field .get("Succeeded")); if (tmp < totalBlocks) monitor.update(tmp - completedBlocks); completedBlocks = tmp; } if ("PutFailed".equals(r.type)) throw new IOException("FCP Error: " + r); if ("PutSuccessful".equals(r.type) || "PutFetchable".equals(r.type)) return; } } finally { if (monitor != null) monitor.endTask(); } } @Override Collection<String> getPackNames() throws IOException { final Collection<String> packs = new ArrayList<String>(); try { final BufferedReader br = openReader(INFO_PACKS); try { for (;;) { final String s = br.readLine(); if (s == null || s.length() == 0) break; if (!s.startsWith("P pack-") || !s.endsWith(".pack")) throw new PackProtocolException( "invalid advertisement of " + s); packs.add(s.substring(2)); } return packs; } finally { br.close(); } } catch (final FileNotFoundException err) { return packs; } } @Override URIish getURI() { try { return new URIish("freenet://" + currentKey); } catch (final URISyntaxException e) { return null; } } @Override FileStream open(String path) throws FileNotFoundException, IOException { path = resolvePath(path); // small file final TemporaryBuffer b = smallFile.get(path); if (b != null) return new FileStream(new ByteArrayInputStream(b.toByteArray())); // in file list final String rURI = fileList.get(path); if (URI_DELETED.equals(rURI)) throw new FileNotFoundException("deleted"); if (rURI != null) { final GetResult r = conn.simpleGet(rURI); if (r.data == null) throw new IOException("FCP Error: " + r.field.get("CodeDescription") + "(" + r.field.get("Code") + "): " + r.uri + " : " + r.field.get("ExtraDescription")); tmpBuffers.add(r.data); return new FileStream(r.data.getInputStream()); } if (baseArchive != null) { final GetResult r = conn.simpleGet(baseArchive + path); if (r.data == null) { if (NOT_IN_ARCHIVE.equals(r.field.get("Code"))) throw new FileNotFoundException(); throw new IOException("FCP Error: " + r.field.get("CodeDescription") + "(" + r.field.get("Code") + "): " + r.uri + " : " + r.field.get("ExtraDescription")); } tmpBuffers.add(r.data); fileList.put(path, r.uri); return new FileStream(r.data.getInputStream()); } throw new FileNotFoundException(); } @Override synchronized void deleteFile(final String path) throws IOException { String resolvedPath = resolvePath(path); checkWrite(); smallFile.remove(resolvedPath); fileList.put(resolvedPath, URI_DELETED); } @Override OutputStream writeFile(final String path, final ProgressMonitor monitor, final String monitorTask) throws IOException { checkWrite(); TemporaryBuffer tb = new TemporaryBuffer() { @Override public void close() throws IOException { super.close(); insert(path, this, monitor, monitorTask); } }; tmpBuffers.add(tb); return tb; } private synchronized void insert(final String path, TemporaryBuffer buf, final ProgressMonitor monitor, final String monitorTask) throws IOException { checkWrite(); String resolvedPath = resolvePath(path); fileList.remove(resolvedPath); smallFile.remove(resolvedPath); if (buf.length() < 2048) { smallFile.put(resolvedPath, buf); } else { final Message r = conn.simplePut("CHK@", buf, monitor, monitorTask); if ("PutFailed".equals(r.type)) throw new IOException("FCP PutFailed: " + r.field); fileList.put(resolvedPath, r.field.get("URI")); } } private String resolvePath(String path) { while (path.endsWith("/")) path = path.substring(0, path.length() - 1); while (path.startsWith("/")) path = path.substring(1); String k = "objects/"; while (path.startsWith(ROOT_DIR)) { k = ""; path = path.substring(ROOT_DIR.length()); } return k + path; } private void checkWrite() throws IOException { if (privateKey == null) throw new IOException("No private key defined - read only"); if (!privateKey.startsWith("USK@")) throw new IOException("Private key not USK@ - read only"); } @Override Collection<WalkRemoteObjectDatabase> getAlternates() throws IOException { return null; } @Override WalkRemoteObjectDatabase openAlternate(final String location) throws IOException { throw new IOException("Open alternate '" + location + "' not supported."); } Map<String, Ref> readAdvertisedRefs() throws TransportException { final TreeMap<String, Ref> avail = new TreeMap<String, Ref>(); readInfoRefs(avail); readRef(avail, Constants.HEAD); return avail; } private Map<String, Ref> readInfoRefs(final TreeMap<String, Ref> avail) throws TransportException { try { final BufferedReader br = openReader(INFO_REFS); for (;;) { final String line = br.readLine(); if (line == null) break; final int tab = line.indexOf('\t'); if (tab < 0) throw invalidAdvertisement(line); String name; final ObjectId id; name = line.substring(tab + 1); id = ObjectId.fromString(line.substring(0, tab)); if (name.endsWith("^{}")) { name = name.substring(0, name.length() - 3); final Ref prior = avail.get(name); if (prior == null) throw outOfOrderAdvertisement(name); if (prior.getPeeledObjectId() != null) throw duplicateAdvertisement(name + "^{}"); avail.put(name, new Ref(Ref.Storage.NETWORK, name, prior.getObjectId(), id, true)); } else { final Ref prior = avail.put(name, new Ref( Ref.Storage.NETWORK, name, id)); if (prior != null) throw duplicateAdvertisement(name); } } return avail; } catch (final FileNotFoundException noRef) { return null; } catch (final IOException err) { throw new TransportException(INFO_REFS + ": cannot read available refs", err); } } private Ref readRef(final TreeMap<String, Ref> avail, final String rn) throws TransportException { final String s; String ref = ROOT_DIR + rn; try { final BufferedReader br = openReader(ref); try { s = br.readLine(); } finally { br.close(); } } catch (FileNotFoundException noRef) { return null; } catch (IOException err) { throw new TransportException(getURI(), "read " + ref, err); } if (s == null) throw new TransportException(getURI(), "Empty ref: " + rn); if (s.startsWith("ref: ")) { final String target = s.substring("ref: ".length()); Ref r = avail.get(target); if (r == null) r = readRef(avail, target); if (r == null) return null; r = new Ref(Storage.LOOSE_PACKED, rn, r.getObjectId(), r .getPeeledObjectId(), r.isPeeled()); avail.put(r.getName(), r); return r; } if (ObjectId.isId(s)) { final Ref r = new Ref(Storage.LOOSE_PACKED, rn, ObjectId .fromString(s)); avail.put(r.getName(), r); return r; } throw new TransportException(getURI(), "Bad ref: " + rn + ": " + s); } private PackProtocolException outOfOrderAdvertisement(final String n) { return new PackProtocolException("advertisement of " + n + "^{} came before " + n); } private PackProtocolException duplicateAdvertisement(final String n) { return new PackProtocolException("duplicate advertisements of " + n); } private PackProtocolException invalidAdvertisement(final String n) { return new PackProtocolException("invalid advertisement of " + n); } @Override public String toString() { return "PUB: " + publicKey // + "\nPRI: " + privateKey // + "\nCUR: " + currentKey // + "\n BA: " + baseArchive // + "\n FL: " + fileList; } @Override void close() { for (TemporaryBuffer b : tmpBuffers) b.destroy(); tmpBuffers.clear(); smallFile.clear(); fileList.clear(); } } }