V2004 06 Vbj60

  • October 2019
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View V2004 06 Vbj60 as PDF for free.

More details

  • Words: 31,246
  • Pages: 68
ED I T O R I A L E

online.infomedia.it n. 60 - novembre/dicembre 2004 bimestrale - anno decimo

Direttore Responsabile Marialetizia Mari ([email protected])

Direttore Esecutivo Francesco Balena ([email protected])

Technical Editor

Le community e CodeZone

Alberto Falossi ([email protected])

Managing Editor Renzo Boni ([email protected])

Collaboratori Marco Bellinaso, Daniele Bochicchio, Lorenzo Braidi, Gianluca Cannalire, Dino Esposito, Massimiliano Mariconda, Davide Mauri, Paolo Pialorsi, Ingo Rammer, Marco Russo, Fabio Scagliola, Lorenzo Vandoni

Direzione Natale Fino ([email protected])

Marketing & Advertising Segreteria: 0587/736460 [email protected]

Amministrazione Sara Mattei ([email protected])

Grafica Manola Greco ([email protected])

Technical Book Lisa Vanni ([email protected])

Segreteria Enrica Nassi ([email protected])

Stampa TIPOLITOGRAFIA PETRUZZI Citta’ di Castello (PG)

Ufficio Abbonamenti Tel. 0587/736460 - Fax 0587/732232 e-mail: [email protected] www.infomedia.it

S

crivo questa pagina al ritorno dall’evento Connect a Barcellona, dove Microsoft ha invitato gli animatori delle principali community e siti Web dedicati alle tecnologie e ai linguaggi di programmazione .NET. Sono stati due giorni molto intensi e molto interessanti, soprattutto per la possibilità di confrontarsi sulla nuova iniziativa sulle community che Microsoft sta portando avanti da qualche tempo. Immagino che qualcuno di voi abbia già sentito parlare di CodeZone, per gli altri ecco un piccolo riassunto. Si tratta di una specie di portale+motore di ricerca che i programmatori .NET potranno usare per arrivare facilmente agli articoli più interessanti su queste tecnologie, tra quelli prodotti appunto dai numerosi siti Web e community sparsi per il mondo. La cosa interessante è che ci saranno tanti portali CodeZone, uno per ciascuna area geografica o lingua. Potete trovare maggiori informazioni a http://www.codezone.info e potete persino abbonarvi ad una rivista abbastanza ben fatta (gratuita, ma in inglese). L’iniziativa è molto interessante e quasi certamente destinata al successo. Perché quasi? Non certo perché a Microsoft mancano i mezzi per imporre CodeZone al mondo degli sviluppatori. Il motivo che ho per dubitare (ma solo un pochino...) ha un nome ben preciso: Google. Ce la farà CodeZone ad imporre la sua specificità e guadagnare le simpatie degli sviluppatori che da anni oramai si affidano alla completezza di Google? Nessuno lo può sapere, ma sarà interessante vedere cosa succederà. Di certo è la prima volta che la più grande software house del mondo deve combattere un osso così duro, e il fatto che la Borsa continui a premiare Google non può che alzare il livello della contesa. Termino questa pagina con una piccola novità (per voi) ma grande (per il sottoscritto). Quando leggerete questo editoriale sarà già attivo www.dotNet2TheMax.it, la versione italiana del sito VB2TheMax, che ho fondato nel ’99 e a cui hanno collaborato alcuni tra i migliori autori ed esperti italiani e non solo (ecco spiegata la mia partecipazione all’evento Connect). Sul nuovo sito troverete tantissimi tip e articoli e stiamo anche preparando una sezione speciale dedicata ai programmatori VB6 che vogliono migrare a VB.NET. Arrivederci sulla rete.

Gruppo Editoriale Infomedia srl Via Valdera P., 116 - 56038 Ponsacco (PI) Italia Tel. 0587/736460 - Fax 0587/732232 [email protected] Sito Web www.infomedia.it

Francesco Balena [email protected]

Manoscritti e foto originali anche se non pubblicati, non si restituiscono. È vietata la riproduzione anche parziale di testi e immagini. Si prega di inviare i comunicati stampa e gli inviti stampa per la redazione all’indirizzo: [email protected] Visual Basic Journal è una rivista di Gruppo Editoriale Infomedia S.r.l. Via Valdera P, 116 Ponsacco - Pisa.

Registrazione presso il Tribunale di Pisa n. 20/1999

N. 60 - Novembre/Dicembre 2004 VBJ

3

SOMMARIO

NOVEMBRE/DICEMBRE

N.60

SPECIALE

Vulnerabilità del codice

7

Analisi dei casi più comuni di vulnerabilità delle applicazioni software.

di Paolo Pialorsi

La sicurezza del codice managed

13

Vediamo alcune delle principali caratteristiche del Framework .NET rivolte alla sicurezza del codice e delle applicazioni.

di Paolo Pialorsi

Applicazioni sicure con IIS 6 e ASP .NET

20

L’accoppiata IIS 6 e ASP.NET garantisce la possibilità di creare e far funzionare in maniera sicura applicazioni web con pochi e semplici sforzi. Anche se entrambi i sistemi sono già sicuri appena installati, è necessario un ulteriore tocco per garantire alle nostre applicazioni un elevato livello di sicurezza.

di Daniele Bochicchio

Sviluppare senza essere amministratori

25

Sviluppare applicazioni senza essere amministratori è importante per la sicurezza e per scrivere applicazioni eseguibili con il minimo dei privilegi necessari. Vediamo cosa c’è da sapere su Windows e Visual Studio .NET e quali tool sono utili per affrontare questo scenario.

di Marco Russo

BEGINNER

Programmare Excel da Visual Basic .NET

31

Analisi di tre soluzioni per trasferire dati a Excel usando Visual Basic .NET e le classi del .NET framework

di Fabio Scagliola

ENTERPRISE

Servizi NT con VB .NET (seconda parte)

38

Caso reale di realizzazione e implementazione di servizi Windows con .NET

di Gianluca Cannalire

WEB

Grafici dinamici in ASP .NET Vale la pena di sprecare tempo per creare grafici da zero? Molto meglio utilizzare una libreria altamente personalizzata. Vediamo tutti i pregi di .netCHARTING

di Dino Esposito

4

VBJ N. 60 - Novembre/Dicembre 2004

45

RUBRICHE

Editoriale Posta

3 54

a cura di Alberto Falossi

Recensione libri

61

.NET Tools

62

a cura di Davide Mauri

ARCHITECT'S CORNER

Applicazioni scalabili in pratica

50

“Two is meglio che one” diceva una nota pubblicità. In questa puntata daremo dei consigli pratici per progettare e implementare un’applicazione scalabile.

di Ingo Rammer

SOFTWARE ENGINEERING

L'accesso ai dati nei linguaggi object-oriented Tutti i linguaggi object-oriented devono fare i conti con la necessità di gestire dati in formato relazionale

di Lorenzo Vandoni

56

DIFENDERSI DAGLI HACKER

Vulnerabilità del codice Analisi dei casi più comuni di vulnerabilità delle applicazioni software.

di Paolo Pialorsi uando si parla di sicurezza delle applicazioni e del codice ci si riferisce al fatto che le applicazioni devono essere progettate e realizzate considerando e prevenendo i casi più diffusi di attacco. Questo articolo ha l’obiettivo di fornire una panoramica dei principali e più diffusi casi di vulnerabilità, insieme ai consigli per evitare di sviluppare applicazioni che siano in tal senso attaccabili.

Q

Buffer Overrun È sicuramente il più famoso tipo di attacco e non potevamo non parlarne in un articolo come questo. Riguarda tipicamente le applicazioni sviluppate con linguaggi che consentono la gestione diretta delle aree di memoria come C e C++. È mediamente difficile da realizzare nei confronti di applicazioni sviluppate in Visual Basic o in ambienti di esecuzione con memoria gestita (come .NET e Java) anche se in taluni casi, quando queste applicazioni si appoggiano a librerie esterne scritte per esempio in C/C++, è comunque possibile essere vulnerabili. Si basa sul fatto che l’accesso diretto e non controllato alla memoria può consentire a un hacker di sovrascriverne delle aree. Esistono diverse tipologie di buffer overrun, una delle più diffuse e che prenderemo come esempio è quella sulle variabili di stack (Figura 1). Nei linguaggi come il C le variabili stringa non sono altro che sequenze di byte in memoria, terminate dal carattere ASCII 0. Pensiamo a una procedura definita come la seguente: C void foo(const char* input) { char buf[10]; strcpy(buf, input); }

Paolo Pialorsi è un consulente e autore specializzato nello sviluppo di Web Service e soluzioni Web con il Framework .NET di Microsoft. Lavora nell’omonima società Pialorsi Sistemi S.r.l. e fa parte del gruppo DevLeap. Può essere contattato via email: [email protected].

La variabile buf può contenere al massimo 10 byte. Del codice invocherà foo, che riceverà in ingresso un puntatore a caratteri chiamato input, che copiamo con la funzione strcpy dentro alla variabile buf. Cosa accade se input è di dimensione maggiore di 10 byte? La funzione strcpy non svolge alcun controllo sulla dimensione di input e di buf, il nostro codice nemmeno. L’eccedenza sarà copiata in memoria oltre lo spazio allocato nello stack di foo per la variabile buf. Per come funziona l’esecuzione del codice in memoria, nella parte finale dello stack allocato per l’esecuzione di foo, cioè dopo buf, avremo l’indirizzo di ritorno della procedura. Se il contenuto di input è sufficientemente lungo potrà sovrascrivere questo indirizzo di ritorno, cambiando l’esecuzione dell’applicazione. Un trucco come questo, eseguito ai danni di un servizio del sistema operativo, che tipicamente è eseguito con un buon livello di privilegi, consente di eseguire del codice molto pericoloso. I più famosi attacchi ai sistemi Windows hanno spesso sfruttato dei buffer overrun di IIS o di altri servizi molto diffusi, per cambiare l’esecuzione del codice ed eseguire delle escalation di privilegi. La soluzione a questo problema è controllare sempre la dimensione dei buffer prima di usarli, in particolare con funzioni a rischio (strcpy, strncpy, CopyMemory, MultiByteToWideChar). Con l’ultima versione di compilatore C++ di Microsoft si può utilizzare l’opzione /GS che aiuta a individuare i buffer overrun (anche se di per sé non è una protezione sicura al 100%). Inoltre possiamo utilizzare delle funzio-

N. 60 - Novembre/Dicembre 2004

VBJ

7

DIFENDERSI DAGLI HACKER

Questo codice, oltre che poco elegante, è anche decisamente insicuro! Finché sul nostro sito navigano utenti normali è tutto ok. Io cerco “birra rossa” e la query risultante sarà “ SELECT * FROM Prodotti WHERE Descrizione = ‘birra rossa’ ”. Ma cosa accade se la ricerca viene utilizzata da un hacker, che magari ne capisce un po’ di codice SQL? Potrebbe cercare “qualcosa’ OR ‘1’ = ‘1” che inserito nel nostro codice produrrebbe la seguente query: SQL SELECT * FROM Prodotti WHERE Descrizione = ‘qualcosa’ OR ‘1’ = ‘1’

Figura 1

Schema di un buffer overrun su stack

ni per le stringhe “sicure”, disponibili nel file di inclusione strsafe.h. Infine, conviene sempre controllare tutti gli indici delle matrici prima di usarle, così come le lunghezze massime dei path di file e cartelle. Se poi è possibile, conviene passare a codice interamente gestito (.NET).

SQL Injection Con questa espressione ci si riferisce agli attacchi rivolti a database, tipicamente esposti da applicazioni Web, portati a termine sfruttando un errato o insufficiente controllo dell’input dell’utente da parte di noi programmatori. Si tratta per altro di una delle vulnerabilità ancora oggi più diffuse e utilizzate per violare la sicurezza di un server e delle reti alle loro spalle. È doveroso dire che attacchi di questo tipo hanno quale unica causa una scarsa attenzione da parte del programmatore, non esistono infatti ragioni “tecnologiche” che giustifichino simili vulnerabilità. Vediamo di cosa si tratta. Pensiamo a una classica applicazione Web che consente agli utenti di ricercare dei prodotti in un sito di commercio elettronico. La pagina di ricerca potrebbe fornire una casella di testo, chiamiamola txtRicerca, e un pulsante di ricerca, cmdCerca. Alla pressione di cmdCerca sul server verrà eseguito del codice che, concatenando a mano i pezzi di codice SQL, costruisce una query che rappresenta la ricerca da eseguire sul database, come mostrato nel seguente esempio: VBScript <% Dim sQuery ‘As String sQuery = “SELECT * FROM Prodotti WHERE Descrizione = ‘” + Request(“txtRicerca”) + “’” … %>

8

VBJ

N. 60 - Novembre/Dicembre 2004

Il risultato sarebbe la lista di tutti i prodotti del nostro sito Web di commercio elettronico, perchè ‘1’ = ‘1’ è una condizione sempre vera. Poco male, direte, tanto se quei prodotti li vendo l’hacker non scopre nulla di nuovo. In questo caso è proprio così, ma pensiamo a un caso diverso: una pagina di login che utilizza i campi txtUserID e txtPassword per autenticare un utente. Provo a ipotizzare del codice che mi capita di vedere abbastanza spesso: VBScript <% Dim sQuery ‘As String sQuery = “SELECT * FROM Utenti WHERE UserID = ‘” + Request(“txtUserID”) + “’ AND Password = ‘” + Request(“txtPassword”) + “’” ‘ Ottengo I dati all’interno di un ADODB.Recordset If Not rs.EOF Then ‘ Se ho almeno un record significa che l’utente esiste End If %>

Applicando lo stesso ragionamento di prima, in questo caso un valore intenzionalmente modificato del campo txtUserID o txtPassword consente a un hacker di accedere al sistema anche senza password! Ma come può un hacker sapere che dietro la nostra pagina Web c’è proprio del codice di quel tipo? È più semplice di quello che sembra. Molto spesso una delle parti più trascurate di un’applicazione è la gestione degli errori, che a volte è addirittura inesistente. La prima cosa che un hacker può fare è scrivere dei valori non troppo casuali che facciano andare in errore il motore SQL, nel caso in cui il nostro codice si limiti a concatenare le stringhe per costruire le query. Per esempio può mettere nella casella txtUserID un valore come: “qualcosa’ SELECT”. Ovviamente concantenando le stringhe otterrebbe un errore simile al seguente: Errore VBScript Microsoft OLE DB Provider for SQL Server (0x80040E14) Line 1: Incorrect syntax near ‘ AND Password = ‘.

© 2004 Microsoft. Tutti i diritti riservati. Tutti i marchi registrati citati sono di proprietà delle rispettive società.

Scrivi il codice che conta. Al resto ci pensa Visual Studio .NET 2003

Oggi le tue idee possono diventare ancora più grandi.

Visual Studio .NET 2003 riduce notevolmente la quantità di codice da scrivere, lasciandoti libero di concentrarti sulle cose più importanti. Leader, il maggior distributore italiano di videogiochi, ha sviluppato un’applicazione di Sales Force Automation in sei mesi/uomo: “Visual Studio .NET 2003 ci ha garantito un notevole risparmio dei tempi di sviluppo, grazie alla ridotta curva di apprendimento del nuovo linguaggio C# e all’immediatezza dell’ambiente visuale. L’applicazione sviluppata ha incrementato l’efficienza dei nostri processi di vendita: ora è sufficiente un Tablet PC per avere la completa disponibilità in tempo reale delle informazioni di prodotto, fornire preventivi personalizzati al cliente e gestire gli ordini.” Per scoprire come Visual Studio .NET 2003 può aiutarti a concentrarti sulle cose più importanti, visita il sito microsoft.com/italy/vstudio/value/

004910-VisualBasic-205x275 1

1-09-2004, 17:12:50

DIFENDERSI DAGLI HACKER

Da questo errore potrebbe capire che stiamo proprio concatenando i pezzi di testo senza verificarli. L’aspetto ancor più grave di questa vulnerabilità è che, una volta riscontrata, spesso è possibile ottenere l’accesso non solo alla tabella oggetto della query ma, componendo opportunamente le stringhe, anche ad altre tabelle di dati, eseguendo per esempio delle UNION. Come riesce un hacker a conoscere i nomi delle tabelle e delle colonne da richiedere al nostro server tramite SQL Injection? Spesso il modo più semplice è chiederle proprio al database stesso. Pensando a un SQL Server come server di database, nessuno vieta di interrogare le tabelle di sistema per sapere quali sono le tabelle e le loro colonne. I gradi di libertà che lasciamo a un hacker sono legati a quanto abbiamo messo in sicurezza la nostra soluzione software. Se per esempio stiamo utilizzando il database con un utente ad alto livello di privilegi, non voglio dire l’utente SA di SQL Server (quello ormai dovremmo aver imparato tutti a non usarlo e a mettergli una password!), ma comunque un utente dbo... beh, probabilmente siamo fritti! L’utente dbo può tutto sul database corrente, quindi l’hacker, dopo essersi preso i nostri dati, può anche decidere di cancellarli con una bella DELETE o DROP TABLE inserita via SQL Injection in una query. Se poi l’utente con cui gira il servizio del server di database (non pensiamo solo a SQL Server) è per esempio LOCAL_SYSTEM, che è il default, l’hacker avrà modo di arrivare anche sul sistema operativo che ospita il server di database e farsi una bella escalation di privilegi sulla macchina… È importante capire che questi problemi non sono legati ad ASP o a SQL Server, ma sono più in generale legati ad applicazioni scritte male! Se scrivete male un sito Web in PHP+MySQL o JSP+Oracle o ASP.NET+SQL non cambia nulla: prima o poi qualcuno potrà farvi il servizio completo! Ok, vediamo allora come ci si deve comportare per non essere vulnerabili agli attacchi di questo tipo. Innanzitutto è consigliabile utilizzare utenti con privilegi minimi, così se anche ci dovessero violare l’applicazione non avranno a disposizione tutti gli strumenti di un SA o di un dbo o di LOCAL_SYSTEM. Poi dobbiamo abituarci a scrivere query parametriche o stored procedure parametriche, smettendo di concatenare le stringhe SQL nel codice. Nel caso del login avremmo dovuto scrivere codice simile al seguente: VBScript <% Dim sQuery ‘As String sQuery = “SELECT * FROM Users WHERE UserID = ? AND Password = ?” Dim cmd ‘As ADODB.Command Set cmd = Server.CreateObject(“ADODB.Command”)

10

VBJ

N. 60 - Novembre/Dicembre 2004

cmd.CommandText = sQuery cmd.Parameters.Append cmd.CreateParameter(“@UserID”, adVarChar, adParamInput, 50, Request(“txtUserID”)) cmd.Parameters.Append cmd.CreateParameter(“@Password”, adVarChar, adParamInput, 50, Request(“txtPassword”)) ‘ [...] Ottengo I dati all’interno di un ADODB.Recordset If Not rs.EOF Then ‘ Se ho almeno un record significa che l’utente esiste End If %>

Anche in ASP.NET possiamo utilizzare delle query parametriche nel modo seguente: C# String query = “SELECT * FROM Users WHERE UserID = @UserID AND Password = @Pwd”; SqlConnection cn = new SqlConnection(“...”); SqlCommand cmd = new SqlCommand(query, cn); cmd.Parameters.Add(“@UserId”, SqlDbType.VarChar, 50).Value = Request[“txtUserID”]; cmd.Parameters.Add(“@Pwd”, SqlDbType.VarChar, 50).Value = Request[“txtPassword”]; SqlDataReader dr = cmd.ExecuteReader(); // Leggo i dati ottenuti...

Inoltre è sempre opportuno verificare l’input dell’utente. Molti pensano che questi problemi siamo solo ricollegabili a parametri testuali e che quindi la soluzione al problema sia semplicemente fare una sostituzione degli apici con doppi apici su tutte le stringhe, prima di concatenarle al codice SQL. Non dimentichiamo mai che gli hacker sono in media più furbi di noi :-) o quanto meno più “smanettoni”. Controllare le stringhe non basta, anche gli input numerici, le date o qualunque altro input possono essere utilizzati. Alla peggio un hacker può farsi a mano una paginetta di submit verso il nostro sito, con la quale può camuffare i dati da inviare in POST. Se utilizzate un database server che supporta le stored procedure, poi, una soluzione ancora più comoda è quella di sfruttare le stored procedure parametriche (a patto di non concatenare le stringhe all’interno della stored procedure!): in questo modo potrete unire al vantaggio dei parametri, che di fatto impediscono l’injection, anche un incremento medio delle prestazioni. Infine, è sempre opportuno dare all’utente corrente solo i permessi minimi sulle tabelle: su un sito di pubblicazione in sola lettura, per esempio, è inutile avere anche il diritto di UPDATE o di DELETE (sarebbero utili solo all’hacker di turno).

DIFENDERSI DAGLI HACKER Cross Site Scripting Si tratta di una vulnerabilità legata al mondo Web e basata nuovamente sul fatto che spesso gli sviluppatori si fidano troppo dei loro utenti e non ne controllano l’input. Molto spesso nei siti Web capita di inserire dei dati all’interno di campi form, per la ricerca o per la registrazione, e di vedere questi stessi dati riproposti in fasi successive. Per esempio, se eseguiamo una ricerca di solito la pagina con il risultato ci ricorda anche quale era il testo da noi cercato. Ora proviamo a scrivere invece di “birre rosse” il seguente testo: “birre rosse”. In HTML abbiamo scritto il testo in grassetto. Se la pagina con il risultato della ricerca ci ripropone il testo birre rosse in grassetto e non “birre rosse” allora siamo in presenza di un sito che potrebbe rendersi vulnerabile al Cross Site Scripting. Proviamo infatti a scrivere “birre rosse <script>window.alert(‘Cross Site Scripting’);” e a premere nuovamente il pulsante di ricerca ... sorpresi? Vi troverete di fronte a una finestra con il messaggio “Cross Site Scripting”. Queste prime prove ci permettono di capire che il programmatore che ha scritto il codice della pagina di ricerca prende i dati in input da parte dell’utente e li mette sulla pagina senza fare alcun controllo. Fino a qui non c’è nulla di sconvolgente. Ora pensiamo al fatto che molto spesso i siti Web offrono la possibilità di registrarsi e di farsi riconoscere. Pensiamo a un sito di commercio elettronico che registra la nostra carta di credito e ci riconosce sfruttando un cookie. A questo punto proviamo a cercare “birre rosse <script>window.alert (document.cookie);” su quel sito. Se il sito è vulnerabile al Cross Site Scripting vedremo sullo scher-

mo il nostro cookie. Anche in questo caso, finché siamo noi a leggere il nostro cookie la situazione pare non essere grave. Ma se quel codice anziché mostrare con un alert il cookie lo inviasse al sito di un hacker? Un hacker può inviare migliaia di email a utenti della rete, tanto gli hacker la banda di rete non la pagano mai :-) a quanto pare, presentandole come email spedite dal sito affetto da Cross Site Scripting e chiedendo agli utenti di premere un link, per ottenere qualche offerta o regalo. Dietro a quel link ci sarà una pagina reale del sito che accetta Cross Site Scripting. Nel link ci sarà anche del codice javascript inserito nella query string. Questo codice leggerà il vostro cookie per inviarlo a un sito dell’hacker (ecco perchè si dice “Cross Site”) che a questo punto potrà farsi credere voi e potrà fare shopping con la vostra carta di credito. Carino vero? Anche in questo caso la prima misura da adottare è non fidarsi mai dell’input ottenuto dagli utenti e, se desideriamo riscriverlo in output, conviene sempre farne prima un HTML Encode. VBScript <% Response.Write Server.HtmlEncode(Request(“UserName”)) %>

C# Response.Write(Server.HtmlEncode(Request[“UserName”]));

Inoltre possiamo decidere di aggiungere ai cookie che rilasciamo un attributo “HttpOnly” che ne impedirà l’accesso da parte del codice JavaScript, rendendo di fatto inutile un attacco di questo tipo.

Canonicalization

Figura 2

Evitare la navigazione sui percorsi superiori a quelli in cui è ospitata l’applicazione Web

Con questo termine ci si riferisce a quella famiglia di attacchi che si basano sull’uso di indirizzi di file o risorse contraffatti o che si rivolgono ad applicazioni che utilizzano l’input dell’utente per generare o fornire file. Hanno fatto storia, nel recente passato, le email false firmate da hacker che si facevano credere Microsoft e che raccomandavano di installare applicazioni o fix disponibili a indirizzi che sembravano essere quelli di Microsoft, ma che in realtà celavano gli indirizzi dei server utilizzati dagli hacker. Infatti un indirizzo come il seguente http:// www.microsoft.com:[email protected]/ a prima vista sembra corrispondere a www.microsoft.com, in realtà il vero indirizzo è dopo il carattere @, quindi www.devleap.com, mentre ciò che precede la chiocciola è solo la coppia nome utente e password, separati dai due punti (:). Questo problema ha richiesto addirittura una fix di Internet Explorer che ha disabilitato il supporto a questo tipo di indirizzi. Ma nella Canonicalization rientrano anche i casi di attacco in cui le applicazioni richiedono agli utenti dei nomi

N. 60 - Novembre/Dicembre 2004

VBJ

11

DIFENDERSI DAGLI HACKER

espliciti di file o da fornire o da salvare sul server. Pensiamo a un’applicazione Web sviluppata con ASP.NET nella quale si chiede all’utente di fornire un nome di file da utilizzare per salvare il suo carrello prodotti. Dietro le quinte la nostra applicazione utilizza quel nome per creare un file nella cartella relativa ./carrelli dell’applicazione Web. Trovo decisamente insicuro e generalmente errato salvare file su disco nelle applicazioni Web, ancor di più quando è l’utente finale a scegliere i nomi dei file, ma ciò non toglie che vi siano parecchie applicazioni in produzione su Internet che consentano di farlo. Ebbene, in un caso come quello appena descritto un utente potrebbe fornire come nome di file “../web.config” ottenendo quale risultato quello di sovrascrivere il file web.config dell’applicazione, bloccandola. Oppure, nel caso di un download di risorse, potrebbe tentare di spostarsi sul file system usando percorsi relativi (come . e ..) riuscendo così a scaricare file per i quali non è autorizzato. La soluzione a problemi di questo tipo è quella di impostare correttamente e in modo rigido i permessi sulle ACL (Access Control List) del file system, evitare con l’apposita opzione di IIS la navigazione sui percorsi superiori a quelli in cui è ospitata l’applicazione (“Enable Parent Paths” disabilitato, Figura 2) e se possibile evitare in assoluto di richiedere agli utenti percorsi espliciti di file o risorse fisiche.

12

