Package org.telehash

Source Code of org.telehash.SwitchHandler

package org.telehash;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;

import org.apache.commons.lang.StringUtils;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.buffer.SimpleBufferAllocator;
import org.apache.mina.core.future.IoFuture;
import org.apache.mina.core.future.IoFutureListener;
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.transport.socket.nio.NioDatagramAcceptor;
import org.eclipse.emf.json.JsonMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.telehash.SwitchState.ConnectionStatus;
import org.telehash.model.Line;
import org.telehash.model.TapRule;
import org.telehash.model.TelehashFactory;
import org.telehash.model.TelehashPackage;
import org.telehash.model.Telex;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

public class SwitchHandler extends IoHandlerAdapter {

  static private Logger logger = LoggerFactory.getLogger(SwitchHandler.class);

  static private TelehashFactory tf = TelehashFactory.eINSTANCE;
 
  private final NioDatagramAcceptor acceptor;
  private final SimpleBufferAllocator allocator;
  private final SwitchState state = new SwitchState();

  private List<TelexHandler> telexHandlers = Lists.newArrayList();

  private SwitchScanner scannerThread;

  private List<TapRule> tapRules = Lists.newArrayList();

  public SwitchHandler(NioDatagramAcceptor acceptor) {
    this.acceptor = acceptor;
    allocator = new SimpleBufferAllocator();
   
    addTelexHandler(new SeeHandler());
    addTelexHandler(new TapHandler());
    addTelexHandler(new EndSignalHandler());
  }
 
  @Override
  public void sessionCreated(IoSession session) throws Exception {
    super.sessionCreated(session);
    Line line = state.getOrCreateLine((InetSocketAddress)session.getRemoteAddress());
    session.setAttribute("line", line);
    logger.debug("Session[{}] created", session.getId());
  }

  @Override
  public void sessionClosed(IoSession session) throws Exception {
    super.sessionClosed(session);
    logger.debug("Session[{}] closed", session.getId());
  }

  public Line getLine(Hash endHash) {
    return state.getLines().get(endHash);
  }
 
  public Collection<InetSocketAddress> getLineAddresses() {
    return Collections2.transform(state.getLines().values(),
        new Function<Line, InetSocketAddress>() {
          @Override
          public InetSocketAddress apply(Line from) {
            return from.getAddress();
          }
        });
  }
 
  public InetSocketAddress getAddress() {
    return state.getSelfAddress();
  }

  public Hash getAddressHash() {
    return state.getSelfHash();
  }

  public void addTelexHandler(TelexHandler handler) {
    telexHandlers.add(handler);
  }
 
  public void removeTelexHandler(TelexHandler handler) {
    telexHandlers.remove(handler);
  }
 
  public void seed(final InetSocketAddress seedAddress) {
    state.seeding(new Runnable(){
      @Override
      public void run() {
        state.setSeedAddress(seedAddress);
        Line seedLine = state.getOrCreateLine(seedAddress);
        send(tf.createTelex().withTo(seedLine)
            .withEnd(Hash.of(state.getSeedAddress())));
      }
    });
  }

  private void startScannerThread() {
    if (scannerThread != null && scannerThread.isRunning()) {
      scannerThread.stopRunning();
    }
   
    scannerThread = new SwitchScanner(this);
    scannerThread.start();
  }

  private void stopScannerThread() {
    if (scannerThread != null) {
      scannerThread.stopRunning();
    }
    scannerThread = null;
  }

  public void send(final Telex telex) {
      final Line line = state.getOrCreateLine(telex.getTo());
     
      // check br and drop if too much
      if (line.getBsent() - line.getBrIn() > 10000) {
          logger.warn("MAX SEND DROP");
          return;
      }
     
      // if a line is open use that, else send a ring
      if (line.isSetLineId()) {
          telex.setLine(line.getLineId());
      }
      else {
        telex.unsetLine();
          telex.setRing(line.getRingOut());
      }
     
      // update our bytes tracking and send current state
      line.setBrOut(line.getBr());
      telex.setBytesReceived(line.getBr());
     
      try {
          final String msg = JsonMapper.toJson(telex);
          final byte[] bytes = msg.getBytes();
          final IoBuffer buffer = allocator.allocate(bytes.length, true);
          buffer.put(bytes);
          buffer.flip();
         
          final IoFutureListener<IoFuture> onWriteComplete = new IoFutureListener<IoFuture>() {
            public void operationComplete(IoFuture future) {
                line.setBsent(line.getBsent() + bytes.length);
                line.setSentAt(time());
            };
      };
     
          if (line != null && line.getSession() != null && line.getSession().getAttribute("line") == line) {
              logger.debug("SEND[{}]: {} bytes: {}", new Object[]{
                  line.getSession().getId(), bytes.length, msg});
            line.getSession().write(buffer).addListener(onWriteComplete);
          }
          else {
            IoSession session = acceptor.newSession(telex.getTo(), acceptor.getLocalAddress());
                line.setSession(session);
                session.setAttribute("line", line);
                logger.debug("SEND[{}]: {} bytes: {}", new Object[]{
                    session.getId(), bytes.length, msg});
                session.write(buffer).addListener(onWriteComplete);
          }
      }
      catch (IOException e) {
        logger.error("Failed to write Telex to JSON: " + e.getMessage(), e);
      }
  }

