Jak programować w Linuksie

Rus3k
Ten artykuł wymaga dopracowania!

Jeżeli możesz popraw ten artykuł według zaleceń, które możesz znaleźć na stronie [[Artykuły do poprawy]]. Po dopracowaniu tego tekstu można usunąć ten komunikat.

Wymagania ogólne:
*mózg
*trochę wolnego czasu i chęci

Wymagania szczegółowe:
*znajomość języka C
*znajomość systemu Linux

Wymagania techniczne:
*Linux + biblioteki (development)

1 Wprowadzenie
2 Funkcje systemowe i biblioteczne: porównanie.
     2.1 Kompilacja
3 Inne funkcje systemowe.
     3.2 4.1. Funkcje wyświetlające.
     3.3 4.2. Funkcje pobierające.
4 Katalogi i pliki
     4.4 Pliki
     4.5 Katalogi
5 Krótko o procesach.
6 Programowanie sieciowe
7 telnet host 110

Wprowadzenie

Systemy Unix (Linux) są w ogromnym stopniu oparte na systemie plików, prawie wszystko może być potraktowane jako plik. Wystarczy zaglądnąć do katalogu /dev. Znajdują się tam plikowe reprezentacje urządzeń fizycznych. Np. żeby zamontować sobie dysk C:, na którym masz DOS-a wpisujesz:
mount /dev/hda1 /mnt/c
o czym jako użytkownik Linuksa na pewno wiesz. Pamięć również jest traktowana jest jako plik w tym systemie. Jako plik traktuje się również gniazdka sieciowe, a więc za pomocą funkcji systemowych pisze się do nich i czyta jak do i z plików. Z tego względu umiejętność programowania w Unixie, to umiejętność posługiwania się funkcjami obsługującymi pliki. Oczywiście to nie wszysko (dobrze by było ;). Z drugiej jednak strony budowa systemów Unixowych jest dosyć prosta i przejrzysta (of kors na odpowiednim poziomie).

Tutorial ten ma Ci pokazać podstawy programowania w Linuksie. Aby osiągnąć więcej trzeba pisać i czytać kod, którego jest mnóstwo w sieci. Mam nadzieję, że gdy zobaczysz jakiś exploit, to będziesz mniej więcej rozumiał o co w nim chodzi i nie będziesz się nim bezmyślnie posługiwał.

Funkcje systemowe i biblioteczne: porównanie.

Pliki w systemie Linux można na poziomie programowania obsługiwać na dwa sposoby: za pomocą funkcji systemowych oraz bibliotecznych, przy czym funkcji bibliotecznych jest więcej i są one bardziej przyjazne.

Funkcje systemowe to takie funkcje, które wykorzystują gotową obsługę udostępnioną przez jądro systemu. Jądro jest cały czas podczas działania systemu w pamięci i kontroluje wszystkie procesy i każdy dostęp do systemu plików. Również funkcje biblioteczne używają funkcji systemowych.

Podstawowa różnica między tymi dwoma sposobami leży w kodzie samego programu. Kiedy program korzysta z funkcji systemowych, sam kod nie jest dołączany do programu - program zleca to zadanie dla jądra (jest to zwykle wykonywane za pomocą przerwań programowych). W przypadku biblioteki kod w niej zawarty jest dołączany do programu.

Dlaczego zatem używa się funkcji bibliotecznych? Przede wszystkim funkcje te są bardziej przyjazne - nie trzeba na przykład przejmować się obsługą buforów, o wiele trudniej jest się pomylić. Funkcje biblioteczne są też bardziej rozbudowane.

Poniżej znajdują się przykłady prostych programów, które wykonują te same zadania wykorzystując funkcje systemowe lub biblioteczne.

system.c

/*wykorzytanie funkcji systemowych do operacji na pliku*/
/*program liczy znaki w pliku 'plik' Ilosc znakow to rownoczesnie 
  wielkosc pliku w bajtach*/

#include <stdlib.h> //wykorzystanie dla funkcji exit()
#include <fcntl.h>

#define BUFROZ 512
main()
{
    int desp;		//deskryptor pliku
    char buf[BUFROZ];
    ssize_t czytaj;
    long licz_zn; 
    /*
    */
    if((desp = open("plik", O_RDONLY)) == -1) //w wypadku błędu funkcja
						//open zwraca -1
    {
	printf("Nie moge otworzyc pliku 'plik'n");
	exit(1);
    }
    
    while((czytaj = read(desp, buf, BUFROZ)) >0)
	licz_zn+=czytaj;
    
    printf("Ilosc znakow w pliku wynosi : %ldn", licz_zn);
    
    close(desp);
    
    exit(0);	
}

Jak widzimy program użył kilku funkcji systemowych: open, read, close.


Funkcja open:

int open(const char *ścieżka, int flagi, [mode_t mode]);

służy do otwierania pliku. Drugi argument tej funkcji jest typu int. Wykorzystujemy tu odpowiednie flagi (w naszym przypadku O_RDONLY), które są zdefiniowane w pliku nagłówkowym < fcntl.h >:

O_RDONLY - otwórz tylko do odczytu
O_WRONLY - otwórz tylko do zapisu
O_RDWR - otwórz do odczytu i zapisu

Trzeci argument [mode_t mode] jest używany jedynie z flagą O_CREAT oznaczający utworzenie pliku, natomiast argument [mode_t mode] będzie ustawiał prawa dostępu (np. 0622).


Funkcja read:

ssize_t read(desp, buf, BUFROZ);

służy do czytania z otworzonego pliku. Argumenty:
desp - deskryptor pliku uzyskany podczas wywołania funkcji open
buf - bufor, który będzie zawierał odczytane z pliku dane
BUFROZ - rozmiar bufora (w jakich porcjach będzie funkcja read czytać)

Zaleca się używanie BUFROZ równego wielokrotności 512. Jeżeli funkcja zwróci wartość ssize_t <0 oznacza to koniec pliku. (-1 - EOF)


Funkcja close:

close(desp);

służy do zamknięcia pliku poprzez zwolnienie deskryptora.

proced.c

/*wykorzystanie procedur bibliotecznych*/
/*program liczy znaki w 'plik'*/

#include <stdio.h> //biblioteka I/O 
#include <stdlib.h>

main()
{
    FILE *p;
    int c;
    long licz_zn = 0;
    
    if ((p = fopen("plik", "r")) == NULL)
    {
	printf("Nie moge otworzyc pliku 'plik'n");
	exit(1);
    }
    
    while ((c = getc(p)) != EOF)
	{
	    licz_zn+=1;
	}    
	
    printf("Ilosc znakow w pliku wynosi : %ldn", licz_zn);	
    
    fclose(p);
    
    exit(0);	
}

Funkcja fopen i fclose:

	FILE* fopen(const char *nazwa_pliku, const char *typ);
	int fclose(FILE* stream);

Plik jest identyfikowany za pomocą wskaźnika do struktury FILE.
typ:
"r" - tylko do odczytu
"w" - tylko do zapisu (jesli nie istnieje utwórz)
"a" - do odczytu, dane będą dodawane na końcu pliku
"r+"- do odczytu i zapisu
"w+"- do odczytu i zapisu (jesli nie istnieje utwórz)
"a+" - do odczytu i zapisu, dane będą dodawane na końcu pliku


Funkcja getc:

	int getc(FILE *p);

pobiera numer znaku z pliku opisanego przez FILE

system2.c

/*   kopiuje plik1 do plik2  używając funkcji systemowych*/

#include <unistd.h>
#include <fcntl.h>

#define BUFSIZE 512
#define PERM	0644

/* funkcja kopiujaca */
int copyfile(const char *plik1, const char *plik2)
{
    int infile, outfile;
    ssize_t nread;
    char buffer[BUFSIZE];
    
    if((infile = open(plik1, O_RDONLY)) == -1)
	return(-1);
    
    if((outfile = open(plik2, O_WRONLY|O_CREAT|O_TRUNC, PERM)) == -1)
    {
	close(infile);
	return(-2);
    }	
    
    /*teraz czytaj z plik1 po BUFSIZE znakow naraz*/
    while((nread = read(infile, buffer, BUFSIZE))>0)
    {
	/*zapisz bufor do ploku wyjsciowego*/
	write(outfile, buffer, nread);
    }
    close(infile);
    close(outfile);
    
}
/////////////////////////////
int main(int argc, char** argv)
{
    if (argc<3)
    {
     printf("Uzyj: %s plik_zrodlo plik_celn",argv[0]);
     exit(1);
    }
    
    copyfile(argv[1],argv[2]);
}

Program używa znanych już funkcji (oprócz write) do skopiowania jednego pliku do drugiego.


Funkcja write:

ssize_t write(int desp, const char buffer, ssize_t nread);

służy do zapisu danych do pliku o deskryptorze desp, danych zawartych w buffer w liczbie ssize_t nread.


