LOA Hacklab - Corso di Perl - 13/05/2001 ----------------------------------------------------------------------- 1) Intro + Bibliografia 2) I socket 3) Perlfunc 4) IO::Socket (saddafa') IO::Socket::INET IO::Socket::UNIX 5) Net::SMTP e Net::POP3 Un po' di teoria Un po' di pratica (saddafa') 6) Esempi INTRO + BIBLIOGRAFIA ----------------------------------------------------------------------- All'interno di questa lezione impareremo a gestire i socket con Perl, grazie a una serie di librerie fornite insieme all'interprete o scaricabili dalla Rete. Le librerie standard, infatti, vi consentiranno di aprire e gestire connessioni tramite socket, di creare server che utilizzano protocolli gia' esistenti o ideati da voi e, nel caso delle funzioni a livello piu' alto, di gestire diversi tipi di connessioni (ad esempio ai server web o di posta) in modo semplice e rapido. Scaricando un pacchetto come Net::RawIP, invece, avrete la possibilita' di forgiare i vostri pacchetti e di gestire le connessioni a un livello decisamente piu' basso. Per quanto riguarda la documentazione utilizzata per questa lezione, mi sono basato soprattutto su "Network programming in Perl", scaricabile dal sito http://www.1024kb.net: credo che ringraziare la santa persona che l'ha scritto (Sander Wahls) sia il minimo, visto che gli ho fregato pure gli esempi! Per chiarire il funzionamento delle varie funzioni messe a disposizione da IO::Socket ho controllato, invece, la documentazione del package fornita con ActivePerl, la quale fa riferimento anche alle pagine perlfunc (cercate "low-level socket functions") e perlipc (per dei buoni esempi sulla programmazione di socket in Perl). Per i socket raw, invece, potete scaricare il package e la relativa documentazione dal sito http://quake.skif.net/RawIP. Infine, per un esempio di applicazione che fa uso massiccio di socket potete dare un'occhiata al "progetto ANO", che si trova all'interno del numero 8 di Butchered From Inside scaricabile dal sito http://www.s0ftpj.org. I socket ----------------------------------------------------------------------- I socket sono l'interfaccia software che sta ai capi di una connessione di rete: detto in parole semplici, quando create un socket dovete connetterlo a un altro da qualche parte nella rete e tutto cio' che scrivete sul vostro socket puo' essere letto dall'altro (e viceversa). Utilizzando la libreria IO::Socket, che fornisce una serie di funzioni e di classi per gestire i socket in modo abbastanza semplice e rapido, possiamo aprire un socket semplicemente creando una nuova istanza della classe Socket tramite il suo costruttore new. Una volta aperto il socket, possiamo chiuderlo utilizzando l'handle del socket come se fosse un normale file handle. #!/usr/bin/perl # include the package use IO::Socket; # open the socket $socket = IO::Socket::INET->new(Proto=>"tcp", LocalPort=>"12345", Listen=>"1") or die "Can't open socket\n"; # and close it close $socket; Come potete notare, in fase di creazione di un socket e' necessario passargli alcuni parametri per decidere, ad esempio, quale porta desideriamo aprire o a quale vogliamo connetterci. I parametri possibili sono i seguenti: PeerAddr Remote host address PeerPort Remote port or service LocalAddr Local host bind address LocalPort Local host bind port Proto Protocol name Type Socket type Listen Queue size for listen Reuse Set SO_REUSEADDR before binding Timeout Timeout value for various operation Naturalmente, non e' obbligatorio passare tutti i parametri al metodo: basta inserire quelli che servono al particolare tipo di socket, come ad esempio Proto, LocalPort e Listen per i server e PeerAddr, PeerPort e Proto per i programmi client. In precedenza abbiamo detto che e' possibile chiudere il socket tramite l'handle nello stesso modo in cui chiudiamo un file: questo vale anche per le operazioni di lettura e di scrittura! Se date un'occhiata al seguente esempio (un piccolo webgrep, che invia una richiesta a un server web e mostra a schermo la risposta del server) potrete farvi un'idea di come funziona la comunicazione via socket: #!/usr/bin/perl use IO::Socket; if ($#ARGV < 1) { die "perl example-2.pl \n" } $server = $ARGV[0]; $document = $ARGV[1]; $remote = IO::Socket::INET->new(Proto=>"tcp", PeerAddr=>$server, PeerPort=>"80", Reuse=>1) or die "Can't connect to \"$server\"\n"; # set buffering off $remote->autoflush(1); # write HTTP request to server print $remote "GET $document HTTP/1.0\n\n"; # receive everything the server sends and print it to the screen while (<$remote>) { print } close $remote; Notate il comando autoflush... Perlfunc ----------------------------------------------------------------------- Visto che molti dei metodi di IO::Socket sono poco piu' che dei front-end alle funzioni standard di Perl, ecco un elenco dei comandi relativi ai socket che potete anche nella pagina perlfunc della documentazione ufficiale. accept NEWSOCKET, GENERICSOCKET Accetta una connessione socket in entrata. Restituisce l'indirizzo packed se la connessione ha avuto successo, FALSE altrimenti. bind SOCKET, NAME Effettua il bind di un indirizzo di rete con un socket. Restituisce TRUE se ha successo, FALSE altrimenti. NAME dev'essere un indirizzo packed del tipo appropriato per il socket. (man 2 bind) connect SOCKET, NAME Cerca di connettersi a un socket remoto, restituendo TRUE se la chiamata va a buon fine, FALSE altrimenti. NAME dev'essere un indirizzo packed del tipo appropriato per il socket. (man 2 connect) getpeername SOCKET Restituisce l'indirizzo packed dell'altro capo della connessione socket. Puo' essere utilizzato in questo modo: use Socket; $hersockaddr = getpeername(SOCK); ($port, $iaddr) = sockaddr_in($hersockaddr); $herhostname = gethostbyaddr($iaddr, AF_INET); $herstraddr = inet_ntoa($iaddr); (man 2 getpeername) getsockname SOCKET Restituisce l'indirizzo packed di questo capo della connessione socket. Puo' tornare utile nel caso non si conosca il proprio indirizzo IP. (man 2 getsockname) getsockopt SOCKET, LEVEL, OPTNAME Insieme a setsockopt, serve a manipolare le opzioni associate a un socket. Tali opzioni possono essere presenti a vari livelli del protocollo, ma sono sempre presenti al livello superiore. Per accedere all'opzione desiderata, vengono utilizzati i parametri LEVEL e OPTNAME. La funzione restituisce la socket option richiesta, o undef se si verifica un errore. (man 2 getsockopt) listen SOCKET, QUEUESIZE Effettua la medesima operazione della chiamata di sistema listen, mettendosi in attesa di connessioni in arrivo e impostando un limite massimo di connessioni. Restituisce TRUE se l'operazione va a buon fine, FALSE in caso contrario. (man 2 listen) recv SOCKET, SCALAR, LENGTH, FLAGS Riceve un messaggio da un socket, cercando di ricevere LENGTH byte di dati nella variabile SCALAR dal SOCKET specificato. Riceve gli stessi flag dell'omonima chiamata di sistema (man 2 recv). Se il protocollo del socket lo supporta, restituisce l'indirizzo del mittente, altrimenti la stringa vuota. Se si verifica un errore, restituisce undef. (v. anche man 2 recvfrom) send SOCKET, MSG, FLAGS, [TO] Invia un messaggio a un socket. Per i socket non connessi dovete specificare una destinazione in TO. Restituisce il numero di caratteri inviati, o il valore undef se si verifica un errore. (man 2 send) setsockopt SOCKET, LEVEL, OPTNAME, OPTVAL Imposta l'opzione socket come richiesto, restituendo undef se si verifica un errore. OPTVAL deve avere il valore undef se non si desidera passare un argomento. (man 2 setsockopt) shutdown SOCKET, HOW Chiude una connessione socket nel modo specificato da HOW, che puo' assumere i seguenti valori: shutdown(SOCKET, 0); # ho finito di leggere dati shutdown(SOCKET, 1); # ho finito di scrivere dati shutdown(SOCKET, 2); # ho finito di usare questo socket Questo puo' tornare particolarmente utile quando desiderate dire a un socket che avete finito di scrivere dati ma non di leggerli (o viceversa). E' anche una forma di chiusura piu' definitiva, in quanto disabilita il file descriptor in tutte le copie forkate negli altri processi. (man 2 shutdown) socket SOCKET, DOMAIN, TYPE, PROTOCOL Apre un socket del tipo specificato e lo collega al filehandle SOCKET. DOMAIN, TYPE e PROTOCOL sono gli stessi utilizzati dalla chiamata di sistema socket (man 2 socket): DOMAIN PF_UNIX, PF_LOCAL Comunicazioni locali PF_INET IPV4 Internet protocols PF_INET6 IPV6 Internet protocols PF_IPX IPX - Novell protocols etc. TYPE SOCK_STREAM SOCK_DGRAM SOCK_SEQPACKET SOCK_RAW SOCK_RDM SOCK_PACKET (obsoleto) PROTOCOL specifico al "communication domain" (vi /etc/protocols) (man 5 protocols) socketpair SOCKET1, SOCKET2, DOMAIN, TYPE, PROTOCOL Crea una coppia di socket connessi fra di loro nel dominio e del tipo specificati, restituendo TRUE se l'operazione va a buon fine. Ecco ora un paio di esempi (presi da perlipc) di come vengono utilizzati i vari comandi appena elencati: CLIENT ------ #!/usr/bin/perl -w use strict; use Socket; my ($remote,$port, $iaddr, $paddr, $proto, $line); $remote = shift || 'localhost'; $port = shift || 2345; # random port if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') } die "No port" unless $port; $iaddr = inet_aton($remote) || die "no host: $remote"; $paddr = sockaddr_in($port, $iaddr); $proto = getprotobyname('tcp'); socket(SOCK, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; connect(SOCK, $paddr) || die "connect: $!"; while (defined($line = )) { print $line; } close (SOCK) || die "close: $!"; exit; SERVER ------ #!/usr/bin/perl -Tw use strict; BEGIN { $ENV{PATH} = '/usr/ucb:/bin' } use Socket; use Carp; $EOL = "\015\012"; sub logmsg { print "$0 $$: @_ at ", scalar localtime, "\n" } my $port = shift || 2345; my $proto = getprotobyname('tcp'); $port = $1 if $port =~ /(\d+)/; # untaint port number socket(Server, PF_INET, SOCK_STREAM, $proto) || die "socket: $!"; setsockopt(Server, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)) || die "setsockopt: $!"; bind(Server, sockaddr_in($port, INADDR_ANY)) || die "bind: $!"; listen(Server,SOMAXCONN) || die "listen: $!"; logmsg "server started on port $port"; my $paddr; $SIG{CHLD} = \&REAPER; for ( ; $paddr = accept(Client,Server); close Client) { my($port,$iaddr) = sockaddr_in($paddr); my $name = gethostbyaddr($iaddr,AF_INET); logmsg "connection from $name [", inet_ntoa($iaddr), "] at port $port"; print Client "Hello there, $name, it's now ", scalar localtime, $EOL; } IO::Socket ----------------------------------------------------------------------- accept([PKG]) perform the system call accept on the socket and return a new object. The new object will be created in the same class as the listen socket, unless PKG is specified. This object can be used to communicate with the client that was trying to connect. In a scalar context the new socket is returned, or undef upon failure. In an array context a two-element array is returned containing the new socket and the peer address; the list will be empty upon failure. socketpair(DOMAIN, TYPE, PROTOCOL) Call socketpair and return a list of two sockets created, or an empty list on failure. --------------- timeout([VAL]) Set or get the timeout value associated with this socket. If called without any arguments then the current setting is returned. If called with an argument the current setting is changed and the previous value returned. sockopt(OPT [, VAL]) Unified method to both set and get options in the SOL_SOCKET level. If called with one argument then getsockopt is called, otherwise setsockopt is called. sockdomain Returns the numerical number for the socket domain type. For example, for a AF_INET socket the value of &AF_INET will be returned. socktype Returns the numerical number for the socket type. For example, for a SOCK_STREAM socket the value of &SOCK_STREAM will be returned. protocol Returns the numerical number for the protocol being used on the socket, if known. If the protocol is unknown, as with an AF_UNIX socket, zero is returned. connected If the socket is in a connected state the the peer address is returned. If the socket is not in a connected state then undef will be returned. Net::SMTP e Net::POP3 ----------------------------------------------------------------------- Un po' di teoria ---------------- Un server POP3 funziona grossomodo cosi': - Una volta avviato, esso attende la connessione su una particolare porta TCP (di norma, la 110): quando un client effettua una connessione TCP con il server, esso risponde con un messaggio di benvenuto e si prepara a dialogare con il client finche' la connessione non viene chiusa. - I comandi da inviare al server POP3 consistono di una parola chiave, solitamente lunga 3 o 4 caratteri, seguita da un argomento. Le risposte del server consistono di un indicatore di stato ("+OK" o "-ERR") seguito eventualmente da una parola chiave e da alcuni argomenti. Nei casi in cui la risposta del server sia composta da piu' linee, essa termina con una riga contenente un singolo punto. - Il server attraversa tre stati distinti durante una connessione: nel primo, chiamato AUTHORIZATION, consente al client di identificarsi; nel secondo, chiamato TRANSACTION, esegue azioni per il client come ad esempio l'invio di messaggi o di statistiche relative alla mailbox; nel terzo, chiamato UPDATE, il server cancella le risorse non piu' utili e chiude la connessione. Segue ora un rapido elenco dei principali comandi riconosciuti da un server pop3, insieme allo stato in cui essi vengono riconosciuti e a una breve spiegazione. STATO | COMANDO | DESCRIZIONE -------+----------------+--------------------------------------------------- AUTH | | Alla connessione, senza bisogno di inviare alcun | | comando, il server risponde con un +OK seguito da | | una stringa arbitraria. +----------------+--------------------------------------------------- | USER login | Il client invia la login per l'account con questo | | comando: il server risponde con un +OK se la login | | e' valida, altrimenti invia un -ERR. La stringa | | che segue +OK o -ERR e' arbitraria. +----------------+--------------------------------------------------- | PASS password | Quando il client riceve l'ok per la login, invia | | la password dell'account con questo comando. Anche | | in questo caso, le risposte valide sono +OK e -ERR | | seguite da una stringa arbitraria. +----------------+--------------------------------------------------- | QUIT | Il comando, quando viene inviato in questo stato, | | causa la disconnessione dal server senza il | | passaggio allo stato di UPDATE. -------+----------------+--------------------------------------------------- TRANS | STAT | Il server risponde con un +OK seguito da un "drop | | listing", cioe' una stringa contenente alcune | | informazioni (per esattezza, due numeri che | | corrispondono, rispettivamente, al numero di | | messaggi e al numero totale di byte da scaricare). | | Ad esempio, nel caso di due messaggi per un totale | | di 2k, la risposta sara' +OK 2 2048. | | (NOTA: per semplicita' gli "octet" son chiamati | | semplicemente byte :) +----------------+--------------------------------------------------- | LIST [msg] | Il server risponde con uno "scan listing" del msg | | qualora esso sia specificato. Se il comando e' | | lanciato senza parametri, il server risponde con | | un +OK seguito dagli scan listing di tutti i | | messaggi, uno per riga, terminato da un punto. Lo | | scan listing equivale a +OK nn mmm, dove nn e' il | | numero del messaggio e mmm il numero di byte. +----------------+--------------------------------------------------- | RETR msg | Se il messaggio esiste, il server risponde con un | | +OK e invia il messaggio, terminandolo con un | | punto. +----------------+--------------------------------------------------- | DELE msg | Marca un messaggio per la cancellazione. +----------------+--------------------------------------------------- | NOOP | Il server non fa nulla e risponde semplicemente | | con un +OK. +----------------+--------------------------------------------------- | RSET | Toglie la marcatura per la cancellazione da tutti | | i messaggi. +----------------+--------------------------------------------------- | TOP msg n | Se il messaggio esiste, il server risponde con un | | +OK, quindi invia al client gli header, una riga | | bianca e le prime "n" righe del messaggio. Se il | | numero "n" e' maggiore del numero totale delle | | righe del messaggio, il server invia semplicemente | | tutto il messaggio. -------+----------------+--------------------------------------------------- UPD | QUIT | Il comando quit, se inviato durante TRANSACTION, | | permette di passare allo stato di update dove | | vengono cancellati gli eventuali file marcati | | per l'eliminazione e viene chiusa la connessione. I seguenti sono, invece, i comandi normalmente accettati da un server SMTP: COMANDO | RISPOSTA DEL SERVER | AZIONE EFFETTUATA -----------+--------------------------+------------------------------------- HELO | 250 Hello... | Nessuna: il server si limita a | | rispondere con un saluto al client. -----------+--------------------------+------------------------------------- MAIL FROM | 250 ... Sender ok | Il server da' l'ok e salva | | nome e indirizzo del mittente. -----------+--------------------------+------------------------------------- NOOP | 200 OK | Nessuna. -----------+--------------------------+------------------------------------- RSET | 250 Reset State | Il server effettua un "reset". -----------+--------------------------+------------------------------------- QUIT | 221 closing | Il server chiude la connessione. | connection | -----------+--------------------------+------------------------------------- RCPT TO: | 250 ... Recipient ok | Il server da' l'ok e salva l'indi- | | rizzo del destinatario. -----------+--------------------------+------------------------------------- DATA | 354 Enter mail, end with | Il server si mette in attesa della | a "." on a line by | mail e accetta tutto il testo | itself | inserito finche' non vede una riga | | composta da un solo punto: a questo | | punto considera il messaggio | | terminato e lo invia. Esempi ----------------------------------------------------------------------- FINGERD.PL------------------------------------------------------------------- Un semplice demone aperto sulla porta finger (ma che in realta' con finger c'entra ben poco!) #!/usr/bin/perl # Simple finger daemon use IO::Socket; # $\="\r\n"; # output line delimiter # listen on the well-known finger port my $port = 79; # create a server socket to listen for # connections $sock = new IO::Socket::INET (LocalPort => $port, Proto => 'tcp', Listen => 5, Reuse => 1) || die "Socket error: $!\n" unless $sock; print "Finger server started...\n"; # listen for connections # don't disconnect after the first connection # just keep listening $i=1; while (i) { # new connection accepted $new_sock = $sock->accept(); # get the IP of the client, and the hostname $ip = $new_sock->peerhost(); ($name,$alias,$addrtype,$length,$new_addr) = gethostbyaddr(inet_aton($ip),AF_INET); # print the hostname and IP to the screen print "Connection from: $name [$ip]\n"; send $new_sock, "Hi\r\n\n>", 0; $i=1; while ($i==1){ # get the query from the client and display it $query = <$new_sock>; chop $query; chop $query; if ($query) { print "Query: $query\n"; } else { print "$name [$ip] disconnected.\n"; $i=0; } # send a message back to the client send $new_sock, "$name [$ip] queried $query\n\r>", 0; if ($query =~ /^quit$/i) { close($new_sock); print "$name [$ip] disconnected.\n"; $i=0; } if ($query =~ /^stop$/i) { close($new_sock); print "$name [$ip] disconnected.\n"; $i=0; die "Switching off the server.\n"; } } } FINGERC.PL------------------------------------------------------------------- Questo dovrebbe essere un client finger, pure funzionante :) #!/usr/bin/perl use IO::Socket; # This line does some very simple checking # to see that the proper number of arguments # (ie, 1) are passed. Admittedly, it's a # little messy if it exits... $test = shift || die usage(); # Separate the username and the host from # the argument string ($user,$host) = split(/\@/,$test); # If no username was given (ie, @domain.com) # then use a carriage return as the username $user="\n" if ($user eq ""); finger($user,$host); ################################################# # Send data to a port, read back all data # (finger) ################################################# sub finger { my ($user,$host) = @_; print "Finger [$host]...\n"; $remote = IO::Socket::INET -> new ( Proto => "tcp", PeerAddr => $host, PeerPort => 79) || die "Could not connect to $host: $!\n"; # Now that the socket is set up and connected, # we just need to send the username to the server. # That's how a finger query is done... if ($user eq "\n") { print $remote "\n"; } else { print $remote "$user\n"; } # Read in data as long as the server continues to # send it to us...and print it out to the screen. # Notice that we're going to let the remote server # close the connection...we're not using a close() # method call. while(<$remote>){ print; } print "\n"; } ################################################# # usage() ################################################# sub usage { print "finger.pl, by Keydet89\n"; print "copyright 1999 Keydet89\n\n"; print "finger.pl user\@host\n";