APIs e Microsserviços

20 dez, 2017

Port Scanner Chatbot – Parte 02: Criando a API e configurando o Slack App

Publicidade

No primeiro artigo da série, vimos como implementar a lógica que realiza o scan em todas as portas de um servidor. Utilizamos as APIs do Java 8 para paralelizar o processamento e ganhar performance. Agora veremos como externalizar essa lógica e, aí sim, finalizar o chat criando um slack app.

Links do artigo anterior:

O Slack possui uma documentação completa e com vários guias que ajudam bastante na hora construir integrações, apps e bots.

Para criar um chatbot que funcione com o Slack, precisamos expor uma API que será consumida pelo Slack. Cada vez que digitarmos uma mensagem, o Slack funcionará como um client que consome um endpoint específico da nossa API. Focaremos primeiro no código fonte da API e, em seguida, falaremos das ações finais que precisam ser executadas no painel de configurações da conta Slack.

Enviando e recebendo requisições via Slack

Primeiramente devemos criar POJOs com todos os atributos que o Slack envia em suas requests a serviços externos. Vejamos quais são esses atributos:

package com.portscanner.dto;

public class SlackRequestDTO {
	private String token;
	private String team_id;
	private String team_domain;
	private String enterprise_id;
	private String enterprise_name;
	private String channel_id;
	private String channel_name;
	private String user_id;
	private String user_name;
	private String command;
	private String text;
	private String response_url;
  
  // getters e setters

}

Para ver mais sobre parâmetros da request, acesse: https://api.slack.com/slash-commands#triggering_a_command

Assim como os parâmetros da request, precisamos lidar com os parâmetros da response. O Slack espera que o response da request possua atributos específicos:

package com.portscanner.dto;

import java.util.List;

public class SlackResponseDTO {
	
	private String text;
	private List<String> attachments;
  
	// getters e seters
}

Para ver mais sobre parâmetros do response, acesse: https://api.slack.com/slash commands#responding_to_a_command

Assim como os parâmetros da request, precisamos lidar com os parâmetros da response. O Slack espera que o response da request possua atributos específicos:

package com.portscanner.dto;

import java.util.List;

public class SlackResponseDTO {
	
	private String text;
	private List<String> attachments;
  
	// getters e seters
}

Para ver mais sobre parâmetros do response: https://api.slack.com/slash-commands#responding_to_a_command

Criando a API

Estrutura do projeto:

Pra quem já trabalha com REST APIs, nada de novo por aqui.

  • business: neste pacote temos a lógica do port scanner, que procura pelas portas disponíveis. A criação deste serviço foi detalhada no artigo anterior.
  • controller: aqui colocamos o REST controller criado com o Spring. Ele contém o endpoint acessado pela requisição do Slack. Veremos em detalhes sua implementação.
  • dto: este é o pacote mais simples. Possui os DTOs SlackRequestDTO e SlackResponseDTO, que são utilizados como parâmetros dos endpoints.

Encapsulando o processamento – pacote business

Para que os endpoints da API acessem a lógica que busca por portas disponíveis, criamos classes Business que encapsulam todo o processamento. Vejamos como ficaram:

package com.portscanner.business;

import java.util.concurrent.ExecutionException;

import com.portscanner.dto.SlackRequestDTO;
import com.portscanner.dto.SlackResponseDTO;

public interface PortScannerBusiness {
	
  SlackResponseDTO getOpenPorts(SlackRequestDTO slackRequestDTO) throws InterruptedException, ExecutionException;
	
}

A interface mostra o único método que precisamos implementar: getOpenPorts(), que aceita como parâmetro um DTO com os atributos enviados pelo Slack e retorna outro DTO com os campos necessários para que a resposta seja corretamente exibida como uma mensagem no chat. Vejamos a implementação deste método:

package com.portscanner.business;

import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.springframework.stereotype.Component;

import com.portscanner.dto.SlackRequestDTO;
import com.portscanner.dto.SlackResponseDTO;

@Component
public class PortScannerBusinessImpl implements PortScannerBusiness {
	
	private final ExecutorService executorService = Executors.newFixedThreadPool(20);
	private static final int TIMEOUT = 200;
	
