Cześć, chciałbym prosić o code review małego projektu.
Założenia:
- napisanie dwóch programów klienta oraz serwera, które używają autoryzacji przy użyciu protokołu Port Knocking(https://en.wikipedia.org/wiki/Port_knocking).
- serwer powinien być w stanie obsłużyć wiele zapytań równocześnie
- serwer otwiera określoną liczbę porów UDP, a następnie zaczyna na nich nasłuchiwać na przychodzące pakiety
- jeżeli jeden klient (taki sami adres i port ) wyśle odpowiednią sekwencje zapytań, wtedy serwer wysyła do tego klienta adres portu TCP na
którym będzie nasłuchiwać na komunikacje od niego, - klient inicjalizuje komunikacje TCP na tym porcie,
- po zakończeniu komunikacji port jest zamykany.
Stworzyłem więc klasę Server który zawiera moduł autoryzacyjny
public class Server {
private Authorization authorization;
public static void main(String[] args) throws IOException {
new Server(new int[]{8000, 9000, 10000, 11000, 12000, 15555});
}
private Server(int[] portNumbers) throws SocketException {
authorization = new Authorization(portNumbers, this);
}
}
Uznałem, że wygodniej będzie jeżeli autoryzacja będzie odbywała się w osobnym module. Do serwera będą przekazywane tylko dane klientów, którzy autoryzacje uzyskali.
Klasa Authorization przyjmuje tablice z sekwencją portów( na które klient musi wysłać datagramy, by uzyskać autoryzacje), oraz obiekt Server( do którego będą wysyłane dane klientów którzy autoryzacje przeszli). Zawiera zbiór klientów którzy są w trakcie autoryzacji.
public class Authorization {
private Server server;
private int[] portNumbers;
private Set<Client> clientSet = new HashSet<>();
public Authorization(int[] portNumbers, Server server) throws SocketException {
this.portNumbers = portNumbers;
this.server = server;
createSocketListeners();
}
Authorization tworzy wątki które na każdym z portów nasłuchują na przychodzące datagramy.
private void createSocketListeners() throws SocketException {
byte[][] buffers = new byte[portNumbers.length][256];
DatagramPacket[] packets = new DatagramPacket[portNumbers.length];
DatagramSocket[] sockets = new DatagramSocket[portNumbers.length];
for (int i = 0; i < portNumbers.length; i++) {
packets[i] = new DatagramPacket(buffers[i], buffers[i].length);
sockets[i] = new DatagramSocket(portNumbers[i]);
new Thread(new SocketListener(packets[i], sockets[i], this, portNumbers[i])).start();
}
}
Klasa SocketListner otrzymuje port UDP i nasłuchuje na nim przychodzących pakietów. Gdy do portu przyjdzie pakiet, socketListner zapisuje z jakiego adresu oraz portu on pochodzi. Następnie wysyła te dane z powrotem do authorization.
class SocketListener implements Runnable {
private DatagramPacket packet;
private DatagramSocket socket;
private int portNumber;
private Authorization authorization;
SocketListener(DatagramPacket packet, DatagramSocket socket,
Authorization authorization, int portNumber) {
this.packet = packet;
this.socket = socket;
this.authorization = authorization;
this.portNumber = portNumber;
}
@Override
public void run() {
while (true) {
try {
socket.receive(packet);
ClientDTO c = new ClientDTO(packet.getAddress(),packet.getPort());
authorization.processClient(c, this);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Klasa authorization w metodzie procesClient przyjmuje obiekt ClientDTO oraz instancje SocketListenera, który otrzymał datagram. Na podstawie ClientDTO, i sekwencji portów tworzy obiekt Client. Następnie sprawdza czy klient o takim adresie jest już w trakcji autoryzacje. W przeciwnym razie dodaje go do zbioru. Kolejnym krokiem jest wywołanie w obiekcie klienta metody, która sprawdza czy sekwencja autoryzacji została ukończona. Jeżeli tak to obiekt ten zostaje przekazany do serwera jako zaufany.
Zastosowałem tu trochę dziwną konstrukcje, z iteratorem. Chodzi o to żeby do zmiennej client, do której na początku przypisywany jest nowy obiekt klienta, przypisać obiekt clienta ze zbioru ( jeżeli taki się tam znajduje). Później to właśnie na zmiennej client wywoływana jest metoda, sprawdzająca czy sekwencja została ukończona. Spowodowane jest to ograniczeniem interfejsu Set, z którego nie można bezpośrednio pobierać instancji obiektów. To jest jedno z miejsc do ewentualnej poprawki, może trzeba zamiast Set użyć Map?
synchronized void processClient(ClientDTO dto, SocketListener listener) {
Client client = new Client(dto, getSequence());
boolean contains = false;
for (Client c : clientSet) {
if (c.equals(client)) {
client = c;
contains = true;
}
}
if (!contains){
clientSet.add(client);
}
if (client.moveOnSequence(listener.getPortNumber())) {
clientSet.remove(client);
server.setAuthorizedClient(client);
}
}
Sekwencja, o której mowa w nazwie metody to Queue<Integers>, gdzie Integers są po prostu kolejnymi numerami porów sekwencji. Metoda
ta przyjmuje numer portu, na który klient wysłał datagram. Jeżeli port ten jest taki sam jak ten który znajduje się na początku kolejki. Numer tego portu jest usuwany z kolejki i sekwencja "idzie do przodu". Gdy z kolejki zostaną usunięte wszystkie numery portów, zakłada się że prawidłowa sekwencja została wysłana. Klient może uzyskać autoryzacje. Jeżeli numer portu nie będzie się zgadał to sekwencja zostanie cofnięta do momentu początkowego.
synchronized boolean moveOnSequence(int portNumber) {
if ( sequence != null && sequence.peek().equals(portNumber) ){
sequence.poll();
if ( sequence.isEmpty()) {
return true;
}
} else{
// resetSequence();
}
return false;
}
Jeżeli klient otrzymał autoryzacje jego instancja zostaje przekazana do obiektu Server, który losuje numer portu na którym będzie nasłuchiwał, oraz tworzy wątek obsługujący komunikacje z serwerem na tym porcie.
void setAuthorizedClient(Client client) {
int randomPortNumber = (int) (Math.random() * 50000) + 10000;
new Thread(new AuthorizedConnection(randomPortNumber, this, client)).start();
}
AuthorizedConnection tworzy port UDP z którego wysyła do klienta numer portu TCP na którym będzie nasłuchiwać na komunikacje od niego. Gdy komunikacje zostanie utworzona, klient może żądać zasobów serwera.
@Override
public void run() {
sendPortNumberByUDP();
TcpCommunication();
}
private void TcpCommunication(){
try (
ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
) {
String inputLine, outputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(inputLine);
if ((outputLine = server.getRecourse(inputLine)) != null){
out.println(outputLine);
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void sendPortNumberByUDP() {
try (DatagramSocket socket = new DatagramSocket()){
byte[] portMessage = String.valueOf(portNumber).getBytes();
byte[] buf = new byte[256];
DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length,client.getInetAddress(),client.getPortNumber());
datagramPacket.setData(portMessage);
socket.send(datagramPacket);
} catch (IOException e){
e.printStackTrace();
}
}
Procesu Klienta nie będę przedstawiał, jest bardzo prosty. Wysyła datagramy na odpowiednie porty, oczekuje na numer portu TCP, otwiera komunikacje TCP na danym porcie i może wysyłać zapytania.
Chciałbym prosić o ocenę poprawności podejścia. Czy założenia są spełnione? Czy program napisany jest zgodnie ze sztuką, oraz co trzeba w nim poprawić by z tą sztuką był zgodny. Problem wydawał mi się ciekawy, dlatego chciałbym go dobrze zrozumieć. Jeżeli opis jest w którymś miejscu nie jasny, postaram się go jak najszybciej poprawić.
P.S Jestem młodym programistą, dlatego niektóre rzeczy mogą być dla mnie nieoczywiste :P.