@Override
 public Destination lookup(String hostname) {
   Destination rv = super.lookup(hostname);
   if (rv != null) {
     hostname = hostname.toLowerCase(Locale.US);
     // If it's long, assume it's a key.
     if (hostname.length() < 516 && hostname.endsWith(".i2p") && !hostname.endsWith(".b32.i2p")) {
       File f = new File(_context.getRouterDir(), DEFAULT_HOSTS_FILE);
       if ((f.exists()) && (f.canWrite())) {
         synchronized (this) {
           FileOutputStream fos = null;
           try {
             fos = new FileOutputStream(f, true);
             String line = hostname + '=' + rv.toBase64() + System.getProperty("line.separator");
             fos.write(DataHelper.getASCII(line));
           } catch (IOException ioe) {
             System.err.println("Error appending: " + ioe);
           } finally {
             if (fos != null)
               try {
                 fos.close();
               } catch (IOException cioe) {
               }
           }
         }
       }
     }
   }
   return rv;
 }
Ejemplo n.º 2
0
  private void x_startTorrent() {
    boolean ok = _util.connect();
    if (!ok) fatal("Unable to connect to I2P");
    if (coordinator == null) {
      I2PServerSocket serversocket = _util.getServerSocket();
      if (serversocket == null) fatal("Unable to listen for I2P connections");
      else {
        Destination d = serversocket.getManager().getSession().getMyDestination();
        if (_log.shouldLog(Log.INFO))
          _log.info(
              "Listening on I2P destination "
                  + d.toBase64()
                  + " / "
                  + d.calculateHash().toBase64());
      }
      if (_log.shouldLog(Log.INFO))
        _log.info("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient");
      activity = "Collecting pieces";
      coordinator = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
      coordinator.setUploaded(savedUploaded);
      if (_peerCoordinatorSet != null) {
        // multitorrent
        _peerCoordinatorSet.add(coordinator);
      } else {
        // single torrent
        acceptor = new ConnectionAcceptor(_util, new PeerAcceptor(coordinator));
      }
      // TODO pass saved closest DHT nodes to the tracker? or direct to the coordinator?
      trackerclient = new TrackerClient(_util, meta, additionalTrackerURL, coordinator, this);
    }
    // ensure acceptor is running when in multitorrent
    if (_peerCoordinatorSet != null && acceptor != null) {
      acceptor.startAccepting();
    }

    stopped = false;
    if (coordinator.halted()) {
      coordinator.restart();
      if (_peerCoordinatorSet != null) _peerCoordinatorSet.add(coordinator);
    }
    if (!trackerclient.started()) {
      trackerclient.start();
    } else if (trackerclient.halted()) {
      if (storage != null) {
        try {
          storage.reopen();
        } catch (IOException ioe) {
          try {
            storage.close();
          } catch (IOException ioee) {
            ioee.printStackTrace();
          }
          fatal("Could not reopen storage", ioe);
        }
      }
      trackerclient.start();
    } else {
      if (_log.shouldLog(Log.INFO)) _log.info("NOT starting TrackerClient???");
    }
  }
	public AddressBean getLookup() {
		if (this.detail == null)
			return null;
		if (isDirect()) {
			// go to some trouble to make this work for the published addressbook
			this.filter = this.detail.substring(0, 1);
			this.search = this.detail;
			// we don't want the messages, we just want to populate entries
			super.getLoadBookMessages();
			for (int i = 0; i < this.entries.length; i++) {
				if (this.search.equals(this.entries[i].getName()))
					return this.entries[i];
			}
			return null;
		}
		Properties nsOptions = new Properties();
		Properties outProps = new Properties();
		nsOptions.setProperty("list", getFileName());
		Destination dest = getNamingService().lookup(this.detail, nsOptions, outProps);
		if (dest == null)
			return null;
		AddressBean rv = new AddressBean(this.detail, dest.toBase64());
		rv.setProperties(outProps);
		return rv;
	}
Ejemplo n.º 4
0
 public void readBytes(InputStream rawConfig) throws DataFormatException, IOException {
   _destination = Destination.create(rawConfig);
   _options = DataHelper.readProperties(rawConfig);
   _creationDate = DataHelper.readDate(rawConfig);
   _signature = new Signature();
   _signature.readBytes(rawConfig);
 }
