2009. január havi bejegyzések

Nagy méretű weblapok fotózása – Webpage Capture

Akinek kellett már dokumentálás vagy UI prototípus egyeztetés céljából weblapról képernyőképet készítenie, az tudja, hogy a feladat nem minden esetben egyszerű. Akkor nincs gond, ha az oldal kifér a képernyőre, de mi van akkor, ha függőlegesen csak három képernyőnyi scrollozás után lehet az oldal aljára érni? Ebben az esetben marad Nikhil Web Development Helpere vagy a print screenek egymás alá ragasztgatása. Mivel az előbbi elkezdett nem működni nálam, kénytelen voltam írni egy célalkalmazást.

Mivel évente kb. egy vastag kliens alkalmazást írok, úgy látszik ez lesz az idei termés, íme a Webpage Capture:

Webpage Capture

És íme a kimenet, egy fotó a Weblaborról 976 x 3565 pixel méretben:

WebpageCapture: Weblabor

Bár az alkalmazás egészen pofásra sikerült (a saját gyermek ugye 🙂 ) és használhatóan is működik, azért van egy apró szépséghibája: mivel nem akartam P/Invoke-t használni, ezért inkább egy nem támogatott megoldást választottam a képernyőképek elkészítésére. A WebBrowser kontrollnak van egy DrawToBitmap metódusa, amely bár kiválóan működik, “supports the .NET Framework infrastructure and is not intended to be used directly from your code”.

A másik lényeges pont, hogy a DrawToBitmap azt rajzolja a bitmapbe, amit a WebBrowser kontroll éppen mutat, ezért a rajzolás előtt át kell méretezni a kontrollt akkorára, amekkora a weboldal. A dokumentum mérete pedig a WebBrowser.Document.Body.ScrollRectangle tulajdonságból derül ki.

Lényegében ennyi kódolás kellett a fenti alkalmazás elkészítéséhez, a többi sallang, ami viszont igazán kényelmesen használhatóvá tette, szinte összekattintgatható: a böngészőben korábban használt URL-ek felkínálása és automatikus kiegészítése, az utoljára használt mappa, URL és checkbox állapot felhasználónkénti elmentése, a mentési útvonal tallózása, a beágyazott erőforrásként tárolt ikonokkal ellátott gombok, progress bar és link a status barra, na és persze mindez egy kattintással admin jogok nélkül a webről települve és önmagát frissítve ClickOnce segítségével. Azt kell hogy mondjam, igazán jó dolgok vannak a Windows Formsban, amiket én fontosabbnak ítélek, mint a 3D-ben forgazható grafikonokat 🙂

Aki szeretné kipróbálni az alkalmazást, ide kattintson:

http://www.msdnkk.hu/Storage/balassy/Enclosures/WebpageCapture

Known issue: bizonyos weblapok (pl. index) esetén többször készül el a kimeneti fájl, mert valamilyen okból az oldal letöltését jelző esemény többször sül el. Mindez persze csak akkor idegesítő, ha a View after capture be van kapcsolva. Megoldás: kapcsoljuk ki 🙂

 

Response.Redirect kliens oldalon

Számtalanszor előfordul, hogy egy oldal feldolgozása során a felhasználót át kell irányítanunk egy másik oldalra. Erre vannak jól bevált megoldások, a Response.Redirect, a Server.Transfer vagy a Server.Execute. Mindegyiknek megvan a maga előnye és hátránya, van azonban egy közös nagy hátrányuk, amit hajlamosak vagyunk elfelejteni.

Gyakran kell például Cancel jellegű gombot készítenünk, ami visszairányítja a felhasználót az előző oldalra, ilyenkor általában ezt a megoldást látom:

    <asp:Button ID="btnGo" runat="server" OnClick="btnGo_Click" Text="Vissza a Kezdőlapra" />

    protected void btnGo_Click( object sender, EventArgs e )
    {
        this.Response.Redirect( "Default.aspx" );
    }

Azaz egy szerver oldali gomb, szerver oldali eseménykezelővel. Érdemes megfigyelni, hogy nincs benne semmi olyan, ami szerver oldali feldolgozást igényelne. Az ilyen jellegű átirányításra lehetne egyszerű HTML <a> taget használni, de amikor ezt felvetem, általában azt a választ kapom, hogy a Cancel mellett van egy OK gomb is, ami viszont igenis igényli a szerver oldali logikát és hogy nézne ki, ha az egyik link, a másik pedig gomb lenne.