    static private final CharsetDecoder decoder = Charset.forName("UTF-8").newDecoder();
   
    @Override
  public void messageReceived(IoSession session, Object message)
      throws Exception {
    if (message instanceof IoBuffer) {
      IoBuffer buffer = (IoBuffer) message;
      String response = buffer.getString(decoder);
      int br = response.getBytes().length;
      logger.debug("RECV[{}] from {}: {} bytes: {}", new Object[]{
          session.getId(),
          (InetSocketAddress)session.getRemoteAddress(), br, response});
      Telex telex = (Telex)
          JsonMapper.fromJson(response, TelehashPackage.Literals.TELEX);
      switch (state.getConnectionStatus()) {
      case SEEDING:
        completeBootstrap(session, telex, br);
        break;
      case CONNECTED:
        processTelex(session, telex, br);
        break;
      }
    }
  }

  protected void processTelex(IoSession session, final Telex telex, int br) {
    Line line = (Line) session.getAttribute("line");
    if (line == null) {
      session.close(true);
      return;
    }
   
    boolean lineStatus = checkLine(line, telex, br);
    if (lineStatus) {
      logger.debug("LINE [{}] STATUS {}", line.getAddress(),
          telex.isSetLine() ? "OPEN" : "RINGING");
    }
    else {
      logger.debug("LINE [{}] FAIL", line.getAddress());
      return;
    }
   
    for (TelexHandler handler : telexHandlers) {
      if (handler.isMatch(telex)) {
        handler.telexReceived(this, line, telex);
      }
    }
   
        Integer hop = (Integer) telex.get("_hop");
        if (hop == null) {
          hop = 0;
        }
       
        // if not last-hop, check for any active taps (todo: optimize the matching, this is just brute force)
        if (hop < 4) {
          for (Line matchedLine : Iterables.filter(state.getLines().values(), new Predicate<Line>() {
            @Override
            public boolean apply(Line input) {
              return !input.getRules().isEmpty() && input.isRulesMatch(telex);
            }
      })) {
           
                // it's us, it has to be our tap_js       
                if (matchedLine.getAddress().equals(state.getSelfAddress())) {
                    try {
            logger.debug("STDOUT[{}]", JsonMapper.toJson(telex));
          } catch (IOException e) {
            logger.error(e.getMessage(), e);
          }
                }
                else{
                    Telex telexOut = tf.createTelex();
                    for (String fieldName : telex.getFieldNames()) {
                      telexOut.with(fieldName, telex.get(fieldName));
                    }
                    telexOut.with("_hop", hop + 1);
                    send(telexOut);
                }
          }
        }
  }
 
  protected void completeBootstrap(final IoSession session, final Telex telex, final int br) {
    state.connected(new Runnable(){
      @Override
      public void run() {
        state.setSelfAddress(telex.getTo());
        logger.debug("SELF[{} = {}]", state.getSelfAddress(), state.getSelfHash());
       
        Line line = state.getOrCreateLine(state.getSelfAddress());
        if (line == null) {
          return;
        }
       
        line.setVisible(true);
        line.getRules().addAll(tapRules);
       
        if (state.getSelfAddress().equals(session.getRemoteAddress())) {
          logger.debug("We're the seed.");
        }
       
        processTelex(session, telex, br);
       
        // start scanning thread
        startScannerThread();
      }
    });
  }

  static public int time() {
    return (int)(System.currentTimeMillis() / 1000);
  }
 