Flaga O_TRUNC razem z O_CREAT sprawia, że jeśli plik już istnieje zostanie on obcięty do zerowej długości. Oczywiście jeśli nie chcemy stracić ważnych danych powinniśmy użyć razem O_CREAT|O_EXCL wtedy funkcja open zwróci błąd jeżeli już taki plik istnieje.

Jeszcze małe wyjaśnienie funkcji int main(int argc, char** argv):
int argc - to liczba argumentów podanych w linii poleceń, w naszym przypadku musimy podać ich trzy: nazwa_programu plik_zrodlo plik_cel
char** argv - jest to tablica wskaźników pokazujących na nasze argumenty podane w linii poleceń podczas uruchamiania skompilowanego programu (np. argv[0] wskazuje na nazwę naszego programu).

proced2.c

/*program wykorzystauje procedury biblioteczne*/
/*
program kopiuje plik1 do plik2
*/

#include <stdio.h>
#include <stdlib.h>

/*funkcja kopiujaca*/
int copyfile(const char *plik1, const char *plik2)
{
    FILE *in, *out;
    int c;
    
    if ((in = fopen(plik1,"r")) == NULL)
	return(-1);
    
    if ((out = fopen(plik2,"w")) == NULL)
    {
	fclose(in);
	return(-2);
    }		
    
    while((c = getc(in)) != EOF)
	putc(c, out);
	
    fclose(in);
    fclose(out);	
}
////////////////////////////////
 int main(int argc, char** argv)
 {
     if(argc<3)
     {
         printf("Uzyj: %s plik_zrodlo plik_celn",argv[0]);
	 exit(1);
     }
     
     copyfile(argv[1],argv[2]);
 }

Funkcja putc:

int putc(int c, FILE* out);

pisze do out -opisany przez FILE- znak c.


Kompilacja

W Linuksie kompilację wykonuje się w następujący sposób:
gcc [opcje] plik.c
Zajmijmy się trochę opcjami.

Kompilacja gcc plik.c automatycznie tworzy plik binarny o nazwie a.out. Jeżeli chcemy aby plik wyjściowy miał konkretną nazwę używamy opcji -o.
gcc -osystem system.c
w ten sposób kompilator utworzy plik system gotowy do uruchomienia. Inną ciekawą opcją jest -S - kompilator tworzy plik wyjściowy, a raczej przejściowy, w asemblerze. Po wpisaniu polecenia
gcc -S system.c
kompilator utworzy plik system.s. Dla naszego przykładu wygląda on tak:

.file	"system.c"
	.version	"01.01"
gcc2_compiled.:
.section	.rodata
.LC0:
	.string	"plik"
	.align 32
.LC1:
	.string	"Nie moge otworzyc pliku 'plik'n"
	.align 32
.LC2:
	.string	"Ilosc znakow w pliku wynosi : %ldn"
.text
.align 4
.globl main
	.type	 main,@function
main:
	pushl %ebp
	movl %esp,%ebp
	subl $512,%esp
	pushl %edi
	pushl %esi
	pushl %ebx
	pushl $0
	pushl $.LC0
	call open
	movl %eax,%esi
	addl $8,%esp
	cmpl $-1,%esi
	jne .L17
	pushl $.LC1
	call printf
	pushl $1
	call exit
	.p2align 4,,7
.L17:
	leal -512(%ebp),%ebx
	jmp .L18
	.p2align 4,,7
.L20:
	addl %eax,%edi
.L18:
	pushl $512
	pushl %ebx
	pushl %esi
	call read
	addl $12,%esp
	testl %eax,%eax
	jg .L20
	pushl %edi
	pushl $.LC2
	call printf
	pushl %esi
	call close
	pushl $0
	call exit
.Lfe1:
	.size	 main,.Lfe1-main
	.ident	"GCC: (GNU) egcs-2.91.66 19990314/Linux (egcs-1.1.2 release)"

Jak widać polecenia asemblerowe różnią się nieco od tych znanych z kompilatrów dosowych. W Linuksie dodajemy do większości poleceń literę "l" np.
movl %eax,%esi -kopiuj wartość z rej. esi do eax
Litery "l" nie dodaje się do polecenia call (chyba wiadomo dlaczego) i do funkcji skoków.

W samym kodzie źródłowym polecenia asemblerowe trzeba poprzedzić odpowiednią dyrektywą np.
asm("movl %esp,%eax");
asm wstawiamy przed każdym poleceniem lub

 __asm__("clcn"
                "1:t"
                "lodslnt"
                "adcl %%eax, %%ebxnt"
                "loop 1bnt"
                "adcl $0, %%ebxnt"
                "movl %%ebx, %%eaxnt"
                "shrl $16, %%eaxnt"
                "addw %%ax, %%bxnt"
                "adcw $0, %%bx");

asm używamy do zgrupowania naszych asemblerowych poleceń. Oczywiście mam świadomość, że asemblera będziesz używać bardzo rzadko lub wcale, jednak jest on dość często używany w różnych exploitach. Programowanie na niskim poziomie daje możliwość pełnej kontroli nad komputerem, ale jest stosunkowo trudne.

Chciałbym przedstawić jeszcze jeden argument wydawany kompilatorowi. Jest to argument optymalizacji -O[numer_optymalizacji]. Generalnie preferuje się optymalizację na poziomie 2, zwiększanie jego poziomu daje często odwrotne do zamieżonych rezultaty. Przykład:

gcc -osystem -O2 system.c

W ten sposób nasz kompilator spróbuje poprawić szybkość i zmniejszyć wielkość programu wynikowego. Aby sprawdzić w jaki sposób to robi można kompilować z opcją -S przed optymalizacją i po niej, a póżniej porównywać kod przejściowy.

Inne funkcje systemowe.

Oprócz funkcji open z flagą O_CREAT do tworzenia pliku można wykorzystać funkcję systemową creat.

Funkcja creat:

int creat(const *ścieżka, mode_t mode);

np.

	int desp;
	desp = creat("/home/reksio/plik", 0622);

Do zmiany pozycji wskaźnika odczytu-zapisu wykorzystujemu funkcję
systemową lseek.

Funkcja lseek:

off_t lseek(int desp, off_t offset, int pozycja_poczatkowa);

Do programu należy dołączyć plik nagłówkowy <unistd.h>.
Przykład użycia:

	off_t pozycja;
	int desp;
	desp = open("plik", O_RDONLY);	
	pozycja = lseek(desp, (off_t)+10, SEEK_SET);

Pozycja wskaźnika jest w tym przypadku 10 bajtem liczonym od początku pliku.

Argument [int pozycja_poczatkowa] może przybierać następujące wartości:
SEEK_SET - początek pliku
SEEK_CUR - bieżace położenie
SEEK_END - koniec pliku


Za pomocą funkcji lseek można w łatwy sposób sprawdzić wielkość pliku np.

	int desp;
	off_t w_pliku;
	desp = open("plik", O_RDONLY);
	w_pliku = lseek(desp, (off_t)0, SEEK_END);

Do obsługi błędów można użyć pliku nagłówkowego <errno.h>, która za pomocą zmiennej errno przekazuje numer błędu, który z kolei jest powiązany z komunikatami o błędach. Aby użyć świadomie tego numeru trzeba znać listę kodów błędów. Na szczęście Linux dostarcza funkcję biblioteczną perror, która pobiera kod błędu i pokazuje odpowiedni komunikat (jeżeli twoje ustawienia lokalizacyjne są konfigurowane na język polski, komunikaty o błędach będą również po polsku).

np.

#include <fcntl.h>
#include <errno.h>
main()
{
	int desp;

	if ((desp = open("plik", O_RDONLY)) == -1)
		perror("Blad podczas otwarcia pliku");
}

Dzięki funkcji perror w razie błędu (-1) zostanie pobrany odpowiedni kod błędu i wyświetlony czytelny komunikat. W naszym przypadku w razie nieistnienia pliku "plik" w bieżącym katalogu zostanie wyświetlony komunikat:

Blad podczas otwarcia pliku: Nie ma takiego pliku lub katalogu

Do usuwania plików można zastosować dwie metody: unlink i remove. Remove korzysta z stdio.h

Funkcja unlink i remove:

	
	#include <unistd.h>
	int unlink(const char *ścieżka)

	#include <stdio.h>
	int remove(const char *ścieżka)

  1. Procedury biblioteczne {<stdio.h>}.
    =============

Plik stdio.h jest również nazywany Standardową Biblioteką I/O. Głównym zadaniem tej biblioteki jest ułatwienie dostępu do pliku oraz zapewnienie efektywnej operacji wejścia/wyjścia. Część funkcji była już pokazana wcześniej: fopen, fclose, getc, putc. Do SB I/O należą również funkcje:printf, scanf, getchar. Są jedne z częściej używanych funkcji służących do komunikacji z użytkownikiem.

4.1. Funkcje wyświetlające.

Funkcja printf:

int printf(const char *kom, arg1, arg2....arg3);