A Response.Redirectnek azonban van egy közismerten rossz tulajdonsága: kliens oldali átirányítást végez, azaz visszaküld egy HTTP Error 302-t (Object moved), mire a böngésző egy másik HTTP requestben kéri le a hivatkozott oldalt. Felesleges roundtrip, ami bizonyos esetekben elfogadható, a fentiben azonban semmiképpen.

Nosza, készítsünk egy egyszerű gombot, ami kliens oldalon irányít át! Ennyi az egész, csupasz HTML, semmi szerver oldali kód nincs benne:

    <input type="button" value="Vissza" onclick="window.location='Default.aspx';" />        

Ja, hogy ilyen vezérlő nincs a Toolboxon? “És akkor mi van, ember?!”

Csak egy kis JavaScript kell és még barátságosabb lesz a gombunk:

    <input type="button" value="Vissza" onclick="if( confirm('Biztosan?') ) window.location='Default.aspx';" />        

Sőt, ha csak egy Vissza gombra van szükségünk, akkor talán a history-val is próbálkozhatunk, hátha a böngésző kivágja az oldalt a gyorsítótárból és a szerverünk nem is kap kérést:

    <input type="button" value="Vissza" onclick="history.back()" />    

Ez a megoldás nem csak akkor működik, ha Cancel funkcióra van szükségünk, hanem akkor is, ha master-detail jellegű oldalakat készítünk, ahol az egyik listáz minden elemet, a másik oldal pedig egy kiválasztott elem részleteit mutatja. A részletes oldalt gyakran úgy oldjuk meg, hogy a kiválasztott elem azonosítóját query stringben adjuk át, tehát a lista oldalon úgy kell átirányítanunk, hogy az azonosító bekerüljön az URL-be. Ehhez sem kell szerver oldali kód, adakötéssel ez is megoldható:

    <asp:ListView runat="server" DataSourceID="sdsCustomers">
        <LayoutTemplate>
            <asp:PlaceHolder ID="itemPlaceholder" runat="server" />
        </LayoutTemplate>
        <ItemTemplate>
            <%# Eval( "CompanyName" ) %> - <%# Eval( "ContactName" ) %>
            <input type="button" value="Részletek" 
               onclick="<%# Eval( "CustomerID", "window.location='Customer.aspx?Id={0}';" ) %>" /> 
        </ItemTemplate>
        <ItemSeparatorTemplate>
            <br />
        </ItemSeparatorTemplate>
    </asp:ListView>
    
    <asp:SqlDataSource ID="sdsCustomers" runat="server" 
      ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" 
      SelectCommand="SELECT [CustomerID], [CompanyName], [ContactName] FROM [Customers] ORDER BY [CompanyName]">
    </asp:SqlDataSource>

Itt sem használok szerver oldali kódot, az adatkötés ugyanis runat=”server” nélkül is tud működni. Az aláhúzott kifejezésben az az érdekes, hogy az idézőjelek párosítása szemmel láthatóan rossz, mégis működik 🙂

Adott tehát a lehetőség: lustán és gyorsan olyan kódot írni, amely feleslegesen terheli a szervert és várakoztatja a felhasználót vagy némi kézimunkával mindenkinek a kedvére tenni.

Te melyik utat szoktad választani?

 

A cikkhez tartozó forráskód letölthető innen: ClientRedirectSampleWebSite.zip

Technorati Tags: ,

Azonos típusú fájlok kiszedése alkönyvtárakból

Gyakran előfordul, hogy egy mappa végtelen mennyiségű alkönyvtárából kellene minden fájlt vagy éppen egy feltételnek megfelelő fájlokat kimásolni. Régen erre a Windows Explorer Search funkcióját használtam, de a Vista óta erre a célra egyszerűen nem áll kézre. Mivel nem vagyok Total Commander függő, ezért jobb megoldást kerestem és hamar sikerült kikötnöm a PowerShellnél.

Mint ahogy azt a Powershellnél már megszokhattuk, egyetlen sor a megoldás:

    Get-ChildItem -filter *.xls -recurse | Copy-Item -destination C:celmappa

Elsőre furcsa volt, hogy bár a Get-ChildItemre működik a dir alias, a /s helyett mégis –recurse kell, és a copynak megfelelő Copy-Itemet is kicsit másként kell paraméterezni, hamar hozzá lehet szokni.

Technorati Tags:

Aszinkron kérés leállítása

Mint bármely HTTP kérésnél, AJAX esetén is előfordulhat, hogy a válasz lassan érkezik meg a szervertől. A felhasználó türelmetlen lesz, de nem tudja, mihez nyúljon. Mivel a böngésző Stop gombját feleslegesen nyomogatja, a webfejlesztő feladata marad, hogy az AJAX-os kérés leállítására lehetőséget adjon.

