net.spfbl.core.Server.java Source code

Java tutorial

Introduction

Here is the source code for net.spfbl.core.Server.java

Source

/*
 * This file is part of SPFBL.
 * 
 * SPFBL is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * SPFBL is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with SPFBL.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.spfbl.core;

import net.spfbl.data.Provider;
import net.spfbl.data.NoReply;
import net.spfbl.data.Block;
import net.spfbl.data.White;
import net.spfbl.data.Trap;
import net.spfbl.data.Ignore;
import net.spfbl.spf.SPF;
import net.spfbl.whois.AutonomousSystem;
import net.spfbl.whois.Domain;
import net.spfbl.whois.Handle;
import net.spfbl.whois.NameServer;
import net.spfbl.whois.Owner;
import net.spfbl.whois.Subnet;
import net.spfbl.whois.SubnetIPv4;
import net.spfbl.whois.SubnetIPv6;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeSet;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
import net.spfbl.data.Generic;
import net.spfbl.dns.QueryDNS;
import org.apache.commons.lang3.SerializationUtils;
import org.apache.commons.net.whois.WhoisClient;

/**
 * Representa um modelo de servidor com mtodos comuns.
 * 
 * @author Leandro Carlos Rodrigues <leandro@spfbl.net>
 */
public abstract class Server extends Thread {

    /**
     * Varivvel que determina se o servio deve continuar rodando.
     */
    private boolean run = true;

    /**
     * Armazena todos os servidores intanciados.
     */
    private static final LinkedList<Server> SERVER_LIST = new LinkedList<Server>();

    /**
     * Instancia um servidor.
     * @param name nome do servidor e da thread.
     */
    protected Server(String name) {
        super(name);
        // Adiciona novo servidor na lista.
        SERVER_LIST.add(this);
    }

    protected boolean continueListenning() {
        return run;
    }

    /**
     * Carregamento de cache em disco.
     */
    public static void loadCache() {
        Client.load();
        User.load();
        Owner.load();
        Domain.load();
        AutonomousSystem.load();
        SubnetIPv4.load();
        SubnetIPv6.load();
        Handle.load();
        NameServer.load();
        Peer.load();
        Analise.load();
        Reverse.load();
        Generic.load();
        Block.load();
        White.load();
        Trap.load();
        Ignore.load();
        Provider.load();
        SPF.load();
        NoReply.load();
        Defer.load();
        QueryDNS.load();
    }

    private static Semaphore SEMAPHORE_STORE = new Semaphore(1);

    private static class Store extends Thread {

        public Store() {
            super("BCKGROUND");
            super.setPriority(MIN_PRIORITY);
        }

        @Override
        public void run() {
            try {
                storeAll(true, true);
            } finally {
                SEMAPHORE_STORE.release();
            }
        }
    }

    /**
     * Armazenamento de cache em disco.
     */
    public static boolean tryStoreCache() {
        if (SEMAPHORE_STORE.tryAcquire()) {
            new Store().start();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Armazenamento de cache em disco.
     */
    private static void storeCache() {
        try {
            SEMAPHORE_STORE.acquire();
            try {
                storeAll(false, false);
            } finally {
                SEMAPHORE_STORE.release();
            }
        } catch (Exception ex) {
            Server.logError(ex);
        }
    }

    private static void storeAll(boolean simplify, boolean clone) {
        Client.store();
        User.store();
        Peer.store();
        Provider.store();
        Ignore.store();
        Generic.store();
        Trap.store();
        NoReply.store();
        QueryDNS.store();
        White.store(simplify);
        Analise.store();
        Reverse.store();
        Defer.store();
        SPF.store(clone);
        Owner.store();
        Domain.store();
        AutonomousSystem.store();
        SubnetIPv4.store();
        SubnetIPv6.store();
        Handle.store();
        NameServer.store();
        Block.store(simplify);
        System.gc();
    }

    private static SecretKey privateKey = null;

    private static SecretKey getPrivateKey() {
        if (privateKey == null) {
            try {
                File file = new File("./data/server.key");
                if (file.exists()) {
                    FileInputStream fileInputStream = new FileInputStream(file);
                    try {
                        privateKey = SerializationUtils.deserialize(fileInputStream);
                    } finally {
                        fileInputStream.close();
                    }
                } else {
                    KeyGenerator keyGen = KeyGenerator.getInstance("AES");
                    keyGen.init(new SecureRandom());
                    SecretKey key = keyGen.generateKey();
                    FileOutputStream outputStream = new FileOutputStream(file);
                    try {
                        SerializationUtils.serialize(key, outputStream);
                    } finally {
                        outputStream.close();
                    }
                    privateKey = key;
                }
            } catch (Exception ex) {
                Server.logError(ex);
            }
        }
        return privateKey;
    }

    public static String encrypt(String message) throws ProcessException {
        if (message == null) {
            return null;
        } else {
            try {
                return encrypt(message.getBytes("UTF8"));
            } catch (Exception ex) {
                throw new ProcessException("ERROR: ENCRYPTION", ex);
            }
        }
    }

    public static String encrypt(ByteBuffer buffer) throws ProcessException {
        if (buffer == null) {
            return null;
        } else {
            return encrypt(buffer.array());
        }
    }

    public static String encrypt(byte[] byteArray) throws ProcessException {
        if (byteArray == null) {
            return null;
        } else {
            try {
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.ENCRYPT_MODE, getPrivateKey());
                byte[] code = cipher.doFinal(byteArray);
                return new String(Base64Coder.encode(code));
            } catch (Exception ex) {
                throw new ProcessException("ERROR: ENCRYPTION", ex);
            }
        }
    }

    public static String encryptURLSafe(byte[] byteArray) throws ProcessException {
        if (byteArray == null) {
            return null;
        } else {
            try {
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.ENCRYPT_MODE, getPrivateKey());
                byte[] code = cipher.doFinal(byteArray);
                return Core.BASE64.encodeAsString(code);
            } catch (Exception ex) {
                throw new ProcessException("ERROR: ENCRYPTION", ex);
            }
        }
    }