  /**
   * Check a line's status.
   * True if open, false if ringing.
   */
  public boolean checkLine(Line line, Telex telex, int br) {
      if (line == null) {
          return false;
      }
     
//      Integer _line = (Integer) telex.get("_line");
//      if (_line == null) {
//        _line = Line.NOT_SET;
//      }
     
      // first, if it's been more than 10 seconds after a line opened,
      // be super strict, no more ringing allowed, _line absolutely required
      if (line.getLineAt() > 0 && time() - line.getLineAt() > 10) {
          if (!telex.isSetLine() || telex.getLine() != line.getLineId()) {
              return false;
          }
      }
     
      // second, process incoming _line
      if (telex.isSetLine()) {
          if (line.getRingOut() <= 0) {
              return false;
          }
         
          // must match if exist
          if (line.isSetLineId() && telex.getLine() != line.getLineId()) {
              return false;
          }
         
          // must be a product of our sent ring!!
          if (telex.getLine() % line.getRingOut() != 0) {
              return false;
          }
         
          // we can set up the line now if needed
          if (line.getLineAt() == 0) {
              line.setRingIn(telex.getLine() / line.getRingOut()); // will be valid if the % = 0 above
              line.setLineId(telex.getLine());
              line.setLineAt(time());
          }
      }
     
//      Integer _ring = (Integer) telex.get("_ring");
//      if (_ring == null) {
//        _ring = Line.NOT_SET;
//      }
     
      // last, process any incoming _ring's (remember, could be out of order, after a _line)
      if (telex.isSetRing()) {
       
          // already had a ring and this one doesn't match, should be rare
          if (line.isSetRingIn() && telex.getRing() != line.getRingIn()) {
            logger.debug("unmatched _ring from " + line.getAddress());
              return false;
          }
         
          // make sure within valid range
          if (telex.getRing() <= 0 || telex.getRing() > 32768) {
              return false;
          }
         
          // we can set up the line now if needed
          if (line.getLineAt() == 0) {
              line.setRingIn(telex.getRing());
              line.setLineId(line.getRingIn() * line.getRingOut());
              line.setLineAt(time());
          }
      }
     
      // we're valid at this point, line or otherwise, track bytes
      logger.debug(
          "BR " + line.getAddress() + " [" + line.getBr() + " += "
            + br + "] DIFF " + (line.getBsent() - telex.getBytesReceived()));
      line.setBr(line.getBr() + br);
      line.setBrIn(telex.getBytesReceived());
     
      // they can't send us that much more than what we've told them to, bad!
      if (line.getBr() - line.getBrOut() > 12000) {
          return false;
      }
     
      // XXX if this is the first seenat,
      // if we were dialing we might need to re-send our telex as this could be a nat open pingback
      line.setSeenAt(time());
      return true;
  }

  /**
   * Update status of all lines, removing stale ones.
   */
  public void scanLines() {
      long now = time();
      int numValid = 0;
     
      logger.debug("SCAN " + state.getLines().size() + " lines");
     
      for (Iterator<Entry<Hash, Line>> entryIter = state.getLines().entrySet().iterator(); entryIter.hasNext(); ) {
        Entry<Hash, Line> entry = entryIter.next();
        Hash hash = entry.getKey();
          if (hash.equals(state.getSelfHash())) {
              continue; // skip our own endpoint and what is this (continue)
          }
         
          Line line = entry.getValue();
          if (!line.getEnd().equals(hash)) {
            continue; // empty/dead line (continue)
          }
         
          if ((line.getSeenAt() == 0 && now - line.getInit() > 70)
                  || (line.getSeenAt() != 0 && now - line.getSeenAt() > 70)) {
              // remove line if they never responded or haven't in a while
              logger.debug("PURGE[" + hash + " " + line.getAddress() + "] last seen "
                  + Long.toString(now - line.getSeenAt()) + "s ago");
              entryIter.remove();
              continue;
          }
         
          numValid++;
         
          if (state.getConnectionStatus() == ConnectionStatus.CONNECTED) {
         
              // +end ourselves to see if they know anyone closer as a ping
            Telex telexOut = tf.createTelex().withTo(line).withEnd(state.getSelfHash());
             
              // also .see ourselves if we haven't yet, default for now is to participate in the DHT
              if (!line.isAdvertised()) {
                line.setAdvertised(true);
                telexOut.getSee().add(state.getSelfAddress());
              }
             
              // also .tap our hash for +pop requests for NATs
              TapRule tapRule = tf.createTapRule();
              tapRule.setIs(tf.createTelex().withEnd(state.getSelfHash()));
              tapRule.getHas().add("+pop");
              telexOut.getTap().add(tapRule);
              send(telexOut);
          }
      }
     
      if (state.getConnectionStatus() == ConnectionStatus.CONNECTED && numValid == 0
          && !state.getSelfAddress().equals(state.getSeedAddress())) {
          state.offline(new Runnable(){
            @Override
            public void run() {
              stopScannerThread();
              seed(state.getSeedAddress());
            }
          });
      }
  }
 