Szerencsére erre van támogatás az ASP.NET AJAX-ban: a kliens oldali Sys.WebForms.PageRequestManager osztály abortPostBack() metódusát kell meghívnunk. Egy UpdateProgress vezérlőnk biztosan lesz, hiszen a felhasználót tájékoztatjuk (ugye!?), hogy valami történik a háttérben. Tegyünk rá egy kliens oldali gombot, amely egy saját JavaScript függvényt hív:

    <asp:UpdateProgress runat="server" 
        AssociatedUpdatePanelID="UpdatePanel1" DisplayAfter="0">
        <ProgressTemplate>
            Frissítés folyamatban, türelem...
            <input type="button" value="Mégsem" onclick="cancelRequest()" />
        </ProgressTemplate>
    </asp:UpdateProgress>

A függvényt akár az oldal head részében is implementálhatjuk, annyira általános:

    <script language="javascript" type="text/javascript">
        function cancelRequest() 
        {
            var mgr = Sys.WebForms.PageRequestManager.getInstance();
            if( mgr.get_isInAsyncPostBack() ) 
            {
                mgr.abortPostBack();
            }
        }
    </script>

Ennyi az egész, lehet kopipésztelni minden oldalra, legyen boldog vele a felhasználó.

A cikkhez tartozó forráskód letölthető innen.

 

System Center Remote Hacking Edition

A tegnapi System Center konferencia után jutott eszembe, hogy van egy csuda egyszerű módja annak, hogy az ember távoli rendszerfelügyeleti szoftverre tegyen szert, ráadásul teljesen ingyen! Sőt, a szoftverhez még egy távoli rendszergazda is jár, ő is szinte ingyen! Mindössze egy egyszerű weblapot kell készítenünk.

A lényeg az, hogy hágjunk át minden szoftver fejlesztéssel és üzemeltetéssel kapcsolatos biztonsági ajánlást és a szerverünkre készítsünk egy olyan weblapot, amellyel bárki bármit feltölthet a szerverünkre és utána futtatja is azt. (Amint megvagyunk, kezdjük el írni az önéletrajzunkat…)

Épp a múlt héten találtunk egy olyan szervert, ahol egy upload.php állományon keresztül bármilyen fájlt fel lehetett tenni a szerverre. Sőt, ha a feltöltött fájl épp .php kiterjesztésű volt, futott is vígan! Nem is kellett hozzá egy hét és máris 3 komplett rendszerfelügyeleti megoldást töltöttek fel “jóakaróink” a szerverre, lett egy g00nshell, egy c99shell és egy r57shell a gépen.

Ezek mind egyetlen 150-170 kB méretű .php fájlok annyi okossággal, hogy miután sikerült feltölteni a szerverre, tényleg bármit elvégezhetünk távolról. Az alábbi screenshotokat egy a Google segítségével talált, most is működő szerverről készítettem.

Kapunk például egy teljes remote file explorert:

c99shell_1

Navigáció a fájlrendszerben, listázás, feltöltés, letöltés, tulajdonságok megtekintése, jogosultságok kezelése, tartalom átírása, szinte Total Commander:

c99shell_13_fileinfo

Ha keresnénk valamit:

c99shell_2_commands

Nem csak wildcardos és regexes keresés van, hanem beépítetten tudja a tipikusan fontos fájlok keresését is:

c99shell_3_search

Az Encoder fülön kódolhatunk és hashelhetünk kedvünkre:

c99shell_4_encoder

A Binding fül:

c99shell_5_binding

Teljes process lista KILL-lel:

c99shell_6_processes

Van FTP szervered? Most már nekem is:

c99shell_7_ftp

A biztonságról mindent egy helyen, az /etc/passwd közvetlen letöltésével:

c99shell_8_security

Ide nekem az adatbázis szervert:

c99shell_9_sql

Tetszőleges PHP kód futtatása:

c99shell_10_exec

Meguntad a honlapod? Egy kattintás átírni mindet:

c99shell_12_deface

És hogy a funkcionalitás teljes legyen, nem csak self remove funkció van, de még feedbacket is küldhetünk a szerzőnek, angolul vagy oroszul:

c99shell_11_feedback

Ráadásként az újabb verzió még frissíteni is tudja magát! Nem beszélve a mailbomb funkcióról…

