background image

16

 

POCZĄTKI

HAKIN9 6/2008

P

odczas wypełniania różnorodnych formularzy 
na stronach WWW bardzo często musimy 
udowodnić, że nie jesteśmy botami. Zwykle 

służą do tego specjalnie spreparowane obrazki, z 
których przeciętny człowiek powinien bez problemu 
odczytać nieco zniekształcony tekst (Rysunek 1).

Taki rodzaj zabezpieczeń przydaje się 

szczególnie przy różnego rodzaju rejestracjach, 
np. darmowych kont e-mail, lub wypowiedziach 
na forach internetowych. Chroni on przed 
automatycznym tworzeniem kont przez automaty 
(np. roboty spamerskie). Technika ta nosi nazwę 
CAPTCHA, co jest skrótem angielskiego określenia 
Completely Automated Public Turing Test to Tell 
Computers and Humans Apart
. Luis von Ahn, 
Manuel Blum, Nicholas Hopper i John Langford z 
Carnegie Mellon University stworzyli ją na potrzeby 
Yahoo. Chcieli w ten sposób ukrócić automatyczne 
tworzenie spamerskich kont e-mail na tym portalu. 
Na początku CAPTCHA sprawdzała się wyśmienicie. 
Jednak – jak to w przyrodzie bywa – każdej akcji 
towarzyszy reakcja. Opracowanych zostało kilka 
typów ataków, które w mniejszym bądź większym 
stopniu umożliwiały obejście tego zabezpieczenia. 
Najprostszy atak polega na zatrudnieniu 
podstawionych osób, które zakładały konta e-mail 
chronione przez system CAPTCHA. W końcu osoby te 
były w stanie odczytać zniekształcony tekst. Problem 
w tym, że nikt nie będzie chciał robić czegoś takiego 
za darmo – należy za to zapłacić, co może okazać 
się mało opłacalne dla spamera. Drugi problem to 
niska efektywność rozwiązania. Inne, dosyć ciekawe 

SŁAWOMIR ORŁOWSKI

Z ARTYKUŁU 

DOWIESZ SIĘ

co to jest test CAPTCHA,

jakie są wady i zalety tego 

rozwiązania, 

jak z poziomu kodu C# 

wygenerować obrazek 

CAPTCHA,

o klasach platformy .NET 

obsługujących grafikę 2D.

CO POWINIENEŚ 

WIEDZIEĆ

znać podstawy języka C# 

i technologii ASP.NET.

rozwiązanie polega na użyciu podstawionych stron 
– najlepiej pornograficznych. Jeśli użytkownik takiego 
serwisu WWW zechce obejrzeć jakąś jego część 
(zdjęcie), musi wpisać kod z obrazka. Wystarczy, że 
obrazek zabezpieczający do takiej strony zostanie 
wklejony z formularza znajdującego się na stronie, 
którą chcemy zaatakować. Nieświadomy użytkownik 
sam rozwiąże za nas zagadkę. Jest to kolejny rodzaj 
ataku, w którym wykorzystujemy człowieka. Sprytne, 
bardziej efektywne, tańsze – ale czy wystarczająco 
skuteczne? Ludzie odpowiedzialni za projekt 
CAPTCHA twierdzą, że nie. Spamerzy twierdzą 
oczywiście, że tak. Jednak proszę sobie wyobrazić 
serwis WWW, w którym za każdym razem, kiedy 
chcemy zobaczyć coś nowego, musimy wpisywać 
jakiś kod. Wątpię, żeby taka strona przyciągała 
wielu użytkowników. Lepszym rozwiązaniem są 
programy OCR (ang. Optical Character Recognition), 
które za pomocą pewnych algorytmów potrafią 
odczytać tekst z obrazka. Oczywiście standardowe 
aplikacje OCR nie poradzą sobie z przeciętnym 
obrazkiem CAPTCHA. Potrzebne są narzędzia 
bardziej wyspecjalizowane, stworzone specjalnie na 
potrzeby łamania akurat tego typu zabezpieczeń. 
Jednym z takich projektów jest program PWNtcha 
(ang. Pretend We’re Not a Turing Computer but a 
Human Antagonist
). Potrafi on prawdopodobnie 
zdekodować tekst pochodzący ze stosunkowo 
mało skomplikowanych obrazów. Nie należy jednak 
oczekiwać, że poradzi sobie z każdym obrazkiem. 
Aby w miarę dobrze działał, należy go dostroić do 
konkretnego problemu. Napisałem prawdopodobnie