wyświetla ciąg znaków zawarty w między " ". Dodatkowow można wysyłać tzw. conversions specifications. Np.

	
	pi = 3.1415926
	printf("tLiczba pi wynosi: %fn", pi);

po kolei:
t - znak tabulacji przesuwa ciąg wyświetlanych znaków
%f - wyświetli wartość typu float ze zmiennej pi
n - przejście do następnej linii

a więc:

Liczba pi wynosi: 3.1415926

Do wyświetlania znaku % trzeba użyć %%.
Po znaku % używa się róznych liter w celu wyświetlenia zmiennych różnych typów, a więc:

%d - typu int
%u - unsigned int
%o - typ int ósemkowo
%x - typ int szesnastkowo
%X - typ int szesnastkowo ale z dużymi literami ABCDEF
%ld - typ long

%f - typ float lub double
%e - typ float lub double w formie naukowej(wykładniczej)
%g - w zależności od wielkości wyświetli tak jak %f lub %e

%c - typu char
%s - wskaźnik znakowy (napis)


Funkcja fprintf:

int fprintf(FILE *plik, const char *kom, arg1, arg2...arg3);

służy do wyświetlania komunikatów o błędach.

Za każdym razem gdy uruchamiany jest program Linuks otwiera trzy pliki:
stdin - standardowe wejście(klawiatura)
stdout - standardowe wyjście(ekran)
stderr - standardowe wyjście komunikatów o błędach
Wszystkie są ponumerowane odpowiednio 0, 1, 2. Z reguły używa się ich znakowych odpowiedników.

Rozgraniczenie stdout i stderr wynika z tego, że nie zawsze jest potrzeba wysyłania błędów na ekran, równie dobrze można odpowiednie komunikaty wysyłać do pliku.

Funkcja fprintf jest prawie identyczna jak printf, jedynie pierwszy argument zawiera stderr. Np.

fprintf(stderr, "BLAD nr %d w %s", arg1, arg2);

Funkcja sprintf:

int sprintf(char *string, const char *kom, arg1, arg2...argn);

nie jest typową funkcją wypisującą, gdyż wyprowadza komunikat do tablicy
znakowej string. Np.

	  char prmpt[81];
	  sprintf(prmpt,"%s ",ProgName);

Poniższy program pokazuje parametry środowiska oraz argumenty wpisane w linii poleceń:

#include <stdio.h>
#include <stdlib.h>
    
int i=0;
main(int argc, char* argv[], char* env[])
{
    printf("Parametry srodowiska Linux: n");
    do {
	printf("%s n", env[i]);
	i++;
    }
    while (env[i]!=NULL);
	    printf("Lista parametrow programu: n");

   for(i=1;i<=argc-1;i++) printf("%s n", argv[i]);
	    
    printf("Nazwa programu: n");
    printf("%s n", argv[0]);
    return 0;
}

4.2. Funkcje pobierające.

Są to funkcje odwrotne do funkcji wypisujących. Pobierają one z klawiatury lub z pliku i zapisują do zmiennej odpowiedniego typu.


Funkcja scanf:

int scanf(const char *napis, &zmienna1, &zmienna2...);

pobiera ze standardowego wejścia (stdin) i zapisuje w zmiennej. Znak & (ampresand) to adres w pamięci, gdzie znajduje się zmienna. Np.

	int a, b;
	float c;

	scanf("%2d %2d %f", &a, &b, &c);

Funkcja fscanf:

int fscanf(FILE *wskaz, const char *napis, &zmienna1, &zmienna...);

czyta z pliku wskazywanego przez wskaz


Funkcja sscanf:

int sscanf(const char *string, const char *napis, &zmienna1...);

pobiera dane z tablicy string i wpisuje do zmiennych.


Inną często używaną funkcją ze Standardowej Biblioteki I/O jest getchar.
Czyta ona jeden znak ze standardowego wejścia programu (zwykle klawiatura)

getchar:

int getchar(void);

Np.

	int z;

	z = getchar();
	if (z==10) printf("Nacisnales ENTER");

Poniższy przykład usuwa plik w argumencie podanym jako ścieżka, program czeka na potwierdzenie operacji.

/*del --program do kasowania pliku*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

FILE *p;
char *uzycie = "Uzycie: del sciezka_do_plikun";
char c[0];

int usun(const char *plik)
{
	/*sprawdzanie za pomoca funkcji open czy plik istnieje*/	
	if ((p = fopen(plik,"r")) == NULL)	
	{		
		fprintf(stderr, "Plik %s nie istniejen",plik);		
		return(-1);	
	}	

	fclose(p);	

	printf("Czy mam na pewno usunac %s? T/Nn", plik);		
	scanf("%s", c);	

	if(c[0] == 't' || c[0] == 'T')	
	{		
		remove(plik);		
		printf("Plik usunietyn");	
		return(0);
	}

//	printf("%sn",c);
	printf("Anulowano...n");

}

int main(int argc, char** argv)
{
	if (argc<2)	
	{		
		fprintf(stderr, uzycie);		
		exit(1);	
	}	

	usun(argv[1]);
	exit(0);
}

Istnieje oczywiście większa liczba funkcji SB I/O. Jeżeli jesteś ich ciekawy to zaglądnij do stdio.h (/usr/include).

Katalogi i pliki

Pliki

Katalogi i pliki to podstawa każdego systemu plików. W Unixach plik traktowany jest w specjalny sposób. Oprócz tego każdy plik jest czyjąś własnością, a to daje pewne przywileje jego właścicielowi, który może z tym plikiem zrobić co zechce. Przede wszystkim właściciel pliku może nadawać odpowiednie uprawnienia według własnego uznania. Uprawnienia określają prawa dostępu dla różnych użytkowników. Istnieją ich trzy typy:
*właściciel
*grupa do której należy właściciel
*pozostali użytkownicy.

Prawa dostępu to:
*czytanie z pliku (4)
*pisanie do niego (2)
*wykonywanie (jeżeli jest to plik wykonywalny) (1).

System przechowuje o każdym pliku dokładne dane, które można podglądnąć używając funkcji stat zawartej w <sys/stat.h>. Najczęściej uprawnienia ustawia się za pomocą zapisu ósemkowego np.

0777 - każdy użytkownik może czytać, pisać i wykonywać plik(4+2+1),
0644 - właściciel może czytać i pisać, osoby należące do tej samej grupy
mogą czytać, czytać mogą pozostali użytkownicy,

Oprócz tego możemy plikom nadać dodatkowe uprawnienia:

04000 - sławny SUID, czyli chwilowe uprawnienia właścieiela są nadawane
każdemu, kto jest właścicielem procesu. Jeżeli właścicielem jest
root w trakcie działania programu z suidem kazdy ma uprawnienia
root'a
02000 - SGID - to samo co SUID tylko dla grupy
01000 - SVTX - obecnie wykorzystywany tylko dla katalogów (bit cachujący)

Do sprawdzenia, czy plik jest dostępny używa się funkcji access


Funkcja access:

	#include <unistd.h>
	int access(const char *ścieżka, int mode);

Argument [int mode]:
R_OK - czy proces może czytać?
W_OK - czy proces może pisać?
X_OK - czy proces może wykonać plik?

W razie błędu funkcja zwraca wartość -1

/*program sprawdza czy jest możliwy dostęp do pliku*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char** argv)
{
	if (argc<2)
	{
		printf("Uzycie: %s plikn", argv[0]);
		exit(1);
	}

	if(access(argv[1], R_OK) == -1)
		fprintf(stderr, "Nie mozesz czytac z pliku %sn", argv[1]);

	printf("Czytanie z pliku dozwolonen");

	if(access(argv[1], W_OK) == -1)
		fprintf(stderr, "Nie mozesz pisać do pliku %sn", argv[1]);

	printf("Pisanie do pliku dozwolonen");

	if(access(argv[1], X_OK) == -1)
		fprintf(stderr, "Nie mozesz uruchamiać pliku %sn", argv[1]);

	printf("Uruchamianie pliku dozwolonen");

	exit(0);

}

Do zmiany uprawnień dla pliku służy funkcja chmod (podobnie jak z linii
poleceń).

Funkcja chmod:

#inlcude <sys/types.h>
#include <sys/stat.h>
int chmod(const char *ścieżka, mode_t nowy);

Funkcja ta może być używana przez właściciela pliku lub administratora.

	if(chmod("/etc/passwd",0777) == -1)
		perror("");

Do zmiany właściciela pliku oraz grupy służy funkcja chown.

Funkcja chown:

	#include <sys/types.h>
	#include <unistd.h>

	int chown(const char *ścieżka, uid_t id_wlaściciela, gid_t id_grupy);

np.

	int wartosc
	if(wartosc = chown("/etc/passwd", 44, 7) == -1)
		perror("");

Możliwa jest przekazanie komuś właśności pliku. Taka operacja nie może być anulowana. Chroni to przed kradzieżą przywilejów.


Za pomocą funkcji link można utworzyć do pliku tzw. twarde łącze. Utworzenie twardego łącza sprawia, że tzw. licznik łączy jest zwiększony. Dopiero usunięcie wszystkich twardych łączy oznacza pełne usunięcie pliku z systemu (funkcja unlink). Licznik łaczy powinien dla tego pliku wynosić 0. Funkcja link nie tworzy kopii pliku, sprawia, że z innego miejsca mamy łączność z plikiem macierzystym. Łącze twarde jest swoistym wskaźnikiem do pliku.

Funkcja link:

#include <unistd.h>
int link(const char *ścieżka_do_pliku_macierzystego, const char *nowa);

np.

link("/home/neo/matrix", "/home/cartman/platrix");

W razie błędu funkcja zwraca -1.


Jak już wcześniej wspomniałem istnieje metoda poznania wszystkich własności
pliku, które zawiera system plików.


Funkcja stat:

#include <sys/types.h>
#inlcude <sys/stat.h>
int stat(const char *ścieżka, struct stat *statystyka);

Strukturę stat najlepiej poznać w praktyce. Oto kod wykorzystujący stat:

/*program pokazuje strukture stat dla podanego pliku*/