	public SlackResponseDTO getOpenPorts(SlackRequestDTO slackRequestDTO) throws InterruptedException, ExecutionException {
		final List<Future<Boolean>> futures = new ArrayList<>();
		String ip = slackRequestDTO.getText();
		
		for (int porta = 1; porta <= 65535; porta++) {
			futures.add(isPortOpen(executorService, ip, porta, TIMEOUT));
		}
		
		int amountOfOpenPorts = 0;
		int openPort = 0;
		List<String> openPorts = new ArrayList<>();
		
		for (final Future<Boolean> future : futures) {
			openPort++;

			if (future.get()) {
				openPorts.add(Integer.toString(openPort));
				amountOfOpenPorts++;	
			}
		}
		
		
		return new SlackResponseDTO(buildSlackResponseTextMessage(amountOfOpenPorts, openPorts, ip), null);
	}

	private Future<Boolean> isPortOpen(ExecutorService executorService, String ip, int port, int timeout) {
		return executorService.submit(new Callable<Boolean>() {
			@Override
			public Boolean call() {
				try {
					Socket socket = new Socket();
					socket.connect(new InetSocketAddress(ip, port), timeout);
					socket.close();
					return true;
					
				} catch (Exception ex) {
					return false;
				}
			}
		});
	}
	
	private String buildSlackResponseTextMessage(int amountOfOpenPorts, List<String> openPorts, String ip) {
		StringBuilder message = new StringBuilder("There is " + amountOfOpenPorts + " available ports on " + ip + ":");
		
		if (amountOfOpenPorts > 0) {
			openPorts.forEach(
					port -> message.append("\n " + ip + ":" + port));
		}
		return message.toString();
	}
}

Para entender os detalhes da lógica de processamento que encontra as portas disponíveis utilizando as APIs de multi-thread Future, veja o artigo anterior. Aqui, vamos falar apenas dos aspectos de implementação que envolvem o Slack App.

A primeira coisa que podemos notar: o IP a ser visitado pelo nosso port scanner está disponível no atributo text do DTO da request. Isso porquê a única informação que o usuário precisa enviar via Slack é seu IP, com uma mensagem do tipo: “/scan 192.168.28.14”. Fazendo isso, o Slack enviará à API apenas o conteúdo presente após o comando /scan pelo atributo text.

Em seguida, utilizamos o IP para verificar quais portas se encontram disponíveis no endereço, invocando o método privado que criamos isPortOpen(). Este método apenas utiliza um Socket para tentar estabelecer uma conexão com determinada porta naquele IP, e na sequência, retorna um valor booleano indicando a disponibilidade ou indisponibilidade da porta testada.

Voltando à execução do método getOpenPorts(): após termos os resultados de quais são as portas disponíveis na lista de futures List<Future<Boolean>> futures, na linha 39 verificamos quais são essas portas, para todas as que foram testadas, afim de armazenar duas informações importantes para dar um bom feedback ao usuário via chat:

  1. A quantidade de portas disponíveis amountOfOpenPorts++;.
  2. Quais são as portas diponíveis, por exemplo: 8080. openPorts.add(Integer.toString(openPort));.

Por fim, com essas informações retornamos um SlackResponseDTO que construímos usando um método muito simples responsável por formatar uma mensagem amigável ao usuário buildSlackResponseTextMessage().

Expondo os endpoints – pacote controller

Tendo corretamente encapsulado a lógica responsável por nos entregar a mensagem já formatada em um DTO, contendo todos os parâmetros necessários para exibirmos a mensagem no Slack, nos resta criar um ponto de acesso a esses recursos. Faremos isso utilizando o Spring Rest Service que facilita muito a criação de endpoints em aplicações Spring. Vejamos como fica nosso controller:

package com.portscanner.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.portscanner.business.PortScannerBusiness;
import com.portscanner.business.SlackVerificationTokenBusiness;
import com.portscanner.dto.SlackRequestDTO;
import com.portscanner.dto.SlackResponseDTO;

@RestController
public class PortScannerController {
	
	@Autowired
	private PortScannerBusiness portScannerBusiness;
	
	@Autowired
	private SlackVerificationTokenBusiness slackVerificationTokenBusiness;
	