Stopień trudności

Test CAPTCHA

Jak odróżnić człowieka od automatu w Internecie? Istnieje prosty 

test, który może to sprawdzić. Wystarczy wygenerować obrazek 

ze zniekształconym tekstem i kazać użytkownikowi go odczytać.

background image

17

 

TEST CAPTCHA

HAKIN9 

6/2008

ponieważ autor PWNtcha nie udostępnia 
tego programu do testów. Jest to dosyć 
logiczne, ponieważ narzędzie mogłoby być 
użyte przez spamerów. Innym projektem jest 
aiCAPTCHA. Używa on algorytmów sztucznej 
inteligencji. Nie sądzę jednak, że zostanie 
stworzony program, który będzie sobie radził 
ze wszystkimi rodzajami obrazków CAPTCHA. 
Ostatnio na świecie pojawiło się coraz więcej 
przeciwników tego zabezpieczenia. Osoby 
niedowidzące mają poważne problemy przy 
odczytaniu tekstu. Obrazki te są również 
coraz trudniejsze do odczytania przez 
dobrze widzące osoby. Taki system wymaga 
również od użytkownika pewnej fatygi, a to 
może skutecznie zniechęcić użytkowników. 
Przecież korzystanie z Internetu powinno być 
lekkie, łatwe i przyjemne. Istnieje też sporo 
innych, darmowych i komercyjnych rozwiązań 
przeciwko botom, jednak test CAPTCHA 
nadal jest – i pewnie jeszcze długo będzie 
– wykorzystywany.

W tym artykule chciałbym zaproponować 

stworzenie własnej klasy, która będzie miała 
możliwość generowania obrazków CAPTCHA. 
Klasa będzie napisana w języku C#, w 
związku z tym będzie ją można używać w 
projektach ASP.NET. Użyjemy standardu C# 
2.0 i .NET 2.0. Program napisany będzie przy 
użyciu Visual C# 2008 Express Edition. Niech 

klasa nosi nazwę Captcha i będzie częścią 
przestrzeni nazw Hakin9. Na początku 
zadeklarujemy pola i konstruktory klasy 
(Listing 1.).

Pola 

width

 i 

height

 będą 

przechowywały odpowiednio szerokość 
i wysokość obrazka. Pole 

fontSize

 

odpowiada za wielkość czcionki. Pole 

text

 zawierać będzie tekst, jaki ma 

być umieszczony na obrazku. Pole 

random

 będzie pomocne przy generacji 

liczb pseudolosowych, potrzebnych 
do losowego dodawania pewnych 
elementów utrudniających programom 
OCR odczytanie tekstu. Pora teraz napisać 
główną metodę klasy, która będzie 
generowała obrazek (Listing 2.).

Metoda ta zwracać będzie obiekt 

typu 

Bitmap

, czyli mapę bitową. Ponieważ 

w definicji klasy zadeklarowaliśmy trzy 
konstruktory, na początku metody 

Generate

 

musimy sprawdzić, czy wszystkie wymagane 

pola są wypełnione danymi. Jeśli nie, 
generowany jest wyjątek. Dalej deklarujemy 
egzemplarz klasy 

Bitmap

, który zawierać 

będzie obrazek. Na jego podstawie budujemy 
obiekt klasy 

Graphics

, który umożliwi nam 

rysowanie. Pole, które będziemy wypełniać 
grafiką, definiujemy za pomocą klasy 

Rectangle

.

Płótno, na którym będziemy malować, 

jest już przygotowane – pora teraz na pędzel. 
Użyjemy do tego klasy 

HatchBrush

. W 

konstruktorze tej klasy przekazujemy styl, kolor 
rysowania i kolor podkładu. Zbiór styli zawiera 
typ wyliczeniowy 

HatchStyle

 – mamy 

do wyboru aż 56 rodzajów wypełnienia. 
Teraz za pomocą referencji 

g

 wypełniamy 

całość obrazka używając do tego metody 

FillRectangle

. W tym przypadku wybrany 

został styl 

SmallConfetti

, ale nic nie stoi 

na przeszkodzie, aby Czytelnik przeciążył 
metodę 