  public Collection<Hash> nearTo(final Hash endHash, InetSocketAddress address) {
    Hash addrHash = Hash.of(address);
      Line addrLine = getLine(addrHash);
      if (addrLine == null) {
          return Collections.emptyList(); // should always exist except in startup or offline, etc
      }
     
      // of the existing and visible cached neighbors, sort by distance to this end
      List<Hash> visibleNeighbors = Lists.newArrayList(
        Iterables.filter(addrLine.getNeighbors(),
          new Predicate<Hash>() {
            @Override
            public boolean apply(Hash hash) {
              Line line = getLine(hash);
              return line != null && line.isVisible();
            }
        }));
      Collections.sort(visibleNeighbors, new Comparator<Hash>(){
        @Override
        public int compare(Hash o1, Hash o2) {
          return endHash.diffBit(o1) - endHash.diffBit(o2);
        }
      });
     
//      console.log("near_to: see[]=" + JSON.stringify(see));
//      console.log("near_to: line=" + JSON.stringify(line));
     
      if (visibleNeighbors.isEmpty()) {
          return Collections.emptyList();
      }
     
      Hash firstSeeHash = visibleNeighbors.get(0);
     
      logger.debug(StringUtils.join(
          new String[]{
            "NEARTO " + endHash,
            address.toString(),
            endHash.toString(),
            Integer.toString(addrLine.getNeighbors().size()),
            ">",
            Integer.toString(visibleNeighbors.size()),
            ";",
            firstSeeHash.toString(),
            "=",
            Integer.toString(addrLine.getEnd().diffBit(endHash))
          }, " "));
     
      // it's either us or we're the same distance away so return these results
      if (firstSeeHash.equals(addrLine.getEnd())
              || (firstSeeHash.diffBit(endHash) == addrLine.getEnd().diffBit(endHash))) {
         
          // this +end == this line then replace the neighbors cache with this result
          // and each in the result walk and insert self into their neighbors
          if (addrLine.getEnd().equals(endHash)) {
            logger.debug("NEIGH for " + endHash + " was "
                + StringUtils.join(addrLine.getNeighbors(), ",")
                + " " + visibleNeighbors.size());
           
            addrLine.getNeighbors().clear();
            Iterables.addAll(addrLine.getNeighbors(),
                Iterables.limit(visibleNeighbors, 5));
             
            logger.debug("NEIGH for " + endHash + " is now "
                + StringUtils.join(addrLine.getNeighbors(), ",")
                + " " + visibleNeighbors.size());
           
            for (Hash neighborHash : addrLine.getNeighbors()) {
              Line neighborLine = getLine(neighborHash);
              if (neighborLine == null || neighborLine == addrLine
                  || neighborLine.getNeighbors().contains(endHash)) {
                continue;
              }
             
              neighborLine.getNeighbors().add(endHash);
                    logger.debug("SEED " + address + " into " + neighborLine.getAddress());
            }
           
          }
         
          logger.debug("SEE distance=" + endHash.diffBit(firstSeeHash)
              + " count=" + visibleNeighbors.size());
          return visibleNeighbors;
      }

      // whomever is closer, if any, tail recurse endseeswitch them
      return nearTo(endHash, getLine(firstSeeHash).getAddress());
  }

  public void taptap() throws IOException {
    for (TapRule tapRule : tapRules) {
      if (state.getConnectionStatus() != ConnectionStatus.CONNECTED) {
        break;
      }
     
      Hash tapEnd = (Hash) tapRule.getIs().get("+end");
          if (tapEnd == null) {
            continue;
          }
         
          Collection<Hash> nearestHashes = nearTo(tapEnd, state.getSelfAddress());
          for (Hash hash : Iterables.limit(nearestHashes, 3)) {
              Line line = getLine(hash);
             
              if (line.isSetTapLastAt() && line.getTapLastAt() + 50 > time()) {
                  return; // only tap every 50sec
              }
             
              line.setTapLastAt(time());
              Telex telexOut = (Telex) tf.createTelex().withTo(line)
                .with(".tap", Lists.newArrayList(tapRule)); // tap the closest ipp to our target end
              logger.info("TAPTAP to {} end {} tap {}", new Object[]{
                  line.getAddress(), tapEnd, JsonMapper.toJson(telexOut)});
              send(telexOut);
          }
      }
  }
 
  public void addTapRule(TapRule rule) {
    tapRules.add(rule);
  }

  public void setAddress(InetSocketAddress selfAddress) {
    state.setSelfAddress(selfAddress);
  }
 
}
TOP

Related Classes of org.telehash.SwitchHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.