    public static String decrypt(String code) throws ProcessException {
        if (code == null) {
            return null;
        } else {
            try {
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.DECRYPT_MODE, getPrivateKey());
                byte[] message = cipher.doFinal(Base64Coder.decode(code));
                return new String(message, "UTF8");
            } catch (Exception ex) {
                throw new ProcessException("ERROR: DECRYPTION", ex);
            }
        }
    }

    public static boolean isValidTicket(String code) {
        if (code == null) {
            return false;
        } else {
            try {
                decryptToByteArrayURLSafe(code);
                return true;
            } catch (ProcessException ex) {
                return false;
            }
        }
    }

    public static byte[] decryptToByteArrayURLSafe(String code) throws ProcessException {
        if (code == null) {
            return null;
        } else {
            try {
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.DECRYPT_MODE, getPrivateKey());
                return cipher.doFinal(Core.BASE64.decode(code));
            } catch (Exception ex) {
                throw new ProcessException("ERROR: DECRYPTION", ex);
            }
        }
    }

    public static byte[] decryptToByteArray(String code) throws ProcessException {
        if (code == null) {
            return null;
        } else {
            try {
                Cipher cipher = Cipher.getInstance("AES");
                cipher.init(Cipher.DECRYPT_MODE, getPrivateKey());
                return cipher.doFinal(Base64Coder.decode(code));
            } catch (Exception ex) {
                throw new ProcessException("ERROR: DECRYPTION", ex);
            }
        }
    }

    private static final SimpleDateFormat FORMAT_DATE = new SimpleDateFormat("yyyy-MM-dd");

    /**
     * Constante de formatao da data no log.
     * Padro ISO 8601
     * 
     * Um objeto SimpleDateFormat no  thread safety,
     * portanto  necessrio utilizar sincronismo
     * nos mtodos que o utilizam.
     */
    private static final SimpleDateFormat FORMAT_DATE_LOG = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    /**
     * Constante de formatao da data no ticket.
     * Baseado no padro ISO 8601
     * 
     * Um objeto SimpleDateFormat no  thread safety,
     * portanto  necessrio utilizar sincronismo
     * nos mtodos que o utilizam.
     */
    private static final SimpleDateFormat FORMAT_DATE_TICKET = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSZ");

    private static long LAST_UNIQUE_TIME = 0;

    public static synchronized long getNewUniqueTime() {
        long time = System.currentTimeMillis();
        if (time <= LAST_UNIQUE_TIME) {
            // No permite criar dois tickets 
            // exatamente com a mesma data para 
            // que o hash fique sempre diferente.
            time = LAST_UNIQUE_TIME + 1;
        }
        return LAST_UNIQUE_TIME = time;
    }

    public static String getNewTicketDate() {
        long time = getNewUniqueTime();
        return formatTicketDate(time);
    }

    public static synchronized String formatTicketDate(long time) {
        return FORMAT_DATE_TICKET.format(new Date(time));
    }

    public static synchronized String formatTicketDate(Date date) {
        return FORMAT_DATE_TICKET.format(date);
    }

    public static synchronized Date parseTicketDate(String value) throws ParseException {
        return FORMAT_DATE_TICKET.parse(value);
    }

    /**
     * Constante que representa a quantidade de tempo de um dia em milisegundos.
     */
    public static final long HOUR_TIME = 1000L * 60L * 60L;

    /**
     * Constante que representa a quantidade de tempo de um dia em milisegundos.
     */
    public static final long DAY_TIME = HOUR_TIME * 24L;

    /**
     * Registra uma linha de LOG
     * 
     * Utiliza os seguintes campos:
     *    - Data do incio do processo;
     *    - Latncia do processamento em milisegundos com 4 dgitos fixos;
     *    - Tipo de registro de LOG com 5 caracteres fixos e
     *    - Mensagem do LOG.
     * 
     * Nenhum processamento deve durar mais que 9999 milisegundos.
     * Por este motivo o valor foi limitado a 4 digitos.
     * Se acontecer do valor ser maior que 9999, significa que o cdigo 
     * tem graves problemas de eficincia e deve ser revisto com urgncia.
     * Outros valores grandes abaixo deste limite podem 
     * ser investigados com cautela.
     * 
     * @param time data exata do inicio do processamento.
     * @param type tipo de registro de LOG.
     * @param message a mensagem do registro de LOG.
     */
    public static void log(long time, Core.Level level, String type, String message, String result) {
        if (level.ordinal() <= Core.LOG_LEVEL.ordinal()) {
            int latencia = (int) (System.currentTimeMillis() - time);
            if (latencia > 99999) {
                // Para manter a formatao correta no LOG,
                // Registrar apenas latncias at 99999, que tem 5 digitos.
                latencia = 99999;
            } else if (latencia < 0) {
                latencia = 0;
            }
            if (message != null) {
                message = message.replace("\r", "\\r");
                message = message.replace("\n", "\\n");
                message = message.replace("\t", "\\t");
            }
            if (result != null) {
                result = result.replace("\r", "\\r");
                result = result.replace("\n", "\\n");
                result = result.replace("\t", "\\t");
            }
            Date date = new Date(time);
            String text = FORMAT_DATE_LOG.format(date) + " " + LATENCIA_FORMAT.format(latencia) + " "
                    + Thread.currentThread().getName() + " " + type + " " + message
                    + (result == null ? "" : " => " + result);
            PrintWriter writer = getLogWriter(date);
            if (writer == null) {
                System.out.println(text);
            } else {
                writer.println(text);
            }
        }
    }

    private static File logFolder = null;
    private static File logFile = null;
    private static PrintWriter logWriter = null;
    private static short logExpires = 7;

    public static synchronized void setLogFolder(String path) {
        if (path == null) {
            Server.logFolder = null;
        } else {
            File folder = new File(path);
            if (folder.exists()) {
                if (folder.isDirectory()) {
                    Server.logFolder = folder;
                } else {
                    Server.logError("'" + path + "' is not a folder.");
                }
            } else {
                Server.logError("folder '" + path + "' not exists.");
            }
        }
    }

    public static synchronized void setLogExpires(String expires) {
        if (expires != null && expires.length() > 0) {
            try {
                setLogExpires(Integer.parseInt(expires));
            } catch (Exception ex) {
                setLogExpires(-1);
            }
        }
    }

    public static synchronized void setLogExpires(int expires) {
        if (expires < 1 || expires > Short.MAX_VALUE) {
            Server.logError("invalid LOG expires integer value '" + expires + "'.");
        } else {
            Server.logExpires = (short) expires;
        }
    }

    private static synchronized PrintWriter getLogWriter(Date date) {
        try {
            if (logFolder == null || !logFolder.exists()) {
                return null;
            } else if (logWriter == null) {
                logFile = new File(logFolder, "spfbl." + FORMAT_DATE.format(date) + ".log");
                FileWriter fileWriter = new FileWriter(logFile, true);
                return logWriter = new PrintWriter(fileWriter, true);
            } else if (logFile.getName().equals("spfbl." + FORMAT_DATE.format(date) + ".log")) {
                return logWriter;
            } else {
                logWriter.close();
                logFile = new File(logFolder, "spfbl." + FORMAT_DATE.format(date) + ".log");
                FileWriter fileWriter = new FileWriter(logFile, true);
                return logWriter = new PrintWriter(fileWriter, true);
            }
        } catch (Exception ex) {
            return null;
        }
    }

    private static final FilenameFilter logFilter = new FilenameFilter() {
        @Override
        public boolean accept(File dir, String name) {
            return name.startsWith("spfbl.") && name.endsWith(".log");
        }
    };

    public static synchronized void deleteLogExpired() {
        if (logFolder != null && logFolder.exists()) {
            for (File logFileLocal : logFolder.listFiles(logFilter)) {
                long lastModified = logFileLocal.lastModified();
                long period = System.currentTimeMillis() - lastModified;
                int days = (int) (period / (1000 * 60 * 60 * 24));
                if (days > logExpires) {
                    if (logFileLocal.delete()) {
                        Server.logDebug("LOG '" + logFileLocal.getName() + "' deleted.");
                    } else {
                        Server.logDebug("LOG '" + logFileLocal.getName() + "' not deleted.");
                    }
                }
            }
        }
    }

    /**
     * O campo de latncia do LOG tem apenas 4 digitos.
     * Serve para mostrar quais processamentos levam mais tempo
     * e para encontrar com mais facilidade cdigos
     * do programa que no esto bem escritos.
     */
    private static final DecimalFormat LATENCIA_FORMAT = new DecimalFormat("00000");

    private static void log(long time, Core.Level level, String type, Throwable ex) {
        if (ex != null) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            PrintStream printStream = new PrintStream(baos);
            ex.printStackTrace(printStream);
            printStream.close();
            log(time, level, type, baos.toString(), (String) null);
        }
    }

    /**
     * Registra as mensagens para informao.
     * @param message a mensagem a ser registrada.
     */
    public static void logInfo(String message) {
        log(System.currentTimeMillis(), Core.Level.INFO, "INFOR", message, (String) null);
    }

    /**
     * Registra as mensagens para depurao.
     * @param message a mensagem a ser registrada.
     */
    public static void logDebug(String message) {
        log(System.currentTimeMillis(), Core.Level.DEBUG, "DEBUG", message, (String) null);
    }

    public static void logSendMTP(String message) {
        log(System.currentTimeMillis(), Core.Level.DEBUG, "SMTPS", message, (String) null);
    }

    /**
     * Registra as mensagens para depurao de cdigo.
     * @param message a mensagem a ser registrada.
     */
    public static void logTrace(String message) {
        log(System.currentTimeMillis(), Core.Level.TRACE, "TRACE", message, (String) null);
    }

    /**
     * Registra as mensagens de manipulao do banco de dados.
     * @param message a mensagem a ser registrada.
     */
    public static void logMySQL(String message) {
        log(System.currentTimeMillis(), Core.Level.DEBUG, "MYSQL", message, (String) null);
    }

    /**
     * Registra as mensagens de manipulao do banco de dados.
     * @param message a mensagem a ser registrada.
     */
    public static void logMySQL(long time, String message) {
        log(time, Core.Level.DEBUG, "MYSQL", message, null);
    }

    /**
     * Registra as mensagens de manipulao do banco de dados.
     * @param message a mensagem a ser registrada.
     */
    public static void logMySQL(long time, String message, String result) {
        log(time, Core.Level.DEBUG, "MYSQL", message, result);
    }

    /**
     * Registra as mensagens de manipulao do banco de dados.
     * @param message a mensagem a ser registrada.
     */
    public static void logMySQL(long time, String message, SQLException ex) {
        String result = "ERROR " + ex.getErrorCode() + " " + ex.getMessage();
        log(time, Core.Level.DEBUG, "MYSQL", message, result);
    }

    /**
     * Registra as mensagens de manipulao do banco de dados.
     * @param statement the statement executed.
     */
    public static void logMySQL(long time, PreparedStatement statement, String result) {
        String message = statement.toString();
        int beginIndex = message.indexOf(' ') + 1;
        int endIndex = message.length();
        message = message.substring(beginIndex, endIndex);
        log(time, Core.Level.DEBUG, "MYSQL", message, result);
    }

    /**
     * Registra as mensagens de manipulao do banco de dados.
     * @param statement the statement executed.
     */
    public static void logMySQL(long time, PreparedStatement statement, SQLException ex) {
        String message = statement.toString();
        int beginIndex = message.indexOf(' ') + 1;
        int endIndex = message.length();
        message = message.substring(beginIndex, endIndex);
        String result = "ERROR " + ex.getErrorCode() + " " + ex.getMessage();
        log(time, Core.Level.DEBUG, "MYSQL", message, result);
    }

    /**
     * Registra as gravaes de cache em disco.
     * @param file o arquivo armazenado.
     */
    public static void logStore(long time, File file) {
        log(time, Core.Level.INFO, "STORE", file.getName(), (String) null);
    }

    /**
     * Registra os carregamentos de cache no disco.
     * @param file o arquivo carregado.
     */
    public static void logLoad(long time, File file) {
        log(time, Core.Level.INFO, "LOADC", file.getName(), (String) null);
    }

    /**
     * Registra as mensagens de checagem DNS.
     * @param host o host que foi consultado.
     */
    public static void logCheckDNS(long time, String host, String result) {
        log(time, Core.Level.DEBUG, "DNSCK", host, result);
    }

    /**
     * Registra os tiquetes processados.
     * @param tokenSet o conjunto de tokens.
     */
    public static void logTicket(long time, TreeSet<String> tokenSet, String ticket) {
        log(time, Core.Level.DEBUG, "TIKET", tokenSet.toString(), ticket);
    }

    public static void logPeerSend(long time, String address, String token, String result) {
        log(time, Core.Level.DEBUG, "PEERS", address, token, result);
    }

    /**
     * Registra as consultas DNS.
     */
    public static void logLookupDNS(long time, String type, String host, String result) {
        log(time, Core.Level.DEBUG, "DNSLK", type + " " + host, result);
    }

    /**
     * Registra as consultas DNS para HELO.
     */
    public static void logLookupHELO(long time, String host, String result) {
        log(time, Core.Level.DEBUG, "HELOL", host, result);
    }

    /**
     * Registra as consultas de mecanismo A de SPF.
     */
    public static void logMecanismA(long time, String host, String result) {
        log(time, Core.Level.DEBUG, "SPFMA", host, result);
    }

    /**
     * Registra as consultas de mecanismo exists de SPF.
     */
    public static void logMecanismExists(long time, String host, String result) {
        log(time, Core.Level.DEBUG, "SPFEX", host, result);
    }

    /**
     * Registra as consultas de mecanismo MX de SPF.
     */
    public static void logMecanismMX(long time, String host, String result) {
        log(time, Core.Level.DEBUG, "SPFMX", host, result);
    }

    /**
     * Registra verificaes de macth de HELO.
     */
    public static void logMatchHELO(long time, String query, String result) {
        log(time, Core.Level.DEBUG, "HELOM", query, result);
    }

    /**
     * Registra interaes de atrazo programado.
     */
    public static void logDefer(long time, String id, String result) {
        log(time, Core.Level.DEBUG, "DEFER", id, result);
    }

    /**
     * Registra verificaes de DNS reverso.
     */
    public static void logReverseDNS(long time, String ip, String result) {
        log(time, Core.Level.DEBUG, "DNSRV", ip, result);
    }

    /**
     * Registra as mensagens de erro.
     * @param message a mensagem a ser registrada.
     */
    public static void logError(String message) {
        log(System.currentTimeMillis(), Core.Level.ERROR, "ERROR", message, (String) null);
    }

    /**
     * Registra as mensagens de erro.
     * Uma iniciativa para formalizao das mensagens de log.
     * @param ex a exceo a ser registrada.
     */
    public static void logError(Throwable ex) {
        log(System.currentTimeMillis(), Core.Level.ERROR, "ERROR", ex);
    }

    /**
     * Registra as consultas ao SPF do host 
     * que no foram encontrados erros de sintaxe.
     * Uma iniciativa para formalizao das mensagens de log.
     * @param hostname o nome do host.
     * @param result o resultado SPF do host.
     */
    public static void logLookupSPF(long time, String hostname, String result) {
        log(time, Core.Level.DEBUG, "SPFLK", hostname, result);
    }

    public static void logQueryP2PUDP(long time, InetAddress ipAddress, String query, String result) {
        logQuery(time, "P2PUDP", ipAddress, query, result);
    }

    /**
     * Registra as consultas ao DNSBL do host.
     * Uma iniciativa para formalizao das mensagens de log.
     * @param query a expresso da consulta.
     * @param result o resultado a ser registrado.
     */
    public static void logQueryDNSBL(long time, InetAddress ipAddress, String query, String result) {
        logQuery(time, "DNSBL", ipAddress, query, result);
    }

    public static void logQueryDNSBL(long time, String address, String query, String result) {
        logQuery(time, "DNSBL", address, query, result);
    }

    /**
     * Registra os resultados do WHOIS.
     * Uma iniciativa para formalizao das mensagens de log.
     * @param server o servidor WHOIS.
     * @param query a expresso da consulta.
     * @param result o resultado a ser registrado.
     */
    public static void logWhois(long time, String server, String query, String result) {
        log(time, Core.Level.DEBUG, "WHOIS", server + " " + query, result);
    }

    /**
     * Registra as mensagens de consulta.
     * Uma iniciativa para formalizao das mensagens de log.
     * @param time data exatata no inicio do processamento.
     * @param ipAddress o IP do cliente da conexo.
     * @param time o tempo de processamento da consulta.
     * @param query a expresso da consulta.
     * @param result a expresso do resultado.
     */
    public static void logQuery(long time, String type, InetAddress ipAddress, String query, String result) {
        String origin = Client.getOrigin(ipAddress);
        if (query == null) {
            log(time, Core.Level.INFO, type, origin + ":", result);
        } else {
            log(time, Core.Level.INFO, type, origin + ": " + query, result);
        }
    }

    public static void logQuery(long time, String type, String client, Set<String> tokenSet) {
        String message;
        if (tokenSet == null || tokenSet.isEmpty()) {
            message = "";
        } else {
            message = null;
            for (String token : tokenSet) {
                if (message == null) {
                    message = token;
                } else {
                    message += ' ' + token;
                }
            }
        }
        log(time, Core.Level.INFO, type, (client == null ? "" : client + ": ") + message, null);
    }

    public static void log(long time, Core.Level level, String type, String client, String query,
            Set<String> tokenSet, String recipient) {
        String result;
        if (tokenSet == null || tokenSet.isEmpty()) {
            result = "";
        } else {
            result = null;
            for (String token : tokenSet) {
                if (result == null) {
                    result = token;
                } else {
                    result += ' ' + token;
                }
            }
            if (recipient != null) {
                result += " >" + recipient;
            }
        }
        log(time, level, type, client, query, result);
    }

    public static void log(long time, Core.Level level, String type, String client, String query, String result) {
        log(time, level, type, (client == null ? "" : client + ": ") + query, result);
    }

    public static void logQuery(long time, String type, String client, String query, String result) {
        if (result != null && result.length() > 1024) {
            int index1 = result.indexOf('\n', 1024);
            int index2 = result.indexOf(' ', 1024);
            int index = Math.min(index1, index2);
            index = Math.max(index, 1024);
            result = result.substring(0, index) + "... too long";
        }
        log(time, Core.Level.INFO, type, (client == null ? "" : client + ": ") + query, result);
    }

    /**
     * Registra as mensagens de comando administrativo.
     * Uma iniciativa para formalizao das mensagens de log.
     * @param ipAddress o IP da conexo.
     * @param command a expresso do comando.
     * @param result a expresso do resultado.
     */
    public static void logAdministration(long time, InetAddress ipAddress, String command, String result) {
        String origin = Client.getOrigin(ipAddress);
        if (result != null && result.length() > 1024) {
            int index1 = result.indexOf('\n', 1024);
            int index2 = result.indexOf(' ', 1024);
            int index = Math.min(index1, index2);
            index = Math.max(index, 1024);
            result = result.substring(0, index) + "... too long";
        }
        log(time, Core.Level.INFO, "ADMIN", origin + ": " + command, result);
    }

    /**
     * Desliga todos os servidores instanciados.
     * @throws Exception se houver falha no fechamento de algum servidor.
     */
    public static boolean shutdown() {
        // Inicia finalizao dos servidores.
        Server.logInfo("interrupting analises...");
        Analise.interrupt();
        Server.logInfo("interrupting user theads...");
        User.interrupt();
        Server.logInfo("shutting down server...");
        for (Server server : SERVER_LIST) {
            server.run = false;
        }
        boolean closed = true;
        for (Server server : SERVER_LIST) {
            try {
                server.close();
            } catch (Exception ex) {
                closed = false;
                Server.logError(ex);
            }
        }
        // Finaliza timer local.
        WHOIS_SEMAPHORE_TIMER.cancel();
        // Finaliza timer SPF.
        Core.cancelTimer();
        // Armazena os registros em disco.
        storeCache();
        // Fecha pooler de conexo MySQL.
        Core.closeConnectionPooler();
        return closed;
    }

    /**
     * Finaliza servidor liberando memria e respectivos recursos.
     * @throws Exception se houver falha durante o fechamento do servidor.
     */
    protected abstract void close() throws Exception;

    /**
     * Timer que controla a liberao dos semforos do WHOIS.
     */
    private static final Timer WHOIS_SEMAPHORE_TIMER = new Timer("TIMEWHOIS");

    /**
     * Semphoro que controla o nmero mximo de consultas no WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 5 minutos.
     */
    private static final int WHOIS_QUERY_LIMIT = 30; // Taxa de 30 consultas.
    private static final int WHOIS_FREQUENCY = 5 * 60 * 1000; // Libera o direito  consulta em 5 min.
    private static final Semaphore WHOIS_QUERY_SEMAPHORE = new Semaphore(WHOIS_QUERY_LIMIT);

    /**
     * Classe de tarefa que adquire e libera o semforo de consulta comum do WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 5 minutos.
     */
    private static class WhoisSemaphore extends TimerTask {

        public WhoisSemaphore() throws ProcessException {
            if (!WHOIS_QUERY_SEMAPHORE.tryAcquire()) {
                // Estouro de limite de consultas ao WHOIS.
                throw new ProcessException("ERROR: WHOIS QUERY LIMIT");
            }
        }

        @Override
        public void run() {
            WHOIS_QUERY_SEMAPHORE.release();
        }
    }

    /**
     * Adquire o direito a uma consulta comum no WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 5 minutos.
     * @throws ProcessException se houver falha no processo.
     */
    public static void acquireWhoisQuery() throws ProcessException {
        WhoisSemaphore whoisSemaphore = new WhoisSemaphore();
        WHOIS_SEMAPHORE_TIMER.schedule(whoisSemaphore, WHOIS_FREQUENCY);
        WHOIS_SEMAPHORE_TIMER.purge(); // Libera referncias processadas.
    }

    /**
     * Remove o direito a uma consulta comum no WHOIS por um dia.
     * As vezes a consulta WHOIS restringe as consultas.
     * Este mtodo  uma forma de reduzir drasticamente a frequncia.
     * @throws ProcessException se houver falha no processo.
     */
    public static void removeWhoisQueryDay() throws ProcessException {
        WhoisSemaphore whoisSemaphore = new WhoisSemaphore();
        WHOIS_SEMAPHORE_TIMER.schedule(whoisSemaphore, DAY_TIME);
    }

    /**
     * Remove o direito a uma consulta comum no WHOIS por uma hora.
     * As vezes a consulta WHOIS restringe as consultas.
     * Este mtodo  uma forma de reduzir drasticamente a frequncia.
     * @throws ProcessException se houver falha no processo.
     */
    public static void removeWhoisQueryHour() throws ProcessException {
        WhoisSemaphore whoisSemaphore = new WhoisSemaphore();
        WHOIS_SEMAPHORE_TIMER.schedule(whoisSemaphore, HOUR_TIME);
    }

    /**
     * Semphoro que controla o nmero mximo de consultas no WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 24 horas.
     */
    private static final Semaphore WHOIS_ID_QUERY_SEMAPHORE = new Semaphore(30);

    /**
     * Classe de tarefa que adquire e libera o semforo de consulta comum do WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 24 horas.
     */
    private static class WhoisIDSemaphore extends TimerTask {

        public WhoisIDSemaphore() throws ProcessException {
            if (!WHOIS_ID_QUERY_SEMAPHORE.tryAcquire()) {
                // Estouro de limite de consultas ao WHOIS.
                throw new ProcessException("ERROR: WHOIS QUERY LIMIT");
            }
        }

        @Override
        public void run() {
            WHOIS_ID_QUERY_SEMAPHORE.release();
        }
    }

    /**
     * Adquire o direito a uma consulta de identificao no WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 24 horas.
     * @throws ProcessException se houver falha no processo.
     */
    private static void acquireWhoisIDQuery() throws ProcessException {
        WhoisIDSemaphore whoisIDSemaphore = new WhoisIDSemaphore();
        WHOIS_SEMAPHORE_TIMER.schedule(whoisIDSemaphore, DAY_TIME); // Libera o direito  consulta em 24h.
        WHOIS_SEMAPHORE_TIMER.purge(); // Libera referncias processadas.
    }

    /**
     * Semphoro que controla o nmero mximo de conexes simutneas no WHOIS.
     * Limite de 2 conexes simultneas por IP de origem.
     */
    private static final Semaphore WHOIS_CONNECTION_SEMAPHORE = new Semaphore(2);

    /**
     * Consulta de identificao no WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 24 horas.
     * @param query a consulta a ser realizada.
     * @param server o servidor que contm a informao.
     * @return o resultado do WHOIS para a consulta.
     * @throws ProcessException se houver falha no processamento da informao.
     */
    public static String whoisID(String query, String server) throws ProcessException {
        long time = System.currentTimeMillis();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            if (WHOIS_CONNECTION_SEMAPHORE.tryAcquire()) {
                try {
                    acquireWhoisIDQuery();
                    WhoisClient whoisClient = new WhoisClient();
                    try {
                        whoisClient.setDefaultTimeout(3000);
                        whoisClient.connect(server);
                        InputStream inputStream = whoisClient.getInputStream(query);
                        int code;
                        while ((code = inputStream.read()) != -1) {
                            outputStream.write(code);
                        }
                    } finally {
                        whoisClient.disconnect();
                    }
                } finally {
                    WHOIS_CONNECTION_SEMAPHORE.release();
                }
            } else {
                throw new ProcessException("ERROR: TOO MANY CONNECTIONS");
            }
        } catch (ProcessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new ProcessException("ERROR: WHOIS CONNECTION FAIL", ex);
        }
        try {
            String result = outputStream.toString("ISO-8859-1");
            logWhois(time, server, query, result);
            return result;
        } catch (UnsupportedEncodingException ex) {
            throw new ProcessException("ERROR: ENCODING", ex);
        }
    }

    /**
     * Constante do servidor WHOIS brasileiro.
     */
    public static final String WHOIS_BR = "whois.nic.br";

    /**
     * Consulta de registros de nome de domnio.
     */
    private static InitialDirContext INITIAL_DIR_CONTEXT;

    public static Attributes getAttributesDNS(String hostname, String[] types) throws NamingException {
        return INITIAL_DIR_CONTEXT.getAttributes("dns:/" + hostname, types);
    }

    //    public static String getProviderDNS() {
    //        try {
    //            Hashtable env = (Hashtable) INITIAL_DIR_CONTEXT.getEnvironment();
    //            return (String) env.get("java.naming.provider.url") + '/';
    //        } catch (NamingException ex) {
    //            Server.logError(ex);
    //            return "";
    //        }
    //    }

    static {
        try {
            initDNS();
        } catch (Exception ex) {
            Server.logError(ex);
            System.exit(1);
        }
    }

    private static String DNS_PROVIDER = null;

    public static void setProviderDNS(String ip) {
        if (ip != null && ip.length() > 0) {
            if (Subnet.isValidIP(ip)) {
                Server.DNS_PROVIDER = Subnet.normalizeIP(ip);
                Server.logInfo("using " + ip + " as fixed DNS provider.");
            } else {
                Server.logError("invalid DNS provider '" + ip + "'.");
            }
        }
    }

    @SuppressWarnings("unchecked")
    public static void initDNS() throws NamingException {
        Hashtable env = new Hashtable();
        env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
        env.put("com.sun.jndi.dns.timeout.initial", "3000");
        env.put("com.sun.jndi.dns.timeout.retries", "1");
        if (DNS_PROVIDER != null) {
            env.put("java.naming.provider.url", "dns://" + DNS_PROVIDER);
        }
        INITIAL_DIR_CONTEXT = new InitialDirContext(env);
    }

    /**
     * Consulta comum no WHOIS.
     * Controla a taxa de 30 consultas no intervalo de 5 minutos.
     * @param query a consulta a ser realizada.
     * @param server o servidor que contm a informao.
     * @return o resultado do WHOIS para a consulta.
     * @throws ProcessException se houver falha no processamento da informao.
     */
    public static String whois(String query, String server) throws ProcessException {
        long time = System.currentTimeMillis();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            if (WHOIS_CONNECTION_SEMAPHORE.tryAcquire()) {
                try {
                    acquireWhoisQuery();
                    WhoisClient whoisClient = new WhoisClient();
                    try {
                        whoisClient.setDefaultTimeout(3000);
                        whoisClient.connect(server);
                        InputStream inputStream = whoisClient.getInputStream(query);
                        try {
                            int code;
                            while ((code = inputStream.read()) != -1) {
                                outputStream.write(code);
                            }
                        } finally {
                            inputStream.close();
                        }
                    } finally {
                        whoisClient.disconnect();
                    }
                } finally {
                    WHOIS_CONNECTION_SEMAPHORE.release();
                }
            } else {
                throw new ProcessException("ERROR: TOO MANY CONNECTIONS");
            }
        } catch (ProcessException ex) {
            throw ex;
        } catch (Exception ex) {
            throw new ProcessException("ERROR: WHOIS CONNECTION FAIL", ex);
        }
        try {
            String result = outputStream.toString("ISO-8859-1");
            result = result.replace("\r", "");
            logWhois(time, server, query, result);
            return result;
        } catch (UnsupportedEncodingException ex) {
            throw new ProcessException("ERROR: ENCODING", ex);
        }
    }

    //    public static synchronized void tryRefreshWHOIS() {
    //        // Evita que muitos processos fiquem 
    //        // presos aguardando a liberao do mtodo.
    //        if (WHOIS_QUERY_SEMAPHORE.availablePermits() == WHOIS_QUERY_LIMIT) {
    //            refreshWHOIS();
    //        }
    //    }

    /**
     * Atualiza os registros quase expirando.
     */
    public static synchronized boolean tryRefreshWHOIS() {
        if (WHOIS_QUERY_SEMAPHORE.availablePermits() == WHOIS_QUERY_LIMIT) {
            if (Domain.backgroundRefresh()) {
                return true;
            } else {
                return Subnet.backgroundRefresh();
            }
        } else {
            return false;
        }
    }

    /**
     * Processa a consulta e retorna o resultado.
     * @param query a expresso da consulta.
     * @return o resultado do processamento.
     */
    protected String processWHOIS(String query) {
        try {
            String result = "";
            if (query.length() == 0) {
                result = "INVALID QUERY\n";
            } else {
                StringTokenizer tokenizer = new StringTokenizer(query, " ");
                String token = tokenizer.nextToken();
                boolean updated = false;
                if (token.equals("UPDATED")) {
                    token = tokenizer.nextToken();
                    updated = true;
                }
                if (Owner.isOwnerID(token) && tokenizer.hasMoreTokens()) {
                    Owner owner = Owner.getOwner(token);
                    while (tokenizer.hasMoreTokens()) {
                        String key = tokenizer.nextToken();
                        String value = owner.get(key, updated);
                        if (value == null) {
                            result += '\n';
                        } else {
                            result += value + '\n';
                        }
                    }
                } else if (Subnet.isValidIP(token) && tokenizer.hasMoreTokens()) {
                    Subnet subnet = Subnet.getSubnet(token);
                    while (tokenizer.hasMoreTokens()) {
                        String field = tokenizer.nextToken();
                        String value = subnet.get(field, updated);
                        if (value == null) {
                            result += "\n";
                        } else {
                            result += value + "\n";
                        }
                    }
                } else if (Domain.containsDomain(token) && tokenizer.hasMoreTokens()) {
                    Domain domain = Domain.getDomain(token);
                    if (domain == null) {
                        result = "NOT FOUND\n";
                    } else {
                        while (tokenizer.hasMoreTokens()) {
                            String key = tokenizer.nextToken();
                            String value = domain.get(key, updated);
                            if (value == null) {
                                result += '\n';
                            } else {
                                result += value + '\n';
                            }
                        }
                    }
                } else {
                    result = "INVALID QUERY\n";
                }
            }
            return result;
        } catch (ProcessException ex) {
            Server.logError(ex.getCause());
            return ex.getMessage() + "\n";
        } catch (Exception ex) {
            Server.logError(ex);
            return "ERROR: FATAL\n";
        }
    }

}