ma prefissata. Avere 1000 thread che lavorano in parallelo a volte è peggio che non averne 100 che lavorano e una coda di attività da svolgere non appena vi saranno dei thread liberi per farlo. Un tipico esempio che faccio in questo caso è quello relativo alla gestione degli errori. Pensiamo a un’applicazione Web che ad ogni errore manda una email al supporto prodotto in modalità sincrona, cioè utilizzando lo stesso thread che esegue la richiesta in errore. L’invio di una email richiede un minimo di tempo per risolvere l’indirizzo del server di posta e per il dialogo SMTP. Un hacker potrebbe sfruttare questo fatto per “ingozzare” il nostro server di richieste che generano errori, consapevole del fatto che un errore consuma molte più risorse di una pagina normale, potendo così saturare prima il nostro servizio. Se noi utilizzassimo un thread o un pool di thread, diverso da quello predefinito di .NET, sul quale accodare le richieste di invio di email, le notifiche di errore non sarebbero più un’attività pesante per i singoli thread che evadono le richieste, ma diventerebbero un’attività che viene svolta in background, quando ci sono risorse per farlo. Inoltre non ha senso mandare continuamente email tutte uguali che descrivono uno stesso errore. Oltre un certo limite è meglio mettersi in “regime di protezione” per evitare di intasare il server di posta, oltre che la casella, del supporto prodotto.

Denial of Service

Conclusioni

Sono attacchi organizzati e finalizzati a interrompere l’erogazione di un servizio o di un’applicazione. Di solito non sono realizzati per caso, ma sono rivolti a degli obiettivi precisi e scelti. Questi attacchi consistono nel saturare le risorse (rete, memoria, CPU, ecc.) di un sistema al fine di renderlo non disponibile. A volte lo scopo finale è solo quello di neutralizzare un servizio, altre volte il servizio viene neutralizzato per poi sostituirvisi con un servizio fittizio. Come programmatori possiamo solo fare del nostro meglio per evitare che le nostre applicazioni siano vulnerabili a questo tipo di attacchi, senza dimenticare comunque che anche siti Web autorevoli e importanti in passato sono stati messi in ginocchio da questo tipo di attacchi, a volte portati a termine da ragazzi poco più che adolescenti. A noi programmatori conviene per esempio monitorare sempre il consumo di risorse e riciclare autonomamente i servizi qualora consumino più di una determinata percentuale di soglia. Per un servizio che normalmente consuma il 5% della CPU un consumo del 90% è una situazione sospetta e conviene non permettergli di arrivare a un simile livello, meglio fermarlo e riavviarlo prima. Nel nostro codice possiamo cercare di arginare queste situazioni evitando di creare indiscriminatamente oggetti, thread e risorse in generale, ma magari creare dei pacchetti (pool) di risorse, di dimensione massi-

Le vulnerabilità del codice sono numerose e in questo articolo abbiamo sfiorato le più frequenti e diffuse. In bibliografia trovate alcuni testi e link per approfondire e integrare l’argomento. In generale occorre essere consapevoli del fatto che scrivere codice sicuro non è un’attività né semplice, né tantomeno banale. Non bisogna mai assumere di essere più furbi degli hacker e non si devono mai inventare delle soluzioni fatte in casa. Scrivere codice sicuro richiede tempo, attenzione e risorse. Come ho letto qualche tempo fa sul sito della Università di Salerno: “Scrivere codice sicuro è lodevole, ma irrimediabilmente costoso”. Se vogliamo essere sviluppatori e consulenti seri dobbiamo mettere a budget questo costo, perchè l’alternativa è fare gli azzeccagarbugli.

VBJ

N. 60 - Novembre/Dicembre 2004

Bibliografia [1] Michael Howard, David LeBlanc - “Writing Secure Code, Second Edition”, Microsoft Press, 2002 [2] Frank Swiderski, Window Snyder - “Threat Modeling”, Microsoft Press, 2004 [3] Jason Bock, Tom Fischer, Nathan Smith, Pete Stromquist - “.NET Security”, APress, 2002

Riferimenti [4] http://msdn.microsoft.com/security/

DIFENDERSI DAGLI HACKER

La sicurezza del codice managed Vediamo alcune delle principali caratteristiche del Framework .NET rivolte alla sicurezza del codice e delle applicazioni.

di Paolo Pialorsi e applicazioni sviluppate con il Framework .NET di Microsoft si dicono “managed” in quanto la loro esecuzione è “gestita” dal motore di esecuzione del Framework stesso, cioè dal Common Language Runtime (CLR). Tra i vantaggi offerti dal CLR vi è il fatto che ci viene messa a disposizione un’infrastruttura di sicurezza nativa, in molti casi automatica e per noi trasparente. Vediamo in questo articolo alcune delle caratteristiche più importanti orientate alla sicurezza del codice managed.

L

C# // Dichiaro uno StringBuilder per una stringa da 1 a 10 caratteri StringBuilder bld = new StringBuilder(1, 10); for (Int32 c = 0; c < 10; c++) { bld.Append(“a”); } bld.ToString();

Gestione della memoria Una delle prime caratteristiche che il Framework .NET ci offre è la gestione automatica della memoria, sia in termini di allocazione che di rilascio delle risorse. Grazie a questa possibilità i classici problemi di sicurezza quali buffer overrun, overflow su numeri o indici e simili, diventano tecnicamente impossibili da realizzare. Pensiamo al buffer overrun su stack, ottenuto sovraccaricando il contenuto di una stringa, come mostrato nell’articolo precedente. Con .NET le stringhe sono oggetti che vengono allocati in memoria nel momento in cui vengono assegnate. Alla loro variazione di contenuto avremo una riallocazione, in una nuova area di memoria, dello spazio necessario a contenere il nuovo valore. Per come ho appena descritto la situazione, è impossibile utilizzare una stringa .NET per eseguire un attacco di tipo buffer overrun. Nel caso in cui poi vogliamo costruire dinamicamente delle stringhe di dimensione massima prefissata, esiste la classe StringBuilder che di lavoro fa proprio questo:

Paolo Pialorsi è un consulente e autore specializzato nello sviluppo di Web Service e soluzioni Web con il Framework .NET di Microsoft. Lavora nell’omonima società Pialorsi Sistemi S.r.l. e fa parte del gruppo DevLeap. Può essere contattato via email: [email protected].

Nell’esempio appena visto, un eventuale tentativo di scrivere più di 10 caratteri all’interno dello StringBuilder solleverebbe un’eccezione (ArgumentOutOfRangeException). Il gestore di memoria del CLR è inoltre in grado di controllare l’accesso alle singole aree di memoria, consentendoci di isolare eventuali assembly in esecuzione all’interno di uno stesso processo, sfruttando il concetto di Application Domain. Possiamo pensare agli AppDomain come a dei sotto-processi, completamente isolati e protetti tra loro, eseguiti all’interno di un unico processo del sistema operativo. Tutte le volte che abbiamo l’esigenza di eseguire del codice managed con privilegi particolari o isolandolo dal resto del contesto applicativo, possiamo creare un AppDomain ed eseguire al suo interno il codice. In questo modo avremo pieno isolamento tra i vari livelli di accesso al sistema. Il motore di ASP.NET sfrutta proprio il concetto

N. 60 - Novembre/Dicembre 2004

VBJ

13

DIFENDERSI DAGLI HACKER

Innanzitutto si deve creare un’istanza di IsolatedStorageFile, in questo esempio si richiede uno storage locale in base all’utente e all’assembly corrente (GetUserStoreForAssembly). Ottenuto un riferimento allo storage possiamo creare file, directory, stream di byte su file, ecc. Nell’esempio creiamo un IsolatedStorageFileStream, per scrivere sul disco virtuale un file di testo puro. Per impostazione predefinita le applicazioni managed che scarichiamo da Internet non hanno accesso, per motivi di sicurezza, al file system fisico delle nostre macchine, ma possono tranquillamente utilizzare l’Isolated Storage. Esiste anche un tool a riga di comando (STOREADM.EXE) che permette di gestire ed eventualmente cancellare gli Isolated Storage creati.

Figura 1

L’applicazione di amministrazione di NET Framework

di AppDomain per eseguire diverse applicazioni Web all’interno di uno stesso processo, garantendo così l’isolamento completo tra le aree di memoria assegnate alle singole applicazioni.

Isolated storage Sempre a proposito di isolamento il Framework .NET ci offre la possibilità di utilizzare un file system virtuale, chiamato Isolated Storage, che consente di salvare file anche ad applicazioni non autorizzate ad accedere al disco fisico della macchina client. Più avanti in questo articolo, quando parleremo di Code Access Security, le cose risulteranno più chiare, per ora assumiamo che l’Isolated Storage sia un disco virtuale, sul quale un nostro applicativo managed può lavorare come se fosse un disco vero. Inoltre rispetto a un disco reale vi è il fatto che possiamo definire delle quote e associare il file system a singoli utenti e/o applicazioni (assembly), per un’eventuale condivisione di informazioni. Per utilizzarlo dobbiamo fare riferimento alle classi del namespace Syste m.IO.IsolatedStorage nel modo seguente: C# IsolatedStorageFile isoFile = IsolatedStorageFile.GetUserStore ForAssembly();

Strong name Quando creiamo un progetto con Visual Studio .NET o con qualsiasi editor di codice .NET e poi lo compiliamo, otteniamo come risultato uno o più file .EXE, .DLL o .NETMODULE, che rappresentano degli assembly o dei moduli di un assembly. In .NET l’assembly può essere definito come “unità minima di deployment” e in pratica rappresenta uno o più file, che costituiscono una particolare versione di una porzione di codice. Dal punto di vista del deployment, cioè del rilascio del codice, con .NET non esiste nulla di più piccolo rispetto all’assembly. Questo significa che al minimo potremo gestire l’installazione e il versioning degli assembly, ma non dei singoli file di cui sono costituiti. Ogni volta che generiamo un assembly, di solito compilando un progetto con Visual Studio .NET, otteniamo un assembly mono-modulo, cioè costituito di un solo file. Durante la compilazione possiamo decidere di associare all’assembly una serie di informazioni come il nome, la cultura, la versione e volendo anche una firma digitale basata sul suo contenuto, sfruttando un algoritmo a chiavi asimmetriche (chiave pubblica e chiave privata). Queste informazioni in Visual Studio .NET si trovano nei file autogenerati con nome AssemblyInfo.cs o AssemblyInfo.vb. Come impostazione predefinita un assembly autogenerato da Visual Studio .NET è dotato solo di nome e cultura neutrale. Possiamo però modificare il file AssemblyInfo.* e inserire anche altre informazioni: C#

using(IsolatedStorageFileStream fs = new IsolatedStorageFile Stream(“doc.txt”, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) { String testo = “testo da scrivere”; fs.Write(System.Text.Encoding.ASCII.GetBytes(testo), 0, testo.Length); } isoFile.Close();

14

VBJ

N. 60 - Novembre/Dicembre 2004

using System.Reflection; using System.Runtime.CompilerServices; [assembly: [assembly: [assembly: [assembly: [assembly: [assembly:

AssemblyTitle(“Applicazione dimostrativa”)] AssemblyCompany(“DevLeap”)] AssemblyProduct(“ClassLibraryDemo”)] AssemblyCopyright(“(C) Paolo Pialorsi, 2004”)] AssemblyCulture(“it-IT”)] AssemblyVersion(“1.0.0.1”)]

DIFENDERSI DAGLI HACKER

[assembly: AssemblyDelaySign(false)] [assembly: AssemblyKeyFile(@”d:\keys.snk”)]

In questo esempio si dichiarano delle voci puramente informative come il titolo, l’azienda produttrice, il nome del prodotto e il copyright e delle informazioni particolari come la versione (1.0.0.1), la cultura (it-IT) e un file (keys.snk) da utilizzare per firmare digitalmente l’assembly. Il file keys.snk a cui si riferisce il codice di esempio può essere generato con il tool a riga di comando SN.EXE nel modo seguente: Command Prompt di Visual Studio .NET SN.EXE –k Keys.snk

Figura 2 dove –k indica di generare una coppia di chiavi e il secondo parametro è il nome del file che conterrà le chiavi. Se compiliamo un assembly che presenta tutte queste informazioni (nome, cultura, versione e firma digitale), esso si dirà dotato di uno “strong name”. Potremmo tradurre questa espressione in “nome solido” o “nome sicuro”, in realtà trovo che suoni molto meglio la definizione inglese, anche se il suo significato è proprio quello di dare solidità e sicurezza a un assembly. Tutti gli assembly dotati di strong name infatti sono verificati e verificabili rispetto alla firma digitale applicatagli. Gli assembly con strong name possono essere installati nella stessa cartella delle singole applicazioni che li usano, in questo caso si dicono assembly locali, oppure nella Global Assembly Cache (GAC) e si dicono assembly condivisi. Se realizziamo un’applicazione .NET (client del componente) che utilizza un assembly locale (componente server) con strong name, il motore del CLR, prima di caricare in memoria l’assembly che rappresenta il componente server, ne verificherà l’integrità rispetto alla firma digitale. Se qualcuno dovesse manomettere il contenuto dell’assembly rispetto a quando l’applicazione client è stata compilata, il CLR se ne accorgerà, bloccandone il caricamento. Se invece utilizziamo un assembly condiviso, quindi inserito nelle GAC, la verifica dell’integrità dell’assembly non sarà ripetuta a ogni caricamento, ma sarà eseguita solo all’atto dell’inserimento nella GAC. La GAC infatti di default è accessibile in modifica solo ed esclusivamente a utenti membri del gruppo Administrators, quindi si assume che gli amministratori non manomettano intenzionalmente gli assembly rilasciati. Da questo ultimo concetto si evince un’altra ragione (se non ve n’erano già abbastanza!) per cui è di fondamentale importanza non sviluppare codice utilizzando un utente con diritti amministrativi. Se poi un hacker dovesse manomettere un assembly e successivamente ri-firmarlo con una nuova firma, basata su una chiave privata diversa, il CLR si accorgerebbe

La gestione dei permessi facenti parte di un Permission Set

comunque della manomissione in quanto all’interno del client è presente non solo il riferimento all’assembly, ma anche il riferimento alla coppia di chiavi utilizzate originariamente per firmare l’assembly. Questa informazione è detta “publicKeyToken” e nuovamente, se non c’è corrispondenza tra il publicKeyTone del client e quello del server, il CLR blocca il caricamento dell’assembly segnalando la manomissione.

Code Access Security Quando il CLR carica in memoria un assembly svolge una serie di verifiche finalizzate a decidere quali permessi di esecuzione concedergli. Queste verifiche sono basate su una serie di prove (evidence) che consentono al CLR di capire quale sia la provenienza dell’assembly (per es. PC locale, internet, intranet, ecc.), se ha uno strong name o meno, se è firmato con Authenticode, ecc. Sulla base di queste prove gli assembly vengono quindi associati a dei gruppi (Code Group) ai quali poi sono associati dei permessi (Permission Set). I Code Group sono suddivisi in diversi livelli di Policy:

• • • •

Enterprise: definisce le politiche di sicurezza a livello di intera infrastruttura di rete (enterprise) Machine: dichiara le politiche relative al PC corrente User: descrive le politiche di sicurezza per l’utente corrente AppDomain: configurabile da codice a livello di AppDomain

Questi livelli sono configurati su ogni sistema con .NET installato e sono gestibili sia dal prompt dei comandi, utilizzando il tool CASPOL.EXE (CASPOL = Code Access Security Policy) sia - più comodamente - utilizzando un tool amministrativo con interfaccia grafica. Quest’ultimo tool si chiama “Microsoft .NET Framework 1.1 Configu-

N. 60 - Novembre/Dicembre 2004

VBJ

15

DIFENDERSI DAGLI HACKER

ration” (Figura 1) e consente di creare, all’interno dei primi tre livelli di policy, dei Code Group e Permission Set personalizzati. Vediamo quindi come ragiona il CLR per decidere i permessi da associare a un assembly. Gli assembly sono associati ai Code Group in base a condizioni di appartenenza (Membership Conditions) come le seguenti:

• • • • • • • •

Application Directory: tutti gli assembly caricati dalla stessa cartella o da una cartella figlia di quella in cui è stato caricato l’assembly principale Hash: l’hash code dell’assembly Publisher: il produttore dell’assembly, individuato tramite un certificato Authenticode Site: il site di provenienza Strong name: il publicKeyToken del produttore ed eventualmente il nome e la versione esplicite dell’assembly URL: la URL di provenienza o porzione di essa Zone: la zona di provenienza del codice (My Computer, Local Intranet, Internet, Trusted Sites, Untrusted Sites) Custom: criteri di appartenenza personalizzati

I permessi di cui sono costituiti i Permission Set possono essere scelti tra quelli predefiniti di .NET oppure essere permessi personalizzati, che possiamo creare e configurare noi stessi. Alcuni dei permessi più utilizzati e predefiniti del Framework .NET sono (Figura 2):



• • • • • 16

File IO: consente di definire le regole di accesso al file system, in termini di Read, Write, Append e Browse di file e risorse, eventualmente consentendo anche di impostare dei filtri sui path ai quali si concede l’accesso. Potremmo cioè concedere un accesso completo al path C:\Temp e in sola lettura al path C: \Documenti. Isolated Storage File: abilita l’accesso all’Isolated Storage (vedi sezioni precedenti) indicando anche la quota massima consentita. Registry: consente di abilitare l’accesso in varie modalità (Read, Write, Create) alle chiavi del registro di sistema. Security: permette di configurare alcuni permessi quali l’esecuzione di codice, l’utilizzo di .NET Remoting, la serializzazione di oggetti, le chiamate a codice unmanaged, ecc. User Interface: consente di esporre un’interfaccia utente a finestre o meno, controllando anche il tipo di messaggi Windows che si possono ricevere e l’accesso alla Clipboard. SQL Client: abilita l’accesso a server di dati Micro-

VBJ

N. 60 - Novembre/Dicembre 2004

soft SQL Server, eventualmente impedendo l’uso di server SQL con connessioni a password nulla. Infine i Code Group hanno due flag che permettono rispettivamente di dichiarare l’esclusività dei loro Permission Set per gli assembly che vi appartengono e la non ridefinibilità dei Permission Set che descrivono, a livelli di Policy più bassi.

Il caricamento di un assembly Vi siete mai chiesti perchè un programma sviluppato con .NET, se eseguito da un disco di rete anzichè dal PC locale, può cambiare comportamento e in alcuni casi addirittura non si avvia nemmeno, restituendoci un errore di sicurezza? Vediamo insieme, in questa sezione, il perchè di questo comportamento. Al caricamento dell’assembly il CLR legge le sue evidence e stabilisce i Code Group, di ogni livello di Policy, ai quali appartiene. Per ogni livello di Policy poi viene fatta l’unione dei Permission Set associati ai Code Group cui appartiene l’assembly. Nel caso di Code Group con il flag di esclusività, verrà preso solo il suo Permission Set. Il risultato di questa prima fase è, per ogni livello di Policy, un insieme di permessi, quelli ottenuti dall’unione dei Permission Set. A questo punto il CLR esegue l’intersezione di questi tre insiemi di permessi, per ricavare i reali permessi da associare all’assembly. Lo so, sembra un esame di Algebra del primo anno di Ingegneria :-), ma cerchiamo di capire con un esempio concreto. Consideriamo un assembly (di nome ServiceProxy), di tipo class library, con le seguenti caratteristiche:

• •

URL di provenienza: http://www.devleap.com/ Strong name publicKeyToken: 714cc0ec76570e52

Ipotizziamo che venga caricato in memoria da un altro assembly che rappresenta un’applicazione Console (ServiceClient). Immaginiamo poi che nel nostro sistema siano configurati i seguenti Code Group a livello di macchina:





CodeGroupDevLeapSiteMachine: vi appartengono tutti gli assembly che provengono dalla URL http:// www.devleap.com/. È associato al seguente Permission Set: • File IO: sola lettura su C:\Program Files\DevLeap e basta • Isolated Storage File: permesso di accesso con quota di 20Kb CodeGroupDevLeapSNMachine: vi appartengono tutti gli assembly che hanno uno strong name con il pu-

DIFENDERSI DAGLI HACKER

blicKeyToken ricavato dalla coppia di chiavi di DevLeap. È associato al seguente Permission Set: • File IO: controllo completo sul path C:\Temp Il CLR analizzando queste informazioni saprà che l’assembly ServiceProxy appartiene a entrambi i CodeGroup, quindi i suoi permessi, relativamente al solo livello di policy di tipo Machine, saranno:

• • •

File IO: sola lettura su C:\Program Files\DevLeap e basta Isolate Storage File: permesso di accesso con quota di 20Kb File IO: controllo completo sul path C:\Temp

Verifica e richiesta di permessi

Se compiliamo un assembly provvisto di nome, cultura, versione e firma digitale, esso si dirà dotato di uno “strong name”

Cioè appunto l’unione dei Permission Set dei singoli Code Group del livello Machine ai quali ServiceProxy appartiene. Ora ipotizziamo che esista a livello di Policy Enterprise un Code Group, definito dall’amministratore di rete, come il seguente:



Finchè siamo noi a creare e installare le nostre applicazioni non dovremmo avere problemi a darci o farci assegnare i permessi necessari al corretto funzionamento del nostro codice. Quando però il software che sviluppiamo viene reso disponibile a numerosi utenti, magari pacchettizzato, abbiamo l’esigenza di rilevare l’esistenza o meno di permessi per noi necessari, per evitare che le nostre applicazioni vadano in crash (sollevando una SecurityException) nel momento in cui cercano di svolgere operazioni non consentite. Possiamo assolvere a questa esigenza utilizzando due differenti tecniche tra loro alternative. Il primo modo di lavorare prevede l’utilizzo di classi che descrivono permessi, cioè classi che implementano l’interfaccia IPermission, chiedendo al CLR, per esempio tramite l’apposito metodo Demand dell’interfaccia, se il particolare permesso è concesso o meno. C#

CodeGroupDevLeapSiteEnterprise: vi appartengono tutti gli assembly che provengono dalla URL http:// www.devleap.com/. È associato al seguente Permission Set: • Isolated Storage File: permesso di accesso con quota di 20Kb

Quindi il nostro ServiceProxy apparterrà anche a questo Code Group. Il livello di Policy relativo all’utente corrente consideriamolo dotato di un solo Code Group, che comprende qualunque assembly, con associato un unico Permission Set che consente tutto a tutti (FullTrust). Si tratta tra l’altro della impostazione predefinita. Il CLR dovrà a questo punto intersecare i permessi di ciascun livello di Policy, ottenendo come risultato il seguente permesso:



Infatti questo è l’unico permesso in comune a tutti e tre i livelli di Policy. L’intersezione è l’operazione insiemistica corretta da eseguire in questo contesto, per evitare che un utente o un PC possano avere permessi più ampi di quelli che la macchina o le regole di sicurezza della rete gli concederebbero. Cosa accade a un assembly, come quello dell’esempio precedente, che non ha il permesso di accedere al disco fisico del PC, ma solo al disco virtuale (Isolated Storage), qualora tentasse di aprire un file, per esempio su C:\temp? Una SecurityException ci informerà del problema, nel caso in cui si tenti esplicitamente di eseguire un’operazione non consentita.

Isolated Storage File: permesso di accesso con quota di 20Kb