Mégegyszer: mindez szabadon letölthető az internetről, egyetlen kattintással feltölthető egy webszerverre és böngészőn keresztül ad mindent. Mindössze egy rosszul megírt feltöltő oldal és egy rosszul üzemeltetett szerver kell hozzá. Meg egy lelkes, gyakran hozzá nem értő, de kísérletező kedvű vállalkozó. Ész nem.

A fejlesztő a szokásos hibát követte el: megbízott a felhasználói inputban, semmilyen ellenőrzést nem végzett a feltöltött fájlon! Pedig a legveszélyesebb scenarioról van szó: anonymous felhasználó tölt fel adatot a szerverre. Minden input az ördögtől való!

Az üzemeltető pedig ott hibázott, hogy minden mappára adott futtatási jogot, ráadásul a futtató accountnak túl sok joga van. Ne légy admin! Minimális jogosultságot mindenkinek!

Egy egyszerű Google keresés 114 000 olyan oldalt hozott ki, amely jó eséllyel fertőzött. És ez csak egy a sokféle hacker szkript közül. Aki azt gondolja, hogy mind Linux, nagyot téved, a szkript tökéletesen fut Windowson is. Aki pedig azt gondolja, hogy csak az van bajban, aki PHP-t futtat a szerveren, az is téved. Van ugyanilyen ASP.NET-ből is, nem is egy…

 

Technorati Tags: ,,

Barátságos HTTPS átirányítás

Gyakori üzemeltetői feladat, hogy egy oldalt csak biztonságos HTTPS csatornán keresztül szeretnénk elérhetővé tenni. Sajnos nem minden üzemeltetőnek tűnik fel, hogy az is a feladat része, hogy az apró “s” betűt be nem gépelő felhasználókat barátságosan átirányítsuk a biztonságos címre: tegye fel a kezét, aki még nem látott 403.4 Forbidden: SSL is required to view this resource hibaüzenetet. Na ugye. Mennyivel szebb lenne, ha az alapértelmezett hibaüzenet helyett eljuttatnánk a felhasználót oda, ahova indult, csak éppen nem HTTP-n, hanem HTTPS-en keresztül.

A feladat megoldására számos módszer létezik, mutatok egy nagyon primitív megoldást, ami biztosan megy minden webkiszolgáló esetén. A módszer lényege, hogy felüldefiniáljuk az alapértelmezett 403.4 hibaüzenetet egy saját HTML oldallal. IIS 6 esetén például így:

Web Site Properties: Custom Errors

403.4 Custom Error Properties

A megadott sslredirect.htm fájl pedig mindössze ennyi:

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
    <html>
        <head>
            <script language="javascript">
                location.href = 'https://' + location.href.substr( 7 );
            </script>
        </head>
        <body>
        </body>
    </html>

Lehet, hogy a megoldás nem a legszebb, de egyszerűen telepíthető, sima HTML, nem kell hozzá sem ASP.NET, sem PHP, de még XML buherálás sem. Ráadásul nem csak a http://intranet, hanem a http://intranet/sites/akarmi típusú címekkel is megbírkózik, tehát SharePointhoz is tökéletes.

Ti hogyan oldjátok ezt meg?

 

Tábla típusú paraméterek használata ASP.NET-ben

Egyre gyakrabban merül fel az igény, hogy egy SQL lekérdezés egyik paramétereként azonos típusú, ám ismeretlen számú értéket kell átadnunk. Például egy terméket CheckBoxList segítségével több csoportba sorolhat a felhasználó vagy épp egy keresésnél választhatunk több kategória közül. Az SQL Server korábbi verzióinál tipikusan úgy oldottuk meg ezt a feladatot, hogy az értékeket egyetlen string változóban adtuk át, melyben XML vagy egyszerű separator karakterekkel elválasztott értékek szerepeltek. Az SQL Server 2008-tól kezdve viszont már közvetlenül adhatunk át tábla típusú paramétert is.

Egy olyan oldalt akartam készíteni, amely a Northwind adatbázisból azokat a Customereket listázza ki, akik a kiválasztott ország valamelyikében vannak:

Partnerek szűrése ország szerint

ASP.NET szinten az oldal nagyon egyszerű. Fent van egy CheckBoxList, amit egy SqlDataSource tölt fel:

    <asp:CheckBoxList ID="cblCountries" runat="server" DataSourceID="sdsCountries" 
        DataTextField="Country" />
        
    <asp:SqlDataSource ID="sdsCountries" runat="server" 
        ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" 
        SelectCommand="SELECT DISTINCT TOP 5 Country FROM Customers ORDER BY Country">
    </asp:SqlDataSource>    