Ejemplo n.º 5
0
  @Override
  public byte[] toByteArray() {
    ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
    DataOutputStream dataStream = new DataOutputStream(arrayOutputStream);

    try {
      writeHeader(dataStream);
      dataStream.writeShort(peers.size());
      for (Destination peer : peers)
        // write the first 384 bytes (the two public keys)
        dataStream.write(peer.toByteArray(), 0, 384);
    } catch (IOException e) {
      log.error("Can't write to ByteArrayOutputStream.", e);
    }

    return arrayOutputStream.toByteArray();
  }
Ejemplo n.º 6
0
  public void notifyStreamIncomingConnection(Destination d) throws IOException {
    if (getStreamSession() == null) {
      _log.error("BUG! Received stream connection, but session is null!");
      throw new NullPointerException("BUG! STREAM session is null!");
    }

    if (!writeString(d.toBase64() + "\n")) {
      throw new IOException("Error notifying connection to SAM client");
    }
  }
Ejemplo n.º 7
0
  /* Parse and execute a NAMING message */
  protected boolean execNamingMessage(String opcode, Properties props) {
    if (opcode.equals("LOOKUP")) {
      if (props == null) {
        _log.debug("No parameters specified in NAMING LOOKUP message");
        return false;
      }

      String name = props.getProperty("NAME");
      if (name == null) {
        _log.debug("Name to resolve not specified in NAMING message");
        return false;
      }

      Destination dest = null;
      if (name.equals("ME")) {
        if (getRawSession() != null) {
          dest = getRawSession().getDestination();
        } else if (getStreamSession() != null) {
          dest = getStreamSession().getDestination();
        } else if (getDatagramSession() != null) {
          dest = getDatagramSession().getDestination();
        } else {
          _log.debug("Lookup for SESSION destination, but session is null");
          return false;
        }
      } else {
        try {
          dest = SAMUtils.getDest(name);
        } catch (DataFormatException e) {
        }
      }

      if (dest == null) {
        return writeString("NAMING REPLY RESULT=KEY_NOT_FOUND NAME=" + name + "\n");
      }

      return writeString(
          "NAMING REPLY RESULT=OK NAME=" + name + " VALUE=" + dest.toBase64() + "\n");
    } else {
      _log.debug("Unrecognized NAMING message opcode: \"" + opcode + "\"");
      return false;
    }
  }
Ejemplo n.º 8
0
  public void notifyStreamIncomingConnection(int id, Destination d) throws IOException {
    if (getStreamSession() == null) {
      _log.error("BUG! Received stream connection, but session is null!");
      return;
    }

    if (!writeString("STREAM CONNECTED DESTINATION=" + d.toBase64() + " ID=" + id + "\n")) {
      throw new IOException("Error notifying connection to SAM client");
    }
  }
Ejemplo n.º 9
0
  /**
   * Create a new connected socket. Blocks until the socket is created, unless the connectDelay
   * option (i2p.streaming.connectDelay) is set and greater than zero. If so this will return
   * immediately, and the client may quickly write initial data to the socket and this data will be
   * bundled in the SYN packet.
   *
   * @param peer Destination to connect to
   * @param options I2P socket options to be used for connecting, may be null
   * @return I2PSocket if successful
   * @throws NoRouteToHostException if the peer is not found or not reachable
   * @throws I2PException if there is some other I2P-related problem
   */
  public I2PSocket connect(Destination peer, I2PSocketOptions options)
      throws I2PException, NoRouteToHostException {
    if (options == null) options = _defaultOptions;
    ConnectionOptions opts = null;
    if (options instanceof ConnectionOptions)
      opts = new ConnectionOptions((ConnectionOptions) options);
    else opts = new ConnectionOptions(options);

    if (_log.shouldLog(Log.INFO))
      _log.info(
          "Connecting to "
              + peer.calculateHash().toBase64().substring(0, 6)
              + " with options: "
              + opts);
    // pick the subsession here
    I2PSession session = _session;
    if (!_subsessions.isEmpty()) {
      updateUserDsaList();
      Hash h = peer.calculateHash();
      if (_dsaOnly.contains(h) || (!_userDsaOnly.isEmpty() && _userDsaOnly.contains(h))) {
        // FIXME just taking the first one for now
        for (I2PSession sess : _subsessions) {
          if (sess.getMyDestination().getSigType() == SigType.DSA_SHA1) {
            session = sess;
            break;
          }
        }
      }
    }
    verifySession(session);
    // the following blocks unless connect delay > 0
    Connection con = _connectionManager.connect(peer, opts, session);
    if (con == null)
      throw new TooManyStreamsException("Too many streams, max " + _defaultOptions.getMaxConns());
    I2PSocketFull socket = new I2PSocketFull(con, _context);
    con.setSocket(socket);
    if (con.getConnectionError() != null) {
      con.disconnect(false);
      throw new NoRouteToHostException(con.getConnectionError());
    }
    return socket;
  }