FileIOPermission fp = new FileIOPermission(FileIOPermission Access.Read, @”C:\TEMP\INFO.TXT”); try { fp.Demand(); } catch (SecurityException secEx) { // Se rilevo una SecurityException // significa che non ho il permesso // di accedere il file Console.WriteLine(secEx.Message); }

Nell’esempio precedente si richiede il permesso di accesso in lettura al file C:\TEMP\INFO.TXT. Si noti il fatto che la richiesta è inclusa in un blocco try...catch al fine di interecettare l’eventuale SecurityException. Il funzionamento di tutti i metodi Demand di tutti i permessi prevede infatti lo scatenarsi di una SecurityException, se la richiesta del permesso fallisce. Quella appena vista si dice verifica imperativa dei permessi.

N. 60 - Novembre/Dicembre 2004

VBJ

17

DIFENDERSI DAGLI HACKER

In alternativa all’approccio imperativo possiamo utilizzare la sicurezza cosiddetta dichiarativa, che prevede l’utilizzo di attributi .NET, derivati dalla classe base CodeAccessSecurityAttribute. In questo secondo caso sarà il motore del CLR a eseguire per conto nostro la verifica del permesso da noi dichiarato tramite attributo, al momento del caricamento dell’assembly o della classe, piuttosto che all’esecuzione di un particolare metodo. Anche in questo caso, se il permesso è negato, avremo una SecurityException da intercettare e gestire. C# static void Main(string[] args) { try { LeggiFile(); } catch (SecurityException secEx) { Console.WriteLine(secEx.Message); } } [FileIOPermission(SecurityAction.Demand, Read=@”C:\TEMP\ INFO.TXT”)] static void LeggiFile() { //... }

Come si vede nell’esempio, il metodo LeggiFile è decorato con l’attributo FileIOPermission, che informa il CLR del fatto che per essere eseguito deve avere accesso in lettura al solito file C:\TEMP\INFO.TXT. Un aspetto di Code Access Security interessante e da non sottovalutare è il fatto che i permessi, così come le classi per il loro utilizzo dichiarativo e/o imperativo, sono personalizzabili ed estendibili. Possiamo cioè creare all’interno delle nostre applicazioni .NET una nostra logica di sicurezza, basata su permessi personalizzati e classi *Permission e *PermissionAttribute personalizzate.

Stack di chiamata e FullTrust L’ultimo aspetto da valutare, in questa breve carrellata sulle funzionalità di sicurezza di .NET, riguarda il caso in cui un hacker tenti di utilizzare un nostro assembly, autorizzato a svolgere determinate operazioni in quanto nostro. In sostanza a un hacker potrebbe venire la tentazione di creare una sorta di “Cavallo di Troia” che sfrutti i permessi concessi ai nostri assembly per i suoi scopi. Fortunatamente per come funziona il motore di autorizzazione del CLR, in caso di chiamate annidate, il CLR non si limita a verificare che l’assembly corrente abbia i permessi per eseguire una determinata operazione, ma ripercorre a ritroso l’intero stack di chiama-

18

VBJ

N. 60 - Novembre/Dicembre 2004

Figura 3

Schema dell’esame dello stack di chiamata durante la verifica dei permessi

ta, verificando che tutti gli assembly nello stack abbiano quel permesso. Se e solo se tutti gli assembly nello stack hanno il permesso di eseguire una determinata operazione, allora quest’ultima sarà autorizzata (Figura 3), altrimenti avremo la solita SecurityException. Qualora poi avessimo un assembly che deve sempre e comunque poter eseguire una determinata operazione, per la quale è autorizzato, a prescindere dai permessi dei chiamanti, possiamo richiedere - assumendocene tutti i rischi - che venga disattivata la verifica a ritroso nello stack di chiamata, per un particolare permesso. Ecco un esempio: C# public void LeggiFile() { FileIOPermission fp = new FileIOPermission(FileIOPermission Access.Read, @”C:\TEMP\INFO.TXT”); fp.Assert(); // ... FileIOPermission.RevertAssert(); }

In questo caso il metodo LeggiFile potrà accedere al file C:\TEMP\INFO.TXT, qualora ne abbia il permesso, anche se dovesse essere invocato da un oggetto che non ha quel permesso, perchè il metodo Assert spegne la verifica dello stack per quel particolare permesso (FileIOPermission), con quella configurazione (Read su C: \TEMP\INFO.TXT).

DIFENDERSI DAGLI HACKER

Si noti che viene richiamato anche un metodo statico FileIOPermission.RevertAssert che abilita nuovamente la verifica del permesso sullo stack di chiamata. Una chiamata al metodo Assert ha effetto solo fino alla fine dell’esecuzione del metodo che la contiene, tuttavia per avere il livello massimo di sicurezza conviene disabilitare la verifica di un particolare permesso sullo stack di chiamata, solo per il tempo strettamente necessario. Sempre a proposito della verifica dei permessi sull’intero stack di chiamata, si presenta un'ulteriore situazione interessante. Gli assembly inseriti nella GAC, dal momento che sono dotati di strong name e che solo gli utenti del gruppo Administrators possono gestirli, vengono automaticamente dotati di un Permission Set illimitato (FullTrust). Da un lato questo aspetto deve farci riflettere sull’opportunità o meno di mettere un assembly nella GAC, dall’altro pone un problema: per impostazione predefinita un assembly non può essere utilizzato da altri assembly con un livello di trust più basso del suo.

Quando il CLR carica in memoria un assembly svolge una serie di verifiche finalizzate a decidere quali permessi di esecuzione concedergli Questo fatto è ancora una volta dovuto alla volontà di evitare che un hacker tenti di sfruttare componenti altrui a proprio vantaggio. Nel caso in cui si voglia quindi utilizzare un assembly inserito nella GAC, invocandolo con altri assembly che contrariamente a lui non siano dotati di FullTrust, per esempio perchè non hanno uno strong name e non sono inseriti nella GAC, dobbiamo esplicitamente autorizzarli. Per farlo si utilizza un attributo .NET che si chiama AllowPartiallyTrustedCallersAttribute che va esplicitato nell’assembly da inserire nella GAC. Nel file AssemblyInfo.* dovremmo inserire una riga come la seguente: C# [assembly:AllowPartiallyTrustedCallers]

Per evitare poi che chiunque possa caricare il nostro assembly dalla GAC e invocarne gli oggetti, possiamo uti-

lizzare l’attributo StrongNameIdentityPermissionAttribute, che prevede la possibilità di dichiarare esplicitamente da quali assembly accettiamo di essere invocati, applicandolo alle classi e/o ai metodi da proteggere. In questo caso però gli assembly chiamanti devono avere almeno uno strong name, per poter essere identificati. C# [StrongNameIdentityPermission(SecurityAction.LinkDemand, PublicKey=”024048...”)] public class Utility { //... }

Non abbassiamo mai la guardia Quando si parla di sicurezza si dice spesso che il massimo livello di sicurezza di un’applicazione è dato dal suo punto più debole. Prestiamo molta attenzione a questo concetto e ricordiamoci che anche se il Framework .NET è decisamente più sicuro rispetto agli ambienti di sviluppo precedenti, ogni volta che utilizziamo dei componenti unmanaged come DLL sviluppate in C++ o le API di Windows, possiamo perdere alcune delle sicurezze che .NET ci offre. Per esempio se passiamo una stringa da .NET a una API e per caso questa API è soggetta a problemi di buffer overrun, sapere che in .NET i buffer overrun non sono possibili ci servirà a poco... l’hacker tenterà di attaccarci sfruttando la vulnerabilità della API. Teniamo inoltre presente il fatto che questi meccanismi di sicurezza non si sostituiscono ai criteri di sicurezza nativi di Windows, ma si integrano con essi. La cosa migliore è sfruttare tutte le possibilità a nostra disposizione per realizzare soluzioni il più possibile sicure. Infine non dimentichiamo mai di costruire le nostre applicazioni affinché richiedano i privilegi minimi sul sistema. Un utente appartenente al gruppo degli Administrators infatti sarà in grado di spegnere completamente la sicurezza del CLR, rendendo inutili tutti i nostri sforzi.

Bibliografia [1]

Jason Bock, Tom Fischer, Nathan Smith, Pete Stromquist - “.NET Security”, APress, 2002

Riferimenti [2] http://msdn.microsoft.com/security/ [3] http://www.devleap.com/Default.aspx?IdCategoria= SECURITY [4] http://www.devleap.com/SchedaArticolo.aspx?IdAr ticolo=10150 [5] http://msdn.microsoft.com/msdnmag/issues/04/04/ SecurityBriefs/default.aspx [6] http://blogs.msdn.com/shawnfa/

N. 60 - Novembre/Dicembre 2004

VBJ

19

DIFENDERSI DAGLI HACKER

Applicazioni sicure con IIS 6 e ASP .NET L’accoppiata IIS 6 e ASP.NET garantisce la possibilità di creare e far funzionare in maniera sicura applicazioni web con pochi e semplici sforzi. Anche se entrambi i sistemi sono già sicuri appena installati, è necessario un ulteriore tocco per garantire alle nostre applicazioni un elevato livello di sicurezza. di Daniele Bochicchio pesso quando si parla di sicurezza, si pensa a strani e complicati esorcismi, trucchi o danze propiziatorie da eseguire per aggiungere alle nostre applicazioni web quello che è in realtà uno dei punti di partenza e dei requisiti imprescindibili di ogni software. La sicurezza in realtà si compone di poche e semplici regole da seguire, sia in fase di sviluppo che in fase di definizione dei requisiti delle nostre applicazioni. Nel caso del web, infatti, ogni singola richiesta che arriva al web server è potenzialmente dannosa e maligna, fino a prova contraria: dobbiamo quindi cominciare ad imparare a schermare il nostro lavoro dall’esterno, per fare in modo che sia a prova di attacco e che gli eventuali dati trattati siano al sicuro da occhi indiscreti.

S

La sicurezza di un sistema infatti si può suddividere in questi ulteriori punti:

• • •

sicurezza della rete fisica: firewall hardware, politiche di accesso alle porte TCP/IP e conseguenti filtri sul traffico; sicurezza del web server e del database server: adeguata configurazione, in base alle best practices; sicurezza a livello applicativo: scrivere codice (ASP.NET e T-SQL) che non presenti punti di ingresso per chi cerca di attaccarci.

La sicurezza viene da lontano Dobbiamo prestare attenzione, in realtà, ai diversi ambiti in cui la sicurezza va applicata. Possiamo scrivere applicazioni sicure dal punto di vista applicativo, ma poi farle girare su un server non configurato a dovere per inficiarne comunque la bontà. Dunque prima di analizzare alcune tecniche utili per scrivere applicazioni web sicure, dobbiamo assicurarci che la piattaforma sulla quale andiamo ad installare le nostre applicazioni non sia da meno.

In questo articolo ci soffermeremo in modo particolare sugli aspetti del secondo punto, che riguardano nello specifico IIS 6, e su alcuni “trucchi” da poter sfruttare per rendere più sicure le nostre applicazioni basate su ASP.NET. Potrete trovare ampie trattazioni della sicurezza a livello applicativo negli altri articoli di questo numero della rivista.

La sicurezza è essere proattivi: proteggiamo il web server Daniele Bochicchio è il content manager di ASPItalia.com, community che si occupa di ASP.NET, Classic ASP e Windows Server System. Si occupa di consulenza e formazione, specie su ASP.NET, e scrive per diverse riviste e siti. È Microsoft .NET MVP, un riconoscimento per il suo impegno a supporto delle community e per l’esperienza maturata negli anni. Il suo blog è all’indirizzo http://blogs.aspitalia.com/daniele/

20

VBJ

N. 60 - Novembre/Dicembre 2004

Essere proattivi vuol dire prevedere eventuali pericoli che possono essere causati in maniera volontaria o casuale nella nostra applicazione. Un’applicazione che si blocca (per qualsiasi motivo) non deve in alcun modo influire sulle altre presenti sullo stesso

DIFENDERSI DAGLI HACKER Meno privilegi, più robustezza

Figura 1

Per ogni application pool va specificato un relativo utente di Windows da utilizzare. In figura la maschera che mostra come cambiarlo

web server. Può sembrare un’eventualità remota ed un concetto del tutto scontato, ma se state usando IIS 5 (e quindi Windows 2000 Server) con molta probabilità una situazione del genere è tutt’altro che lontana da realizzarsi. Ci sono metodi per evitare che ciò accada se state ancora utilizzando IIS5, ma sono solo palliativi. Un approccio che può funzionare, ma che porta ad un consumo maggiore di risorse, è la modalità di isolamento alta, così chiamata per via della possibilità, in questo stato, di isolare il processo che causa problemi rispetto agli altri. Tuttavia si tratta solo di un palliativo temporaneo. IIS 5 nasce in un periodo profondamente differente rispetto ad IIS 6, di cui ci occuperemo a breve, e quindi risente di alcune mancanze a livello architetturale che fortunatamente la versione 6 ha colmato in toto. Tanto per cominciare, “IIS 6 è secure by default”, che vuol dire, molto semplicemente, che è sicuro anche appena installato. A differenza di IIS 5, non viene installato se non esplicitamente chiesto dall’amministratore del server, e non viene abilitata di default nessuna delle estensioni esterne. Questo vuol dire che appena installato, il web server si limiterà ad elaborare solo le semplici pagine HTML e che il supporto per ASP.NET, ASP, WebDav e Front Page Extensions va attivato esplicitamente ed a parte, dalla console di gestione di IIS. Questo è un vantaggio perché ci permette di rendere funzionanti solo quelle caratteristiche che sono strettamente necessarie, evitando di tenere attive funzionalità, come WebDav, che ci espongo al rischio di attacchi da remoto. In caso di motore disabilitato, la richiesta produrrà un errore 404 (risorsa non trovata). È da notare che l’errore 404 si avrà anche in presenza del file richiesto sul disco fisso: se il motore associato è disabilitato, non ci sarà verso per il web server di fornire quel file al client.

IIS 6 gira sfruttando “local service” come utente anziché “network service”, potendo contare dunque su privilegi decisamente inferiori. Se c’è un problema realmente legato ad IIS è la configurazione delle ACL di Windows, ovvero dei privilegi di accesso al file system. Appare ovvio che meno privilegi un processo ha, più robusta risulterà l’intera piattaforma. La regola che vige in questi casi è quella di impostare sul server i permessi in modo che ogni singola applicazione possa accedere solo ai file che sono strettamente necessari e sicuramente in modo che non possa assolutamente arrivare a file di sistema. Questa semplice regola molte volte è ignorata anche in ambienti di hosting condiviso, dove ogni utente ha accesso ai file degli altri, in barba alla privacy (oltre che al buon senso). Per fare in modo che ognuno possa accedere solo a ciò che gli è concesso, è sufficiente creare per ogni applicazione un utente, da inserire in un gruppo specifico chiamato ad esempio “Web Users”, ed impostare sulle directory che compongono l’applicazione l’accesso solo a quell’utente. Successivamente dovremo dire ad IIS 6 di utilizzare come utente per quell’applicazione l’utente appena creato. Si può trovare un’insieme di schermate che spiegano meglio il procedimento in Figura 1 e 2. Si arriva a questa maschera dalle proprietà dell’application pool, alla voce Identity. Il risultato sarà che l’accesso alle risorse del file system (oltre che alle altre risorse del sistema) sarà effettuato proprio con questo utente, rendendo impossibile per una pagina scritta male (o i cui parametri siano forzati ad arte) l’accesso a risorse contenute al di fuori dallo spazio riservato all’applicazione.

Cosa può fare IIS 6 per noi: gli application pool Dal punto di vista della sicurezza, un ruolo fondamentale è giocato anche dagli application pool, che sono un po’ il concetto di web application (o virtual directory) di IIS 5 rivisto in una chiave differente, dove all’interno di un application pool possono essere presenti più web application che condividono certi parametri. IIS 5 ha un’architettura profondamente diversa rispetto a quella di IIS 6. In pratica è tutto racchiuso in un solo processo, inetinfo.exe, che si occupa di gestire i processi necessari all’esecuzione delle richieste, l’amministrazione del web server ed il controllo sullo stato di salute dello stesso. È chiaro che se un sito web porta ad un crash di inetinfo.exe, il relativo sistema di controllo va in crash con il servizio stesso, rendendo di fatto non funzionante il sistema di controllo.

N. 60 - Novembre/Dicembre 2004

VBJ

21

DIFENDERSI DAGLI HACKER

IIS 6 invece si basa su 3 processi separati, ciascuno dei quali assolve a compiti specifici ([1]). In particolare il processo responsabile per la buona salute degli application pool è W3SVC, che si occupa di far partire i worker process e di monitorarne lo stato. Questo vuol dire che se un application pool ha problemi, IIS 6 è in grado di isolarlo e terminarne l’esecuzione, se necessario, senza intaccare minimamente gli altri application pool (e quindi le altre applicazioni) presenti. Tra l’altro viene effettuato un recovery automatico in caso di consumo eccessivo di risorse, come CPU o memoria (parametro impostabile dall’amministratore), in caso di blocco del processo per un numero di secondi stabilito e cosa molto importante, anche in caso di buffer overflow, togliendo quindi una potenziale porta di ingresso. Gli altri due processi, non meno importanti in senso assoluto ma di minor interesse per quanto riguarda la sicurezza, sono http.sys e W3Core: HTTP.SYS, come l’estensione stessa suggeri• sce, è un driver in kernel mode, che si occupa semplicemente di prendere le richieste che arrivano allo stack di rete di Windows Server 2003 e di smistarle, attraverso W3Core, al worker process per l’esecuzione. Il fatto che http.sys sia nella kernel area anziché nella user area non rappresenta un pericolo, perché è un processo che di fatto non esegue codice, ma smista solo richieste e risposte sullo stack di rete. W3Core, si occupa dell’hosting vero e proprio • dei worker process (sono quei processi w3wp.exe che possiamo notare con il task manager).

Cosa possiamo fare noi per IIS 6 Che IIS 6 sia sicuro è ormai, a circa un anno e mezzo dalla sua uscita ufficiale, un dato di fatto. Lo dimostra anche la realtà: in questi 18 mesi i bug scoperti per IIS 6 non hanno paragone con le precedenti versioni. Sono semplicemente zero. Tutto dipende dal fatto, con buona probabilità, che IIS 6 è stato riscritto completamente rispetto ad IIS 4 o 5 per essere, prima di tutto, un web server sicuro, affidabile e dalla garantite prestazioni. Per rendere però ancora più sicuro il nostro web server dobbiamo metterci del nostro, evitando alcuni comportamenti ed osservando alcune semplici regole. Partiamo da ciò che bisogna sempre evitare. Di sicuro, come già detto, una cattiva pratica è quella di usare un utente per tutti gli application pool (e quindi per tutte le web application). Vanno impostati i permessi in modo tale che non sia possibile sfruttare problemi delle applicazioni per arrivare a leggere il contenuto del disco. Allo stesso modo, è consigliabile tenere i siti in una partizione separata, in modo che eventuali tentativi di forzatura dei parametri non permettano facilmente l’accesso al disco di sistema. È poi ovviamente consigliato non tenere attivi motori non utilizzati (come WebDav) e servizi non sfruttati (come POP3, SMTP o NNTP). Il sistema più semplice per verificare che non lo siano è mostrato in Figura 3. Come in tutto ciò che riguarda IIS, ci si arriva seguendo la voce “Internet Information Services” all’interno di “Administrative tools” nel pannello di controllo. È poi un atto quasi dovuto disattivare il default web site e l’administrative web site. Infine IIS 6 non andrebbe usato su domain controller, per evitare di compromettere un server che ricopre un ruolo centrale all’interno di una rete basata su Windows. Di particolare interesse per la serie “cose da fare assolutamente”, c’è la buona regola di tenere sempre da parte gli eseguibili CGI, in modo che il permesso di esecuzione venga dato solo ad un certo percorso, e l’eliminazione del permesso Execute per l’utente anonimo sulla directory %windir%. Ed ovviamente vige la regola di tenere sempre aggiornato, con patch e quant’altro, il server.

Protezione contro il code injection in ASP.NET 1.1

Figura 2

22

VBJ

Più di un sito web può appartenere ad un application pool, anche se è consigliabile specificarne uno differente per ogni sito, in modo da isolare l’accesso alla risorse

N. 60 - Novembre/Dicembre 2004

Nelle altre pagine della rivista avrete certamente trovato spunti di riflessioni su questa tecnica. Senza soffermarci ulteriormente sugli aspetti specifici di questo attacco, cerchiamo di capire invece cosa ASP.NET 1.1 fa di default per noi, in modo da poter valutare, eventualmente, gli ambiti in cui possiamo muoverci per personalizzarne l’efficacia. Nella versione 1.1 (ed attenzione che questo controllo non viene effettuato con la 1.0) di ASP.NET c’è dunque

DIFENDERSI DAGLI HACKER

Un sistema per evitare questa incertezza consiste nella creazione di un semplice HttpModule (vedi [2]). Gli HttpModule sono delle classi particolari che permettono di intercettare gli eventi di un’applicazione ASP.NET. Nel nostro caso l’evento che sfrutteremo è ovviamente OnError della classe HttpApplication. C# Private void OnError (Object o, EventArgs e) { // verifica tipo di errore Exception ex = Server.GetLastError();

Figura 3

// intercetta solo l’errore specifico if (ex.GetType().ToString() == “System.Web.HttpRequest ValidationException”) { Response.Redirect(“injection.aspx”); }

Si può notare come sia semplice disattivare il supporto per un motore non richiesto. In figura WebDav, ad esempio, è disattivato

}

un controllo built-in, che può essere disattivato con la proprietà validateRequest della direttiva @Pages (o dell’analoga proprietà nel web.config su base di tutta l’applicazione), che in breve controlla che all’interno di cookie, querystring o campi inviati tramite il metodo post, non si verifichino queste condizioni:

• • • • •

presenza del carattere “<” seguito da un carattere alfanumerico, oppure da “!”; presenza della sequenza “&#”, per prevenire l’inserimento di codici particolari, legati ad entities (X)HTML; presenza del termine “script”, seguita da spazi o virgole, per evitare inserimenti di codice Javascript; presenza del termine “expression”, per evitare injection di codice Javascript attraverso la forma “style=a: expression(javascript)”; presenza del termine “on” preceduto da spazi, seguito da caratteri alfanumerici, seguito da spazi e dal simbolo “=”, per prevenire injection di codice come “onClick=”.

Se uno di questi controlli non viene passato, viene scatenata un’eccezione di tipo HttpRequestValidationException. Di default purtroppo non c’è modo di visualizzare una pagina personalizzata che spieghi all’utente il motivo dell’errore, visto che ASP.NET lo considera una normale eccezione non gestita e per questo viene invocata la pagina di errore personalizzata o viene mostrato il solito messaggio. Di fatto l’utente non è in grado di capire se l’errore è stato provocato da qualcosa che ha fatto o dalla nostra applicazione.

La pagina injection.aspx mostrerà un testo specifico che avverte l’utente del problema, migliorando quindi allo stesso tempo l’usabilità della nostra applicazione. È utile notare che questo controllo non mette al riparo da XSS (Cross Site Scripting) o SQL-injection, dunque è solo un primo scudo, ma vale sempre la pena di effettuare successivamente dei controlli personalizzati sull’input.

Nascondere gli errori e metterli in cassaforte Quante volte vi è capitato di arrivare su un sito con un’immagine oggettivamente antiestetica, come quella mostrata in Figura 4? Oltre alla bruttezza (ed alla poca cura che chi ha sviluppato quel sito ha messo nel proprio lavoro), c’è da considerare anche la pericolosità che un errore così dettagliato può offrire all’utente. Non è raro che venga mostrato, in certi ambiti e con il debug attivato, il pezzo di codice che ha causato l’errore, che magari può offrire all’utente un meccanismo per forzare la nostra applicazione. Spesso il motivo per cui viene rimossa la pagina personalizzata risiede nel fatto che se c’è bisogno di fare “debug” in tempo reale su un sito che è già in produzione, non ci sono altre strade percorribili con quello che ASP.NET ci offre. Di default possiamo infatti impostare una pagina d’errore personalizzata che viene visualizzata sempre, mai o solo se si accede alla pagina dallo stesso indirizzo IP sul quale gira. Molto spesso invece l’intervento è effettuato da un indirizzo IP remoto, che magari è sempre fisso e non cambia, e può tornare utile aggiungere un ulteriore opzione a quelle offerte all’interno del web.config, per po-

N. 60 - Novembre/Dicembre 2004

VBJ

23

DIFENDERSI DAGLI HACKER

ter specificare un ulteriore indirizzo IP a cui dare accesso all’errore completo. La soluzione in questo caso è, ancora una volta, l’uso di un HttpModule che banalmente verifichi se la richiesta arriva dall’IP specificato all’interno del web.config. Solo in questo caso il flusso della risposta sarà bloccato e sarà stampato a video, sfruttando il codice che segue, lo stesso identico errore che ASP.NET mostra quando gli errori personalizzati sono disabilitati. C# private void OnError(object o, EventArgs e) { HttpContext Context = HttpContext.Current; // ricava indirizzo IP locale e in web.config string localIP = Context.Request.UserHostAddress; string localIPconfig = ConfigurationSettings.AppSettings [“localIP”]; if (localIP == localIPconfig) { Context.Response.Clear();

); // ripulisco gli errori Context.ClearError(); // blocco l’esecuzione del resto Context.Response.End(); } }

Perché il tutto abbia un senso, ancora una volta andremo ad intercettare l’evento OnError della classe HttpApplication, costruendo un HttpModule che se ne occupi [3], oppure aggiungendo più semplicemente questo controllo all’HttpModule che abbiamo creato in precedenza. Una volta compilata la classe, l’assembly generato va inserito nella directory /bin/ dell’applicazione (o in GAC) e va registrato il relativo riferimento all’HttpModule all’interno del web.config. Il codice può essere ovviamente modificato, in modo che il controllo non venga fatto solo sull’IP, ma ad esempio sfruttando la Forms Authentication di ASP.NET, in base al role associato all’utente [4]. Con questo semplice accorgimento abbiamo dunque reso il nostro lavoro ancora più sicuro.

HttpException ex = (HttpException)Context.Server.GetLast Error(); // errore specifico o generico Context.Response.Write((ex.GetHtmlErrorMessage()!= null)? ex.GetHtmlErrorMessage(): String.Concat(“<pre>”, Context.Server.GetLastError().ToString(), “”)

Conclusioni Come si può notare, la sicurezza è un argomento davvero semplice, che spesso si risolve più che altro nell’applicazione del buon senso, oltre che di qualche trucco imparato con il tempo (o con letture a tema). Nel caso specifico, sia IIS 6 che ASP.NET ci danno un’ottima mano, considerato che partire su una piattaforma robusta permette di arrivare più facilmente a quello che è un prerequisito del quale non ci si può dimenticare: la sicurezza.

Riferimenti

Figura 4

24

VBJ

Un anonimo ma molto dettagliato errore può contenere molte informazioni utili per chi tenta di attaccare la nostra applicazione!

N. 60 - Novembre/Dicembre 2004

[1] Gli application pool di IIS 6 – Daniele Bochicchio http://www.aspitalia.com/articoli/ win2003/iis6_application_pool.aspx [2] HttpModule e HttpHandler – Daniele Bochicchio http://blogs.aspitalia.com/daniele/ post53.aspx [3] #528 - Visualizzare l’errore esteso di ASP.NET in base all’indirizzo IP di connessione – Daniele Bochicchio http://www.aspitalia.com/liste/usag/ script.aspx?ID=528 [4] Autenticazione di ASP.NET: Forms Authentication con roles - Andrea Zani http://www.aspitalia.com/articoli/ aspplus/formauthroles.aspx

DIFENDERSI DAGLI HACKER

Sviluppare senza essere amministratori Sviluppare applicazioni senza essere amministratori è importante per la sicurezza e per scrivere applicazioni eseguibili con il minimo dei privilegi necessari. Vediamo cosa c’è da sapere su Windows e Visual Studio .NET e quali tool sono utili per affrontare questo scenario. di Marco Russo crivere software è un’operazione complessa che richiede al programmatore un controllo più o meno completo sulla macchina che sta utilizzando. Questa esigenza, unita a un errato luogo comune secondo cui molte attività (come il debug) richiedono un utente amministratore, contribuisce ad avere una percentuale altissima di sviluppatori che usano quotidianamente un utente con privilegi amministrativi per fare tutte le operazioni, dalla navigazione in internet alla lettura della posta all’installazione dei tool di cui ha bisogno. La mia personale statistica, basata su sondaggi empirici fatti in conferenze e corsi tenuti nell’ultimo anno, valuta questa maggioranza nel 90-95% dei programmatori. Bene, è ora di cambiare queste brutte abitudini! Già, perché usare quotidianamente un account amministratore è pericoloso per un utente normale, ma per un programmatore è addirittura sbagliato. Accusare in questo modo una percentuale così alta di colleghi è volutamente provocatorio, ma ci sono delle buone ragioni per dirlo: vediamo quali sono e come fare per cambiare abitudini.

S

Perché è giusto farlo La sicurezza è un problema che va affrontato da tanti punti di vista. Quello che ci interessa esaminare in questo articolo è la necessità di eseguire il codice con il minimo dei privilegi necessari, così da limitare i danni in caso di errore o attacco. Un classico esempio è IIS: se l’utente che esegue il servizio

Marco Russo è un consulente specializzato nello sviluppo di applicazioni con .NET Framework e nella realizzazione di soluzioni di Business Intelligence. Autore, docente, speaker a conferenze, fa parte del gruppo DevLeap. Può essere contattato via email: [email protected]. Mantiene un blog all’indirizzo: http://blogs.devleap.com/marco.blog.

è LocalSystem un’eventuale vulnerabilità può consentire l’accesso a tutte le risorse del sistema, mentre se l’utente ha i diritti minimi per eseguire l’applicazione web, ma non può accedere ad altre risorse, i danni derivati da un attacco sono più limitati. Analogamente, un utente amministratore che apre l’allegato a una mail o naviga sul web può installare (magari inavvertitamente) qualsiasi software sul suo PC e ha i diritti di modificare tutti i file del sistema operativo e l’intero registry. Cosa c’entrano i programmatori in tutto questo? Prima di tutto, anche noi siamo utenti e in quanto tali dovremmo adottare dei comportamenti sicuri. In secondo luogo, il software che realizziamo deve essere progettato per poter essere eseguito con il minimo dei privilegi necessari: può sembrare un’esigenza banale, ma se quotidianamente si usa un utente con privilegi amministrativi, come si può essere sicuri di scrivere del codice che sarà utilizzabile anche da utenti normali? Certo, si potrebbe usare un utente di test con cui effettuare tutte le verifiche del caso, ma nulla è più istruttivo che mettersi nei panni dell’utente comune e vedere che tipo di esperienza può dover subire. Usare un utente non amministratore per un programmatore ha un duplice vantaggio: consente di adottare un comportamento più sicuro in qualità di utente e permette di comprende-