Alatta található egy GridView, amit szintén egy SqlDataSource segítségével töltök fel:

    <asp:GridView ID="gvCustomers" runat="server" AutoGenerateColumns="True" 
        DataSourceID="sdsCustomers" EmptyDataText="Válasszon országot!" />

    <asp:SqlDataSource ID="sdsCustomers" runat="server" 
        ConnectionString="<%$ ConnectionStrings:NorthwindConnectionString %>" 
        SelectCommand="GetCustomersInCountries" SelectCommandType="StoredProcedure" 
        OnSelecting="sdsCustomers_Selecting">
        <SelectParameters>
            <asp:Parameter Name="SelectedCountries" />
        </SelectParameters>
    </asp:SqlDataSource>

A GridViewt a GetCustomersInCountries tárolt eljárás (lásd később) fogja feltölteni, ami egy SelectedCountries nevű paramétert vár. Ebbe szeretnénk betölteni a fenti listából kiválasztott országok neveit.

Itt rögtön újabb ékes bizonyítékát láthatjuk annak, hogy túl nagy a Microsoft: az ASP.NET csapat nem tudta, mit csinál az ADO.NET Team 🙂 Tábla típusú paraméterek használatához ugyanis egy olyan paramétert kell adnunk az SqlCommandhoz, amelyben az SqlDbType értéke SqlDbType.Structured. Az ilyen típusú paraméter értékeként pedig egy DataTable-t kell megadni, ami táblaként fog megérkezni az SQL Serverhez. Ezt az ADO.NET csapat jól kitalálta.

Azonban ASP.NET-ben a parancs paramétereinek inicializálását az SqlDataSource végzi, így neki kellene tudnunk megmondani, hogy a SelectedCountries paraméter tábla típusú. Csakhogy az asp:Parameter elemben átadható TypeCode attribútum felsorolt típusából kimaradt a Structured érték! Azaz szerintem a feladatot nem lehet deklaratívan megoldani, ami nekem személy szerint nagyon fáj 😦

Nézzük mi kell az SQL Server oldalán! Először is definiálnunk kell egy új típust. Én Itemsnek neveztem el, semmi köze nincs az országokhoz, 15 karakteres sztringekből tud akármennyit tárolni (lehetne több oszlopa is):

    -- Sajat tipus letrehozasa
    CREATE TYPE dbo.Items AS TABLE 
    (
        Item nvarchar( 15 )
    )
    GO

Ezek után létrehozhatjuk a tárolt eljárásunkat, amelynek Items típusú bemenő paramétere lesz:

    -- Tarolt eljaras letrehozasa
    CREATE PROC dbo.GetCustomersInCountries @SelectedCountries Items READONLY AS
    (
        SELECT ContactName, Country, City
        FROM Customers
        WHERE Country IN
        (
            SELECT Item FROM @SelectedCountries
        )
    )
    GO

Fontos, hogy a tábla típusú bemenő paraméter csak READONLY lehet.

Ezek után TSQL-ből ki is lehet próbálni, például így:

    -- Teszteles TSQL-bol
    DECLARE @Countries Items
    INSERT INTO @Countries ( Item )
        VALUES ( 'Argentina' ), 
               ( 'Germany' ),
               ( 'Finland' )
    SELECT * FROM @Countries
    EXEC dbo.GetCustomersInCountries @Countries

Már csak az maradt hátra, hogy CheckBoxListből kiolvassuk a beikszelt országokat és átadjuk őket a tárolt eljárásnak. Erre kiváló pillanat az SqlDataSource OnSelecting eseménye, itt ugyanis közvetlenül hozzáférünk az SqlCommandhoz:

    protected void sdsCustomers_Selecting( object sender, SqlDataSourceSelectingEventArgs e )
    {
        DataTable dt = new DataTable();
        dt.Columns.Add( "item", typeof( string ) );

        foreach( ListItem item in this.cblCountries.Items )
        {
            if( item.Selected )
            {
                dt.Rows.Add( item.Text );
            }
        }

        e.Command.Parameters[ "@SelectedCountries" ].Value = dt;
    }

Mindez akkor fog lefutni, amikor a felhasználó rákattint a Szűrés gombra:

    protected void btnFilter_Click( object sender, EventArgs e )
    {
        this.gvCustomers.DataBind();
    }

Bár SQL Server oldalon elő kell kicsit készíteni ezt a megoldást a CREATE TYPE hívásával, ami csak SQL Server 2008-on fog működni, mégis átláthatóbb, és gyanítom gyorsabb is a megoldás, mint a korábbi string összefűzős megközelítés.

A teljes forráskód letölthető innen.