Ejemplo n.º 10
0
 public void writeBytes(OutputStream out) throws DataFormatException, IOException {
   if ((_destination == null)
       || (_options == null)
       || (_signature == null)
       || (_creationDate == null))
     throw new DataFormatException("Not enough data to create the session config");
   _destination.writeBytes(out);
   DataHelper.writeProperties(out, _options, true); // UTF-8
   DataHelper.writeDate(out, _creationDate);
   _signature.writeBytes(out);
 }
Ejemplo n.º 11
0
 /** Verify with the "olddest" property's public key using the "oldsig" property */
 public boolean hasValidInnerSig() {
   if (props == null || name == null || dest == null) return false;
   boolean rv = false;
   // don't cache result
   if (true) {
     StringWriter buf = new StringWriter(1024);
     String sig = props.getProperty(PROP_OLDSIG);
     String olddest = props.getProperty(PROP_OLDDEST);
     if (sig == null || olddest == null) return false;
     buf.append(name);
     buf.append(KV_SEPARATOR);
     buf.append(dest);
     try {
       writeProps(buf, true, true);
     } catch (IOException ioe) {
       // won't happen
       return false;
     }
     byte[] sdata = Base64.decode(sig);
     if (sdata == null) return false;
     Destination d;
     try {
       d = new Destination(olddest);
     } catch (DataFormatException dfe) {
       return false;
     }
     SigningPublicKey spk = d.getSigningPublicKey();
     SigType type = spk.getType();
     if (type == null) return false;
     Signature s;
     try {
       s = new Signature(type, sdata);
     } catch (IllegalArgumentException iae) {
       return false;
     }
     rv = DSAEngine.getInstance().verifySignature(s, DataHelper.getUTF8(buf.toString()), spk);
   }
   return rv;
 }
Ejemplo n.º 12
0
 /** connect to the given destination */
 I2PSocket connect(PeerID peer) throws IOException {
   I2PSocketManager mgr = _manager;
   if (mgr == null) throw new IOException("No socket manager");
   Destination addr = peer.getAddress();
   if (addr == null) throw new IOException("Null address");
   if (addr.equals(getMyDestination())) throw new IOException("Attempt to connect to myself");
   Hash dest = addr.calculateHash();
   if (_banlist.contains(dest))
     throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are banlisted");
   try {
     // TODO opts.setPort(xxx); connect(addr, opts)
     // DHT moved above 6881 in 0.9.9
     I2PSocket rv = _manager.connect(addr);
     if (rv != null) _banlist.remove(dest);
     return rv;
   } catch (I2PException ie) {
     _banlist.add(dest);
     _context.simpleScheduler().addEvent(new Unbanlist(dest), 10 * 60 * 1000);
     IOException ioe = new IOException("Unable to reach the peer " + peer);
     ioe.initCause(ie);
     throw ioe;
   }
 }
Ejemplo n.º 13
0
  // SAMDatagramReceiver implementation
  public void receiveDatagramBytes(Destination sender, byte data[]) throws IOException {
    if (getDatagramSession() == null) {
      _log.error("BUG! Received datagram bytes, but session is null!");
      throw new NullPointerException("BUG! DATAGRAM session is null!");
    }

    ByteArrayOutputStream msg = new ByteArrayOutputStream();

    String msgText =
        "DATAGRAM RECEIVED DESTINATION=" + sender.toBase64() + " SIZE=" + data.length + "\n";
    msg.write(msgText.getBytes("ISO-8859-1"));

    if (_log.shouldLog(Log.DEBUG)) _log.debug("sending to client: " + msgText);
    msg.write(data);
    msg.flush();
    writeBytes(ByteBuffer.wrap(msg.toByteArray()));
  }