#include <stdio.h>
#include <sys/stat.h>

int main(int argc, char** argv)
{
    struct stat statystyka;
    
    if(argc<2)
    {
	printf("Uzycie: %s plikn",argv[0]);
	exit(1);
    }
    
	if(stat(argv[1], &statystyka) == -1)
	{	
		perror("");
		return(-1);    
	}
    
    printf("Dane dla %s odczytane za pomoca funkcji 'stat' :n", argv[1]);
    printf("Urzadzenie logiczne (st_dev)   : %dn", statystyka.st_dev);
    printf("Numer i-wezla (st_ino)   : %dn", statystyka.st_ino);
    printf("Uprawnienia (st_mode)  : %dn", statystyka.st_mode);
    printf("Liczba laczy (st_nlink) : %dn", statystyka.st_nlink);
    printf(" UID (st_uid) : %dn", statystyka.st_uid);
    printf(" GID (st_gid) : %dn", statystyka.st_gid);
    printf("Opis urzadzenia (st_rdev) : %dn", statystyka.st_rdev);
    printf("Wielkosc pliku (st_size) : %ldn", statystyka.st_size);   
    printf("Ostatni odczyt (st_atime) : %dn", statystyka.st_atime);   
    printf("Ostatnia modyfikacja (st_mtime) : %dn", statystyka.st_mtime);   
    printf("Ostatnia zmiana w 'stat' (st_ctime) : %dn", statystyka.st_ctime);   
    printf("Wielkosc bloku I/O (st_blksize) : %ldn", statystyka.st_blksize);   
    printf("Ilosc fiz. blokow dla pliku (st_blocks) : %ldn", statystyka.st_blocks);   
    
}

*st_dev i st_ino jednoznacznie identyfikują plik, st_dev jest numerem
urządzenia logicznego, na którym znajduje się plik (numer ten można
również poznać przeglądając /proc/sys/kernel/real-root-dev), st_ino to numer i-węzła,
i-węzeł opisuje plik (katalog) w hierarchicznej strukturze systemu
plików (drzewo katalogów)

*st_mode opisuje uprawnienia do pliku (12 najmłodszych bitów), można
również dowiedzieć się jakie urządzenie reprezentuje plik - 060000 to
urządzenie blokowe, 020000 to urządzenie znakowe

*st_nlink pokazuje ilość łączy dla pliku

  • st_uid i st_gid pkazują numer id użytkownika i grupy

*st_rdev pokazuje numer urządzenia nadrzędengo i podrzędnego, numery
te są potrzebne do jednoznacznej identyfikacji sterownika, z którego
dane uzrządzenie korzysta. Numery nadrzędne przydzielone statycznie
są pokazane w /proc/devices. Lepszym sposobem na poznanie tych numerów
jest polecenie ls -l np. w katalogu /dev:

brw-rw----   1 root     disk       3,   1 May  5  1998 hda1
brw-rw----   1 root     disk       3,  10 May  5  1998 hda10
brw-rw----   1 root     disk       3,  11 May  5  1998 hda11
brw-rw----   1 root     disk       3,  12 May  5  1998 hda12
brw-rw----   1 root     disk       3,  13 May  5  1998 hda13
brw-rw----   1 root     disk       3,  14 May  5  1998 hda14
brw-rw----   1 root     disk       3,  15 May  5  1998 hda15
brw-rw----   1 root     disk       3,  16 May  5  1998 hda16
brw-rw----   1 root     disk       3,   2 May  5  1998 hda2
brw-rw----   1 root     disk       3,  64 May  5  1998 hdb
brw-rw----   1 root     disk       3,  65 May  5  1998 hdb1
brw-rw----   1 root     disk       3,  74 May  5  1998 hdb10
brw-rw----   1 root     disk       3,  75 May  5  1998 hdb11

litera b oznacza, że jest to urządzenie blokowe, numer 3 oznacza, że
wszystkie pokazane urządzenia są obsługiwane przez
ten sam sterownik, numer podrzędny pozwala sterownikowi jednoznaczenie
rozumieć, które urządzenie ma być rzeczywiście obsługiwane

*st_size to jedno z ważniejszych z punktu widzenia użytkownika pole,
jest wielkością jaką zajmuje plik

*st_atime, st_mtime, st_ctime to w kolejności: czas ostatniego odczyty,
czas ostatniej modyfikacji, czas ostatniej zmiany informacji w strukturze
stat

*st_blksize to wielkość bloku I/O w systemie plików, w moim przypadku
wartość 4k (dysk podzielony na 4 kilowe klastry)

*st_blocks jest liczbą bloków jaką zajmuje fizycznie plik, wilekość nie pokrywa
się z rzeczywistą wielkością pliku (jeżeli wielkość pliku = 10k, to plik
zajmuje 3 bloki fizyczne czyli 12k). Im mniejsza wielkość bloku I/O w
systemie plików, tym mniejsze straty pojemności dysku.

Katalogi

Katalogi są w systemach Unix traktowane prawie tak samo jak pliki. Jedyna,
ale bardzo ważna różnica to hierarchia jaką tworzą katalogi. Katalogi
porządkują system plików tworząc drzewo katalogów. Katalog jest kontenerem
dla plików. Katalogami są również . i .. - są to łącza do bieżącego katalogu
i jego przodka. Katalog / jest korzeniem. (wypróbuj polecenie ls -ld)

Każdy katalog jest identyfikowany za pomocą tzw. i-węzła np:

			 /[1]
			 ..[1]
			 |
			home[4]
			/  
		         ..[4]
		    [5]neo  cartman[6]	

Dla przykładu korzeń będzie miał i-węzeł [1], home[4] itd. Katalog .. w neo i cartman ma ten sam i-węzeł co home, podobnie .. i /.

Podobnie jak pliki również katalogi posiadają swych właścicieli jednak prawa dostępu interpretuje się w nieco inny sposób:

*prawo odczytu oznacza możliwość wyświetlenia zawartości katalogu
*prawo zapisu umożliwia utworzenie i usunięcie plików w katalogu (oczywiście dostęp do już istniejących plików zależy od uprawnień na poziomie pliku)
*prawo wykonywania pozwala wejść do katalogu za pomocą polecenia cd

Do obsługi katalogów na poziomie programowania istnieje oddzielna rodzina
funkcji.

Tworzenie i usuwanie katalogów:

Funkcja mkdir i rmdir:

#include <sys/types.h>
#include <sys/stat.h>

int mkdir(const char *ścieżka, mode_t mode);

np:

int wartosc;
wartosc = mkdir("/home/uzyt1/kat1", 0644);

#include <unistd.h>

int rmdir(const char *ścieżka);

Funkcja usuwania zadziała tylko wtedy, gdy katalog będzie pusty tzn. będzie
zawierał (.) i (..).


Otwieranie i zamykanie katalogów:


Funkcje opendir i closedir:

#include <sys/types.h>
#include <dirent.h>

DIR *opendir(const char *nazwa_katalogu);

<dirent.h> zawiera definicję DIR (strumień katalogu). Porównaj z FILE.
W razie błędu funkcja zwraca NULL.

#include <dirent.h>

int closedir(DIR *wskanik_dir);

Np.

DIR *wks_dir;

if((wks_dir = opendir("/home/uzyt1/kat1")) == NULL)
{
	perror("");
	exit(1);
}
...
closedir(wsk_dir);

Funkcja readdir i rewinddir:

#include <sys/types.h>
#include <dirent.h>

struct dirent *readdir(DIR *wks_dir);
void rewinddir(DIR *wks_dir);

readdir - służy do odczytania zawartości katalogu dochodząc od początku
do końca (wskaźnik NULL)
rewinddir - ustawia wskaźnik na początek katalogu