Generate

 z argumentem wywołania, 

który umożliwi własny wybór stylu. Jest to 

Rysunek 2. 

Przykładowe użycie klasy 

Captcha

Listing 1. 

Pola i konstruktory klasy Captcha

using

 

System

;

using

 

System

.

Text

;

using

 

System

.

Drawing

;

using

 

System

.

Drawing

.

Imaging

;

using

 

System

.

Drawing

.

Drawing2D

;

namespace

 

Rysunki

{

    

public

 

class

 

RPicture

    

{

        

private

 

int

 

width

;

        

private

 

int

 

height

;

        

private

 

int

 

fontSize

;

        

private

 

string

 

text

;

        

private

 

Random

 

random

;

        
        
        

public

 

RPicture

()

        

{

            

random

 

=

 

new

 

Random

();

        

}

        

public

 

RPicture

(

int

 

_width

int

 

_height

int

 

_fontSize

)

        

{

            

random

 

=

 

new

 

Random

();

            

width

 

=

 

_width

;

            

height

 

=

 

_height

;

            

fontSize

 

=

 

_fontSize

;

        

}

        

public

 

RPicture

(

int

 

_width

int

 

_height

int

 

_fontSize

string

 

_text

)

        

{

            

random

 

=

 

new

 

Random

();

            

width

 

=

 

_width

;

            

height

 

=

 

_height

;

            

fontSize

 

=

 

_fontSize

;

            

text

 

=

 

_text

;

        

}

   

}

Rysunek 1. 

Przykładowe obrazki 

zabezpieczające 

background image

18

 

POCZATKI

HAKIN9 6/2008

TEST CAPTCHA

19

 

HAKIN9 

6/2008

bardzo proste ćwiczenie, które może wykonać 
Czytelnik samodzielnie.

W kolejnym kroku przygotowujemy napis, 

który zostanie umieszczony na obrazku. Klasa 

StringFormat

 przechowuje informacje 

dotyczące formatowania napisu. Z kolei 
egzemplarz klasy 

GraphicsPath

 posłuży 

nam do umieszczenia tekstu na obrazku. 
Jest to bardzo ciekawa klasa, niezwykle 
użyteczna przy tworzeniu fragmentów 
aplikacji, które obsługują obiekty graficzne. 
Za jej pomocą możemy rysować różnorodne 
kształty, wypełniać wnętrza figur itd. Kształty, 
w tym również tekst, to skończona sekwencja 
połączonych ze sobą linii i łuków. Metodą 

AddString

 dodajemy napis do obiektu 

path

. Jej trzeci argument odpowiada za 

styl czcionki. Można wybierać pomiędzy 
kursywą, pogrubieniem, przekreśleniem i 
podkreśleniem. Jak widać na Listingu 2., style 
można ze sobą łączyć. Myślę, że pozostałe 
argumenty wywołania nie wymagają opisu.

Dla tekstu tworzymy jeszcze nowy 

pędzel. Wybrany został na stałe styl 

LargeConfetti

 – tak, jak w poprzednim 

przypadku można przeciążyć metodę 

Generate

, aby przyjmowała jako argument 

również ten styl. Tekst powinien być 
nieznacznie zaburzony. Posłuży do tego 
metoda 

Warp

 klasy 

GraphicsPath

. Jedna 

z jej wersji przyjmuje cztery argumenty. 
Pierwszym z nich są punkty definiujące 
czworościan, który będzie zawierał 
zmieniony obrazek. Drugi argument 
to obrazek źródłowy, trzeci to macierz 
przekształcenia – użyta została macierz 
zerowa. Ostatnim argumentem jest typ 
wyliczeniowy zawierający dwa sposoby 
przekształcania: 

Bilinear

 i 

Perspective

Proponuję samemu poeksperymentować 
z możliwymi transformacjami. Tutaj zostało 
użyte dosyć proste przekształcenie z 
elementami losowości.

Po tych czynnościach pozostaje nam 

tylko wyświetlić przygotowany tekst za 
pomocą metody 

FillPath

. Do narysowania 

tekstu celowo użyty został nieregularny styl 
pędzla oraz zaburzenie tekstu. Utrudni to 
programom próbującym zdekodować tekst 
odnalezienie liter. W kolejnych dwóch pętlach 
dodajemy do obrazka losowe zakłócenia. Jest 
to konieczne, ponieważ obrazek generowany 

zawsze w ten sam sposób byłby łatwy do 
oczytania przez programy, które wystarczyłoby 
odpowiednio dostosować. Pierwsza pętla 
dodaje wypełnione elipsy umieszczone 
w przypadkowych miejscach (metoda 

FillEllipse

). Za sprawą odpowiednio 

dobranych współczynników elipsy pojawią 
się na obrazku jako kropki, które nie będą 
utrudniały człowiekowi odczytania tekstu z 

Listing 2. 

Metoda generująca obrazek CAPTCHA 

public

 

Bitmap

 

Generate

()

        

{

            

if

 

(

width

 

==

 

0

 

||

 

height

 

==

 

0

 

||

 

text

 

==

 

null

)

                

throw

 

new

 

NullReferenceException

(

"Please select width, height and text 

for image"

);

            

Bitmap

 

picture

 

=

 

new

 

Bitmap

(

width

height

PixelFormat

.

Format32bppArgb

);

            

Graphics

 

g

 

=

 

Graphics

.

FromImage

(

picture

);

            

g

.

SmoothingMode

 

=

 

SmoothingMode

.

AntiAlias

;

            

Rectangle

 

r

 

=

 

new

 

Rectangle

(

0

0

width

height

);

            
            

HatchBrush

 

brush

 

=

 

new

 

HatchBrush

(

HatchStyle

.

SmallConfetti

Color

.

LightGray

Color

.

White

);

            

g

.

FillRectangle

(

brush

r

);

            

StringFormat

 

format

 

=

 

new

 

StringFormat

();

            

format

.

Alignment

 

=

 

StringAlignment

.

Center

;

            

format

.

LineAlignment

 

=

 

StringAlignment

.

Center

;

            

GraphicsPath

 

path

 

=

 

new

 

GraphicsPath

();

            

path

.

AddString

(

text

FontFamily

.

GenericSerif

(

int

)

FontStyle

.

Bold

 

+

 

(

int

)

FontStyle

.

Italic

fontSize

r

format

);

            

brush

 

=

 

new

 

HatchBrush

(

HatchStyle

.

LargeConfetti

Color

.

LightGray

Color

.

DarkGray

);

            
            

float

 

r1

 

=

 

random

.

Next

(

width

 / 

8

);

            

float

 

r2

 

=

 

random

.

Next

(

height

 / 

4

);

            

            

PointF

 

p1

 

=

 

new

 

PointF

(

0

0

);

            

PointF

 

p2

 

=

 

new

 

PointF

(

width

r2

);

            

PointF

 

p3

 

=

 

new

 

PointF

(

r1

height

);

            

PointF

 

p4

 

=

 

new

 

PointF

(

width

height

-

r2

);

            

PointF

[]

 

points

 

=

 

{

 

p1

p2

p3

p4

 

}

;

            

Matrix

 

m

 

=

 

new

 

Matrix

();

            

path

.

Warp

(

points

r

m

WarpMode

.

Perspective

);

            

            

g

.

FillPath

(

brush

path

);

            

for

 

(

int

 

i

 

=

 

0

;

 

i

 

<

 

width

 / 

3

;

 

i

++)

                

g

.

FillEllipse

(

brush

new

 

Rectangle

(

random

.

Next

(

0

width

)

random

.

Next

(

0

height

)

random

.

Next

(

1

width

 / 

20

)

random

.

Next

(

1

width

 / 

20

)));

            

for

(

int

 

i

 

=

 

0

;

 

i

<

width

/

22

;

 

i

++)

                

g

.

DrawEllipse

(

new

 

Pen

(

brush

)

(

float

)

random

.

Next

(

0

width

)

(

float

)

random

.

Next

(

0

height

)

(

float

)

random

.

Next

(

0

width

)

(

float

)

random

.

Next

(

0

height

));

            
            

return

 

picture

;

        

}

 

Listing 3. 

Strona obrazek.aspx, generująca testowy obrazek

    

protected

 

void

 

Page_Load

(

object

 

sender

EventArgs

 

e

)

    

{

        

string

 

tekst

 

=

 

"Hakin9"

;

        

Captcha

 

image

 

=

 

new

 

Captcha

(

150

50

38

tekst

);

        

image

.

Generate

()

.

Save

(

Response

.

OutputStream

System

.

Drawing

.

Imaging

.

ImageForm

at

.

Jpeg

);

        

Session

.

Add

(

"tekst"

tekst

);

    

}

Rysunek 3

. Wynik skanowania obrazka 

CAPTCHA przy pomocy FineReader

background image

18

 

POCZATKI

HAKIN9 6/2008

TEST CAPTCHA

19

 

HAKIN9 

6/2008

obrazka. Dobrym pomysłem jest również 
umieszczenie pewnych nieregularnych 
linii, które nachodzą na napis. Jest to duże 
utrudnienie dla botów, oczywiście pod 
warunkiem, że linie są rysowane losowo. To 
zabezpieczenie realizowane jest w drugiej 
pętli, która rysuje losowe elipsy z odpowiednio 
dobranymi współczynnikami. Na końcu 
metody zwracany jest cały obrazek. I to 
wszystko. Klasa do generowania obrazków 
CAPTCHA jest już gotowa. Oczywiście można 
do niej wprowadzić szereg modyfikacji. 
Najlepiej umieścić ją w projekcie aplikacji 
Windows, gdzie z łatwością możemy ją 
przetestować.

Kolejne zadanie to stworzenie testowego 

serwisu WWW, w którym klasa Captcha 
zostanie użyta praktycznie. Umieszczamy ją w 
projekcie strony ASP.NET. Przypomnę tylko, ze 
pliki klas najlepiej umieszczać w podkatalogu 
App_Code katalogu projektu. Oczywiście 
na potrzeby artykułu serwis będzie bardzo 
prosty. Będzie składał się z dwóch stron: 
Default.aspx i obrazek.aspx. Pierwsza z nich 
jest standardową stroną WWW, natomiast 
za pomocą drugiej wygenerujemy testowy 

obrazek. Obrazek zwracany będzie jako 
odpowiedź. Innym rozwiązaniem może być 
tymczasowy zapis obrazka na dysku twardym 
serwera, jednak przy sporym ruchu mogłoby 
się okazać, że na dysku brakuje miejsca do 
zapisu. Strona obrazek.aspx musi zawierać 
kod obrazka testowego. Odpowiedni kod 
umieścimy w metodzie zdarzeniowej 

Page _

Load

 (Listing 3.). Jest to jedynie przykład i 

pod zmienną 

tekst

 powinno się podstawić 

automatycznie wygenerowany ciąg o danej 
długości. W artykule Automatyczna generacja 
ciągów (Hakin9 5/2008) przedstawiłem jeden 
ze sposobów otrzymania takiego ciągu. 
Dalej generowany jest obiekt klasy 

Captcha

Wygenerowany obrazek od razu zamieniamy 
na strumień wyjściowy i pakujemy do formatu 
JPEG. Na koniec dodajemy jeszcze zmienną 
sesji, która przechowywać będzie tekst 
umieszczony na obrazku. Strona Default.aspx 
zawierać będzie jedynie cztery kontrolki: 

Image1

TextBox1

Button1

 i 

Label1

 

(Listing 4.). Pierwsza z nich wyświetlać będzie 
obrazek CAPTCHA. Jej własność 

ImageUrl

 

ustawiamy na obrazek.aspx. Po kliknięciu 
przycisku Button1 nastąpi sprawdzenie, czy 

tekst wprowadzony do kontrolki 

TextBox1

 jest 

taki sam, jak ten przechowywany w zmiennej 
sesji 

tekst

 (Listing 5.). Serwis WWW w akcji 

przedstawia Rysunek 2. Przeprowadźmy 
jeszcze prosty test wygenerowanego obrazka. 
Użyjemy do tego aplikacji FineReader 9.0 w 
wersji Professional. Jest to jeden z najbardziej 
popularnych programów typu OCR. Jedna z 
jego opcji umożliwia skanowanie obrazków 
zapisanych w najpopularniejszych formatach. 
Obrazek CAPTCHA wygenerowany za 
pomocą stworzonej klasy został zapisany na 
dysku jako mapa bitowa. Został następnie 
wczytany za pomocą programu FineReader, 
po czym dokonano próby odczytania tekstu. 
Wynik przedstawia Rysunek 3.

Program nie jest w stanie odczytać 

tekstu z obrazka. Oczywiście ten test 
należy traktować ostrożnie, ponieważ 
FineReader jest zoptymalizowany specjalnie 
do pracy biurowej. Pokazuje on jednak, 
że standardowe algorytmy OCR w tym 
przypadku zawodzą.

Problem odpowiedniego zabezpieczenia 

wszelakiego rodzaju formularzy 
rejestracyjnych przed automatami używanymi 
przez spamerów to poważna sprawa. 

Podsumowanie

W zeszłym roku ponad 97% wszystkich 
wysłanych maili to niechciana poczta. 
Automatyczne wpisy reklamowe na 
forach internetowych nie przysparzają 
ich administratorom chwały. Sondaże 
przeprowadzane w Internecie mogą paść 
ofiarą botów. Naturalnie test CAPTCHA 
nie rozwiąże tych wszystkich problemów, 
może jednak zmniejszyć ich skalę. 
Dodatkowo możemy używać odpowiednich 
filtrów. Przyznam się, że czasem mam 
problemy z prawidłowym odczytaniem 
tekstu z obrazka. Potrafi to zirytować wielu 
użytkowników. Jednak jeśli może to pomóc 
w ograniczeniu działalności spamerów, 
warto poświęcić temu kilka sekund i nieco 
wysilić swoje zmysły.

Listing 4. 

Strona Default.aspx

<%

Page

 

Language

=

"C#"

 

AutoEventWireup

=

"true"

  

CodeFile

=

"Default.aspx.cs"

 

Inherits

=

"_

Default"

 

%>

<!

DOCTYPE

 

html

 

PUBLIC

 "

-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/

xhtml1/DTD/xhtml1-transitional.dtd">

<

html

 

xmlns

=

"http://www.w3.org/1999/xhtml"

 

>

<

head

 

runat

=

"server"

>

    

<

title

>

Untitled

 

Page

<

/

title

>

<

/

head

>

<

body

>

    

<

form

 

id

=

"form1"

 

runat

=

"server"

>

    

<

div

>

        

<

asp

:

Image

 

ID

=

"Image1"

 

runat

=

"server"

 

ImageUrl

=

"obrazek.aspx"

 /

>&

nbsp

;<

br

 /

>

        

<

asp

:

TextBox

 

ID

=

"TextBox1"

 

runat

=

"server"

><

/

asp

:

TextBox

>

        

<

asp

:

Button

 

ID

=

"Button1"

 

runat

=

"server"

 

OnClick

=

"Button1_Click"

 

Text

=

"Sprawdź!"

 /

>

        

<

br

 /

>

        

<

asp

:

Label

 

ID

=

"Label1"

 

runat

=

"server"

 

BackColor

=

"White"

 

Font

-

Bold

=

"True"

 

Font

-

Size

=

"Larger"

            

Text

=

"Label"

><

/

asp

:

Label

><

/

div

>

    

<

/

form

>

<

/

body

>

<

/

html

>

 

Listing 5. 

Metoda zdarzeniowa Button1_Click

    

protected

 

void

 

Button1_Click

(

object

 

sender

EventArgs

 

e

)

    

{

        

string

 

pass

 

=

 

Session

[

"tekst"

]

.

ToString

();

        

if

 

(

TextBox1

.

Text

 

==

 

pass

)

            

Label1

.

Text

 

=

 

"Nie jesteś botem !"

;

        

else

            

Label1

.

Text

 

=

 

"Jesteś botem !!!"

;

    

}

Sławomir Orłowski

Z wykształcenia fizyk. Obecnie jest doktorantem na 

Wydziale Fizyki, Astronomii i Informatyki Stosowanej 

Uniwersytetu Mikołaja Kopernika w Toruniu. Zajmuje się 

symulacjami komputerowymi układów biologicznych 

(dynamika molekularna) oraz bioinformatyką. 

Programowanie jest nieodzowną częścią jego 

pracy naukowej i dydaktycznej. Ma doświadczenie w 

programowaniu w językach C, C++, Delphi, Fortran, Java, 

C# i Tcl. Współzałożyciel i koordynator grupy .NET WFAiIS. 

Jest autorem artykułów i książek informatycznych. 

Strona domowa: http://www.fizyka.umk.pl/~bigman/.

Kontakt z autorem: bigman@fizyka.umk.pl