Ejemplo n.º 14
0
  /**
   * Unused? see MuxedImpl override
   *
   * @param keyUsed unused - no end-to-end crypto
   * @param tagsSent unused - no end-to-end crypto
   */
  public boolean sendMessage(
      Destination dest,
      byte[] payload,
      int offset,
      int size,
      SessionKey keyUsed,
      Set tagsSent,
      long expires)
      throws I2PSessionException {
    if (_log.shouldLog(Log.DEBUG)) _log.debug("sending message");
    if (isClosed()) throw new I2PSessionException("Already closed");
    updateActivity();

    // Sadly there is no way to send something completely uncompressed in a backward-compatible way,
    // so we have to still send it in a gzip format, which adds 23 bytes (2.4% for a 960-byte msg)
    // (10 byte header + 5 byte block header + 8 byte trailer)
    // In the future we can add a one-byte magic number != 0x1F to signal an uncompressed msg
    // (Gzip streams start with 0x1F 0x8B 0x08)
    // assuming we don't need the CRC-32 that comes with gzip (do we?)
    // Maybe implement this soon in receiveMessage() below so we are ready
    // in case we ever make an incompatible network change.
    // This would save 22 of the 23 bytes and a little CPU.
    boolean sc = shouldCompress(size);
    if (sc) payload = DataHelper.compress(payload, offset, size);
    else payload = DataHelper.compress(payload, offset, size, DataHelper.NO_COMPRESSION);
    // else throw new IllegalStateException("we need to update sendGuaranteed to support partial
    // send");

    int compressed = payload.length;
    if (_log.shouldLog(Log.INFO)) {
      String d = dest.calculateHash().toBase64().substring(0, 4);
      _log.info(
          "sending message to: "
              + d
              + " compress? "
              + sc
              + " sizeIn="
              + size
              + " sizeOut="
              + compressed);
    }
    _context.statManager().addRateData("i2cp.tx.msgCompressed", compressed, 0);
    _context.statManager().addRateData("i2cp.tx.msgExpanded", size, 0);
    if (_noEffort) return sendNoEffort(dest, payload, expires, 0);
    else return sendBestEffort(dest, payload, keyUsed, tagsSent, expires);
  }
Ejemplo n.º 15
0
 public void receiveDatagramBytes(
     Destination sender, byte[] data, int proto, int fromPort, int toPort) throws IOException {
   if (this.clientAddress == null) {
     this.handler.receiveDatagramBytes(sender, data, proto, fromPort, toPort);
   } else {
     StringBuilder buf = new StringBuilder(600);
     buf.append(sender.toBase64());
     if ((handler.verMajor == 3 && handler.verMinor >= 2) || handler.verMajor > 3) {
       buf.append(" FROM_PORT=").append(fromPort).append(" TO_PORT=").append(toPort);
     }
     buf.append('\n');
     String msg = buf.toString();
     ByteBuffer msgBuf = ByteBuffer.allocate(msg.length() + data.length);
     msgBuf.put(DataHelper.getASCII(msg));
     msgBuf.put(data);
     msgBuf.flip();
     this.server.send(this.clientAddress, msgBuf);
   }
 }
Ejemplo n.º 16
0
  // SAMDatagramReceiver implementation
  public void receiveDatagramBytes(
      Destination sender, byte data[], int proto, int fromPort, int toPort) throws IOException {
    if (getDatagramSession() == null) {
      _log.error("BUG! Received datagram bytes, but session is null!");
      return;
    }

    ByteArrayOutputStream msg = new ByteArrayOutputStream(100 + data.length);

    String msgText = "DATAGRAM RECEIVED DESTINATION=" + sender.toBase64() + " SIZE=" + data.length;
    msg.write(DataHelper.getASCII(msgText));
    if ((verMajor == 3 && verMinor >= 2) || verMajor > 3) {
      msgText = " FROM_PORT=" + fromPort + " TO_PORT=" + toPort;
      msg.write(DataHelper.getASCII(msgText));
    }
    msg.write((byte) '\n');

    if (_log.shouldLog(Log.DEBUG)) _log.debug("sending to client: " + msgText);
    msg.write(data);
    msg.flush();
    writeBytes(ByteBuffer.wrap(msg.toByteArray()));
  }