Struktura dirent znajduje się w pliku /bits/dirent.h dołączonym do dirent.h
i wygląda ona następująco:
struct dirent
{
#ifndef __USE_FILE_OFFSET64
__ino_t d_ino;
__off_t d_off;
#else
__ino64_t d_ino;
__off64_t d_off;
#endif
unsigned short int d_reclen;
unsigned char d_type;
char d_name[256]; /* We must not include limits.h! */
};

Generalnie wykorzystuje się d_ino(zakres i-węzła katalogu) oraz d_name(nazwa
pliku o max. dlugości [256]).

Np.
struct dirent *dr;
DIR *wsk_dir;

if((wks_dir = opendir(argv[1])) == NULL)
{
	perror("");
	exit(1);
}

while(dr=readdir(wsk_dir))
{
	if (dr->d_ino != 0)
		printf("%sn",dr->d_name);
}

Funkcja chdir i getcwd:

#include <unistd.h>

int chdir(const char *ścieżka);

sprawia, że ścieżka staje się nowym katalogiem bieżącym dla programu.

#include <unistd.h>

char *getcwd(char *nazwa_katalogu, ssize_t rozmiar);

funkcja wpisuje do [nazwa_katalogu] bieżący katalog roboczy o rozmiarze
[rozmiar]

Np.
char n_kat[50];
getcwd(n_kat, 50);
printf("katalog biezacy to %sn",n_kat);

w razie błędu funkcja zwraca NULL.


Czasami istnieje potrzeba przejścia naszego drzewa katalogów poczynając od
określonego miejsca. Do tego najlepiej wykorzystać gotową funkcję ftw:


Funkcja ftw:

#include <ftw.h>
int ftw(const char *ścieżka, int(*funkcja) (), int głębokość);
  • pierwszy parametr jest ścieżką do katalogu, od którego funkcja rozpocznie
    rekurencyjne przeszukiwanie

  • drugi parametr to funkcja, która zostanie uruchomiona za każdym razem ,gdy
    zmieni się katalog przeszukiwań

  • parametr trzeci to liczba otwartych katalogów (deskryptory), należy uważać
    aby nie przekroczyć ich maksymalnej liczby, którą moze obsługiwać system;
    jeżeli przeszukujemy większą liczbę katalogów to można ustawić ten parametr
    na liczbę większą niż 1, w ten sposób zwiększy się nieco szybkość
    przeszukiwania (katalogi nie będą otwierane po każdej zmianie w ścieżce
    przeszukiwań tylko od razu zostanie pobrany z pamięci kolejny deskryptor)

Funkcja wywoływana za każdym razem po zmianie ścieżki bieżącej powinna
być wywoływana w następujący sposób:

int funkcja(const char *nazwa, const struct stat *statystyka, int typ)

  • nazwa to wskaźnik do nazwy bieżącego katalogu(pliku)

  • statystyka to wskaźnik do struktury stat, za każdym razem dla katalogu
    lub pliku są pobierane dane i umieszczane w strukturze stat (tak jak
    po wywołaniu funkcji stat)

  • typ określa z jakiego rodzaju obiektem mamy doczynienia:

    FTW_F - plik
    FTW_D - katalog
    FTW_DNR - katalog bez możliwości odczytu
    FTW_SL - łącze symboliczne
    FTW_NS - dla tego obiektu nie można wypełnić stat (nie jest łączem
    symbolicznym)

Poniżej znajduje się krótki program korzystający z tej funkcji. Program
pokazuje, które pliki lub katalogi mają uprawnienia 0777.

/program korzystajacy z ftw/

#include <stdlib.h>
#include <sys/stat.h>
#include <ftw.h>

int szukaj(const char *nazwa, const struct stat *statystyka, int typ)
{

if(typ == FTW_D)
    printf("%-79sr",nazwa);	

if (typ == FTW_D && (statystyka->st_mode&0777) == 0777)		
printf("r%-20st%dt%3otkatalogn",nazwa, statystyka->st_ino, 
	statystyka->st_mode&0777);	

if (typ == FTW_F && (statystyka->st_mode&0777) == 0777)		
printf("r%-20st%dt%3otplikn",nazwa, statystyka->st_ino, 
	statystyka->st_mode&0777);	

return 0;	

}

main()
{
printf("SCIEZKAtt I-WEZELtPRAWAtTYPn");
ftw("/", szukaj, 1); //zaczynamy szukać od korzenia
printf("n");
exit(0);
}
--cut--


Krótko o procesach.

Każdy uruchomiony program jest w systemie Linux określany jako proces.
Każdemu procesowi zostaje przydzielona wirtualna przestrzeń adresowa.
Linux rozróżnia trzy typy procesów:

  • interaktywne - uruchamiane przez powłokę i przez nią kontrolowane
  • wsadowe
  • rezydentne (demony)
    System identyfikuje procesy przydzielając im numer(PID). Za pomocą polecenia
    ps można z linii powłoki zobaczyć jakie procesy są uruchomione. Za pomocą
    polecenia kill [numer] można usunąć proces. Stosuje się to głównie w stosunku
    do niedziałających procesów lub do przeresetowania serwerów.

Każdy proces może utworzyć proces potomny. Funkcja jaka jest
używana w tym celu nosi nazwę fork().


Funkcja fork:

#include <sys/types.h>
#include <unistd.h>

pid_t fork(void);

powoduje utworzenie przez jądro nowego procesu, który jest kopią procesu
wywołującego. Proces potomny jest wierną, ale samodzielnie działającą
kopią procesu rodzicielskiego - oznacza to, że proces potomny to coś zupełnie
różnego od tzw. wątku. Wątek posiada kopię kodu programu, ale korzysta z tych
samych zmiennych, co program rodzicielski.

pid_t to liczba identyfikująca proces potomny


Funkcja fork bardzo często jest wykorzystywana w programach sieciowych typu
serwer.

Obsługa procesów to bardzo złożony problem.
Nam wystarczy na razie posługiwanie się funkcją fork().

Programowanie sieciowe

Pisania programów działających w sieci to bardzo ważna umiejętność.
Na szczęście system Linux daje nam odpowiednie narzędzia, aby w prosty
sposób poznać tę "tajemną wiedzę".

Na początku warto się zastanowić w jaki sposób następuje poączenie między
dwoma komputerami.
Weźmy dwa hosty o numerach 204.23.104.8 i 192.168.20.2. Aby połączenie
doszło do skutku trzeba ustalić kilka podstawowych rzeczy:
protokół - określony jednoznacznie mechanizm za pomocą którego będzie
możliwa wymiana danych (TCP, UDP lub inny )
port - numer 16-bitowy określający z jakim procesem aplikacji ma nastąpić
połączenie (telnet - numer 23)
gniazdo - można o nim myśleć na dwa sposoby: pierwszy z punktu widzenia
systemu to numer jednoznacznie identyfikujący proces , port, który
jest przydzielany dynamicznie, drugi z punktu widzenia programisty
to specjalny plik (gniazdko), z którym powiązane są odpowiednie
struktury i który jest końcowym i początkowym elementem wirtualnego
połączenia. Określenie plik w stosunku do gniazda oznacza tu, że
o gniazdku można myśleć jako o pliku (deskryptor).

Port przydzielany dynamicznie to port, który jest przypisany przez system
do programu (klienta). O ile dla sesji telnetowej mamy z góry
założony numer 23- port z którym zawsze trzeba się połączyć, aby móc
skorzystać z usługi, o tyle klient ,aby mógł być jednoznacznie identyfikowany
przez serwer i system hosta, musi mieć przydzielony port (pierwszy wolny
w systemie)

Przykład: (dla TCP)

1.HOST (192.168.20.2)
Uruchomienie klienta
system przydziela
wolny numer portu
1034

2.SERWER (204.23.104.8)
Serwer odbiera rządanie
od 192.168.20.2:1034
zapamiętuje je

3.SERWER (204.23.104.8)
Serwer wysyła sygnał gotowości
na przyjmowanie poleceń

Numery portów przydzielone statycznie do określonych usług znajdują się
w pliku /etc/services.

Aby utworzyć gniazdko sieciowe musimy wypełnić pola w odpowiednich
strukturach. Ogólna struktura adresu gniazda jest zdefiniowana
w pliku nagłówkowym <sys/socket.h>:

struct sockaddr
{	sa_family_t sa_family; /*typ*/
	char		sa_data[]; /*adres*/
};

jest to gniazdo ogólne (może również służyć do komunikacji między procesami).
Gniazdo do komunikacji sieciowej zostało zdefiniowane w <netinet/in.h>:

struct sockaddr_in{
	sa_family_t		sin_family; /*rodzaj adresu internetowego*/
	in_port_t		sin_port;	/*numer portu*/
	struct in_addr	sin_addr;	/*specjalna struktura do przechowania IP*/
	unsigned char	sin_zero[8] /*wypełnienie*/
};

Aby utworzyć gniazdo musimy określić jego rodzaj za pomocą funkcji socket.