N. 60 - Novembre/Dicembre 2004

VBJ

25

DIFENDERSI DAGLI HACKER

runas /user:administrator program.exe

L’uso nidificato di RunAs permette la creazione di una finestra con un utente locale che, però, accede in rete con un access token del dominio (presupponendo che l’utente amministratore locale della macchina non sia anche un utente del dominio). Questa combinazione è importantissima quando si lavora in rete, anche soltanto per installare una patch disponibile su un server remoto. runas /user:administrator “runas /netonly /user:DOMAIN\username cmd.exe”

Figura 1

Per default un utente ha diritti di sola lettura su HKEY_LOCAL_MACHINE

re prima e meglio gli errori da evitare (anche osservando quelli dei programmi che non sono scritti così bene).

Primo ostacolo: l’esperienza da utente Se modificare il proprio utente rimuovendolo dal gruppo di amministratori non presentasse degli effetti collaterali, questo articolo non avrebbe ragione di esistere. L’esperienza iniziale di un utente qualsiasi (non solo di un programmatore) è normalmente traumatica. Dopo aver abbandonato il gruppo degli onnipotenti (gli amministratori), al nuovo logon vi accorgerete di avere molte limitazioni:

• • • •

Non potete installare applicazioni. Non potete installare plug-in e ActiveX su Internet Explorer. Non potete aggiornare le applicazioni esistenti. Alcune applicazioni già installate non si possono più avviare o presentano dei problemi di funzionamento.

Ce n’è abbastanza per desistere da subito. La maggior parte dei problemi si possono aggirare con “RunAs…” (SHIFT + clic col tasto destro sull’icona di un’applicazione) che consente di avviare un processo utilizzando un utente differente, chiedendone l’autenticazione. Il programma è avviato con un access token (informazioni che descrivono l’utente) diverso da quello con cui si stanno eseguendo le altre applicazioni. Un’alternativa all’interfaccia utente è l’uso del comando RunAs, che consente di creare dei batch che eseguono applicazioni, pannelli di controllo o console amministrative:

26

VBJ

N. 60 - Novembre/Dicembre 2004

Avviare un’applicazione con queste tecniche è scomodo (ogni volta viene richiesta la password dell’amministratore), ma il suo uso dovrebbe essere un’eccezione e non la regola. Purtroppo in un primo momento sembra l’unica soluzione per avviare tutti quei programmi che, con un utente non amministratore, si rifiutano di partire senza nemmeno fornire una chiara spiegazione dell’errore che si verifica. Molti a questo punto desistono e tornano nel vituperato gruppo amministrativo, ma sicuramente chi legge questo articolo non vorrà gettare la spugna tanto presto. Il più delle volte il problema di un’applicazione che non parte con un utente normale è attribuibile a uno scorretto uso di Registry o file su disco. Per capire come risolvere i problemi è necessario comprenderne la causa e quindi trarre le prime lezioni su come scrivere un programma eseguibile con il minimo dei privilegi. All’installazione di Windows il registry e i file su disco sono protetti con delle ACL (Access Control List) che definiscono chi può fare che cosa. Nel Registry tutte le voci all’interno di HKEY_LOCAL_MACHINE sono visibili in lettura da tutti gli utenti, ma sono modificabili solo dal gruppo di amministratori (come si può vedere in Figura 1). Per vedere le ACL associate al registry è necessario usare Regedit su Windows XP e Windows 2003, mentre su Windows NT 4 e Windows 2000 si usa Regedt32. Sul disco tutti i dati nelle directory di sistema (tipicamente c:\windows o c:\winnt) e dei programmi (come c:\Programmi o c:\Program Files) e i file nella root (c:\) sono visibili in lettura a tutti gli utenti e modificabili solo dagli utenti nei gruppi Administrators e Power Users. Notare che far parte del gruppo Power Users non è molto diverso, in termini di mancanza di sicurezza, dall’appartenere al gruppo di amministratori, quindi per il nostro ragionamento è ugualmente sconsigliabile associare il proprio utente a tale gruppo. Quando un’applicazione si rifiuta di partire con un utente normale, ma funziona regolarmente se avviata da un amministratore, nel 99% dei casi il motivo è che tenta un’operazione non autorizzata sul registry o sul disco. Se le applicazioni fossero scritte seguendo fedelmente i

DIFENDERSI DAGLI HACKER

Figura 2

Impostazione di uno shortcut equivalente a un RunAs automatico per ogni avvio

dettami delle tante linee guida e dei requisiti per ottenere il logo Windows non ci sarebbe molto da dire, purtroppo il mondo reale non segue le regole e dobbiamo anche considerare che molte applicazioni nascono su Windows 9x, dove in pratica non esiste il concetto di utente come lo intendiamo oggi e quindi tutti possono fare tutto ovunque sul disco… Senza pretendere di essere esaustivi, riassumiamo alcuni degli errori commessi più di frequente, ripassando le regole che violano:





Non si può scrivere su file e directory di sistema (come c:\windows): alcune applicazioni creano e/o scrivono dei file .INI o dei file che dovrebbero lasciare una traccia dell’avvenuto uso del programma in una certa data, come sistema di controllo della scadenza di una versione demo. Non si può scrivere su file e directory contenuti nella directory dei programmi (come c:\programmi): molte applicazioni usano dei file contenuti in queste cartelle come file di dati dell’applicazione, per esempio creano un file di configurazione che viene modificato se l’utente cambia le impostazioni del programma; per queste necessità esistono delle directory apposite.

Va notato che a volte l’errore è veramente banale: se un’applicazione ha bisogno di leggere un file ma al momento dell’apertura non specifica che essa avviene in modalità read-only, il sistema operativo genera un errore (il classico access denied) già in fase di apertura del file; questo spiega lo sconcerto di chi cerca inutilmente nel proprio sorgente delle funzioni di scrittura su file senza trovarne. Attenzione perciò a specificare sempre i privilegi minimi anche quando si apre una risorsa (un file, una connessione di rete, una porta di comunicazione), richiedendo solo i servizi minimi indispensabili per la funzionalità richiesta successivamente. Se il file deve invece essere modificabile, è necessario metterlo in una directory adeguata: all’interno della cartella C:\Documents and Settings esistono directory diverse per ogni utente all’interno delle quali sono memorizzati i documenti personali, la configurazione del menu Start e dati specifici per l’utente delle applicazioni installate. È importante ricordare che tutte le directory “speciali” come queste devono essere ottenute con funzioni apposite come SHGetFolderPath. Se il programma è nostro, quindi, correggiamo l’errore, usiamo la tecnica giusta, ricompiliamo e possiamo usare l’applicazione con un utente normale. Sviluppando senza essere amministratori ci si accorgerà molto presto di problemi simili e il costo necessario a risolvere il problema sarà bassissimo: più un bug invecchia, più la sua eliminazione diventa costosa (il caso peggiore è quando arriva fino all’utente finale). Quando però il programma non è nostro e non abbiamo il codice sorgente, ci sono poche alternative. Se siamo fortunati, c’è una versione più recente che è scritta meglio; se siamo sfortunati, dobbiamo tenerci l’applicazione scritta male. Per evitare di dover ricorrere a RunAs tutte le volte, possiamo modificare (con un utente amministratore!) le ACL della zona incriminata, registry o disco che sia. Ma come sapere cosa toccare? Una tattica un po’ grossolana è quella di individuare il nodo principale sul registry e la directory dell’applicazione sul disco e modificare le ACL per tutti i file e tutte le directory (o le chiavi di registry) contenute, assegnando il privilegio di scrittura anche a tutti gli utenti che vogliamo abilitare all’uso del programma. Un approccio migliore, però, consiste nel limitare la modifica delle ACL ai soli punti in cui ciò è indispensabile: per esempio il singolo file (o chiave di registry) oggetto del tentativo di modifica. Il problema è capire cosa toccare.

Analizzare il comportamento dei programmi Il vantaggio di avere i sorgenti di un programma è che si può avviare il debugger e aspettare che succeda qualcosa, magari controllando quale chiamata genera inizial-

N. 60 - Novembre/Dicembre 2004

VBJ

27

DIFENDERSI DAGLI HACKER

mente un errore che porta all’interruzione ed eventualmente all’uscita dall’applicazione. Se però non si dispone di queste informazioni (o non si ha il tempo e/o la capacità di addentrarsi nella giungla di codice scritto da terzi), una strategia forse addirittura migliore è quella di osservare l’applicazione dall’esterno utilizzando degli strumenti adatti. RegMon e FileMon sono due utility scritte da Mark Russinovich e Bryce Cogswell (e disponibili su www.sysinternals.com) che consentono di tracciare tutte le chiamate alle API di Windows responsabili, rispettivamente, dell’accesso al registry e ai file. Per usare queste utility è necessario essere amministratori, perché installano dinamicamente un driver che intercetta le chiamate API Win32 interessate. Questi strumenti consentono di definire dei filtri (è utile filtrare solo le chiamate fatte dal processo analizzato) e di osservare, tra le altre cose, l’esito della chiamata e i diritti di accesso richiesti. Per esempio, un programma che tenti di aprire in lettura/ scrittura un file di configurazione genera una chiamata Open che fallisce; analizzando il nome del file e i diritti richiesti sarà facile individuare l’entità minima (il file) a cui apportare modifiche della ACL (si concederà il diritto di modifica al singolo utente o al gruppo Users). Per l’accesso al registry la tecnica è analoga anche se gli strumenti usati sono diversi. Dal punto di vista della sicurezza il fatto di aumentare i privilegi di accesso ad alcuni file posti in zone “critiche” del disco non va contro i principi che stiamo cercando

di seguire. Semplicemente, questo è il male minore: un eventuale attacco può danneggiare alcune risorse dell’applicazione attaccata ma non va a intaccare altre risorse o altre applicazioni della stessa macchina. Certo, la soluzione ideale resta quella di correggere il software, ma fino a che ciò non avviene si cerca di limitare i potenziali danni. La mia esperienza è che i primi due giorni sarete molto arrabbiati con chi scrive male i programmi perché tutto il ciclo di attività per individuare i problemi descritti e risolverli è piuttosto lungo e noioso (tolti i primi due casi che potreste trovare divertenti, visto che si tratta di una cosa nuova). Questa arrabbiatura è però un’utile lezione per imparare a scrivere meglio il proprio software.

Visual Studio .NET Terminato il primo periodo di adattamento come utente non amministratore, l’ostacolo successivo consiste nel capire come usare Visual Studio .NET senza limitazioni particolari. In generale, l’utente sviluppatore dovrebbe appartenere a questi gruppi locali:

• • •

Come abbiamo già detto, l’utente non deve appartenere ai gruppi Administrators e Power Users. Per lo sviluppo e il debug di applicazioni Web sono necessarie altre impostazioni che vedremo più avanti. Alcune attività, però, restano necessariamente prerogativa degli amministratori:

• • •

Figura 3

28

VBJ

Impostazione dell’Application Pool con l’utente usato per sviluppare: le directory virtuali corrispondenti alle applicazioni web sviluppate dovranno essere assegnate a questo Application Pool (che va creato manualmente).

N. 60 - Novembre/Dicembre 2004

Users Debugger Users VS Developers

Registrazione di componenti COM (regsvr32/ regasm) Installazione di assembly nella GAC (gacutil) Installazione di componenti .NET nel catalogo COM+ (regsvcs)

In questi casi si è obbligati a rinunciare alla funzionalità dei post-build step di Visual Studio .NET, effettuando la registrazione a mano con un utente amministratore. Per effettuare il debug l’utente deve avere dei diritti particolari, che abbiamo già ottenuto assegnando l’utente al gruppo Debugger Users (in realtà ciò che conta è il diritto SeDebugPrivilege associato all’access token, ma Visual Studio aggiorna già le policy assegnando tale diritto al gruppo Debugger Users). .NET impone però un’ulteriore limitazione rispetto al debug di un processo eseguito con un utente diverso: per fare ciò è necessario essere amministratori (non è così per le applicazioni unmanaged). La limitazione non è percepibile per le applicazioni Windows Forms eseguite dall’interno dell’IDE di Visual Studio,

DIFENDERSI DAGLI HACKER

ma coinvolge chi sviluppa dei servizi o delle applicazioni Web e chi vuole fare debug in remoto. La cosa migliore, in questi casi, è quella di eseguire l’applicazione con lo stesso utente con cui si fa il debug. Se il debug avviene in remoto, l’utente usato deve avere il privilegio SeDebugPrivilege anche sulla macchina remota (dove, se non c’è Visual Studio, il gruppo Debugger Users non è stato creato, quindi bisogna modificare le policy a mano) e deve essere amministratore della macchina remota se l’applicazione è di tipo .NET (managed).

Applicazioni Web Per lo sviluppo e il debug di applicazioni Web vale la pena fare un discorso a parte, perché entrano in gioco diversi fattori. Per quanto riguarda lo sviluppo, la creazione di un’applicazione ASP.NET da Visual Studio può presentare un problema se si usano le Front Page Server Extension anziché il file share [1]: poiché il default è la modalità di connessione file share, questo non è un bug così noto. L’appartenenza al gruppo VS Developers garantisce i diritti necessari per la creazione di una Virtual Directory in IIS, ma in caso contrario (per es. server web remoto) è sempre possibile creare la Virtual Directory con un utente che ha privilegi sufficienti (per es. amministratore) per poi creare il relativo progetto in Visual Studio con l’utente normale. Per il debug di un’applicazione ASP.NET bisogna considerare due aspetti: l’utente con cui gira l’applicazione e i diritti sulle directory usate da ASP.NET. L’utente si può modificare impostando in IIS 5 (Windows XP) il tag processModel di ASP.NET (notare la password in chiaro, che si può cifrare con l’utility aspnet_ setreg [2]): <processModel

enable=”true” userName=”DOMAIN\username” password=”pwd”

/ > Con IIS6 (Windows 2003), invece, si agisce creando un Application Pool: ogni Virtual Directory è assegnata a un

Riquadro 1 Esecuzione con credenziali diverse Nelle impostazioni avanzate delle proprietà di uno shortcut è possibile specificare la richiesta di credenziali diverse per ogni esecuzione, evitando di dover cercare il menu RunAs tutte le volte. La Figura 2 mostra questa configurazione sullo shortcut di FileMon.

Figura 4

Associazione di una directory virtuale all’Application Pool usato per il debug delle applicazioni

Application Pool, consentendo così di differenziare gli utenti per applicazioni ASP.NET diverse. L’utente assegnato all’Application Pool (il nostro utente, se vogliamo fare debug) deve appartenere al gruppo IIS_WPG perché sia utilizzabile con ASP.NET. In Figura 3 è visibile la configurazione di un Application Pool creato per fare debug (l’utente associato è lo stesso usato per sviluppare e fare debug). In Figura 4 è evidenziato come si associa una virtual directory di IIS a un Application Pool (in questo caso Debuggable App). L’utente usato deve avere anche il diritto di accedere in lettura e scrittura alle directory usate da ASP.NET, come la directory in cui sono generati gli assembly a partire dalle pagine ASPX (Temporary ASP.NET Files, per cui il gruppo IIS_WPG ha già Full Control). Il ricorso a Filemon consente di individuare eventuali problemi di questo tipo, in genere causati da componenti o librerie esterne usate dall’applicazione web (in particolare se usate componenti COM). Su MSDN è disponibile un documento [3] che descrive molto bene i passi operativi necessari per impostare le configurazioni che abbiamo analizzato in questo articolo rispetto all’uso di Visual Studio .NET con un utente non amministratore.

Applicazioni Visual Basic 6 Se, anche solo per necessità, sviluppate ancora con Visual Basic 6, purtroppo dovete fare i conti con il fatto che VB6 non è stato minimamente pensato per sviluppare senza essere amministratori; più che l’ambiente di sviluppo, poi, molti problemi li creano i tanti com-

N. 60 - Novembre/Dicembre 2004

VBJ

29

DIFENDERSI DAGLI HACKER

ponenti di cui è dotato VB6. Il problema è in parte aggirabile con i sistemi già descritti, usando Filemon e Regmon per capire dove modificare l’accesso a parti del disco e del registry, ma va detto che a volte la colpa (di sviluppare con utenti amministratori) non è tutta degli sviluppatori ma anche di chi gli ha dato le brutte abitudini...

Per partire con il piede giusto è necessario capire quali sono le problematiche più frequenti e come modificare (per il minimo indispensabile) i diritti di directory e registry necessari a evitare il ricorso al RunAs, che resta indispensabile per alcune attività di tipo amministrativo. Strumenti come Filemon e Regmon sono preziosissimi per individuare queste criticità.

Conclusioni Usare Windows e svilupparci con un utente non-amministratore è doveroso. L’esperienza da utenti non amministratori aumenta la consapevolezza dei comportamenti che vanno evitati per scrivere applicazioni utilizzabili con il minimo dei privilegi. Purtroppo non è semplice e immediato come potrebbe e dovrebbe essere. In futuro (con Longhorn, la prossima versione di Windows) ci saranno molte novità per migliorare la situazione, ma negli anni che ci separano da questo traguardo non ci si può permettere di continuare a seguire comportamenti a rischio.

30

VBJ

N. 60 - Novembre/Dicembre 2004

Riferimenti [1]

Bug di Visual Studio, KB833896, http:// support.microsoft.com/default.aspx?scid=kb;ENUS;833896 [2] Uso di aspnet_setreg, KB329290, http:// support.microsoft.com/default.aspx?scid=kb;ENUS;329290 [3] Developing Software in Visual Studio .NET with NonAdministrative Privileges, http://msdn.microsoft.com/ library/en-us/dv_vstechart/html/tchDevelopingSoftwa reInVisualStudioNETWithNon-AdministrativePrivileges .asp

BEGINNER

Programmare Excel da Visual Basic .NET Analisi di tre soluzioni per trasferire dati a Excel usando Visual Basic .NET e le classi del .NET framework

di Fabio Scagliola uesto articolo descrive tre soluzioni diverse per trasferire dati a Excel usando Visual Basic .NET e le classi del .NET framework. La prima soluzione usa il modello a oggetti di Excel per scrivere nelle celle del foglio di lavoro, come si farebbe attraverso l’interfaccia grafica del programma, e crea un grafico basato sui dati trasferiti. La seconda soluzione crea un file di testo separato da tabulazioni che Excel possa aprire come se fosse una cartella di lavoro.

Q

dati), System.IO (per scrivere nel file di testo) e System.Xml (per manipolare i documenti XML). In particolare il codice dei listati presuppone le direttive seguenti. Imports Imports Imports Imports Imports Imports

System System.Data System.Data.SqlClient System.IO System.Xml System.Xml.Xsl

I dati

Figura 1

Aggiungere un riferimento alla libreria COM di Excel