Ejemplo n.º 17
0
  private byte[] getBytes() {
    if (_destination == null) return null;
    if (_options == null) return null;
    if (_creationDate == null) return null;

    ByteArrayOutputStream out = new ByteArrayOutputStream();
    try {
      // _log.debug("PubKey size for destination: " + _destination.getPublicKey().getData().length);
      // _log.debug("SigningKey size for destination: " +
      // _destination.getSigningPublicKey().getData().length);
      _destination.writeBytes(out);
      DataHelper.writeProperties(out, _options, true); // UTF-8
      DataHelper.writeDate(out, _creationDate);
    } catch (IOException ioe) {
      _log.error("IOError signing", ioe);
      return null;
    } catch (DataFormatException dfe) {
      _log.error("Error writing out the bytes for signing/verification", dfe);
      return null;
    }
    return out.toByteArray();
  }
Ejemplo n.º 18
0
 public static void notifyStreamIncomingConnection(SocketChannel client, Destination d)
     throws IOException {
   if (!writeString(d.toBase64() + "\n", client)) {
     throw new IOException("Error notifying connection to SAM client");
   }
 }
Ejemplo n.º 19
0
 public String lookup(String name) {
   Destination dest = getDestination(name);
   if (dest == null) return null;
   return dest.toBase64();
 }
Ejemplo n.º 20
0
 String getOurIPString() {
   Destination dest = getMyDestination();
   if (dest != null) return dest.toBase64();
   return "unknown";
 }
Ejemplo n.º 21
0
  /**
   * Receive an incoming connection (built from a received SYN) Non-SYN packets with a zero
   * SendStreamID may also be queued here so that they don't get thrown away while the SYN packet
   * before it is queued.
   *
   * @param timeoutMs max amount of time to wait for a connection (if less than 1ms, wait
   *     indefinitely)
   * @return connection received, or null if there was a timeout or the handler was shut down
   */
  public Connection accept(long timeoutMs) {
    if (_log.shouldLog(Log.DEBUG)) _log.debug("Accept(" + timeoutMs + ") called");

    long expiration = timeoutMs + _context.clock().now();
    while (true) {
      if ((timeoutMs > 0) && (expiration < _context.clock().now())) return null;
      if (!_active) {
        // fail all the ones we had queued up
        while (true) {
          Packet packet = _synQueue.poll(); // fails immediately if empty
          if (packet == null || packet.getOptionalDelay() == PoisonPacket.POISON_MAX_DELAY_REQUEST)
            break;
          sendReset(packet);
        }
        return null;
      }

      Packet syn = null;
      while (_active && syn == null) {
        if (_log.shouldLog(Log.DEBUG))
          _log.debug(
              "Accept(" + timeoutMs + "): active=" + _active + " queue: " + _synQueue.size());
        if (timeoutMs <= 0) {
          try {
            syn = _synQueue.take(); // waits forever
          } catch (InterruptedException ie) {
          } // { break;}
        } else {
          long remaining = expiration - _context.clock().now();
          // (dont think this applies anymore for LinkedBlockingQueue)
          // BUGFIX
          // The specified amount of real time has elapsed, more or less.
          // If timeout is zero, however, then real time is not taken into consideration
          // and the thread simply waits until notified.
          if (remaining < 1) break;
          try {
            syn = _synQueue.poll(remaining, TimeUnit.MILLISECONDS); // waits the specified time max
          } catch (InterruptedException ie) {
          }
          break;
        }
      }

      if (syn != null) {
        if (syn.getOptionalDelay() == PoisonPacket.POISON_MAX_DELAY_REQUEST) return null;

        // deal with forged / invalid syn packets in _manager.receiveConnection()

        // Handle both SYN and non-SYN packets in the queue
        if (syn.isFlagSet(Packet.FLAG_SYNCHRONIZE)) {
          // We are single-threaded here, so this is
          // a good place to check for dup SYNs and drop them
          Destination from = syn.getOptionalFrom();
          if (from == null) {
            if (_log.shouldLog(Log.WARN)) _log.warn("Dropping SYN packet with no FROM: " + syn);
            // drop it
            continue;
          }
          Connection oldcon = _manager.getConnectionByOutboundId(syn.getReceiveStreamId());
          if (oldcon != null) {
            // His ID not guaranteed to be unique to us, but probably is...
            // only drop it on a destination match too
            if (from.equals(oldcon.getRemotePeer())) {
              if (_log.shouldLog(Log.WARN)) _log.warn("Dropping dup SYN: " + syn);
              continue;
            }
          }
          Connection con = _manager.receiveConnection(syn);
          if (con != null) return con;
        } else {
          reReceivePacket(syn);
          // ... and keep looping
        }
      }
      // keep looping...
    }
  }