Funkcja socket:

#include <sys/socket.h>
int socket(int rodzaj, int typ, int protokół);

argument 1 - rodzaj adresów:
AF_INET - adresy internetowe
AF_UNIX - do komunikacji między procesami znajdującymi się na tym
samym komputerze

argument 2 - typ gniazda:
SOCK_STREAM - tryb połączeniowy, wykorzystuje strumień, jest to protokół
TCP
SOCK_DGRAM - tryb bezpołączeniowy oparty na datagramach, jest to protokół
UDP (jest to tryb zawodny, ale szybciej działający, dość rzadko
się go używa)

argument 3 - określa, który protokół powinien być używany przez gniazdo
ustawiany na zero (domyślny protokół: dla SOCK_STREAM TCP, dla SOCK_DGRAM UDP)


Do gniazda przypisywane są dwa adresy: hosta i serwera. Adres hosta jest
dopisywany automatycznie przez system. Adres serwera trzeba podać. Można
zrobić to na dwa sposoby:

  1. Jeżeli klient ma służyć do łączenia się z jednym określonym adresem
    możemy ustawić to na sztywno wypełniając strukturę:

struct sockaddr_in adres; //utworzenie struktury adres typu sockaddr_in

adres.sin_addr.s_addr = inet_addr("204.23.104.8");//wypełnienie adresem

  1. O wiele częściej istnieje potrzeba podania adresu (również nazwy).
    Do tego służy funkcja gethostbyname.

Funkcja gethostbyname:

	#include <netdb.h>

	struct hostent *gethostbyname(const char *name)

szuka numeru IP dla podanej nazwy, jeżeli od razu podamy numer funkcja
wypełnia strukturę hostent (funkcja poszukuje IP w kolejności zdefiniowanej
w systemie np. plik /etc/hosts a później serwer DNS)

Struktura hostent znajduje się w netdb.h.

struct hostent {
char	*h_name;	//nazwa hosta
char **h_aliases; //lista aliasów
int	h_addrtype; //typ adresu zdefiniowany w <sys/socket.h>
int	h_length; //rozmiar adresu w bajtach (dla IP zawsze 4)
char	**h_addr_list;
#define h_addr h_addr_list[0]; //lista adresów IP
};

Kiedy mamy wszystkie potrzebne dane należy nawiązać połączenie za pomocą
funkcji connect.


Funkcja connect:

	#include <sys/types.h>
	#include <sys/socket.h>

	int connect(int desp, const struct sockaddr *adres, size_t add_len);
  • 1 -deskryptor gniazda uzyskany po wywołaniu funkcji socket
    (deskryptor ten ma takie samo znaczenie jak w przypadku plików
    dlatego też do obsługi gniazda można użyć funkcji read i write)
  • 2 - adres do struktury typu sockaddr
  • 3 - wielkość struktury w bajtach

Gdy zostaje nawiązane połączenie należy napisać do gniazda coś sensownego,
wszystko zależy z jaką usługą mamy doczynienia. Dla każdej usługi istnieje
pewien z góry określony sposób komunikacji (standardy komunikacji dla usług
są podawane w dokumentach RFC (Request For Comments)).