La terza soluzione crea un XML Spreadsheet. Tutte usano le classi del namespace System.Data (per accedere ai

Fabio Scagliola (MCSD, MCSE, MCT) fornisce “software e consulenza in materia di informatica” in giro per il mondo. Torna spesso a Milano per insegnare nelle aule di Talento Education & Training

Le tre soluzioni estraggono dati dal database Northwind su SQL Server 2000 eseguendo la stored procedure CustOrdersDetail. Dopo aver assegnato il numero di un ordine al suo unico parametro OrderID, la stored procedure restituisce i dettagli di quell’ordine. Per eseguirla, tutte le soluzioni usano ADO .NET e in particolare oggetti SqlConnection e SqlCommand. Poi le prime due useranno un SqlDataReader, e la terza una coppia DataAdapter/ DataSet. Il codice seguente, comune alle tre soluzioni, crea un oggetto SqlConnection per collegarsi al database e un oggetto SqlCommand per eseguire la stored procedure. Dim cn As New SqlConnection(“DATA SOURCE=(local)\INSTANCE1;INITIAL

N. 60 - Novembre/Dicembre 2004

VBJ

31

BEGINNER

Listato 1

Programmare il modello a oggetti di Excel

di prodotti. Per maggiori informazioni su ADO.NET si veda [1].

Programmare il modello a oggetti di Excel Dim cn As New SqlConnection(“DATA SOURCE=(local)\ INSTANCE1;INITIAL CATALOG=Northwind;USER ID=sa;PASSWORD =password”) Dim cm As New SqlCommand(“CustOrdersDetail”, cn) cm.CommandType = CommandType.StoredProcedure Dim OrderID As SqlParameter = cm.Parameters.Add(“@OrderI D”, SqlDbType.Int) OrderID.Value = 11077 Try cn.Open() Dim dr As SqlDataReader = cm.ExecuteReader() Dim xl As Excel.Application = New Excel.Application xl.Visible = True Dim wb As Excel.Workbook = xl.Workbooks.Add() Dim ws As Excel.Worksheet = wb.ActiveSheet ws.Cells(1, 1) = “Product name” ws.Cells(1, 2) = “Unit price” ws.Cells(1, 3) = “Quantity” ws.Cells(1, 4) = “Discount” ws.Cells(1, 5) = “Total price” ws.Range(“A1:E1”).Font.Bold = True Dim RowIndex As Int32 = 2 While dr.Read() ws.Cells(RowIndex, 1) = dr(“ProductName”) ws.Cells(RowIndex, 2) = dr(“UnitPrice”) ws.Cells(RowIndex, 3) = dr(“Quantity”) ws.Cells(RowIndex, 4) = dr(“Discount”) ws.Cells(RowIndex, 5) = dr(“ExtendedPrice”) RowIndex += 1 End While ws.Range(“B2:B” & RowIndex & “;E2:E” & RowIndex).NumberFormat = “€ #,##0.00” Dim ch As Excel.Chart = NuovoGrafico(ws.Range(“A1: A26;E1:E26”), Excel.XlChartType.xlColumnClustered, “Customer Orders Detail”) wb.SaveAs(“soluzione1.xls”) xl.Quit() dr.Close() Catch ex As Exception Console.WriteLine(ex) Finally cn.Close() End Try

I programmatori VB usano il modello a oggetti di Excel (e di tutte le applicazioni Office) con maggiore naturalezza dei programmatori C#. Progettato per Visual Basic for Applications, il modello include molti metodi che accettano argomenti facoltativi (supportati solo da VB). Mentre i programmatori VB possono usare named argument per assegnare valori solo agli argomenti necessari, i programmatori C# devono assegnare a ogni argomento facoltativo il valore Type.Missing o il valore predefinito di quell’argomento. Inoltre, dal momento che C# non supporta proprietà con argomenti (indexer a parte), i programmatori devono invocare i relativi metodi accessori. Per esempio il metodo get_Range sostituisce la proprietà Range. Considerate queste differenze, la prima soluzione (Listato 1) usa il modello a oggetti di Excel per scrivere nelle celle del foglio di lavoro come si farebbe attraverso la sua interfaccia grafica. Il codice presuppone di aver aggiunto al progetto un riferimento alla Microsoft Excel 10.0 Object Library (Figura 1). Prima di tutto si apre il collegamento al database con il metodo Open dell’oggetto SqlConnection. Poi si esegue la stored procedure con il metodo ExecuteReader dell’oggetto SqlCommand per ottenere un oggetto SqlDataReader. A questo punto entra in scena Excel. Il codice seguente avvia Excel, lo rende visibile e crea una nuova cartella di lavoro vuota. Dim xl As Excel.Application = New Excel.Application xl.Visible = True

CATALOG=Northwind;USER ID=sa;PASSWORD=password”) Dim cm As New SqlCommand(“CustOrdersDetail”, cn) cm.CommandType = CommandType.StoredProcedure Dim OrderID As SqlParameter = cm.Parameters.Add(“@OrderID”, SqlDbType.Int) OrderID.Value = 11077

Dopo aver assegnato il valore opportuno alla proprietà CommandType dell’oggetto SqlCommand (per indicare il tipo di comando che sta per essere eseguito), il codice crea un oggetto SqlParameter per assegnare al parametro della stored procedure il numero dell’ordine che contiene il maggior numero

32

VBJ

N. 60 - Novembre/Dicembre 2004

Figura 2

Il risultato del Listato 2

BEGINNER Grafici

Figura 3

Il risultato del Listato 4

Il modello a oggetti di Excel rende semplice anche la creazione di un grafico. Per esempio il Listato 2 mostra il codice del metodo NuovoGrafico che restituisce un oggetto Chart in cambio di tre argomenti. Il primo argomento r deve essere un oggetto Range, per indicare le celle contenenti i dati di origine del nuovo grafico. Il secondo argomento chType deve essere il valore di uno dei membri dell’enumeration XlChartType, per indicare il tipo del nuovo grafico. Il terzo argomento chTitle deve essere una stringa, per indicare il titolo del nuovo grafico. La Figura 2 mostra il risultato del metodo NuovoGrafico applicato alle celle A1:A26;E1:E26 del foglio di lavoro creato dal Listato 1, per ottenere un grafico di tipo “istogramma non in pila” intitolato “Customer Orders Detail”. In pratica mostra il risultato della seguente istruzione.

Dim wb As Excel.Workbook = xl.Workbooks.Add() Dim ws As Excel.Worksheet = wb.ActiveSheet

Dim ch As Excel.Chart = NuovoGrafico(ws.Range(“A1:A26;E1:E26”), Excel.XlChartType.xlColumnClustered, “Customer Orders Detail”)

Per creare una nuova cartella di lavoro vuota, si invoca il metodo Add dell’oggetto Workbooks senza assegnare un valore al suo unico argomento facoltativo Template (che serve a creare una nuova cartella di lavoro partendo da un modello). Il metodo restituisce un oggetto Workbook, la cui proprietà ActiveSheet contiene un riferimento al foglio di lavoro attivo, ovvero il primo dei tre fogli della cartella di lavoro vuota. Per lavorare con le celle del foglio di lavoro si usa un oggetto Range, che rappresenta una singola cella o un intervallo di celle. Sia la proprietà Range sia la proprietà Cells restituiscono un oggetto Range. La proprietà Range permette di riferirsi a un intervallo di celle con la sintassi usata normalmente da Excel. La proprietà Cells permette di riferirsi a una singola cella come a un elemento di un array bidimensionale, indicando gli indici (numerati a partire da 1) di riga e di colonna. La proprietà Range accetta due argomenti: i riferimenti alle due celle agli estremi opposti dell’intervallo. Si possono anche indicare i due riferimenti nel primo argomento separati dai due punti. In questo modo si possono anche definire intervalli di celle non adiacenti. Il codice seguente usa solo il primo argomento della proprietà Range per applicare un formato a un intervallo di celle non adiacenti.

Per creare un grafico a partire dalle celle contenenti i dati di origine, il metodo NuovoGrafico deve risalire al foglio di lavoro che contiene le celle e da qui alla cartella di lavoro che contiene il foglio.

ws.Range(“B2:B” & RowIndex & “;E2:E” & RowIndex).NumberFormat = “€ #,##0.00”

La soluzione usa sistematicamente la proprietà Range per applicare formati a intervalli di celle e la proprietà Cells per scrivere dati nelle singole celle.

Dim ws As Excel.Worksheet = r.Worksheet Dim wb As Excel.Workbook = ws.Parent Dim ch As Excel.Chart = wb.Charts.Add()

Prima di tutto legge il valore della proprietà Worksheet dell’oggetto Range per risalire all’oggetto Worksheet,

Listato 2

Creare un grafico

Function NuovoGrafico(ByVal r As Excel.Range, ByVal chType As Excel.XlChartType, ByVal chTitle As String) As Excel.Chart Dim ws As Excel.Worksheet = r.Worksheet Dim wb As Excel.Workbook = ws.Parent Dim ch As Excel.Chart = wb.Charts.Add() ch.SetSourceData(r) ch.ChartType = chType ch.HasLegend = False ch.HasTitle = True ch.ChartTitle.Text = chTitle ch.PlotArea.Border.LineStyle = Excel.XlLineStyle. xlLineStyleNone ch.PlotArea.Interior.ColorIndex = Excel.XlColorIndex. xlColorIndexNone ch.Location(Excel.XlChartLocation.xlLocationAsObject, ws.Name) Return ch End Function

N. 60 - Novembre/Dicembre 2004

VBJ

33

BEGINNER

poi legge il valore della proprietà Parent dell’oggetto Worksheet per risalire all’oggetto Workbook, infine invoca il metodo Add della collection Charts dell’oggetto Workbook per creare un nuovo oggetto Chart. Creato il grafico, il codice usa il metodo SetSourceData per indicare le celle contenenti i dati di origine assegnando al suo unico argomento il valore di r, ovvero un oggetto Range.

ch.HasTitle = True ch.ChartTitle.Text = chTitle

Infine, dopo aver reso trasparente bordo e sfondo dell’area del tracciato, usa il metodo Location dell’oggetto Chart al fine di rendere il grafico un oggetto sul foglio di lavoro. ch.Location(Excel.XlChartLocation.xlLocationAsObject, ws.Name)

ch.SetSourceData(r)

Il codice assegna poi un valore ad alcune proprietà dell’oggetto Chart. Anzitutto imposta il tipo di grafico assegnando alla proprietà ChartType il valore di chType, ovvero uno dei membri dell’enumeration XlChartType. ch.ChartType = chType

Nasconde la legenda e mostra il titolo usando rispettivamente le proprietà HasLegend e HasTitle e imposta il titolo assegnando la stringa chTitle alla proprietà Text dell’oggetto ChartTitle. ch.HasLegend = False

Listato 3

Creare un file di testo

Dim cn As New SqlConnection(“DATA SOURCE=(local)\ INSTANCE1;INITIAL CATALOG=Northwind;USER ID=sa;PASSWORD =password”) Dim cm As New SqlCommand(“CustOrdersDetail”, cn) cm.CommandType = CommandType.StoredProcedure Dim OrderID As SqlParameter = cm.Parameters.Add(“@OrderI D”, SqlDbType.Int) OrderID.Value = 11077 Try cn.Open() Dim dr As SqlDataReader = cm.ExecuteReader() Dim sw As StreamWriter = New StreamWriter(“soluzione2 .xls”, False, System.Text.Encoding.Unicode) sw.WriteLine(“Product name” & vbTab & “UnitPrice” & vbTab & “Quantity” & vbTab & “Discount” & vbTab & “Total price”) While dr.Read() sw.WriteLine(dr(“ProductName”) & vbTab & dr(“UnitPrice”) & vbTab & dr(“Quantity”) & vbTab & dr(“Discount”) & vbTab & dr(“ExtendedPrice”)) End While sw.Close() dr.Close() Catch ex As Exception Console.WriteLine(ex) Finally cn.Close() End Try

34

VBJ

N. 60 - Novembre/Dicembre 2004

Il metodo Location richiede due argomenti: il primo deve essere un membro dell’enumeration XlChartLocation (in questo caso xlLocationAsObject) e il secondo il nome del foglio di lavoro.

Creare un file di testo Excel permette di importare dati da un file di testo separato da tabulazioni (e non solo) in un foglio di lavoro per mezzo del Text Import Wizard. Se poi il nome del file termina con estensione xls, allora Excel lo apre come se fosse una cartella di lavoro senza nemmeno chiamare in causa il wizard. (Allo stesso modo si potrebbe anche creare un file di testo separato da virgole - CSV, un formato standard - ma il “trucco” dell’estensione xls non funzionerebbe.) La seconda soluzione (Listato 3) parte da questa osservazione per creare un file di testo chiamato soluzione2.xls che contenga nella prima riga i nomi dei campi separati da una tabulazione e poi i dati strutturati,

Listato 4

Creare un XML Spreadsheet

Dim cn As New SqlConnection(“DATA SOURCE=(local)\ INSTANCE1;INITIAL CATALOG=Northwind;USER ID=sa;PASSWORD =password”) Dim cm As New SqlCommand(“CustOrdersDetail”, cn) cm.CommandType = CommandType.StoredProcedure Dim OrderID As SqlParameter = cm.Parameters.Add(“@Order ID”, SqlDbType.Int) OrderID.Value = 11077 Try cn.Open() Dim da As SqlDataAdapter = New SqlDataAdapter(cm) Dim ds As DataSet = New DataSet da.Fill(ds) ds.WriteXml(“DataSet.xml”) Dim xslt As XslTransform = New XslTransform xslt.Load(“soluzione3.xslt”) xslt.Transform(“DataSet.xml”, “soluzione3.xml”, Nothing) Catch ex As Exception Console.WriteLine(ex) Finally cn.Close() End Try

BEGINNER

un record per ogni linea con i campi sempre separati da una tabulazione. Dopo aver ottenuto un oggetto SqlDataReader come nella prima soluzione, il codice seguente mostra come creare un oggetto StreamWriter per scrivere nel file di testo. Dim sw As StreamWriter = New StreamWriter(“soluzione2.xls”, False, System.Text.Encoding.Unicode) sw.WriteLine(“Product name” & vbTab & “UnitPrice” & vbTab & “Quantity” & vbTab & “Discount” & vbTab & “Total price”)

ll metodo WriteLine dell’oggetto StreamWriter scrive testo seguito dai caratteri carriage return e line feed (codici 13 e 10). La costante vbTab concatenata alla stringa rappresenta una tabulazione (codice 9). Così la seconda istruzione del codice precedente scrive i nomi dei campi nella prima riga del file; allo stesso modo l’istruzione contenuta nel ciclo While scrive una riga per ogni record letto.

Creare un XML Spreadsheet Quando Excel versione 2002 (quello di Office XP) o successiva salva una cartella di lavoro come documento XML, rispetta le regole di una grammatica chiamata XML Spreadsheet. Queste regole, ben descritte nell’articolo “XML Spreadsheet Reference” [2], sono rispettate anche dalla terza soluzione (Listato 4) per creare un documento XML che Excel possa aprire come se fosse una cartella di lavoro. Diversamente dalle prime due soluzioni che usano un SqlDataReader, questa ultima usa un DataSet (e un DataAdapter per riempirlo) per sfruttare il suo metodo WriteXml e salvarlo come documento XML con una sola istruzione. ds.WriteXml(“DataSet.xml”) Dim xslt As XslTransform = New XslTransform xslt.Load(“soluzione3.xslt”) xslt.Transform(“DataSet.xml”, “soluzione3.xml”, Nothing)

Tra i molti overload del metodo WriteXml, il codice precedente usa quello che richiede solo il nome del file, percorso incluso. Il resto del codice trasforma il documento ottenuto in un esempio (instance) di XML Spreadsheet con una XSLT (eXtensible Stylesheet Language Transformation). Per farlo, usa un oggetto XslTransform che carica un foglio di stile con il metodo Load ed esegue la trasformazione con il metodo Transform. I tre argomenti richiesti da questo overload del metodo Transform sono due stringhe e un oggetto XmlResolver qui non usato. Le due stringhe contengono rispettivamente il nome del documento di origine e il nome del documento di destinazione, percorso incluso.

Listato 5

Parte del DataSet salvato come documento XML

Chang19.00002420 <ExtendedPrice>364.8000
Aniseed Syrup10.000040 <ExtendedPrice>40.0000
Chef Anton’s Cajun Seasoning 22.0000 1 0 <ExtendedPrice>22.0000


La trasformazione coinvolge il documento di origine (ottenuto salvando il DataSet come documento XML), il documento di destinazione (XML Spreadsheet) e il foglio di stile XSL usato per trasformare il primo nel secondo. Il documento di origine (Listato 5) ha un elemento radice con lo stesso nome del DataSet (per assegnare un nome al DataSet sarebbe stato sufficiente crearlo con un altro costruttore). La radice per ogni record contiene un elemento Table, che a sua volta per ogni campo contiene un elemento figlio con lo stesso nome del campo. Per avere una conferma di queste pur semplici regole grammaticali si potrebbe usare un altro overload del metodo WriteXml che permetta di includere nel documento il suo schema. ds.WriteXml(“DataSet.xml”, XmlWriteMode.WriteSchema)

Il foglio di stile soluzione3.xslt (Listato 6) permette di trasformare la grammatica di questo documento in quella di un XML Spreadsheet. Il codice seguente mostra un esempio di XML Spreadsheet che contiene una cartella di lavoro e un foglio di lavoro chiamato “Il mio primo XML Spreadsheet”. Nella cella A1 e nella cella B1 scrive due numeri e nella cella C1 una formula che somma il valore delle altre due.

N. 60 - Novembre/Dicembre 2004

VBJ

35

BEGINNER

Listato 6

Il foglio di stile soluzione3.xslt

<xsl:stylesheet version=”1.0” xmlns:xsl=”http://www.w3.org/1999/XSL/Transform” xmlns:ss=”urn:schemas-microsoft-com:office:sprea dsheet”> <xsl:template match=”/”> <ss:Workbook xmlns:ss=”urn:schemas-microsoft-com: office:spreadsheet”> <ss:Styles> <ss:Style ss:ID=”Bold”> <ss:Font ss:Bold=”1” /> <ss:Style ss:ID=”Currency”> <ss:NumberFormat ss:Format=”Euro Currency” /> <ss:Worksheet ss:Name=”CustOrdersDetail”> <ss:Table> <ss:Column ss:Width=”192” /> <ss:Column ss:Width=”72” /> <ss:Column ss:Width=”72” /> <ss:Column ss:Width=”72” /> <ss:Column ss:Width=”72” /> <ss:Row ss:StyleID=”Bold”> <ss:Cell> <ss:Data ss:Type=”String”>Product name <ss:Cell> <ss:Data ss:Type=”String”>Unit price <ss:Cell> <ss:Data ss:Type=”String”>Quantity <ss:Cell> <ss:Data ss:Type=”String”>Discount <ss:Cell>

<ss:Workbook xmlns:ss=”urn:schemas-microsoft-com:office:spread sheet”> <ss:Styles> <ss:Style ss:ID=”evidenziato”> <ss:Interior ss:Color=”#ffff00” ss:Pattern=”Solid” /> <ss:Worksheet ss:Name=”Il mio primo XML Spreadsheet”> <ss:Table> <ss:Row> <ss:Cell> <ss:Data ss:Type=”Number”>1

36

VBJ

N. 60 - Novembre/Dicembre 2004

<ss:Data ss:Type=”String”>Total price <xsl:apply-templates /> <xsl:template match=”Table”> <ss:Row> <ss:Cell> <ss:Data ss:Type=”String”> <xsl:value-of select=”ProductName” /> <ss:Cell ss:StyleID=”Currency”> <ss:Data ss:Type=”Number”> <xsl:value-of select=”UnitPrice” /> <ss:Cell> <ss:Data ss:Type=”Number”> <xsl:value-of select=”Quantity” /> <ss:Cell> <ss:Data ss:Type=”Number”> <xsl:value-of select=”Discount” /> <ss:Cell ss:StyleID=”Currency”> <ss:Data ss:Type=”Number”> <xsl:value-of select=”ExtendedPrice” />

<ss:Cell> <ss:Data ss:Type=”Number”>2 <ss:Cell ss:Formula=”=RC[-2]+RC[-1]” ss: StyleID=”evidenziato” />

Tutti gli elementi e gli attributi appartengono al namespace associato al prefisso ss. L’elemento radice Workbook, il cui attributo xmlns indica lo spazio dei nomi, rappresenta la cartella di lavoro e

BEGINNER

contiene un elemento Worksheet per ogni foglio di lavoro. Ogni elemento Worksheet contiene un elemento Table con un elemento Row per ogni riga e un elemento Cell per ogni cella. Per ogni cella che contiene dati, il contenuto di un elemento figlio Data indica il valore e il suo attributo Type indica il tipo. I tipi possibili sono Number, DateTime, Boolean, String ed Error. Prima ancora degli elementi Worksheet la radice contiene un elemento Styles, al cui interno sono definiti gli stili da applicare a singole celle o a intervalli di celle. Ogni elemento Style ha un attributo ID che lo identifica univocamente. Gli elementi figli ne definiscono il formato. Per applicare uno stile a un elemento, si assegna il valore dell’attributo ID dello stile al suo attributo StyleID. Per trasformare in un XML Spreadsheet il documento ottenuto salvando il DataSet (Listato 5), il foglio di stile soluzione3.xslt (Listato 6) contiene due template. Il primo trasforma la radice del documento di origine nella radice del documento di destinazione creando l’elemento Workbook, gli stili, gli elementi Worksheet, Table, e la prima riga con i nomi dei campi. Il secondo trasforma ogni elemento Table nel documento di origine in un elemento Row nel documento di destinazione.

Quale approccio scegliere Sebbene la prima soluzione permetta un controllo assoluto sul risultato (il modello a oggetti di Excel permette di lavorare con le celle del foglio di lavoro come si farebbe attraverso la sua interfaccia grafica), richiede che Excel sia installato sul computer che esegue il programma. Se il programma fosse eseguito da un server, Excel potrebbe non essere disponibile. Al contrario la seconda soluzione da un lato non permette alcun controllo sul risultato (permette solo di scrivere dati “nudi e crudi” nelle celle di un foglio di lavoro), dall’altro non richiede che Excel sia installato sul computer che esegue il programma. La terza soluzione ha i pregi delle prime due senza difetti. Permette controllo assoluto sul risultato (Figura 3) e non richiede che Excel sia installato sul computer che esegue il programma. Permette inoltre di modificare il risultato senza dovere ricompilare, ma modificando solo il foglio di stile.

Bibliografia [1] Pierpaolo Raguso, Corso ADO.NET, Visual Basic & .NET Journal numero 56 e successivi

Riferimenti [2] http://msdn.microsoft.com/library/en-us/dnexcl2k2/ html/odc_xmlss.asp

N. 60 - Novembre/Dicembre 2004

VBJ

37

ENTERPRISE

Servizi NT con VB .NET Caso reale di realizzazione e implementazione di servizi Windows con .NET

Servizi NT2

Seconda parte

di Gianluca Cannalire

N

ella puntata precedente abbiamo esplorato la classe ServiceBase, ovvero la classe dalla quale ereditare per implementare un servizio NT. Per comprendere al meglio l’utilizzo delle classi del .NET Framework dedicate alla creazione di servizi, abbiamo iniziato la realizzazione di un progetto reale (Event Notificator) che prevede la notifica via email delle informazioni che vengono man mano inserite nei vari EventLog di Windows. Naturalmente, questo notificatore verrà eseguito come servizio NT. In questa seconda ed ultima parte dell’articolo completeremo la realizzazione del progetto analizzando le classi che ci serviranno per installare (o rimuovere) il servizio nel sistema e sarà illustrata una tecnica di debug per questo genere di applicazioni, che si differenzia, per alcuni particolari, dal debug di una classica applicazione desktop. Infine, studieremo come interagire con il servizio nello stesso modo in cui lavora il Service Control Manager presente nel Control Panel di Windows.

Installare un servizio L’utilizzo della classe ServiceBase non è di per sé sufficiente per poter realizzare un servizio completo. Infatti, per poter informare il sistema operativo della presenza di un eseguibile che dovrà essere eseguito come servizio, è necessario creare una serie di chiavi nel registry ed in particolare nel ramo HKEY_LOCAL_MACHINE\SYSTEM\ CurrentControlSet\Services ([1]). Ovviamente, seppur ciò sia possibile, non è il caso di provvedere manualmente alla creazione delle particolari chiavi/valori all’interno del regi-

Gianluca Cannalire è diplomato in Elettronica e Telecomunicazioni, si occupa della realizzazione di gestionali in VB6 e .NET. È fondatore e coordinatore di “Visual Basic Tips & Tricks” (http://www.visual-basic.it), una community dedicata completamente a Visual Basic in tutte le sue versioni. Da aprile 2003 è stato nominato Microsoft MVP (Most Valuable Professional) per Visual Basic.

38

VBJ

N. 60 - Novembre/Dicembre 2004

stry: affideremo tale compito ad una specifica classe presente nella libreria del .NET Framework. Per l’esattezza, le classi da utilizzare per implementare il codice necessario all’installazione di un servizio NT sono due: la ServiceProcessInstaller e la ServiceInstaller. La ServiceProcessInstaller, che a sua volta eredita da System.Configuration.Install.Installer, è la classe che si occupa dell’interazione con le utility di setup quali Windows Installer o installutil.exe (che vedremo successivamente) mentre la ServiceInstaller si occupa di gestire le informazioni necessarie alla ServiceProcessInstaller, specifiche per un determinato servizio. È da notare che all’interno di un singolo progetto possiamo avere più di una classe che eredita da ServiceBase (ovvero, più di un servizio eseguibile all’interno di un singolo assembly .NET) e che, per ognuna di queste classi, è necessaria la presenza di una corrispondente istanza di ServiceInstaller, mentre sarà sufficiente una singola istanza di ServiceProcessInstaller che provveda all’installazione di tutti i servizi. Cerchiamo di capire bene la funzione di queste due classi partendo dalla ServiceInstaller: è questa la classe che mantiene effettivamente le informazioni relative al singolo servizio da installare all’interno del sistema operativo. Nella finestra delle proprietà (Figura 1) si nota che ci sono poche

ENTERPRISE

Tabella 1

I valori della proprietà Account

Valori proprietà ‘Account’

Descrizione

LocalService

Account con bassi privilegi sulla macchina locale. Utilizza credenziali anonime per dialogare con server remoti.

LocalSystem

Account con alti privilegi

NetworkService

Account con privilegi estesi sulla macchina locale. Presenza credenziali specifiche per dialogare con server remoti.

User

Account derivato da uno specifico utente definito nel sistema. Indicando User, sarà necessario valorizzare anche le proprietà UserName e Password, o fornirle durante l’installazione del servizio

proprietà pubbliche. Tra queste, spiccano DisplayName e ServiceName, le quali corrispondono al testo che verrà visualizzato all’interno del Service Control Manager di Windows e al nome della classe derivata da ServiceBase per la quale dovrà provvedere all’installazione. Valorizzeremo DisplayName con “VB.NET Event Notificator Service” e ServiceName con “Service1” (che nel nostro progetto corrisponde al nome della classe che eredita da ServiceBase). Ci sono altre due proprietà di cui bisogna tener conto: ServicesDependsOn e StartType, che indicano rispettivamente un elenco di servizi dai quali dipende il corretto funzionamento del servizio (Figura 2) e la modalità di esecuzione dello stesso all’avvio del sistema (Automatic, Manual, Disabled). Imposteremo il valore di StartType su Automatic in modo tale che il nostro servizio venga automaticamente eseguito all’avvio del sistema operativo.

Figura 1

Proprietà del component ServiceInstaller

È interessante notare che la ServiceInstaller gestisce anche una serie di eventi relativi alle varie fasi di installazione (Before/AfterInstall, Before/AfterUninstall, ecc.) del singolo servizio, eventi che permettono allo sviluppatore di eseguire operazioni particolari (ad esempio, la creazione/ rimozione di eventuali chiavi/valori nel registry a supporto del servizio stesso) fornendo pertanto un completo controllo sulla fase di installazione e rimozione del componente software. La ServiceProcessInstaller si occupa invece di effettuare l’installazione vera e propria del servizio (creando e valorizzando le giuste chiavi nel registry) ottenenendo le informazioni dalle varie istanze di ServiceInstaller associate. Tramite la ServiceProcessInstaller provvederemo a fornire al sistema ulteriori informazioni relative al servizio ed in particolare le credenziali con le quali vogliamo che il servizio venga eseguito. A questo scopo utilizzeremo le proprietà Account, Username e Password. Nella Tabella 1 sono illustrati i possibili valori della proprietà Account (di tipo ServiceAccount) notando che solo nel caso il cui il valore venga impostato su ServiceProcess.ServiceAccount.User sarà necessario impostare uno username ed una password riconosciuti validi per il sistema sul quale stiamo installando il servizio. Nel caso in cui non si provvedesse a valorizzare uno username e la relativa password, durante l’installazione apparirà una dialog box di login che richiederà le informazioni mancanti (Figura 3). Inizialmente, per non incorrere in eventuali problemi legati al contesto di sicurezza, lasciamo il valore di Account su ServiceProcess.ServiceAccount.LocalSystem avendo l’accortezza di utilizzare un account con privilegi più bassi (se possibile) una volta terminata la prima fase di debug dell’applicazione. Un particolare, di non poco conto, è che tutta la procedura di installazione del servizio viene effettuata in modo transazionale, ovvero, in caso di errore in uno degli step del processo di installazione (ad esempio per la mancanza di particolari diritti di esecuzione), tutte le modifiche effettuate dall’installer saran-

N. 60 - Novembre/Dicembre 2004

VBJ

39

ENTERPRISE

buto RunInstallAttribute; ci basterà sapere che, con questa particolare configurazione, sarà l’utility di installazione ad invocare i metodi Install esposti dalla ServiceProcessInstaller e dalle dipendenti ServiceInstaller. A questo punto, il servizio è pronto all’uso. Eseguiamo il build del progetto e, dopo aver lanciato il prompt dei comandi di Visual Studio .NET (vsvars32.bat), per avere tutte le variabili d’ambiete correttamente impostate, digitiamo: installutil.exe <path assembly>\nomeassembly.exe

Il risultato dell’operazione, qui sotto riportato, confermerà che tutta l’operazione è stata eseguita all’interno di un’unica transazione:

Figura 2

Dipendenze di un servizio

no sottomesse a rollback e nel sistema non resteranno tracce orfane (file, chiavi del registry) della nostra applicazione. Le classi ServiceProcessInstaller e ServiceInstaller dispongono anche di una serie di metodi che nella maggior parte dei casi non dovranno essere invocati direttamente dallo sviluppatore. Il metodo Install, disponibile in entrambe le classi, è quello che effettivamente scatena la scrittura nel registry delle varie chiavi e viene invocato direttamente dalle utility di installazione quali la installutil.exe.

Running a transacted installation. Beginning the Install phase of the installation. See the contents of the log file for the The file is located at \.InstallLog. The Install phase completed successfully, and the Commit phase is beginning. See the contents of the log file for the The file is located at \.InstallLog. The Commit phase completed successfully. The transacted install has completed.

Aggiungere l’Installer al progetto Vediamo ora come aggiungere le classi di installazione al progetto. In Visual Studio.NET, apriamo il progetto e selezioniamo dal Solution Explorer il componente/ classe “Service1”. Se nell’IDE non compare il designer del componente, assicurarsi che non vi sia una “window code” aperta sul file Service1.vb. Eventualmente sarà necessario chiudere questa finestra e, cliccando con il tasto destro su “Service1”, selezionare View Designer. Solo in questo modo, in fondo alla finestra “Properties” del componente, apparirà il link “Add Installer” (Figura 4). Cliccando su “Add Installer”, Visual Studio aggiungerà automaticamente al progetto un nuovo file (ProjectInstaller.vb) contenente il codice necessario all’installazione del servizio ovvero le instanze di ServiceProcessInstaller e ServiceInstaller opportunamente configurate. Tutto il codice (Listato 1) è implementato nella classe ProjectInstaller (che eredita a sua volta System.Configuration.Install.Installer) che verrà chiamata in causa da InstallUtil.exe grazie all’attributo RunInstaller impostato su True. Per non uscire troppo dall’argomento dell’articolo, non illustrerò il comportamento della classe Installer e dell’attri-

40

VBJ

N. 60 - Novembre/Dicembre 2004

A questo punto, apriamo il Service Control Manager dal pannello di controllo di Windows e noteremo che il nostro servizio compare nella lista (Figura 5), ad ulteriore conferma della corretta installazione del componente; ovviamente non risulta ancora avviato.

Figura 3

Richiesta di Login in fase di installazione

ENTERPRISE

Tabella2

I valori della proprietà Status

Valori proprietà ‘Status’

Descrizione

ContinuePending

Il servizio si sta riavviando

Paused

Il servizio è in pausa

PausePending

Il servizio si sta ponendo in pausa

Running

Il servizio è in esecuzione

StartPending

Il servizio si sta avviando

Stopped

Il servizio non è in esecuzione

StopPending

Il servizio si sta fermando

Manca un piccolo, ma non trascurabile, dettaglio. Nella finestra del SCM del vostro computer non apparirà alcuna descrizione, a differenza di quanto avviene in Figura 5. Per la sostanza del nostro applicativo non cambia molto, ma fornire una descrizione dello scopo del servizio utile all’utente che interagirà con il SCM rende il tutto un po’ più professionale. Come fare per inserire tale descrizione? Considerato che il testo che appare nella colonna “Description” viene rilevato dall’omonima chiave Description presente in HKEY_ LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\ <nome servizio>, dobbiamo aggiungere tale chiave, con il relativo testo descrittivo, sfruttando gli eventi esposti dalla classe ServiceInstaller cui abbiamo precedentemente accennato. Per la precisione, utilizzeremo l’evento AfterInstall che viene generato in seguito alla scrittura nel registry delle coppie chiavi/valori fondamentali (Listato 2). È

bene ricordare che per testare ogni volta le nuove modifiche apportate al codice bisognerà disinstallare e successivamente reinstallare il servizio. Per rimuovere il servizio è sufficiente utilizzare lo switch /u di InstallUtil.exe

Avviare il servizio Non ci resta che avviare il servizio e testarne l’effettivo funzionamento. Pertanto, selezioniamolo dal Service Control Manager di Windows, tramite un click sul tasto destro, e scegliamo Start. Dopo pochi istanti, nella colonna Status, il servizio risulterà Started. Per verificare che tutto funzioni come da manuale, sarà sufficiente forzare la scrittura di un entry qualsiasi all’interno dell’EventLog Application. Nei sorgenti allegati all’articolo, disponibili sul sito FTP di Infomedia, è compreso un piccolo progetto di test che simula la scrittura di record nell’EventLog. Lo scopo del servizio era quello di notificare all’amministratore di un server, tramite email, le informazioni che i vari applicativi scrivono all’interno del registry. Per inviare i vari messaggi di posta elettronica abbiamo fatto uso della classe MailMessage (namespace System.Web.Mail) che, se non diversamente specificato tramite la proprietà SmtpServer di SmtpMail,delega al servizio SMTP di IIS il delivery del messaggio. Questo non vuol dire che per effettuare il test dell’applicazione sia necessario avere il servizio in esecuzione: infatti, sarà sufficiente controllare il contenuto della directory c:\InetPub\Mailroot\Queue dove saranno creati i file di testo relativi ai messaggi da spedire (in formato RFC822).

Debug di un servizio Effettuare il debug di un servizio è un’operazione in parte differente da quella di una normale applicazione WinForms. Se si tenta di avviare nell’IDE il nostro servizio, otterremo infatti come risposta questo messaggio:

Figura 4

Aggiungere l'installer ad un servizio

“Cannot start service from the command line or a debugger. A Windows Service must first be

N. 60 - Novembre/Dicembre 2004

VBJ

41

ENTERPRISE

Listato 1

La classe ProjectInstaller

Imports System.ComponentModel Imports System.Configuration.Install Public Class ProjectInstaller Inherits System.Configuration.Install.Installer #Region “ Component Designer generated code “ Public Sub New() MyBase.New() ‘This call is required by the Component Designer. InitializeComponent() ‘Add any initialization after the InitializeComponent() call End Sub ‘Installer overrides dispose to clean up the component list. Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean) If disposing Then If Not (components Is Nothing) Then components.Dispose() End If End If MyBase.Dispose(disposing) End Sub ‘Required by the Component Designer Private components As System.ComponentModel.IContainer ‘NOTE: The following procedure is required by the Component Designer ‘It can be modified using the Component Designer.

installed (using installutil.exe) and then started with the ServerExplorer, Windows Services Administrative tool or the NET START command.” Come è possibile quindi procedere al debug step-bystep del codice? Utilizzando Visual Studio .NET possiamo agganciare il debugger a qualsiasi processo in esecuzione sul sistema. Dal menù “Debug -> Process...” si accede alla maschera (Figura 6) dalla quale è possibile selezionare il processo da sottoporre a debug (il processo deve essere in esecuzione ovviamente). Se il servizio è eseguito sotto LocalSystem bisogna selezionare la checkbox Show System Process per farlo comparire nell’elenco. Una volta selezionato il processo (EventNotificator.exe) clicchiamo su Attach e poi su Common Language Runtime. Da questo momento possiamo impostare gli opportuni breakpoint nel codice del

42

VBJ

N. 60 - Novembre/Dicembre 2004

‘Do not modify it using the code editor. Friend WithEvents ServiceProcessInstaller1 As System.ServiceProcess.ServiceProcessInstaller Friend WithEvents ServiceInstaller1 As System.Service Process.ServiceInstaller <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent() Me.ServiceProcessInstaller1 = New System.Service Process.ServiceProcessInstaller Me.ServiceInstaller1 = New System.ServiceProcess .ServiceInstaller ‘ ‘ServiceProcessInstaller1 ‘ Me.ServiceProcessInstaller1.Account = System. ServiceProcess.ServiceAccount.LocalSystem Me.ServiceProcessInstaller1.Password = Nothing Me.ServiceProcessInstaller1.Username = Nothing ‘ ‘ServiceInstaller1 ‘ Me.ServiceInstaller1.DisplayName = “VB.NET Event Notificator Service” Me.ServiceInstaller1.ServiceName = “Service1” ‘ ‘ProjectInstaller ‘ Me.Installers.AddRange(New System.Configuration. Install.Installer() {Me.ServiceProcessInstaller1, Me.ServiceInstaller1}) End Sub #End Region End Class

progetto e procedere al debug classico. Ci sono ancora un paio di particolari da tenere in considerazione. Il primo è che, per effettuare il debug, il processo deve essere in esecuzione, ovvero che il servizio sia stato avviato. Il secondo è derivante dal primo: come effettuare il debug del metodo OnStart se non possiamo agganciare il debugger prima dell’avvio del servizio? Dobbiamo trovare un sistema che possa caricare in memoria il nostro processo ancor prima che il metodo OnStart del servizio venga invocato dal Service Control Manager. E il sistema è tutto sommato banale: basta aggiungere al progetto un altra classe ereditata da ServiceBase (senza implementare alcun tipo di codice al suo interno). Come già anticipato all’inizio dell’articolo, all’interno di un singolo assembly possono coesistere più classi che ereditano da ServiceBase e quindi in un singolo processo possiamo avere più servizi in esecuzione (i quali non ne-

ENTERPRISE

Listato 2

Modificare il registry durante l'installazione del servizio

Private Sub ServiceInstaller1_AfterInstall(ByVal sender As System.Object, ByVal e As System.Configuration. Install.InstallEventArgs) Handles ServiceInstaller1. AfterInstall Dim regDescription As Microsoft.Win32.Registry Key Dim strDescription As String = “”

Figura 5

Il Service Control Manager di Windows ed il nostro servizio

cessariamente devono interagire tra loro od essere tutti in esecuzione). Sfruttando il fatto che il processo viene caricato in memoria all’avviamento del primo servizio invocato dal SCM e scaricato quando l’ultimo servizio viene fermato, ecco che l’uso di un servizio fittizio permette di caricare in memoria l’immagine del nostro processo ed agganciare il debugger prima di avviare il servizio vero e proprio da debuggare. Nonostante tutto c’è ancora qualcosa di cui dobbiamo tener conto durante il debug: il SCM quando invoca lo startup di un servizio si aspetta che lo stesso risponda in un tempo massimo di 30 secondi, altrimenti genererà l’errore “Could not start the VB.NET Event Notificator service on Local Computer. Error 1053: The service did not respond to the start or control request in a timely fashion.”

Controllare un servizio Dopo aver visto come creare, installare ed eseguire il debug di un servizio, è il momento di capire come possiamo controllare il suo stato al di fuori del Service Control Manager standard di Windows. Come potete immaginare ancora una volta l’immensa libreria di .NET ci viene in aiuto. All’interno del namespace System.ServiceProcess troviamo il tipo ServiceController, che in sostanza permette di connettersi tramite apposite API ad un servizio installato nel sistema, fornendo la capacità di controllare e/o modificare il suo stato. La ServiceController è una classe abbastanza semplice ed intuitiva da utilizzare: una volta creata un’istanza, passando come parametro al costruttore il nome del servizio per il quale vogliamo ottenere il controllo, sarà sufficiente far riferimento ai metodi Start, Stop, Pause e Continue per modificare lo stato di attività del servizio e alle proprietà DisplayName, ServiceName, MachineName, ServiceType e Status per ottenere informazioni sullo stesso. È bene ribadire che ServiceName e DiplayName sono due cose ben differenti: ServiceName, che corrisponde al nome della classe che eredita da System.ServiceProcess.ServiceBase, è il va-

regDescription = _ Microsoft.Win32.Registry.LocalMachine. OpenSubKey( _ “System\CurrentControlSet\Services\” & _ Me.ServiceInstaller1.ServiceName & “\”, True) If (Not regDescription Is Nothing) Then regDescription.SetValue(“Description”, _ “Questa è la descrizione del servizio”) regDescription.Close() End If End Sub

lore che dovremo utilizzare nel costruttore della classe ServiceController. ServiceName rispecchia la chiave specifica del servizio creata all’interno di HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. DisplayName è invece solo un valore “user friendly” per identificare facilmente un servizio all’interno del SCM. Tra i membri della ServiceController ci sono un paio di metodi che vale la pena segnalare: GetServices (shared) e ExecuteCommand. Il primo restituisce una lista di oggetti ServiceController rappresentanti ogni singolo servizio installato sulla macchina locale; per ottenere l’elenco dei

Figura 6

Aggancio del debugger al processo

N. 60 - Novembre/Dicembre 2004

VBJ

43

ENTERPRISE

Listato 3

zio) tramite l’invio di un valore numerico (i valori ammessi vanno da 128 a 255). Pertanto, se vogliamo sfruttare questo metodo per modificare il comportamento del servizio, all’interno dello stesso sarà necessario prevedere il codice che reagisca alla ricezione di questi valori (intercettati dalla ServiceBase tramite l’evento OnCustomCommand. Nei sorgenti che accompagnano l’articolo è presente un esempio di Service Controller personalizzato che mostra anche l’uso del metodo ExceuteCommand.

Enumerazione dei servizi

Private Sub ListAllServices() Dim mySCMs() As ServiceController mySCMs = ServiceController.GetServices() Dim mySCMTemp As ServiceController ListView1.BeginUpdate() ListView1.Items.Clear()

Conclusioni Come abbiamo potuto vedere, la realizzazione di un servizio, grazie all’infrastruttura offerta dal .NET Framwork, è diventata un’operazione banale anche con Visual Basic(.NET). Basterà aggiungere al nostro servizio un sistema di monitoraggio del file system (vedi System.IO.FileSystemWatcher) e magari implementare anche la notifica via SMS ([2],[3]) per ottenere un tool completo per la notifica eventi.

For Each mySCMTemp In mySCMs Dim myItem As System.Windows.Forms.ListView Item = ListView1.Items.Add(mySCMTemp.ServiceName) myItem.SubItems.Add(mySCMTemp.DisplayName)

servizi di una macchina remota è sufficiente fornire, come argomento del metodo GetServices, il nome di un computer remoto. Nel Listato 3 viene illustrato un possibile uso del metodo GetServices. Tramite ExecuteCommand possiamo invece inviare dei “comandi personalizzati” al nostro servizio. Effettivamente non si tratta di comandi veri e propri: la ExecuteCommand ci permette di comunicare in modalità ‘one-way’ (ovvero solo verso il servi-

Riferimenti [1] “CurrentControlSet\Services Subkeys Entries”, http:/ /support.microsoft.com/?id=103000 [2] “Il protocollo AT+ dei telefoni cellulari”, DEV 115, 116, 117, 118, 119, 120 [3] Vito Vessia , “Programmare il cellulare”, Hoepli, 2002

Come fare? Semplice! Basta inviare la descrizione del proprio articolo a

[email protected] specificando

a chi si rivolge, che cosa spiega, i titoli di paragrafo e i relativi abstract Se ti piace scrivere e l’informatica è la tua passione, non perdere questa occasione! Login, la prima rivista italiana dedicata ai professionisti di Internet, sta cercando nuovi collaboratori e...uno di questi potresti proprio essere tu!

WEB PUBLISHING Per i creativi della rete. HTML, XML, VRML, ASP, scripting, multimedia E-COMMERCE Soluzioni per il commercio elettronico Stato dell’arte, pagamenti sicuri, negozi virtuali, crittografia, tool di sviluppo, ecc. NETWORKING Installazione e configurazione di computer in rete. Protocolli, routing, hub, bridge, router, switch, network management, ecc.

Scrivi anche tu un articolo! 44

VBJ

www.infomedia.it/riviste/login

N. 60 - Novembre/Dicembre 2004

WEB

Grafici dinamici in ASP .NET Vale la pena di sprecare tempo per creare grafici da zero? Molto meglio utilizzare una libreria altamente personalizzata. Vediamo tutti i pregi di .netCHARTING

di Dino Esposito e avete mai avuto a che fare con la programmazione grafica, in particolare quella dei grafici, nell’era pre-.NET – vista oggi sembra una sorta di Jurassic Windows! – potrete solo ringraziare il destino per avervi fatto incontrare questo ramo della programmazione solo nell’era di .NET. Il .NET Framework contiene una libreria avanzata di funzioni conosciuta come GDI+, in onore del mitico Windows Graphics Device Interface (GDI), croce e delizia (bé, più croce) per generazioni di programmatori Windows. I servizi GDI+ rientrano nelle seguenti categorie: grafica vettoriale 2D ed elaborazione delle immagini. La grafica vettoriale 2D permette di disegnare figure semplici come linee, curve e poligoni. Sotto la dicitura “elaborazione immagini” invece ricadono tutte quelle funzioni per visualizzare, manipolare, salvare e convertire immagini bitmap e vettoriali. Infine, può essere definita anche una terza categoria di funzioni, quelle che riguardano il testo, ovvero la sua visualizzazione in tutte le salse, giocando con font, dimensioni e stili. Come potete vedere, GDI+ fornisce solo le basi per la programmazione grafica; utilizzando i suoi tool a basso livello si possono confezionare forme più avanzate, come appunto dei grafici. Fintanto che avete bisogno di visualizzare una serie di barre verticali, ci si può anche arrangiare da soli. Ma come fare quando il cliente chiede di più?

S

Dino Esposito è consulente e formatore per Wintellect, per cui tiene il corso ADO.NET. Dino collabora con MSDN Magazine e MSDN Voices. È autore di Building Web Solutions with ASP.NET e ADO.NET (Microsoft Press), è anche co-fondatore di www.vb2themax.com. È raggiungibile via e-mail all’indirizzo [email protected]

Usare le classi GDI+ per produrre un grafico come quello in Figura 1 è sicuramente possibile, ma certamente non banale. Il codice per piazzare i valori al centro delle sezioni richiede un po’ di tempo. Lo stile 3D con effetti luce pure. E i piccoli particolari come rendere le etichette visibili sia sui colori chiari che scuri non è da meno. Insomma, sono tutte cose che si possono ottenere da soli, ma ne vale la pena in termini di tempo/costo? E siamo sicuri di perdere del tempo e contemporaneamente soddisfare le aspettative del cliente? Vi dico che, anche se riuscissimo a soddisfare le sue aspettative, avremmo perso l’opportunità di superarle, allo stesso costo o anche a meno! Un grafico a torta è ottimo per rappresentare i dati. Ma non tutti i tipi di dati. Il bello è quando abbiamo uno strumento che permette di cambiare tipo di grafico con piccole modifiche. È difficile implementare a mano il codice per il grafico di Figura 2. Effetti come la trasparenza, lo stile 3D, lo sfondo richiedono codice; e il codice richiede tempo e denaro. E, molto importante, quello non è il tipo di codice per il quale siete pagati. Riassumendo, l’aiuto professionale che vi può dare una libreria grafica ben fatta può portare ad un servizio di prima classe per il cliente. Dovreste sempre considerarlo.

N. 60 - Novembre/Dicembre 2004

VBJ

45

WEB Scegliere la giusta libreria Scegliere la giusta libreria può non essere semplice, e in qualche modo è anche una questione di preferenze personali. Comunque, qualsiasi libreria scegliate deve almeno soddisfare i requisiti basilari, in modo da essere d’aiuto allo sviluppatore per offrire un buon servizio al cliente. La mia libreria ideale dovrebbe:

• • • • • •

Essere facile da usare Avere un’interfaccia di programmazione .NET “standard”, tipo quella della FCL Fornire una vasta gamma di grafici Essere efficace in termini di risultati grafici, sforzi di programmazione richiesti e costo Fornire una ricca interfaccia utente che va oltre la solita lunga lista di grafici da scegliere e aggiungere supporto per feature come trasparenza, gradienti, sfondi, legende, ecc. Essere internamente ottimizzata in velocità e offrire controllo su certe caratteristiche come il caching

Mi sono dimenticato niente? Ma certo, ovviamente non deve essere complessa e poco maneggevole. Adoro il software potente che permette comunque di iniziare con un semplice ma significativo esempio. Credo che la libreria .netCHARTING soddisfi tutti questi requisiti ([1]). È eccezionale, per come la vedo io. Tra l’altro è disponibile in versione completa e gratuita per scopi di sviluppi e testing, quindi potrete tranquillamente sperimentare le tecniche illustrate nel seguito dell’articolo.

Lavorare con .netCHARTING Una semplice applicazione ASP.NET con .netCHARTING è, per l’appunto, semplice. Si inizia registrando l’assem-

Figura 1

46

VBJ

Grafico 3D a torta

N. 60 - Novembre/Dicembre 2004

Figura 2

Effetti 3D, background e trasparenza

bly del controllo nella pagina e se ne piazza un’istanza nel sorgente ASPX: <%@ Register TagPrefix=”dotnet” Namespace=”dotnetCHARTING” Assembly=”dotnetCHARTING”%> ... <dotnet:Chart runat=server id=chart />

Poi bisogna collegare il controllo con qualche dato per generare il grafico. Il modello di .netCHARTING consiste in “elementi” che insieme formano una “serie”. .netCHARTING legge i dati da una serie. Una o più serie possono essere raggruppate in una collection e passate al controllo. La classe Element rappresenta un elemento del grafico ed espone proprietà per contenere differenti tipi di dati come i valori X e Y, percentuale di completamento per grafici Gantt, e così via. Inoltre fornisce il controllo su altri attributi come il colore. La classe Series rappresenta un gruppo di elementi ed espone le proprietà di default per tutti i suoi elementi interni, come pure le proprietà specifiche per la serie, come le voci della legenda. Espone dei metodi per manipolare i dati, ad esempio per ordinare gli elementi ed effettuare alcuni calcoli per derivare nuovi elementi della serie. Per associare una serie al controllo si usa la proprietà SeriesCollection: Element e = new Element(); e.Name = “My Element”; e.YValue = 10; Series s = new Series(); s.Name = “My Series”; s.Elements.Add(e); SeriesCollection sc = new SeriesCollection(); sc.Add(s); chart.SeriesCollection.Add(sc);

WEB

Il codice prima istanzia un nuovo elemento e poi gli assegna il valore per l’asse Y. Quindi crea una nuova serie e vi inserisce l’elemento appena creato. Elements elenca gli elementi presenti nella collection. Infine, la serie è aggiunta in un oggetto SeriesCollection che a sua volta è aggiunto all’omonima proprietà del controllo. Questo pezzo di codice si trova di solito nell’evento Page_Load. Anche se è estremamente flessibile, non è proprio realistico creare gli elementi individualmente. Nella maggior parte dei casi infatti i dati arriveranno dai business object che a loro volta li avranno ricavati dal database, documenti Excel, stream XML. I business object di solito espongono i dati usando contenitori ADO.NET come i DataSet o interfacce come il DataReader. .netCHARTING accetta dati provenienti dal DataSet, ma per consistenza il DataSet deve essere impacchettato in un oggetto Series. Ecco un esempio:

te la proprietà CacheDuration. Quando impostata, l’immagine è generata solo al primo hit e conservata per un determinato numero di secondi. Tutte le richieste che arrivano in questo intervallo di tempo accederanno all’immagine in cache senza ulteriore elaborazione da parte del controllo. La proprietà booleana UseFile permette di indicare se l’immagine generata debba essere salvata su file oppure spedita direttamente sotto forma di stream al browser. Inoltre i file obsoleti vengono automaticamente eliminati, senza accumularsi nel tempo. Un’altra classe che vale la pena di menzionare è la DataEngine. Prima di tutto l’oggetto DataEngine può essere configurato per raccogliere i dati da interrogazioni su database oppure da DataSet disconessi. I dati sono convertiti in una SeriesCollection che è il solo tipo che il controllo riconosce. In aggiunta, DataEngine offre molte comode opzioni di manipolazione, specificatamente progettate per le esigenze comuni come l’aggregazione dei dati.

void Page_Load(object sender, EventArgs e) { // Set global properties chart.Title = “Item sales report”; chart.TempDirectory = “temp”; chart.Type = ChartType.Pie;

DataEngine de = new DataEngine(); de.StartDate = “1/1/02 8:00:00 AM”; de.EndDate = “1/1/02 8:00:00 AM”; de.DateGrouping = TimeInterval.Days; de.SqlStatement = “SELECT names, values FROM myTable “ + “WHERE start > #StartDate# AND e” + “nd < #EndDate#”; SeriesCollection sc = de.GetSeries(); chart.SeriesCollection.Add(sc);

// Adding series programatically chart.Series.Name = “Item sales”; chart.Series.Data = GetDataSet(); chart.SeriesCollection.Add(); }

La proprietà Data della classe Series può essere collegata a qualsiasi oggetto enumerabile ADO.NET, inclusi i DataReader. La proprietà TempDirectory controlla la directory dove le immagini generate dal controllo sono temporaneamente salvate. Dovrete avere i permessi di scrittura su tale directory nella macchina server. Ogni immagine è salvata in un file del server per evitare di penalizzare la memoria. Per default l’immagine è rigenerata ad ogni hit, a meno che non impostia-

Figura 3

Il DataEngine supporta anche il raggruppamento delle date ad un desiderato livello di granularità – da anni a minuti.

Le barre del grafico permettono di fare il “drill-down” sui dati

N. 60 - Novembre/Dicembre 2004

VBJ

47

WEB

Dalla versione 2.5 il controllo supporta varie operazioni matematiche come la media, il minimo e il massimo. Precedentemente era supportata solo la somma.

Caratteristiche avanzate .netCHARTING possiede varie caratteristiche avanzate, In questa sede vorrei brevemente discuterne tre: drilldown, file manager e le infinite opzioni per allineare il testo e inserire delle annotazioni (Figura 3). Il drill-down permette di supportare una catena di grafici logicamente correlati. L’idea è quella di visualizzare il grafico iniziale e poi cliccare su un elemento per ottenere una vista dettagliata dei dati. Il livello di annidamento deve essere impostato dal programmatore e sfrutta le capacità di raggruppamento delle date sopra menzionate. Ecco un esempio: Chart.DateGrouping = TimeInterval.Years; Chart.DrillDownChain=”Years,Quarters,Months,Days,Hours,Minutes”; Chart.DefaultSeries.DefaultElement.ToolTip=”%yvalue”;

La proprietà DrillDownChain definisce i vari livelli di dettaglio che si intendono vedere. Non è necessario altro codice. Il tooltip visto in Figura 3 visualizza la vista attuale per ogni elemento ogni volta che ci si sposta col mouse sopra di esso. Tutti i grafici generati dal controllo possono essere salvati in un file nel server. Si può fare usando la classe FileManager. Il suo metodo SaveImage e l’enum ImageFormat permettono di scegliere il nome e il formato dell’immagine. Il codice seguente salva il grafico e assegna il suo percorso alla proprietà ImageUrl di un controllo Image di ASP.NET.

img.ImageUrl = Chart.FileManager.SaveImage(Chart.GetChart Bitmap());

Nella versione 2.5 la legenda può essere salvata in un file separato, per avere un layout flessibile all’interno della pagina web. leg.ImageUrl = Chart.FileManager.SaveImage(Chart.GetLegend Bitmap());

Una annotazione è un riquadro con un testo, una posizione, una sorgente dati e con funzionalità hotspot. Potete associare un’annotazione ad un elemento del grafico e ottenere un effetto come quello in Figura 4. Avete il completo controllo sull’orientamento, stile, colore e layout del testo. I riquadri dei tooltip e delle legende possono essere definiti basandosi su un template, come quello in esempio: chart.LegendBox.Template =”%icon %name”;

Si possono definire template utilizzando dei token speciali predefiniti. Ogni token è preceduto dal simbolo %. Nell’esempio viene visualizzata un’icona e il nome della voce. L’icona rappresenta un piccolo rettangolo con il colore di riempimento del corrispondente elemento del grafico. Il nome invece è il nome dell’elemento.

Conclusioni In .netCHARTING, la lista dei tipi di grafico supportati è incredibilmente ricca, ma non infinita: questo significa che il controllo è ben bilanciato. Non è troppo grande in memoria e non è troppo piccolo per le caratteristiche che ha. Ogni elemento di una serie può essere personalizzato in base a condizioni note a runtime, ad esempio si può cambiare il colore delle fette della torta in base alla percentuale. Un altro aspetto da enfatizzare è che per usare efficacemente il controllo non bisogna essere esperti di .NET. Alcune caratteristiche come l’aggregazione sulle date è resa semplice anche per i non esperti. Tenete presente che con quattro linee di codice si può costruire un bel grafico associato ai dati: chart.TempDirectory=”temp”; chart.DefaultSeries.ConnectionString = “data.mdb”; chart.Series.SqlStatement= “SELECT Customer, Sales FROM Table”; chart.SeriesCollection.Add();

Figura 4

Annotazioni nel grafico

Devo dire che questo articolo ha solo mostrato la punta dell’iceberg .netCHARTING ma credo sia abbastanza per farne capire le potenzialità. A voi la prossima mossa. Enjoy!

Riferimenti [1] http://www.dotnetcharting.com/

48

VBJ

N. 60 - Novembre/Dicembre 2004

ARCHITECT'S CORNER

Applicazioni scalabili in pratica “Two is meglio che one” diceva una nota pubblicità. In questa puntata daremo dei consigli pratici per progettare e implementare un’applicazione scalabile.

di Ingo Rammer Nella puntata precedente abbiamo parlato della differenza tra scalabilità e performance. Quando parliamo di performance ci riferiamo solitamente alla velocità con cui una singola operazione può essere svolta. Invece la scalabilità si riferisce al grado di parallelizzazione delle operazioni o di alcune parti dell’applicazione. Quando si ottimizza un sistema si punta ad entrambe, per migliorare il throughput (capacità di elaborazione). Immaginiamo che un vostro collega abbia ottimizzato un’applicazione talmente bene da gestire 1000 richieste concorrenti su un singolo server. Dall’altra parte voi avete una soluzione alternativa, che in realtà è un po’ più lenta e può gestire solo 500 richieste concorrenti. Però la vostra soluzione può essere eseguita su 10 server in parallelo. Grazie alla possibilità di scalare la vostra applicazione può raggiungere un throughput di circa 5000 richieste concorrenti mentre la prima, che a prima vista era più veloce, solo 1000. Il problema delle architetture di questo tipo è che di solito gli sviluppatori sanno benissimo come ottimizzare la velocità delle applicazioni, ma raramente sanno concentrarsi altrettanto bene sulla realizzazione di una applicazione veramente scalabile. Ma cosa si può fare per creare un’applicazione scalabile, che può essere eseguita su più server in parallelo per distribuire il carico di lavoro? Nel seguito vedremo tre passi che ci aiuteranno a rispondere alla fatidica domanda.

Ingo Rammer è il fondatore di thinktecture, una compagnia che aiuta gli sviluppatori e gli architetti software a progettare e implementare applicazioni e Web service .NET. Partecipa come speaker alle conferenze internazionali dedicate a tali argomenti ed è autore del best-seller Advanced .NET Remoting (APress). È Microsoft Regional Director per l’Austria e può essere contattato tramite il sito http://www.thinktecture.com/staff/ingo.

50

VBJ

N. 60 - Novembre/Dicembre 2004

1. Analizzare i dati Uno dei modi più semplici per aumentare le performance di un’applicazione è utilizzare una cache. Purtroppo le cache sono però il nemico della scalabilità. È semplice mettere in cache più dati possibile fintanto che l’applicazione rimane sullo stesso server, e a volte la velocità migliora anche di un ordine di grandezza. Però quando entra in scena il secondo server la situazione cambia radicalmente. Facciamo un esempio. Supponiamo che degli ipotetici dati dei clienti siano memorizzati nella cache server dell’applicazione per 30 minuti dopo ogni accesso. Ogni volta che un utente modifica i dati, prima viene eseguita la transazione sul database e poi viene aggiornata la cache, in modo che le letture successive restituiscano i dati corretti. Le strategie di caching come queste possono in alcuni casi ridurre drasticamente i tempi di risposta e quindi incrementare il throughput. Ma se ci fosse bisogno di eseguire l’applicazione su una seconda macchina in un cluster, la cache di una macchina dopo ogni aggiornamento non sarebbe corretta (perché solo una cache di una macchina è stata aggiornata). Senza un’adeguata strategia di cache invalidation, applicazioni come

ARCHITECT'S CORNER

queste semplicemente non scalano su più server. Uno dei primi passi per creare un’applicazione scalabile quindi è quello di analizzare tutti i tipi di dati che sono usati dall’applicazione.

La categoria più complessa in termini di stragia di caching è la seconda: infatti ci sono due tipi di dati che cambiano raramente:



Può essere utile suddividere le tabelle del database in tre gruppi:

• • •

Gruppo 1: dati statici Gruppo 2: dati semi-statici, che cambiano raramente o che sono aggiornati solo in determinati momenti (ad esempio ogni giorno a mezzanotte) Gruppo 3: Dati operativi, dinamici

Nel caso di un negozio online, i dati statici sono di solito i CAP, le città, la lista dei metodi di pagamenti accettati (carta di credito, assegno, bonifico…). Molto spesso rientrano in questa categoria i dati che non possono essere cambiati dall’applicazione stessa, ma solo attraverso strumenti di amministrazione. Nel secondo gruppo si trovano i dati che di solito non cambiano più di una volta al giorno. Ad esempio la lista dei prodotti, i prezzi, ecc. Questo gruppo può contenere anche i dati che sono modificati in precisi intervalli di tempo, come una volta al giorno oppure ogni ora. L’ultimo gruppo contiene i dati su tutte le vere transazioni, ovvero il cuore del business. Questi sono i dati che cambiano più frequentemente, come le informazioni sui clienti, i livelli di magazzino, gli ordini. Vedrete che in pratica molte tabelle apparterranno ai primi due gruppi, e solo poche conterranno i dati modificati dalle transazioni. Se poi una stessa tabella contiene dati di due gruppi differenti potete modificare lo schema del database oppure semplicemente trattare i due tipi di dati differentemente. Questo ad esempio succede quando in origine si sono memorizzati nella stessa tabella i livelli di magazzino (che cambiano sempre) insieme alle informazioni sui prodotti. Dovrebbero essere separati, almeno nel modello ad oggetti se non nel database, in modo da avere i dati transazionali (livelli di magazzino) separati dalle informazioni sui prodotti. Seguendo questa classificazione, potrete facilmente adottare una adeguata strategia di caching. La decisione se mettere in cache o no i dati statici è abbastanza semplice: di solito si memorizzano fin tanto che c’è RAM disponibile. Sempre semplice il discorso per i dati operativi del terzo gruppo: non bisognerebbe mai metterli in memoria, ma leggerli e scriverli direttamente dallo storage condiviso, ad esempio il database.



Le informazioni che sono di solito stabili per un lungo periodo di tempo e che sono modificate ad una data conosciuta a priori (ad esempio “listino prezzi valido dalla mezzanotte del 1 ottobre 2004“). Se si conosce la data, queste informazioni possono essere messe in cache facilmente. Bisogna solo assicurarsi di rimuovere la versione obsoleta al momento giusto. Il secondo tipo di dati è quello più difficile da trattare. Sono i dati che di solito non cambiano, ma quando cambiano, le modifiche devono diventare effettive immediatamente. Per esempio i permessi per gli utenti. Se questi dati sono in cache, bisogna assicurarsi di avere una efficace strategia di cache invalidation. Si può ad esempio pensare di esporre un web service su ogni nodo del cluster per comunicare alla macchina esplicitamente di rimuovere alcuni dati dalla memoria.

Le prossime versioni di .NET e SQL Server avranno delle nuove caratteristiche che permetteranno di notificare alle applicazioni server le modifiche critiche ai dati. Ad esempio potrete fare il subscribe a query tipo “select userid, locked_out from users” per fare in modo che SQL Server 2005 automaticamente vi informi quando un record è cambiato.

2. Porsi le giuste domande L’analisi delle strategie di caching che ho appena illustrato è in realtà solo il primo passo di un approccio generico per progettare un sistema scalabile. Per assicurare la scalabilità statica e la parallelizzazione dell’applicazione si può essenzialmente lavorare con un semplice modello mentale. Ogni volta che si sta per chiamare un metodo su un oggetto server-side, bisognerebbe chiedersi “Cosa potrebbe succedere se questo metodo fosse eseguito su una macchina diversa da quella su cui è stato eseguito prima?”. E una seconda domanda dovrebbe essere “Cosa succederebbe se il server fosse stato riavviato proprio prima di questa chiamata?”. Per illustrare il problema in dettaglio, diamo un’occhiata al codice seguente: void UpdateCustomer(Customer cust) { ISomeRemoteObject obj = (ISomeRemoteObject) Activator. GetObject(...); obj.Initialize(_username, _password);

N. 60 - Novembre/Dicembre 2004

VBJ

51

ARCHITECT'S CORNER

obj.StoreCustomer(cust); }

e confrontiamolo con questo: void UpdateCustomer(Customer cust) { ISomeRemoteObject obj = (ISomeRemoteObject) Activator. GetObject(...); obj.StoreCustomer(_username, _password, cust); }

Nel primo caso, dovrebbe suonare una sirena d’allarme. Per assicurare un comportamento corretto infatti è essenziale che entrambe le chiamate a Initialize e StoreCustomer siano invocare sulla stesso oggetto server-side. Altrimenti il load balancing automatico sessionless sarebbe impossibile. E se il server fosse riavviato tra le due chiamate, la seconda fallirebbe. Avrete notato che in generale le applicazioni che sono state sviluppate con un approccio stateless (senza stato), come nel secondo frammento di codice, permettono un alto grado di parallellizzazione. In quel caso infatti ogni richiesta può potenzialmente essere gestita da un server differente in un cluster NLB (network load balanced). Potreste chiedervi se il secondo frammento è davvero conforme alle pratiche di programmazione orientate agli oggetti: tutto è confinato ad una chiamata a metodo e l’identità dell’oggetto non conta niente. Avete ragione! Quando si lavora con oggetti remoti non è una buona abitudine continuare ad usare il paradigma orientato agli oggetti, meglio invece concentrarsi su ogni chiamata a sé stante. I sistemi ad oggetti distribuiti sui quali potreste fare affidamento per avere l’identità degli oggetti server-side vi legherebbero indissolubilmente ad una macchina, limitando la capacità di load balancing e failover automatico.

normali lock su database e resource contention. Possono piantare un sistema in men che non si dica. Capita spesso che questi colli di bottiglia saltino fuori quando più di un’operazione accede alla stessa risorse contemporaneamente. Sebbene questi problemi siano molto comuni, ottimizzare le strategie di accesso è un compito difficile perché non ci sono ricette generiche se non quella di mantenere le transazioni e i lock su database per tempi più brevi possibile. A questo proposito raccomando di leggere un articolo di MSDN [3] che fornisce un’introduzione alle differenti modalità di locking e transazioni disponibili in SQL Server. Ricordate che non bisognerebbe basarsi sul taskmanager o comunque sul livello di carico della CPU per identificare i colli di bottiglia. Infatti il workload della CPU può anche restare al minimo, ad esempio nel caso di lock a livello di tabella invece che di riga. La CPU rimane in idle, attendendo il rilascio di un lock e senza la possibilità di effettuare altre operazioni. E aggiungere CPU al server o macchine al cluster non risolverebbe la situazione. Uno dei tool più utili per tenere traccia dei lock è SQL Profiler, che è installato insieme a SQL Server. Permette di vedere la lista tutti i lock per tutti gli utenti attivi in un certo istante.

Conclusioni È ovviamente impossibile coprire in un singolo articolo tutti gli aspetti della progettazione e dell’implementazione di applicazioni scalabili. Ma la mia esperienza insegna che i tre passi che ho spiegato nell’articolo aiutano a migliorare la scalabilità per molte applicazioni. Per quelli che vogliono approfondire gli argomenti raccomando il libro [4] e i primi capitoli di [5], che contengono informazioni sui fondamenti della elaborazione delle transazioni, condivisione delle risorse e sistemi scalabili.

3. Scovare i colli di bottiglia Nelle applicazioni distribuite di solito si trovano due differenti colli di bottiglia: quelli locali e quelli globali. Un collo di bottiglia locale interessa solo un server e può essere di solito eliminato con miglioramenti sulla performance o aggiungendo una macchina al cluster. Questi tipi di blocchi hanno solitamente una criticità minore in quanto possono essere risolti successivamente (cioè quando si riveleranno). L’identificazione dei colli di bottiglia globali invece è molto più importante. Si verificano ogni volta che si accedono risorse condivise, come tabelle di database. Rientrano nella categoria il deadlock (il classico killer della scalabilità., vedi [2]), i

52

VBJ

N. 60 - Novembre/Dicembre 2004

Bibliografia [1] Ingo Rammer, “La velocità non è tutto“, VB & .NET Journal n° 59 [2] Ingo Rammer, “Accesso ai dati più veloce e scalabile“, VB & .NET Journal n° 56 [3] “Locking Hints”, http://msdn.microsoft.com/library/enus/acdata/ac_8_con_7a_1hf7.asp [4] Jim Gray and Andreas Reuter, “Transaction Processing: Concepts and Techniques”, Morgan Kaufmann. ISBN 1558601902. [5] Tim Ewald, “Transactional COM+”, Addison-Wesley, ISBN 0201615940.

POSTA

a cura di Alberto Falossi

[email protected]

Questo metodo è sfruttato anche dall’IDE e comporta le differenze da te notate.

Gestore globale di eccezioni Sto lavorando ad un applicativo WindowsForms, scritto parte in VB, parte in C#. L’applicativo consta di un Main iniziale che lancia una MDI form all’interno della quale si lanciano tutte le varie funzionalità. Ho implementato un sistema di gestione delle eccezioni che, in ogni metodo, intercetta l’eccezione, crea un oggetto di eccezione (una nostra classe che eredita da Exception), aggiunge le informazioni necessarie, tra cui la sequenza dei metodi chiamati, e poi rilancia l’eccezione al metodo chiamante (se esiste), oppure scrive un log dell’eccezione su file, dando un opportuno messaggio all’utente. Sto osservando un comportamento differente nella gestione delle eccezioni, a seconda che l’eccezione si verifichi eseguendo il codice all’interno dell’ambiente di sviluppo oppure lanciando semplicemente l’eseguibile. Nel primo caso qualunque eccezione riesce a “risalire” fino alla gestione eccezioni presente nel Main. Invece, se si è lanciato semplicemente l’eseguibile, l’eccezione viene intercettata dal gestore di eccezioni di .NET. Credo (ma non ne ho la certezza) che questa differenza si verifichi quando si ha un’eccezione in un metodo che gestisce direttamente o indirettamente un evento (ad esempio il click su un pulsante). A questo punto vorrei capire se le mie supposizioni sono corrette e se è possibile decidere il comportamento del gestore delle eccezioni intervenendo con qualche impostazione. Sarebbe, infatti, molto comodo poter far risalire tutte le eccezioni al Main.

‘ di solito in Form_Load AddHandler Application.ThreadException, AddressOf UnhandledExceptionProc Private Sub UnhandledExceptionProc(ByVal sender As Object, ByVal e As System.Threading.ThreadExceptionEventArgs) Dim msg As String = StringFormat(“{0}\n\nClick Yes to save current data\nClick No to exit without saving\nClick Cancel to ignore error”, e.Exception.Message) Select Case MessageBox.Show(msg, “Unhandled error”, MessageBox Buttons.YesNoCancel, MessageBoxIcon.Error) Case DialogResult.Cancel Exit Sub Case DialogResult.No Application.Exit() Case DialogResult.Yes ‘ salva i dati dell’applicazione ‘ scrive nel log Application.Exit() End Select End Sub

A.F. Giorgio Come intercetti le eccezioni? In genere nelle applicazioni WinForms puoi usare un comodo evento per le eccezioni non gestite, e quindi centralizzare in un unico punto la gestione delle stesse.

Richiamare un DLL C++ da VB6 Ho scritto una DLL con Visual C++, e sto cercando di utilizzarla dall’interno di Visual Basic, ma non viene riconosciuta. Come posso fare?

Alberto Falossi è formatore e consulente per Code Architects (www.codearchitects.com) su .NET e tecnologie correlate. Si occupa del coordinamento editoriale di VBJ e collabora con numerose riviste di programmazione italiane e straniere. È membro del team di .NET-2-The-Maxwww.dotnet2themax.com). ( Può essere contattato tramite il sito www.albertofalossi.it, dove mantiene anche il blog personale.

54

VBJ

N. 60 - Novembre/Dicembre 2004

I compilatori C++ applicano una tecnica nota come name mangling per assegnare nomi differenti a funzioni omonime definite all’interno di classi diverse. Anche se la funzione viene definita globalmente, e quindi esternamente alle classi, questa tecnica viene applicata egualmente, col risultato di ottenere una funzione con un nome diverso da quello effettivamente impostato. Per evitare questo problema occorre utilizzare la direttiva extern “C” davanti al nome della

POSTA

funzione, in modo da indicare al compilatore di considerarla come una funzione C e non C++. Per esempio:

If Len(Giorno) = 1 Then Giorno = “0” & Giorno End If

extern “C” __declspec(dllexport) int MyFunc( char *str ) { … }

Lorenzo Vandoni

Calcolo del codice fiscale: errata corrige Volevo segnalarvi un errore nell’algoritmo per la generazione del codice fiscale apparso nel numero 56. Ho scaricato il sorgente ed ho eseguito il progetto relativo inserendo i miei dati anagrafici: il codice fiscale generato era però più corto di un carattere. Ho notato che i giorni da 01 a 09 vengono formattati senza lo zero, cosi nella riga 122 della “Function Compute” ho inserito un Len(giorno) < 2 per il controllo del giorno di nascita aggiungendo lo zero nella stringa. Saluti, Hansel

È vero. Effettivamente, nel progetto VB allegato all’articolo di cui parli è presente un piccolo errore che influisce sul calcolo del codice restituito. L’articolo in questione illustra in dettaglio quale sia la struttura di un codice fiscale ed afferma che i caratteri che vanno dal settimo all’undicesimo riportano informazioni relative alla data di nascita della persona cui si riferisce. Se andiamo a vedere nel codice, troviamo CF = CF & Right(Anno, 2) CF = CF & Mesi(CInt(Mese)) If Sesso = “F” Then

Essendo la variabile in questione definita come String, questa correzione garantisce che il codice accodato sarà sempre di 2 caratteri. Si possono, comunque, percorrere anche strade alternative. Per esempio si può operare sull’interfaccia utilizzata dall’utente per inserire i dati e fare sì che l’informazione che arriva alla routine sia sempre di due cifre. Una terza possibilità è rappresentata dall’utilizzo della funzione Format nell’istruzione che concatena la variabile Giorno al resto del codice fiscale. Grazie della segnalazione, il sorgente sul server Infomedia è stato aggiornato. Lorenzo Braidi

Scrivere una pagina ASP .NET con diversi linguaggi Nella mia piccola compagnia abbiamo iniziato a sviluppare in ASP.NET. Abbiamo discusso su quale linguaggio usare: alla fine abbiamo preferito VB.NET. Tra le questioni che sono sorte ve n’è una a cui non siamo stati capaci di rispondere: è possibile realizzare una pagina Web con linguaggi differenti? Nel senso di mixare linguaggi di scripting? Probabilmente è più una curiosità che una necessità, ma confidiamo in una sua risposta. Saluti, Federico Una pagina ASP.NET deve essere scritta in un unico linguaggio di scripting, quello specificato nella direttiva language della pagina. Ad esempio

CF = CF & CStr(CInt(Giorno) + 40) <%@ Page language=”C#“ %>

Else CF = CF & Mid(Giorno, 1, 2) End If

Nel caso in cui il soggetto sia di sesso femminile, si somma 40 al giorno di nascita. In questo modo si può essere assolutamente certi del fatto che verranno concatenati alla variabile CF 2 caratteri. Se, invece, il soggetto è di sesso maschile, non si effettua alcuna operazione sul giorno di nascita, ma lo si accoda al codice fin qui ottenuto. L’errore è determinato dal fatto che il giorno di nascita può essere composto anche da una sola cifra. Si verifica un’anomalia, quindi, nel caso in cui il soggetto sia di sesso maschile e sia nato in uno dei primi nove giorni di un mese qualsiasi. Esistono molteplici possibilità per risolvere il problema. La più semplice è manipolare opportunamente la variabile che contiene il giorno di nascita:

Tuttavia non tutti sanno che è possibile realizzare una pagina con linguaggi diversi avvalendosi degli user control. Infatti uno user control può essere scritto in qualsiasi linguaggio ASP.NET, anche differente da quello della pagina che lo ospita. Quindi basta isolare le diverse parti di codice in user control separati e usare il linguaggio preferito. Il problema è che questa modalità è supportata dal framework ed è utilizzabile ad esempio quando si scrivono le pagine con notepad, ma non è supportata da Visual Studio, che obbliga a sviluppare uno user control nello stesso linguaggio della pagina e del progetto. Il discorso vale anche a livello di applicazione: se si usa Visual Studio e quindi il code-behind, tutte le pagine di una Web application devono essere scritte nello stesso linguaggio. Se invece si usa la modalità inline, ogni pagina può essere in un linguaggio differente. A.F.

N. 60 - Novembre/Dicembre 2004

VBJ

55

SOFTWARE ENGINEERING

L'accesso ai dati nei linguaggi object-oriented Tutti i linguaggi object-oriented devono fare i conti con la necessità di gestire dati in formato relazionale di Lorenzo Vandoni l paradigma object-oriented si è ormai decisamente imposto come standard de-facto nell’ambito dei linguaggi di programmazione. L’ultimo caposaldo di resistenza relativamente allo sviluppo su PC, Visual Basic 6, ha dovuto cedere il passo alla sua versione object-oriented Visual Basic .NET, ed anche i linguaggi tipicamente utilizzati in ambienti mini e mainframe, come Cobol, sono stati modificati per permettere la gestione di classi ed oggetti. A difendere gli estimatori della programmazione strutturata rimane ora solo il C. Il paradigma object-oriented, però, non è riuscito a imporsi ovunque. I database object-oriented (ODBMS), in particolare, sono rimasti relegati a campi di applicazione molto particolari, e non di rado si è sentito utilizzare il termine “fallimento” nei loro riguardi. Il modello dei dati tuttora più diffuso rimane quello relazionale, introdotto ormai più di 30 anni fa.

I

Il problema dell’accesso ai dati Tutte le applicazioni sviluppate con linguaggi object-oriented, nel caso in cui abbiano la necessità di accedere a dati permanenti gestiti da un DBMS, dovranno quindi affrontare un problema concettuale, noto come impedance mismatch, dato dal fatto che gli stessi oggetti dovranno essere rappresentati in modi diversi in memoria, dove si potrà sfruttare la ricchezza dei costrutti offerti dal linguaggio di programmazione, e su disco, dove si dovranno fare i conti

Lorenzo Vandoni è laureato in Informatica, ed è uno specialista di progettazione e sviluppo con tecniche e linguaggi object-oriented. Ha collaborato alla realizzazione di framework, strumenti di sviluppo e software commerciali in C++, Java e Visual Basic. Può essere contattato tramite e-mail all’[email protected].

56

VBJ

N. 60 - Novembre/Dicembre 2004

con la relativa povertà semantica del modello relazionale. Tra gli esempi che vengono spesso citati per spiegare tale problema vi è quello relativo ad oggetti complessi, come il disegno tridimensionale di un’automobile all’interno di un programma CAD. Utilizzando un linguaggio di programmazione objectoriented potremo facilmente definire una classe Automobile, dotata di metodi complessi per la sua gestione, per permettere ad esempio di ruotare l’immagine, capovolgerla o stamparla, e potremo sfruttare l’ereditarietà per incapsulare in una classe base Oggetto3D tutte quelle operazioni che possono essere facilmente estese ad altri tipi di grafici tridimensionali. Nel caso in cui molti oggetti di questo tipo dovessero essere memorizzati su un DBMS, e se si volessero permettere interrogazioni del tipo “ricerca tutte le auto con cambio automatico”, ci troveremmo nella necessità di scrivere un’algoritmo per tradurre la rappresentazione in memoria dell’oggetto in un insieme di righe memorizzate in molte tabelle diverse. In più, sarà necessario scrivere l’algoritmo inverso, e tradurre in sintassi SQL un’interrogazione come quella precedentemente citata.

SOFTWARE ENGINEERING

Il problema dell’impedance mismatch è quindi costituito dalla necessità di fornire algoritmi di traduzione per convertire la rappresentazione di uno stesso oggetto tra due formati differenti. La soluzione più naturale per questo problema sarebbe quella di utilizzare un unico formato di rappresentazione, come proposto dai sostenitori degli ODBMS, ma, come già visto, questi sistemi non hanno avuto una sufficiente diffusione.

Uno strato di software per l’accesso ai dati Preso atto della necessità di scrivere uno strato di software intermedio (o middleware) che si preoccupi di convertire i dati da una rappresentazione object-oriented al modello relazionale, e viceversa, occorre trovare il modo migliore per farlo. Le soluzioni possibili sono sostanzialmente due: la prima prende atto della differenza esistente tra i due modelli, e prevede l’introduzione all’interno delle classi dei metodi necessari per espletare tutte le operazioni necessarie per il reperimento, il salvataggio e la ricerca dei dati, mentre la seconda tende a considerare il tipo tabella come un particolare tipo del linguaggio, dotato di caratteristiche e limitazioni particolari, e permette di creare uno strato di software più generico e facilmente riusabile.

Accesso object-oriented ai dati Il primo tipo di soluzione prevede l’aggiunta all’interno delle classi che costituiscono la rappresentazione objectoriented del sistema, dei metodi necessari per gestire gli oggetti di quella classe all’interno del DBMS. Tornando all’esempio della classe Automobile, significa che dovrà essere creata una classe del tipo (in sintassi VB.NET): Class Automobile ‘metodi specifici dell’oggetto Public Sub Ruota(nGradi As Integer) ... Public Sub Capovolgi() ... Public Sub Stampa() ... ‘metodi per la gestione del DBMS Public Sub Salva() ... Public Shared Sub Leggi(nObjId As Long) As Automobile ... Public Shared Sub Trova(sCondition As String) As Automobile ... End Class

I due metodi Leggi e Trova devono essere statici (o Shared, secondo la sintassi utilizzata in VB.NET), perché si preoccupano di creare un nuovo oggetto Automo-

bile caricandone i dati dal DBMS (una soluzione alternativa, pure corretta, potrebbe essere quella di trasformarli in costruttori). Possiamo tralasciare in questa sede i problemi tutt’altro che banali legati alla necessità di disporre di un object identifier e di un formato opportuno per la rappresentazione delle interrogazioni, semplicisticamente costituite, nell’esempio, da un parametro di tipo stringa. Tralasciamo anche una possibile discussione sulla definizione di interfacce o classi base che permettano di meglio formalizzare questo tipo di soluzione, e concentriamoci invece sui possibili vantaggi e svantaggi che questa può offrire, in relazione al problema precedentemente esposto. Il principale vantaggio è dato da un corretto incapsulamento del problema. I dettagli relativi alla traduzione dei dati dell’oggetto tra le due rappresentazioni vengono codificati all’interno dei metodi identificati, lasciando il resto dell’applicazione all’oscuro del problema. Questo significa, tra l’altro, che il tipo di rappresentazione dei dati all’interno del DBMS potrà essere modificato senza che l’applicazione che utilizza la classe ne risenta. Questa soluzione viene citata all’interno di molti testi, e in alcuni “vecchi” libri scritti con l’obiettivo di spiegare ai programmatori Visual Basic 6 perché mai potesse essere necessario creare delle classi, invece di limitarsi come in passato ad usare form e funzioni, questo veniva addirittura indicato come il motivo principale. Si tratta senz’altro di una soluzione corretta, ma presenta un paio di problemi non banali. Il primo è dato dalla proliferazione di classi del tutto simili tra loro. Il caso della classe Automobile, ovvero di una classe di oggetti complessi con problemi di Impedance Mismatch non banali, è di fatto piuttosto raro. Molto più spesso, nelle applicazioni gestionali, si utilizzano oggetti come Cliente, Fornitore o Fattura, che anche all’interno del programma possono essere visti come semplici aggregati di campi, rappresentabili, a seconda del linguaggio utilizzato, con un type o una struttura. Lavorando con oggetti di questo tipo, l’implementazione delle classi di livello intermedio diventerebbe banale e ripetitiva, nonché soggetta ad errori e causa di inutili rallentamenti nel codice. Il secondo problema è dato dalla necessità di accedere dall’interno del codice ai dati corrispondenti ai campi della tabella. Questa necessità porta alla creazione di un insieme di metodi Get e Set per l’accesso ai dati, in corrispondenza ad ognuno dei campi. Oltre ad enfatizzare i rischi di ripetitività e rallentamenti precedentemente riscontrati, questi metodi introducono anche problemi di manutenzione, in quanto ad ogni modifica della struttura del database dovrà corrispondere una modifica del codice della classe. Questo tipo di soluzione, quindi, dimostra di essere adatta per la gestione di oggetti complicati, ma eccessiva e ridondante per il caso di oggetti più semplici.

N. 60 - Novembre/Dicembre 2004

VBJ

57

SOFTWARE ENGINEERING Classi generiche di accesso ai dati Il secondo approccio prevede la creazione di un’unica classe Tabella, che possa essere genericamente utilizzata per accedere al contenuto di qualsiasi tabella sul database. Questo approccio è quello adottato dalla maggior parte delle librerie più diffuse. Un recordset ADO o un dataset ADO.NET, nonostante siano notevolmente differenti tra loro, possono entrambi essere considerati in questa categoria. In questo tipo di soluzione, la classe Tabella (o Recordset, o Dataset) è concettualmente assimilabile alla rappresentazione in memoria di una tabella relazionale. I dati mantenuti al suo interno sono raggruppati in colonne, il cui tipo può essere scelto solo all’interno di un insieme di tipi predefiniti, e le operazioni consentite sono quelle di inserimento, modifica e cancellazione di righe. I vantaggi e gli svantaggi di questo approccio sono speculari rispetto alla soluzione prospettata nel paragrafo precedente. Non è necessario implementare alcuna classe aggiuntiva, in quanto per ogni tabella di database verrà semplicemente creata una nuova istanza della classe Tabella, e non si potranno riscontrare quindi problemi legati a possibili errori di codifica, rallentamenti e necessità di manutenzione. D’altra parte, questo tipo di approccio non risolve il problema dell’impedance mismatch, perché non permette di creare una classe Automobile che mascheri il modo in cui i vari componenti, come volante, motore e ruote, sono rappresentati all’interno del database. La logica applicativa dovrà essere implementata in una classe a parte, che utilizzerà una o più istanze della classe Tabella per reperire i dati utili a costruire una rappresentazione in memoria dell’oggetto.

limitarsi a salvare i dati delle varie colonne, registrerà in modo opportuno anche i dati delle tabelle collegate, prelevandoli da strutture dati mantenute in memoria. Analogamente, il metodo Leggi non si limiterà ad eseguire una sola interrogazione, ma cercherà di reperire, utilizzando diverse query, tutte le informazioni necessarie per creare l’oggetto Automobile in memoria.

Estendere ADO.NET La struttura gerarchica delle classi di ADO.NET, con la separazione di responsabilità fra managed provider (DataReader, DataAdapter) e classi dedicate al mantenimento dei dati in memoria (DataTable, DataSet) rende l’implementazione delle idee precedentemente esposte un po’ più complicata. L’idea di base, infatti, era quella di creare una classe Automobile come estensione di una generica classe Tabella, e di inserire all’interno di questa classe tutta la logica relativa sia alla manipolazione dei dati (Ruota, Capovolgi, Stampa), sia quella relativa al salvataggio e al reperimento dei dati dal database (Salva, Leggi, Trova). La separazione di responsabilità tra le classi base ci costringe ad adottare una soluzione alternativa. Le possibilità sono diverse:





In medio stat virtus L’approccio più conveniente da adottare consiste nell’applicare contemporaneamente le due tecniche precedenti, comportandosi in modo diverso a seconda del livello di complessità dell’oggetto rappresentato. Per oggetti strutturalmente semplici, come Cliente, Fornitore e Fattura, potrà essere sufficiente istanziare una classe Tabella di tipo generico, mentre per oggetti più complessi, come Automobile, potrà essere conveniente creare una classe specifica che mascheri la struttura del database. Con l’avvento di .NET, questo approccio diventa particolarmente conveniente, grazie all’introduzione di meccanismi come ereditarietà e polimorfismo. In pratica, diventa possibile fare in modo che la propria classe Automobile venga creata come sottoclasse della classe più generica Tabella, ereditandone in questo modo tutte le funzionalità di base, come l’accesso ai valori dei campi elementari. In questo modo, la classe potrà essere facilmente dotata di funzionalità supplementari, e si potrà anche fare in modo di sovrascrivere il comportamento di alcuni metodi. Per esempio, il metodo Salva, o Update, invece di

58

VBJ

N. 60 - Novembre/Dicembre 2004



definire la classe Automobile come estensione di Dataset, aggiungendo al suo interno anche un riferimento ad un DataAdapter, in modo da poter implementare la logica relativa ai metodi Salva, Leggi e Trova; creare due classi separate, AutomobileDataSet e AutomobileDataAdapter, mantenendo anche nelle classi derivate la stessa distinzione esistente nelle classi base; creare una nostra classe base Tabella, che incapsuli al suo interno un DataSet e un DataAdapter, e definire la classe Automobile come estensione di quest’ultima.

Per ora, limitiamoci a queste considerazioni. Torneremo su queste possibilità in un prossimo articolo, in cui presenterò più in dettaglio una possibile soluzione.

Conclusioni Il framework che è stato tratteggiato nei paragrafi precedenti non tiene conto della possibilità di sfruttare l’ereditarietà multipla. L’unico linguaggio che permette l’utilizzo dell’ereditarietà multipla, tra quelli più diffusi, è C++, per cui ho preferito non considerare questa ipotesi. La creazione di un insieme di classi che forniscano una gestione completa dei “business object” utilizzati dall’applicazione può essere molto utile per la suddivisione del programma in più strati, o tier, e può semplificare il riuso della logica applicativa in applicazioni diverse.

IN VETRINA VBJ 60 Microsoft Office 2003 for Healthcare Ahmad Hashem

608 pp. euro 31,95 ISBN 0789732114

Scrivi a [email protected] specificando nell’oggetto della e-mail:

IN VETRINA

Mobile Applications. Architecture, Design, and Development

VBJ n. 60 OPPURE inviaci il coupon sottostante al numero di fax

Valentino Lee, Heather Schneider, Robbie Schell

0587/732232

368 pp, euro 46.95 ISBN 0131172638

Linux Shell Scripting with Bash Ken O Burtch

Potrai acquistare i libri qui riportati con uno

SCONTO ECCEZIONALE del 10% anche se acquisti solo un libro

CSS Cookbook Christopher Schmitt

270 pp. euro 34,95 ISBN 0596005768

Head First Servlets and JSP Bryan Basham, Kathy Sierra, Bert Bates 886 pp. euro 44,95 ISBN 0596005407

Managing Security with Snort & IDS Tools Kerry J. Cox, Christopher Gerg

OPPURE

432 pp. euro 36,95 ISBN 0672326426

del 20% se acquisti

3 libri

288 pp. euro 39,95 ISBN 0596006616

VBJ 60

LIBRI Writing Add-ins for Visual Studio .NET Autore Editore ISBN Lingua Anno Pagine Prezzo

Adobe Acrobat6 The Professional User’s Guide Autore Editore ISBN Lingua Anno Pagine Prezzo

Les Smith Apress 1-59059-026-0 Inglese 2002 520 € 53,45

isual Studio .NET è un ottimo ambiente di lavoro integrato per programmatori .NET, sia che usino VB.NET o C#. Anche nello strumento migliore però qualcuno troverà immancabilmente qualche mancanza, e VS.NET non si sottrae certo a questa regola. Tramite il modello ad oggetti esposto un programmatore può però estendere l’IDE con nuovi comandi e finestre. Questo libro si propone appunto come guida allo sviluppo di add-in e macro per VS.NET. Si comincia da un’introduzione alle varie parti dell’IDE, per passare velocemente all’uso dell’Add-in wizard che genera automaticamente uno scheletro per un nuovo add-in, a tecniche (semplici a dire il vero) di debugging, manipolazione di codice, controlli dell’interfaccia utente (solo di Windows Forms però, non si nomina neanche ASP.NET), progetti e soluzioni. Un capitolo è dedicato anche alle macro. Gli argomenti trattati sono parecchi, ma nessuno veramente in dettaglio purtroppo. Moltissime domande che ci si ritrova ad affrontare non appena si comincia un progetto reale restano senza risposta, dal momento che il codice di esempio presentato non è certo molto completo o professionale. La presenza di alcuni errori nel codice poi non aiuta di certo. Sempre riguardo il codice, sono stampati listati anche di quindici pagine dove ne sarebbe bastata una per il codice interessante e relativo all’argomento specifico del capitolo in questione. Inoltre, nonostante in copertina si citi sia VB.NET che C#, a C# sono dedicate solo un paio di pagine, e il codice VB.NET sembra in realtà VB6, dato l’uso estensivo di funzioni come MsgBox, Len, Mid, Left, Right ecc. Addirittura l’addin presentato come progetto finale genera codice in stile VB6! Un capitolo che invece ho apprezzato più degli altri è “How do I…”, che presenta una lista di Q&A con frammenti di codici pronti da usare (sebbene qualche ritocco sia di solito necessario…), perché va direttamente al punto e riesce a dare effettivamente un aiuto al programmatore in cerca di risposte a problemi tipici. In definitiva, il libro può essere un’utile introduzione per farsi un’idea di quelle che sono le possibilità di estensione di VS.NET, per chi ha già sviluppato addin in VB6 e a maggior ragione per chi comincia da zero, ma non può essere considerato come la guida definitiva sull’argomento.

V

Pro La sezione “How do I…?” presenta Q&A e pezzi di codice che possono tornare utili, con le opportune modifiche, in molte situazioni.

Contro Listati troppo lunghi e dispersivi, a volte copiati più volte solo per cambiare una singola riga. Gli argomenti sono trattati troppo superficialmente, e gli esempi sono troppo semplici per essere usati come base per progetti di qualità commerciale.. Marco Bellinaso

D. L. Baker, T. Carson Apress 1-59059-232-8 Inglese 2004 460 € 42,75

uesto libro non è stato scritto per i novizi, ma è rivolto ad una utenza professionale, così come testimoniato dal titolo stesso. La promessa è quella di trarre utilità dall’apprendimento dei segreti di questa ultima versione di Acrobat e la ragione per cui gli autori dicono di aver scritto il testo (e per cui bisognerebbe leggerlo!) è sottolineata da loro stessi: raggiungere l’obiettivo di lavorare di meno facendo lavorare di più le macchine. Un intento del genere, che ha un che di filosofico, viene perseguito illustrando i molti vantaggi di Acrobat 6: già da un veloce sguardo al primo capitolo si intuisce come le differenze della celeberrima suite rispetto alla versione precedente siano sostanziali. Tra le molte funzionalità illustrate impossibile non ricordare il Binder, un potentissimo tool che consente di combinare in un unico file PDF differenti formati di file. Nei primi capitoli si familiarizza con l’interfaccia (sotanzialmente nuova) e si impara a creare e manipolare file PDF. Una volta capito come ottenere il proprio file PDF si entra nello specifico: editing avanzato e strumenti per la “collaboration” vengono descritti nei dettagli. Ormai non si può prescindere dalla sicurezza ed il libro non fa eccezione, illustrando come proteggere da alterazioni i propri documenti e come assicurarne e verificarne l’integrità e la genuinità. Leggendo il libro si ha l’impressione che nulla venga tralasciato: un intero capitolo dedicato alla creazione di forms, ad esempio, non lascia al lettore alcun dubbio riguardo al loro utilizzo. Anche le funzionalità classiche, quali l’indicizzazione e la ricerca, la preparazione dell’output e la stampa vengono opportunamente descritte con grossa attenzione alla accessibilità dei documenti creati. Le sorprese più positive arrivano dalla lettura degli ultimi capitoli: un “All about ebooks” che non tradisce le aspettative del titolo e un capitolo che descrive funzionalità avanzate. Scorgendo l’indice potrebbe soprendere un capitolo su JavaScript, ma leggendolo si apprende come questo linguaggio estenda in modo sostanziale le potenzialità della Suite che pure non è storicamente indirizzata a chi scrive codice. Utilissimo, infine, l’ultimo capitolo che permette di mettere insieme in maniera pragmatica molti dei concetti appresi durante la lettura mediante la realizzazione di un progetto di creazione di una knowledgebase.

Q

Pro Un testo utilissimo per diverse categorie di professionisti: graphic/web designer, architetti, ingegneri ed in generale utenza di tipo business.

Contro Il testo si rivolge ad un utenza di tipo professionale risultando di conseguenza non molto indicato per un pubblico che si avvicina alla materia per la prima volta. Massimiliano Mariconda

N. 60 - Novembre/Dicembre 2004

VBJ

61

.NET TOOLS a cura di Davide Mauri

[email protected]

Reflector Credo che sicuramente abbiate usato tutti, chi per semplice curiosità chi per necessità professionali, il tool ILDASM.exe, ossia il disassembler fornito da Microsoft nel .NET Framework SDK, che permette di visualizzare il codice MSIL generato dal compilatore dal nostro codice sorgente e vedere ciò che viene davvero dato in pasto al CLR. Lutz Roeder deve aver pensato che, se un compilatore può generare MSIL deve essere vero anche il contrario, e quindi passare da questo a C#, VB.NET, e così via. Detto, fatto! Nell’ottobre del 2000 nasce la prima versione di questo ormai popolarissimo tool, che permette a chiunque di sbirciare all’interno di un assembly .NET (quindi in qualsiasi programma basato sul framework), di disassemblarlo e di ricostruirne il codice sorgente (Figura 1). Il codice sorgente così ottenuto ovviamente non è identico all’originale (tranne per i casi più semplici), ma è funzionalmente equivalente, ossia produce lo stesso risultato. La cosa interessante è che Reflector permette di decompilare in quattro diversi linguaggi: MSIL, C#, VB.NET e Delphi.NET. Un esempio pratico è visibile tramite il Listato 1 ed il Listato 2; il primo è il codice originale scritto in C#, il secondo è il risultato della decompilazione utilizzando come destinazione VB.NET. Proprio tramite i due listati credo si possa capire l’enorme utilità di Reflector: grazie a questo è possibile scoprire quello che accade davvero durante l’esecuzione di un’applicazione… che, purtroppo, non sempre è quello che ci aspettiamo o quello che è documentato. Nel caso preso in esame è possibile vedere come dichiarazione, creazione ed inizializzazione del campo statico someStrings siano convertite in realtà (come giustamente ci si aspettava) in una dichiarazione pura separata dalla creazione e dall’inizializzazione che invece vengono eseguite nel costruttore statico, creato in modo automatico dal compilatore. Interessante, vero? Beh, direi che curiosi ed assetati di conoscenza sono serviti! Se le funzionalità fin qui descritte ancora non vi bastano, niente paura! Reflector supporta l’aggiunta di funzionalità tramite plugin. Già diversi se ne trovano in giro, e tra i più interessanti segnalo questi:

Davide Mauri è un consulente freelance che si occupa di SQL Server 2000 e di sviluppo di soluzioni Web basate sul .NET Framework. All’attività di consulenza affianca una cospicua attività di docenza e di formazione presso Mondadori Informatica. Il suo sito personale è www.davidemauri.it.

62

VBJ N. 60 - Novembre/Dicembre 2004

Figura 1

Listato 1

Reflector in azione su un’applicazione di test

Il codice originale C# di una semplice applicazione di esempio

using System; namespace SampleApp { class SampleClass { static string[] someStrings = new string[10] { “a”, “b”, “c”, “d”, “e”, “f”, “g”, “h”, “i”, “l”}; [STAThread] static void Main(string[] args) { foreach(string s in someStrings) { Console.WriteLine(s); } } } }

 Reflector.VisualStudio: come lascia intendere il nome permette di integrare Reflector all’interno di Visual Studio, e rimpiazzare definitivamente il class browser nativo;  Reflector.McppLanguage: per ottenere un decompilato scritto in C++;

.NET TOOLS

 Reflector.FileDisassembler: per salvare il codice prodotto da Reflector su file in modo automatico;  Reflector.OpenRunningAssembly: per caricare in Reflector un assembly in esecuzione;  Reflector.Graph: per visualizzare graficamente collegamenti e dipendenze del nostro codice. Spettacolare! Potete scaricarli al terzo indirizzo nella tabella sottostante. Creare un proprio plugin è relativamente semplice, e su GotDotNet si trova tutto il necessario: nel caso vi servisse qualcosa di particolare potete svilupparne uno in autonomia, e ovviamente non mancate di segnalarmelo! Per concludere il tool è veramente molto semplice da usare, e, oltre all’utilità intrinseca di un’applicazione di questo tipo, tutta una serie di tocchi di classe, come la ricerca integrata su Google o su MSDN dei metodi e delle proprietà dell’assembly in analisi, oppure l’integrazione con la documentazione prodotta in formato XML, ne hanno fatto un tool indispensabile per molti sviluppatori.

Prodotto Reflector

Url di riferimento http://www.aisto.com/roeder/dotnet/ http://www.gotdotnet.com/workspaces/workspace.aspx ?id=0f5746c3-c7aa-4879-8043-e0f4fc233ade http://www.freewebs.com/csharp/Reflector/Addins/

Stato Release 4.1.3.0

Semplicità d’uso 

Semplicissimo! Basta caricare l’assembly desiderato ed è fatta.

Utilità 

Non è un tool che utilizzerete tutti i giorni...ma se non ci fosse bisognerebbe inventarlo!

Qualità prodotto 

Nulla da dire. Ottima, soprattutto per il supporto di plug-in esterni.

Qualità documentazione 

Di documentazione praticamente non ce n’é, ma non se ne sente troppo il bisogno, visto che è tutto molto semplice. Molto buona la documentazione e gli esempi per la scrittura di plugin.

The Regulator Se dovessi scrivere in una sola riga che cos’è The Regulator, mi basterebbe dire “È il Visual Studio delle Regular Expression”. Per fortuna lo spazio non manca, ma penso che quanto appena detto dia immediatamente un’idea della potenza e della qualità del tool di cui stiamo parlando. Come avrete già potuto intuire The Regulator è un editor di Regu-

Figura 2

Il completo IDE di The Regulator

lar Expression, oggettini simpatici, estremamente potenti (in alcuni casi indispensabili) ma parecchio complessi da scrivere. Vi ricordate? Ne avevamo già parlato in un numero precedente di .NET Tools, [1]. Perché questo tool è così bello? Beh, i motivi sono tanti ma probabilmente il principale è l’integrazione con il sito RegExLib.com, tramite il quale è possibile cercare, direttamente dall’interno dell’editor, le regular expression che ci servono. Dovete validare un indirizzo e-mail? Cercate “email”. Vi serve una RegEx per validare un numero di telefono nel formato italiano? Cercate “italian”. Nel caso in cui invece la regular expression debba essere scritta ex-novo, avete a disposizione syntax highlighting ed intellisense, più la possibilità di comporre l’espressione costruendola tramite un menu contestuale. Una volta in possesso della regular expression desiderata, questa può essere provata su un testo di esempio, in modo da verificarne la correttezza. A questo punto è poi possibile generare il codice C# o VB.NET da inserire nella nostra soluzione per applicare l’espressione, oppure un’assembly da referenziare nel progetto, già compilato. Un’ultima feature non meno interessante è il “RegEx Analyzer”, che, leggendo la regular expression digitata, descrive, in inglese, il comportamento della stessa. Ad esempio, questa porzione di espressione: (?\d{2})

viene descritta come: Capture to Any Digit Exactly 2 times

utile, soprattutto se siete agli inizi, per decifrare le criptiche espressioni. Come ultima nota, e come ciliegina sulla torta, viene anche fornito un help che spiega tutte le funzionalità del programma e che contiene anche diversi articoli (alcuni

N. 60 - Novembre/Dicembre 2004 VBJ

63

.NET TOOLS

provenienti direttamente dal sito MSDN) che mostrano dei possibili utilizzi (pratici) delle regular expression. Un tool che non può mancare alla vostra collezione!

Prodotto The Regulator

Url di riferimento http://royo.is-a-geek.com/iserializable/regulator/

Stato Release 2.0.3.0

Semplicità d’uso 

Semplifica enormemente sviluppo e debug di regular expression. Fantastica la possibilità di effettuare ricerche direttamente sul RegexLib.com

 Gestione dei profili È possibile memorizzare la configurazione di deployment per un progetto in modo da poter effettuare il rilascio dello stesso con un solo click.  Multi Profile Deployment È possibile distribuire i file su più profili contemporaneamente. Utile quando si rilasciano applicazioni ospitate su più di un web server.  Integrazione con Visual Studio Cliccando con il tasto destro sulla cartella del progetto, è disponibile una nuova icona che attiva il deployment di UnleashIt. Oltre a tutte queste peculiarità è possibile anche effettuare il backup dei file di cui si sta facendo il deployment, attivare il logging di tutte le operazioni eseguite, e, per una massima personalizzazione, anche definire

Utilità 

Dato che le regular expression sono utili dappertutto, questo lo è di conseguenza.

Listato 2

Qualità prodotto 

Ottima. Non a caso è stato messo tra i top-ten tools da avere assolutamente.

Qualità documentazione  Ottima, con tanto di help in linea.

UnleashIt UnleashIt è un piccolo tool, dalla quale gli sviluppatori di applicazioni web difficilmente si separeranno dopo averlo usato. Il suo scopo è quello di semplificare ed automatizzare al massimo la fase di deploy della soluzione. Se infatti non si utilizzano le FrontPage extensions, in pratica il deploy di un’applicazione web deve essere fatto a mano, a meno di non voler creare un’applicazione di setup; cosa non sempre possibile, soprattutto se volete solamente trasferire i file sul server della società che vi fornisce l’hosting. In questo caso di solito si utilizzano i soliti metodi di copia via FTP o, se possibile, via VPN su una cartella condivisa. L’inconveniente di tale soluzione è generalmente dovuto alle dimensioni dell’applicazione stessa. Se è abbastanza grossa può essere facile dimenticarsi qualche file durante l’upload. Con UnleashIt (che prima di giungere alla versione 2.0 si chiamava WebDeploy) tutto questo non sarà più un problema. Una volta caricato, l’interfaccia grafica, visibile in Figura 3, permette di scegliere la directory nella quale sono presenti i file sorgenti oppure, se preferiamo, il file del progetto generato da Visual Studio. Tramite quest’ultima opzione UnleashIt selezionerà in automatico tutti i file presenti nel progetto. Nel caso si vogliano personalizzare i file da trasferire, è possibile utilizzare le voci visibili sulla destra, in modo da includere od escludere file e/o directory a seconda delle proprie necessità. Fatto ciò è poi necessario specificare il percorso di destinazione, che può essere un percorso di rete, un indirizzo ftp oppure un file .zip, cliccare sul pulsante “Deploy” ed il gioco è fatto. Tra le feature particolarmente interessanti da segnalare troviamo:

64

VBJ N. 60 - Novembre/Dicembre 2004

Il codice VB.NET generato da Reflector tramite decompilazione

Imports System Namespace SampleApp Friend Class SampleClass ‘ Methods Private Shared Sub New() Dim textArray1 As String() = New String(10) {} textArray1(0) = “a” textArray1(1) = “b” textArray1(2) = “c” textArray1(3) = “d” textArray1(4) = “e” textArray1(5) = “f” textArray1(6) = “g” textArray1(7) = “h” textArray1(8) = “i” textArray1(9) = “l” SampleClass.someStrings = textArray1 End Sub Public Sub New() End Sub <STAThread> _ Private Shared Sub Main(ByVal args As String()) Dim textArray1 As String() = SampleClass.someStrings Dim num1 As Integer = 0 Do While (num1 < textArray1.Length) Dim text1 As String = textArray1(num1) Console.WriteLine(text1) ++num1 Loop End Sub

‘ Fields Private Shared someStrings As String() End Class End Namespace

.NET TOOLS

Prodotto UnleashIt

Url di riferimento http://www.eworldui.net/UnleashIt/

Stato Release 2.0

Semplicità d’uso  Più semplice di così è impossibile.

Utilità 

Ormai non posso più farne a meno. Fantastica l’integrazione con Visual Studio!

Qualità prodotto  Ottima. Figura 3

La semplice – ma completa – interfaccia utente di UnleashIt

dei comandi che devono essere eseguiti prima e dopo l’operazione di rilascio.

Qualità documentazione  Tutta contestuale, molto semplice e molto pratica.

Riferimenti [1] Rubrica .NET Tools, Visual Basic & .NET Journal n° 55

&

ABBONAMENTI COD. CLIENTE ____________________ INDIRIZZO DI SPEDIZIONE

ARRETRATI

Nome ___________________________ Cognome ___________________________ Ditta _____________________________ Via ________________________________ C.A.P. _______________ Città _____________________________ Prov. ________ Tel. ____________________________ Fax ____________________________ E-mail _________________________________

Prezzo Abbonamento

Prezzo Rinnovo

CP

€ 52,00 

€ 48,00 

DEV

€ 52,00 

€ 48,00 

VBJ

€ 46,00 

Login

€ 46,00 

Riviste

4 riviste

Prezzo Abbonamento

Prezzo Rinnovo

CP P.P.

€ 70,00 

€ 66,00 

€ 42,00 

DEV P.P.

€ 70,00 

€ 66,00 

€ 42,00 

VBJ P.P.

€ 56,00 

€ 52,00 

Login P.P.

€ 56,00 

€ 52,00 

€ 196,00  € 180,00 

Riviste - Spedizione POSTA PRIORITARIA

4 riviste P.P. CP Web

€ 42,00 

€ 38,00 

DEV Web

€ 42,00 

€ 38,00 

VBJ Web

€ 44,00 

€ 40,00 

Login Web

€ 44,00 

€ 40,00 

4 riviste Web

€ 172,00  € 156,00 

4 riv. + 4 riv. Web

€ 368,00  € 300,00 

TOTALE

€ ________________

 Gli abbonamenti decorrono dal primo numero raggiungibile. Per attivazioni retroattive contattaci al numero 0587/736460 o invia una e-mail all’indirizzo [email protected]  I prezzi degli abbonamenti per l’estero sono maggiorati di spese di spedizione.

€ 252,00  € 236,00 

4 riv. P.P. + 4 riv. Web € 424,00  € 356,00  € ________________

TOTALE

Arretrati    

Numero rivista

Prezzo cadauno

Totale

CP DEV ___________________ Login _________________ VBJ ____________________

€ 10,00

€ _________ € _________ € _________ € _________ € _________

____________________

€ 10,00 € 14,00 € 14,00 TOTALE

+ Spese spedizione (€ 2,50 per 1 rivista,€ 4.00 per 2 o più riviste)

TOTALE

€ _________ € _________ VBJ 60

TIPO DI PAGAMENTO     

Contrassegno (+ € 7,00 per spese postali) Allego fotocopia della ricevuta del versamento su C/C postale n. 14291561 intestato a “Gruppo Editoriale Infomedia S.r.l. Ponsacco” Allego assegno bancario intestato a “Gruppo Editoriale Infomedia S.r.l.” (NON TRASFERIBILE) Allego fotocopia del Bonifico Bancario effettuato sul C/C 000010005424 CAB 71120 ABI 6370 CIN “M” - Cassa di Risparmio di Volterra Agenzia di Ponsacco Autorizzo l’addebito dell’importo di €_______________ sulla mia CARTA VISA/CARTASì Numero Scadenza Titolare ____________________________________________ nato il ___/___/_______ Firma ___________________________________

 Mi collego al sito www.infomedia.it dove potrò effettuare il pagamento con carta di credito in modalità sicura (Banca SELLA) L’IVA sul prezzo dell’abbonamento è assolta dall’Editore e non sussiste l’obbligo di emissione della fattura, ai sensi del D.M. 9 aprile 1993, art.1, comma 5; pertanto, a fini contabili, farà fede la sola ricevuta di pagamento; perciò la fattura verrà emessa solo se esplicitamente richiesta al momento dell’ordine. Garanzia di riservatezza - Il Gruppo Editoriale Infomedia garantisce la massima riservatezza dei dati da lei forniti e la possibilità di richiederne gratuitamente la rettifica o la cancellazione scrivendo a: Responsabile Dati - Gruppo Editoriale Infomedia Srl - Via Valdera P. 116 - 56038 Ponsacco (PI). Le informazioni custodite nel nostro archivio elettronico verranno rtattate in conformità alla legge 675/96 sulla tutela dati personali.

V. Valdera P., 116 - 56038 Ponsacco (Pi) - Tel. 0587/736460 - Fax 0587/732232 P.Iva 01474440508 - [email protected] - www.infomedia.it

Related Documents

V2004 06 Vbj60
October 2019 3
Social Justice V2004
December 2019 1
06-06
August 2019 80
06
October 2019 15
06
October 2019 16
06
November 2019 13