Ejemplo n.º 22
0
	/** Perform actions, returning messages about this. */
	@Override
	public String getMessages()
	{
		if (isDirect())
			return super.getMessages();
		// Loading config and addressbook moved into getLoadBookMessages()
		String message = "";
		
		if( action != null ) {
			Properties nsOptions = new Properties();
			// only blockfile needs this
			nsOptions.setProperty("list", getFileName());
                        if (_context.getBooleanProperty(PROP_PW_ENABLE) ||
			    (serial != null && serial.equals(lastSerial))) {
				boolean changed = false;
				if (action.equals(_("Add")) || action.equals(_("Replace"))) {
					if(hostname != null && destination != null) {
						try {
							// throws IAE with translated message
							String host = AddressBean.toASCII(hostname);
							String displayHost = host.equals(hostname) ? hostname :
							                                             hostname + " (" + host + ')';

							Properties outProperties= new Properties();
							Destination oldDest = getNamingService().lookup(host, nsOptions, outProperties);
							if (oldDest != null && destination.equals(oldDest.toBase64())) {
								message = _("Host name {0} is already in address book, unchanged.", displayHost);
							} else if (oldDest != null && !action.equals(_("Replace"))) {
								message = _("Host name {0} is already in address book with a different destination. Click \"Replace\" to overwrite.", displayHost);
							} else {
								try {
									Destination dest = new Destination(destination);
									if (oldDest != null) {
										nsOptions.putAll(outProperties);
							                        nsOptions.setProperty("m", Long.toString(_context.clock().now()));
									}
						                        nsOptions.setProperty("s", _("Manually added via SusiDNS"));
									boolean success = getNamingService().put(host, dest, nsOptions);
									if (success) {
										changed = true;
										if (oldDest == null)
											message = _("Destination added for {0}.", displayHost);
										else
											message = _("Destination changed for {0}.", displayHost);
										if (!host.endsWith(".i2p"))
											message += "<br>" + _("Warning - host name does not end with \".i2p\"");
										// clear form
										hostname = null;
										destination = null;
									} else {
										message = _("Failed to add Destination for {0} to naming service {1}", displayHost, getNamingService().getName()) + "<br>";
									}
								} catch (DataFormatException dfe) {
									message = _("Invalid Base 64 destination.");
								}
							}
						} catch (IllegalArgumentException iae) {
							message = iae.getMessage();
							if (message == null)
								message = _("Invalid host name \"{0}\".", hostname);
						}
					} else {
						message = _("Please enter a host name and destination");
					}
					// clear search when adding
					search = null;
				} else if (action.equals(_("Delete Selected")) || action.equals(_("Delete Entry"))) {
					String name = null;
					int deleted = 0;
					for (String n : deletionMarks) {
						boolean success = getNamingService().remove(n, nsOptions);
						String uni = AddressBean.toUnicode(n);
						String displayHost = uni.equals(n) ? n :  uni + " (" + n + ')';
						if (!success) {
							message += _("Failed to delete Destination for {0} from naming service {1}", displayHost, getNamingService().getName()) + "<br>";
						} else if (deleted++ == 0) {
							changed = true;
							name = displayHost;
						}
					}
					if( changed ) {
						if (deleted == 1)
							// parameter is a host name
							message += _("Destination {0} deleted.", name);
						else
							// parameter will always be >= 2
							message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted);
					} else {
						message = _("No entries selected to delete.");
					}
					// clear search when deleting
					if (action.equals(_("Delete Entry")))
						search = null;
				}
				if( changed ) {
					message += "<br>" + _("Address book saved.");
				}
			}			
			else {
				message = _("Invalid form submission, probably because you used the \"back\" or \"reload\" button on your browser. Please resubmit.")
                                          + ' ' +
                                          _("If the problem persists, verify that you have cookies enabled in your browser.");
			}
		}
		
		action = null;
		
		if( message.length() > 0 )
			message = "<p class=\"messages\">" + message + "</p>";
		return message;
	}