	@RequestMapping(path = "/ports", method = RequestMethod.POST)
	public ResponseEntity<SlackResponseDTO> getAvailablePorts(SlackRequestDTO slackRequestDTO) {
		SlackResponseDTO slackResponseDTO = new SlackResponseDTO();
		
		if (slackRequestDTO.getToken() == null ||
				!slackVerificationTokenBusiness.isRequestComingFromSlack(slackRequestDTO.getToken())) {
			slackResponseDTO.setText("The provided Validation Token is not valid!");
			return new ResponseEntity<SlackResponseDTO>(slackResponseDTO, HttpStatus.FORBIDDEN);
		}
		
		try {
			slackResponseDTO = portScannerBusiness.getOpenPorts(slackRequestDTO);
		} catch (Exception e) {
			e.printStackTrace();
			return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
		}
		
		return new ResponseEntity<SlackResponseDTO>(slackResponseDTO, HttpStatus.OK); 
	}

}

Aqui definimos que nosso endpoint será disponível via POST através do path /ports.

A primeira ação realizada, logo na linha 28, é verificar se o token recebido é um token válido pertencente a um canal existente do Slack. Não queremos que ninguém mal intencionado seja capaz de utilizar este recurso da API, certo? Apenas usuários autorizados do Slack. Fazemos essa verificação utilizando uma classe business extremamente simples (veja aqui sua implementação), que através do método isRequestComingFromSlack(), verifica se o token é conhecido. Em nosso método do controller, retornamos um HTTP status 403 Forbidden caso o token recebido seja inválido, junto ao status enviamos a mensagem: “The provided Validation Token is not valid!”.

Não tendo problemas com o token, invocamos nosso método do business portScannerBusiness.getOpenPorts(slackRequestDTO); que nos retorna o SlackResponseDTO com a mensagem contendo todas as informações sobre as portas encontradas. Se algum erro acontecer durante o processamento, retornamos ainda um HTTP status 500 Internal Server Error.

O HTTP Status 200 OK com a mensagem de sucesso, só é retornado caso nenhum erro de processamento nem de permição aconteça.

Pronto! Aqui já temos a API 100% pronta para receber as requests do Slack. Faltam apenas alguns detalhes de configuração no próprio Slack para que nosso chat funcione perfeitamente!

Configurando o Slack App

  • Acessando a página https://api.slack.com/apps temos acesso aos apps que já criamos. Clique no botão “Create New App”.
  • Escolha um nome e um “Development Slack Workspace”, que é o ambiente Slack onde você pretende instalar o app port scanner.
  • Na aba “Add features and functionality” clique na opção “Slash Commands”.

  • Nessa tela, ficam visíveis todos os comandos que os usuários do seu ambiente podem enviar ao seu app. Como ainda não temos nenhum, vamos criar um novo clicando em “Create New Command”.
  • Aqui definimos o nome do comando, a URL da nossa API (este é o path onde o Slack fatá a requisição POST quando o usuário utilizar o comando definido) e uma descrição curta da ação a ser executada quando o comando for disparado.

A URL precisa usar https. No exemplo da imagem, a API está sendo executada no Beanstalk da AWS com um certificado SSL oferecido gratuitamente pela cloudflare. Veja como fazer deploy da API no Beanstalk aqui e como habilitar o https na cloudflare aqui.

  • Acesse a aba “Install Your App Into Workspace”, e clique no botão “Install App”. Nessa etapa serão exibidas várias autorizações que seu app precisa para funcionar no seu ambiente. Permita todas elas.

Tudo pronto! Para testar o funcionamento do bot, acesse seu ambiente slack como um usuário qualquer e mande a mensagem de qualquer janela: “/scan localhost”. Lembrando que o comando “/scan” foi o nome que eu escolhi, use o slack command que você escolheu no passo 5.

Concluindo

Se você trabalha no Slack diariamente com uma equipe de desenvolvimento, pode ser bem útil que todos consigam saber o status de utilização de portas de um servidor via Slack. E também funciona em containers!

O código completo da API se encontra no link: https://github.com/andreybleme/portscanner-slackapp.

Qualquer dúvida, problema ou feedbacks, não deixe de me escrever.

Referências e links úteis