Poniżej znajduje się program korzystający z usługi finger. Po wpisaniu
dowolnego parametru program zapisuje w pliku odpowiednie struktury
(pod warunkiem, że mamy uprawnienia root'a)

/*		
*	Nazwa programu : k_finger+ 
*	 
*		Opis   : klient uslugi finger z mozliwoscia podgladu 
*			 struktur
*
*	 Sposob uzycia : k_finger+ host_docelowy uzytkownik [z]
*			 z-dowolny argument (pokazuje struktury)
*		    np : k_finger+ 192.168.20.203 root	
*
*	     Wymagania : UNIX/LINUX
*		dodatkowe: mozliwosc podgladu struktur tylko z poziomu
*			   root'a lub z atrybutem +s dla programu
*		
*
*/

#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
			    //MAIN
int main(int argc, char** argv)
{
    struct hostent *host; //deklaracja struktury host typu hostent
    struct servent *serv; //deklaracja struktury serv typu servent
    struct sockaddr_in adres; //deklaracja struktury adres typu sockaddr_in
    int gnz, i;
    char buf[1024];
    FILE *file;
    
//////////////////////////////////////////////////////////////////////////////
	    printf("ntt------------------------------");
	    printf("ntt  k_finger+ Ver.0.00002 -2000-");
	    printf("ntt------------------------------nn");
	
//////////////////////////////////////////////////////////////////////////////    
    if (argc<3)
    {
	printf("Uzycie: %s [host_docelowy] [uzytkownik] [jakis arg do pok. strukt.]nn",argv[0]);
	return 1;
    }

    adres.sin_family=AF_INET; //adresy internetowe
    
    host=gethostbyname(argv[1]); //poszukiwanie IP dla nazwy serwera
    if (host!=NULL) //jeżeli powiodło się to wypełnij strukturę adres
    {
	bcopy(host->h_addr,(char *)&adres.sin_addr,host->h_length);
	            /////
    /////
    }
	else
	{
	    perror ("Blad: Nie moge znalezc hosta_docelowego");
	    return 1;
	}
    
   adres.sin_port=htons(79); //wpisz do struktury adres numer portu
				//funkcja htons formatuje liczbę 79 (port fingera)
				//w odpowiednio zrozumiały dla kompa sposób	
    	
    gnz=socket(AF_INET,SOCK_STREAM,0); //utworzenie gniazda TCP (mamy deskryptor)
    if (gnz==-1)
    {
	perror("Blad funkcji socket!");
	return 1;
    }	
    if (connect(gnz, (struct sockaddr *)&adres, sizeof(adres))<0) //połącz
    {
	close(gnz); //w razie błędu zamknij gniazdo
	 // i pokaż rodzaj błędu
	perror("Nie moge polaczyc!");
    }
    
	//komunikacja
    write (gnz, argv[2],strlen(argv[2])); //piszemy do gniazda użytkownika
    write (gnz, "rn",2);			// i dodajemy znak powrotu karetki i końca linii
    while ((i=read(gnz,buf,sizeof(buf)))>0) // odbieramy dane z serwera
    write (0,buf,i); //wypisanie na ekran
        
    if(!geteuid()) //jeżeli jest to proces root'a 
    	{
	if(argv[3]) // i podano dodatkowy parametr
	    {
	    file=fopen("struct.log","w");
	    fprintf(file,"ntKontrolne sprawdzanie strukturnntStruktura hostent :n 
	     th_name = %sntth_aliases = %sntth_addrtype = %d
	     th_length = %dntth_addr = %s nntStruktura sockaddr_in :n
	     tsin_family = %dnttsin_port = %d nttsin_addr = %s
	     tsin_zero = %sn",
	     host->h_name,host->h_addrtype,host->h_length,adres.sin_family,
	     ntohs(adres.sin_port),adres.sin_zero);
	     fclose(file);
	    }
	
	}
    
    close(gnz);

    return 0;   
    
}

Plik struct.log może wyglądać tak:

Kontrolne sprawdzanie struktur

Struktura hostent :

     	h_name = 192.168.20.203
	h_addrtype = 2
     	h_length = 4


Struktura sockaddr_in :

     	sin_family = 2
	sin_port = 79 
     	sin_zero = //tu mogą być jakieś śmiecie

W przypadku usługi finger serwer oczekuje od klienta podania nazwy użytkownika
oraz dodania znaków rn. Tylko tak sformatowany tekst jest zrozumiały dla
serwera - w innym przypadku serwer zakończył by połączenie zwracając błąd.

Inaczej jest w usługach typu SMTP - POP3, gdzie należy przeprowadzić
dłuższą konwersację z zastosowaniem określonych reguł - poleceń.

W przypadku SMTP połączenie wygląda następująco (klient):

  1. helo
  2. mail from:<e-mail>
  3. rcpt to:<e-mail>
  4. data
  5. quit

Serwer za każdym razem zwraca odpowiedni numer jako odpowiedź (np. 250
oznacza poprawną pracę serwera i przyjęcie polecenia). Dokładny opis
protokołu znajduje się w dokumencie RFC 821.

Poniżej przykład klienta pocztowego korzystającego z SMTP.

/*
*
*		Nazwa programu : smtp_klient Ver.0.1
*
*			  Opis : program do wysylania poczty
*
*			Uzycie : smtp_klient [do_kogo] [na_jaki_host]
*
*		     Wymagania : UNIX/LINUX + Development	
*
*/
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <string.h>
///////////
///////////
struct hostent *host;
struct sockaddr_in adres;
int gnz,x;
FILE *d;
char buf1[15];
char buf2[40];
char buf3[1024];
///////////////////////////////////MAIN/MAIN/MAIN////////////////////////////
int main(int argc, char** argv)
{
    printf("nttt------------------------n");
    printf("ttt    SMTP-klient Ver.0.1n");
    printf("ttt------------------------nn");
    
    if (argc<3)
    {
	printf("Uzycie: %s [adresat] [host_docelowy]n",argv[0]);
	printf("Uwaga: program uzywa formy adresat@host_docelowyn");
	exit(0);
    }

    host=gethostbyname(argv[2]);
    if (!host)
    {
	perror("gethostbyname");
	exit(1);
    }    
    bzero((char *)&adres,sizeof(adres)); //zerowanie struktury adres
    fprintf(stderr,"Tworzenie wiadomosci: nn");
    bcopy(host->h_addr,(char*)&adres.sin_addr,host->h_length);
    adres.sin_family=host->h_addrtype;
    adres.sin_port=htons(25);
///////////////////////////////////////////////////////////////////////    
    if ((gnz=socket(AF_INET,SOCK_STREAM,0))==-1) 
    {
        perror("BLAD podczas tworzenia gniazda");
	return 1;
    }
    if (connect(gnz,(struct sockaddr *)&adres,sizeof(adres))<0)
    {
	close(gnz);
	switch (errno)
	{
	    default : perror("Nie moge polaczyc!");
		      return 1;
	}
    }
            
    printf("<ADRES NADAWCY>: ");
    scanf("%s",&buf1);
    printf("n<TEMAT>: ");
    scanf("%s",&buf2);
    printf("n<TEKST>: "); //w tym miejscu powinna znalezc sie obsluga
				//wpisywania dlgiego tekstu i konczenie go .
				// jak w programie mail
    scanf("%s",&buf3);

    
    if (!(d=fdopen(gnz,"w"))) { perror(""); exit(0); }
//////////////////////////////////////////////////////////////////////    
    fprintf(d,"helo HOSTn");
    fprintf(d,"mail from: <%s>n",buf1);
    fprintf(d,"rcpt to: <%s>n",argv[1]);
    fprintf(d,"datan");
    fprintf(d,"To: %sn",argv[1]);
    fprintf(d,"Subject: %sn",buf2);
    fprintf(d,"%sn",buf3);
    fprintf(d,".n");
    fprintf(d,"quitn");
    fflush(d);	//wysłanie zawartosci bufora do pliku zwiazanego ze
			//strumieniem (u nas plik to gniazdko)
    shutdown(gnz,2); //zamknięcie połaczenia
    close(gnz); //zwolnienie deskryptora gniazda
}

W kliencie SMTP użyłem nowej funkcji fdopen. Funkcja ta jest niemal
identyczna z fopen - różnica jest taka, że zamiast scieżki do pliku
podaje się deskryptor (u nas gnz uzyskany z funkcji socket()).
Funkcja shutdown przeprowadza proces zamykania połączenia (uwaga: nie zwalnia
deskryptora), jest to jeden ze sposobów zamykania połączenia.
Deksryptor zwalniamy przez close().

Skoro umiemy wysłać pocztę to przydał by się program do jego odbierania.
O ile do wysyłania korzystamy z protokołu SMTP (numer 25), o tyle do odbierania
używamy protokołu POP3. Wynika to z tego, że SMTP zmusza komputer docelowy
do ciagłego działania - inaczej poczta nie może być dostarczona. Dlatego
stworzono POP3 (wcześniej POP2). POP3 operuje na tzw. skrzynkach. Każda
skrzynka to nic innego jak konto (użytkownik+hasło) jednak bez możliwości
logowania (shell ustawiony na false).

SMTP jest instalowany domyślnie w systemie Linux. POP3 jest dodatkową
usługą i wymaga oddzielnego uruchomienia. Trzeba w pliku /etc/inetd.conf
odznaczyć zaremowaną linijkę do pop3 (usunąć #), a następnie zainstalować
pakiet imap. Po tym wystarczy przerestartować i mamy serwer poczty(!).

Poniżej znajduje się krótki klient dla pop-3.

/* pop.c
*
*		Nazwa programu : pop
*
*			  Opis : program do odbioru poczty (pop3)
*
*			Uzycie : pop [host]
*
*		     Wymagania : UNIX/LINUX + Development	
*
*/
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <curses.h>

int gnz, i, q, lp=0;
char buf[1024];
char ret[20]={"RETR "};
char usun[20]={"DELE "};
char znak, wyb;

void przeglad();
void usuwanie();

WINDOW *okno;


int main(int ilarg, char** argv)
{
    
    struct hostent *host;
    struct servent *serv;
    struct sockaddr_in adres;
    char nazwa[100];
    char haslo[100];
    char uz[20]={"USER "};
    char ha[20]={"PASS "};
    
    
    if (ilarg<2) { printf("Uzycie: %s hostn,argv[0]");return 1;}    
    
    okno=initscr();      
    scrollok(okno,1);
    wrefresh(okno);
    cbreak();
    
    bzero((char*)&adres, sizeof(adres)); //zerowanie struktury adres
    adres.sin_family=AF_INET;
    
    
    host=gethostbyname(argv[1]);
    if(host!=NULL)
    bcopy(host->h_addr, (char*)&adres.sin_addr, host->h_length);
     else
     {     
      perror("Blad: nie moge znalezc hosta");
      endwin();
      return 1;
     }
     
     
     serv=getservbyname("pop-3", "tcp"); //łaczy sie z serwerem i sprawdza
							//numer portu dla pop-3
     if(serv!=NULL)
     {
      adres.sin_port=serv->s_port;
      }
     else
     {
      perror("Blad getservbyname");
      endwin();
      return 1;
     }
     
     gnz=socket(PF_INET, SOCK_STREAM,0);
     if (gnz==-1)
     {
      perror("Blad funkcji socket");
      endwin();
      return 1;
     }
     
     if (connect(gnz, (struct sockaddr*)&adres, sizeof(adres))<0)
     {
      close(gnz);
       perror("Nie moge polaczyc");
       endwin();
      } 
    
    
    
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);    
    do
    {
      wprintw(okno, "nPodaj nazwe skrzynki pocztowej: ");
      getstr(nazwa);
      
      for (q=5; q<=4+strlen(nazwa); q++)
      {
        uz[q]=nazwa[q-5];
      }

    
      wprintw(okno,"Podaj haslo: ");      
    
      q=0;
      do
      {
      znak=getch();
      mvwdelch(okno,2,13);
      if (znak==10) break; else haslo[q++]=znak;
      }while(znak!=10);    
        haslo[q]=0;    
     
      
      for (q=5; q<=4+strlen(haslo); q++)
      {
      ha[q]=haslo[q-5];
      }

    
      write(gnz, uz, strlen(uz));
      write(gnz,"rn",2);
      i=read(gnz, buf, sizeof(buf));
      //write(0, buf, i);
    
      if (buf[0]=='+')
      {
        write(gnz, ha, strlen(ha));
        write(gnz,"rn",2);
        i=read(gnz, buf, sizeof(buf));
        //write(0, buf, i);
      }
	clear();
      

      if(buf[0]=='-') wprintw(okno,"Blad logowania");
      lp++;      
    }while((buf[0]=='-')&&(lp<3));
    
    if(buf[0]=='+') wprintw(okno,"nMasz %c wiadomosci nn",buf[18]);


    
    
    if((buf[18]!='0')&&(buf[0]=='+'))
     {
      do
      {  
        wprintw(okno,"          MENU n");
	wprintw(okno,"       1-przeglad n");
	wprintw(okno,"       2-usuwanie n");
        wprintw(okno,"       3-koniec n"); 
	
	wyb=getch();     
	switch(wyb)
	{
	  case '1': przeglad();
		  break;
	  case '2': usuwanie();
	          break;
	  case '3': break;
	}       
      }while(wyb!='3');	
    }

    wrefresh(okno);        
    
    write(gnz, "QUIT", strlen("QUIT"));
    write(gnz,"rn",2);    
    
    wprintw(okno,"n");
    wrefresh(okno);        
    
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);

    close(gnz);
    endwin(); 
    return 0;
}


void przeglad()
{
    wprintw(okno,"nPodaj numer wiadomosci do przegladniecia : n");
    znak=getch();
    ret[5]=znak;
    
    clear();        //czyszczenie okna
    wrefresh(okno);
    
    write(gnz, ret, strlen(ret));
    write(gnz,"rn",2); 
    
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);
    move(0,25);
}

void usuwanie()
{
    wprintw(okno,"nPodaj numer wiadomosci do usuniecia : n");
    znak=getch();
    usun[5]=znak;
            
    write(gnz, usun, strlen(usun));
    write(gnz,"rn",2); 
    
    clear();        //czyszczenie okna
    wrefresh(okno);
    
    i=read(gnz, buf, sizeof(buf));
    write(0, buf, i);
    move(0,25); //przesuwanie do punktu 0,25
}

Sam program nie jest jakąś rewelacją. Przy kompilacji należy dodać opcję
lcurses czyli w linii wpisujemy gcc -opop pop.c -lcurses. Wynika to z tego,
że wykorzystany tu został tryb pełnoekranowy z wykorzystaniem biblioteki
<curses.h>. Na początku deklarujemy chęć użycia okna WINDOW *okno:

okno=initscr();      //inicjujemy okno
wrefresh(okno);	//odświeżamy okno

Odświeżanie powinno następować po każdej mozliwej zmianie w oknie.
Polecenie wprintw to to samo co printf - należy tylko podać do jakiego
okna piszemy (okien moze być kilka). W programie użyto jeszcze jednej
ważnej funkcji: mvwdelch(okno,2,13); - przydała się ona w trakcie wpisywania
hasła do skrzynki, każdy wpisany znak jest kasowany poprzez przeniesienie
kursora do punktu ekranu 2,13 - dzięki temu wpisywane hasło jest niewidoczne.

Protokół jest opisany w RFC 1725 dlatego nie będę szczegółowo go opisywał.
Wymienię tylko podstawowe polecenia:
-USER [uzytkownik]
-PASS [haslo]
-RETR [numer] - pobranie konkretnej wiadomości
-DELE [numer] - skasowanie wiadomości
-QUIT - wyjście

Serwer zwraca w przypadku powodzenie +OK lub niepowodzenia -ERR.

Polecenia podstawowe są użyte w kodzie. Każdy protokół można sprawdzić
bez odpowiedniego klienta, wystarczy do tego telnet np.

telnet host 110

Poniżej znajduje się prosty, ale nieco bliższy hakerce kod. Jest to skaner
portów:

/* -*-C-*- tcpprobe.c */
/* tcpprobe - report on which tcp ports accept connections */
/* IO ERROR, [email protected], Sep 15, 1995 */

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <netdb.h>
#include <signal.h>



int main(int argc, char **argv)
{
  int probeport = 0;
  struct hostent *host;
  int err, i, net, filedes;
  struct sockaddr_in sa;

  if (argc != 2) 
  {
    printf("Usage: %s hostnamen", argv[0]);
    exit(1);
  }
   for (i = 1; i < 1024; i++) 
  {
    strncpy((char *)&sa, "", sizeof sa);
    sa.sin_family = AF_INET;
    
    if (isdigit(*argv[1]))
      sa.sin_addr.s_addr = inet_addr(argv[1]);
      
    else if ((host = gethostbyname(argv[1])) != 0)
      strncpy((char *)&sa.sin_addr, (char *)host->h_addr, sizeof sa.sin_addr);
      
    else 
    {
      herror(argv[1]);
      exit(2);
    }

    sa.sin_port = htons(i);
    net = socket(AF_INET, SOCK_STREAM, 0);

    if (net < 0) 
    {
      perror("nsocket");
      exit(2);
    }

    err = connect(net, (struct sockaddr *) &sa, sizeof sa);

    if (err < 0) 
    {
      printf("%s %-5d %sr", argv[1], i, strerror(errno));
      fflush(stdout);
    } 
    else 
    {
         printf("%s %-5d accepted.                               n", 
	         argv[1], i);
	    
      if (shutdown(net, 2) < 0) 
      {
	perror("nshutdown");
	exit(2);
      }
    }

    close(net);

  }

  printf("                                                                r");
  fflush(stdout);

  return (0);

}

Działanie tego skanera jest proste - łączy się (pętla) z kolejnymi numerami
portów w odległym hoście i sprawdza czy połączenie doszło do skutku, jeśli
tak to zwraca numer portu (accepted). Jest to podstawowe narzędzie pozwalające
zorientować się które usługi serwera są na chodzie. Można nim też sprawdzać,
czy ktoś nie założył sobie na serwerze backdoora pod jakimś
dziwnym portem (należałoby zwiększyć ilość portów do przeszukiwania of cos).

Program można nieco rozbudować np. robiąc plik z nazwami usług, jeżeli
jest to usługa z numerem 7, to powinna się znaleźć w siódemj linijce
w pliku. Za pomocą funkcji lseek można by odczytać nazwę usługi dla
odpowiadającego jej numeru (zmienna i). Prostsze mogłoby być utworzenie
tablicy z tymi nazwami (znajdują się w pliku /etc/services).


Pora teraz na przyjrzenie się programowaniu serwera. Proces serwera różni
się główni od klienta tym, że musi działać, być biernym i dostępnym
w momencie próby połączenia od strony klienta. Serwery popularnych usług
działają jako tak zwane demony (inaczej programy rezydentne).
Dodatkowo w systemie Linuks usługi takie jak: inetd i network są wymagane
przez wszystkie systemy sieciowe. Demon inetd (uruchamiany przez skrypt
/etc/ini.d/inet lub inny podczas ładowania systemu) jest używany do uruchamiania
większości usług sieciowych (tzw. usług na żądanie). Inetd na żądanie
z innego hosta uruchamia odpowiednią usługę. Dzięki temu liczba
demonów może być zmiejszona do minimum.

Zamieszczony przykład będzie uruchamiany w shellu.

Podstawowym zadaniem serwera jest nasłuchiwanie żądań połączeń od klientów.
Z tego też względu serwer zachowuje się biernie. Dodatkowo serwer może
działać na dwa sposoby: tworzy kolejkę połączeń, jest wieloprocesowy.
Kolejka połączeń sprawia, że w jednym momencie może zostać obsłużony
tylko jeden klient, reszta czeka w kolejce (jej max. ilość jest z góry
ustawiana). Lepszym sposobem jest zaprogramowanie serwera wieloprocesowego.
Do tego rozwiązania przydaje się właśnie funkcja fork(). Tworzy ona
dla każdego połączenia odrębny proces.

Aby ustawić serwer w tryb nasłuchiwania trzeba ustawić gniazdko nasłuchujące.
Używamy do tego celu trzech funkcji: socket, bind, listen.


Funkcja bind:

	#include <sys/socket.h>

	int bind(int desp, const struct sockaddr *nazwa, int dl_nazwy);
  • desp - deskryptor gniazda uzyskany z funkcji socket
  • nazwa - struktura sockaddr
  • dl_nazwy - rozmiar struktury nazwa.

Funkcja ta określa numer portu - wszystkie żądania do tego portu będą
kierowane do gniazda serwera.


Funkcja listen:

	#include <sys/socket.h>

	int listen(int desp, int kolejka);
  • desp - deskryptor gniazda uzyskany z funkcji socket
  • kolejka - max. liczba nie obsłużonych nadchodzących połączeń

Funkcja listen może być użyta tylko w gniazdach typu strumieniowego (TCP).
Przepełnienie kolejki powoduje zwrócenie błędu do klienta. Nawet w przypadku
wykorzystania funkcji fork() kolejka powinna mieć odpowiedni rozmiar, gdyż
serwer może nie nadążać z tworzeniem procesów potomnych.


Po ustawieniu gniazda w tryb nasłuchujący serwer powinien móc obsłużyć
pierwsze żądanie z kolejki połaczeń. Do sprawdzania i akceptacji żądania
służy funkcja accept().


Funkcja accept:

	#include <sys/socket.h>
	
	int accept(int desp, struct sockaddr *adres, int *dl_adres);
  • desp - deskryptor gniazda nasłuchującego
  • adres - struktura sockaddr
  • dl_adres - rozmiar struktury adres

Funkcja tworzy nowe gniazdo i zwraca do niego deskryptor. Nowe gniazdo
jest wypełniane adresem hosta żądającego.

12 komentarzy

swietny art oby więcej takich rzeczowych i KONTRETKNYCH artoow dla Linuxa. wiecej wiecej wiecej

poprawilem wiele w Programowanie sieciowe bo nie widzialem naglowkow include //

Poprawiłem kilka kodów (tzn, wziąłem je w bloki code) bo widzę, że nikt nie wprowadzał poprawek :(. Mam nadzieję, że ktoś (może autor? :)) poprawi resztę, żeby było czytelniejsze.

ciekawy art, dopiero teraz go zauwazylem :]
mala uwaga: w tej chwili niektore fragmenty sa nieczytelne (jak np. nazwy bibliotek w dyrektywie include)

Arcik jest gitt... teraz będe wprowadzał w życie te przykłądy :]

Art super;)
Sam jestem uzytkownkiem Linuxa od
niedawna i przyda sie na pewno.

w artykule w czesci o asmie jest napisane tak:
"movl %eax,%esi -kopiuj wartość z rej. esi do eax "
ta instrukcja skopiuje eax do esi, bo w skladnii asmowej AT&T czyta sie odwrotnie!!!

hm.. ja bym wolał żebyś z tego naprzykład sklepał kurs albo podzielił na kilka artów :))

Wszystko ladnie, tylko staraj sie pisac wlasne materialy, a nie przepisywac gotowe...

No właśnie przecież C jest stworzony do środowiska UNIX (Linux) Tak trzymać ! Oby więcej w tym temacie

Do krótkich ten artykuł chyba nie należy....

Ciekawy. Przymierzam się do Lincha może wkońcu zainstaluje sobie...