[bash][telnet server][system][popen] sesja basha z c++ lub c

0

Problem 1 (wymyślony przeze mnie - pomocniczy, być może pomoże mi rozwiązać problem 2):
mam otwarte 2 konsole - konsola 1) i konsola 2)
w konsola 1) robie echo $$ - dostaje '587'
chce w konsola 2) wywołać polecenie które sprawi taki rezultat jaki by miało w konsola 1) wpisanie 'ls' i kliknięcie enter
próbuję echo ls > /proc/587/fd/0 ale to oczywiście nie działa.

Problem 2: (obojetnie czy rozwiązanie w c czy c++) - mam jedyną podpowiedź, że to jest banalne i wystarczy użyć dup i po sprawie, ale jakoś nie ogarniam tego.
Robię server telneta, mam tylko problem ze spawnowaniem nowych instancji basha do których będę mógł pisać (kazać wywołać polecenie) i z nich czytać.
Problem - chcę móc zrobić następujące polecenia

system("mkdir przykladowy_folder")
system("cd przykladowy_folder")
system("touch jakis_plik.txt") 

tyle, że przy takim odpaleniu jakis_plik.txt nie jest tworzony w folderze przykladowy_folder (każde odpalenie system to nowe odpalenie kolejnej instancji basha - sprawdziłem bo testowałem pidy).
Podobnie próbowałem z popen.
Jak to się robi + pytanie za dodatkowe 10 punktów jak zrobić żebym mógł odbierać output takich programów interaktywnych jak top, vim itp.

Jedyne co znalazłem do tej pory to żeby parsować input i szukać polecenia cd i zapamiętywać working directory i będę wówczas robił:

 
system("cd przykladowy_folder") //jest cd  - ustaw zmienną string working_directory="~/"+"przykladowy_folder"
system("cd "+working_directory+" && touch jakis_plik.txt")  //albo jak bym robił w qt i korzystał z QProcess to bym ustwaił workingDirectory

Musi być lepszy sposób! Ponadto to nie rozwiązuje problemu top, vim etc.

7

Ad 1.
Dobrze kombinujesz, ale nie do końca dobrymi metodami. Ale zanim przejdę do rozwiązania: samo to czemu echo ls > /proc/587/fd/0 nie działa jest bardzo ciekawe i warte wyjaśnienia.
Mianowicie, jakby w Twoim przypadku zrobić ls -la /proc/587/fd/0, to się okaże, że jest to otwarte urządzenie /dev/pts/<numer>, czyli 'pseudo-terminal slave'.
Polecenie które podałeś otworzy (na nowo) urządzenie /dev/pts/<numer> jako 'slave'. Wszystko co zostanie wysłane (zapisane do) 'slave' jest odbierane przez terminala, gdzie przez 'terminal' rozumiem program rysujący okno konsoli (np. gnome-terminal, etc).
W związku z czym, samo 'ls<enter>' będzie przekazane jako trzy znaki do konsoli do wyświetlenia, co też konsola zrobi.

Tylko twórca konsoli (patrz openpty()) ma końcówkę 'master' i może pisać na stdin utworzonego procesu.

Natomiast, gdyby to nie był pseudo-terminal, tylko potok, to to co napisałeś by zadziałało. Dość łatwo to sprawdzić:

> cat | /bin/bash
echo $$
15439

I jeśli na drugiej konsoli zrobi się:

> echo ls > /proc/15439/fd/0
To polecenie to zostanie wykonane w Bashu.

W zasadzie to już stanowi pewną podpowiedź, co powinno się zrobić :)

Ad 2.
Zacznijmy od tego co robi system(), bo to akurat jest dość proste.

Mianowicie system() robi dwie rzeczy:

  1. fork()
    Co spowoduje utworzenia procesu-dziecka. Proces rodzica zaczyna czekać na jego zakończenie (waitpid()).

  2. execve()
    Czyli niskopoziomowe uruchomienie procesu (a w zasadzie "konwersja" procesu-dziecka na nowy proces, bo mniej więcej tak to działa pod Linuxem).
    W tym wypadku execve służy do odpalenia polecenia /bin/sh -c 'to co podasz w system jako parametr'

Tym samym wykonane polecenie dziedziczy stdin/out/err od procesu macierzystego.

Ty chcesz zrobić coś podobnego, a konkretniej, ponieważ piszesz "coś jak telnet", to chcesz zrobić:

  1. fork()

  2. w procesie dziecku:

  • dup2 socketu który masz z połączeniem sieciowym na fd 0, 1, 2 (czyli stdin, stdout i stderr) - to spowoduje przekierowanie standardowych wejść/wyjść na socket sieciowy.
  • execve /bin/bash (chociaż prościej execvp - wtedy nie musisz re-definiować zmiennych środowiskowych) - dzięki temu uruchomiony zostanie shell, który będzie sterowany przez połączenie sieciowe

(dup2(stareFD, noweFD) powoduje zamknięcie fd o numerze noweFD, oraz zduplikowanie staregoFD pod numer nowegoFD)

Jeśli chcesz zobaczyć jak to zaimplementować, to najprościej jest popatrzeć na tzw. TCP bind shellcode - te zazwyczaj są w asemblerze, ale jest on na tyle krótki, że bardzo łatwo jest go przeanalizować.

Alternatywnie, jeśli chcesz zrobić prawdziwy shell (taki w którym vim będzie działać, etc), to musisz trochę więcej pokombinować i zrobić openpty() po stronie servera, po czym "ręcznie" przepisywać dane z socketa na master, oraz z master na socket. Tam trochę dodatkowych rzeczy trzeba zaimplementować, tak żeby out-of-bound przekazywać np. informacje o zmianie wielkości konsoli, etc. Ostatnio stalkR o tym pisał na blogu zresztą:
http://blog.stalkr.net/2015/12/from-remote-shell-to-remote-terminal.html

Powodzenia :)

EDIT: Gdzieś w odmętach dysku znalazłem jeszcze to - http://gynvael.vexillium.org/dump/_stuff=pipe_stdinout.html - to nie do końca na temat, ale pokazuje jak dup2 używać z fork/execve w C (warning: to fragment mojej strony sprzed 10 lat, więc niekoniecznie wszystko tam jest OK)

0

czemu nie SSH?

0

Dzięki za pomoc.

Gynvael Coldwind napisał(a):

Ty chcesz zrobić coś podobnego, a konkretniej, ponieważ piszesz "coś jak telnet", to chcesz zrobić:

  1. fork()

  2. w procesie dziecku:

  • dup2 socketu który masz z połączeniem sieciowym na fd 0, 1, 2 (czyli stdin, stdout i stderr) - to spowoduje przekierowanie standardowych wejść/wyjść na socket sieciowy.
  • execve /bin/bash (chociaż prościej execvp - wtedy nie musisz re-definiować zmiennych środowiskowych) - dzięki temu uruchomiony zostanie shell, który będzie sterowany przez połączenie sieciowe

Został mi jeden problem:
a) odpalam basha w ten sposób:

 
int error_val;
char *const args[] = { (char *)"bash",NULL };
error_val=execvp("bash", args);

wówczas jak w bashu wpisze:
;
to dostaje:

 bash: line 1: syntax error near unexpected token `;'
bash: line 1: `;'

i połączenie jest zamykane - w serwerze wypisuje mi
4150 exited with return code 2 gdzie 4150 to pid basha.
Chciałbym żeby po wpisaniu ; zabawa się nie kończyła - żebym dostał wiadomość, że jest error ale żebym dalej mógł rozmawiać z bashem (ale nie w taki sposób, że muszę odpalić kolejnego basha).
b)odpalam basha w ten sposób (interaktywnie):

 
int error_val;
char *const args[] = { (char*)"bash",(char*)"-i", NULL };//"-i" interaktywna z promptem
error_val=execvp("bash", args);

i tu nie mam problemu, że mi zamyka połączenie jak wpisze:
;
Jest za to problem, którego nie potrafię wytłumaczyć:
mój serwer może obsłużyć tylko 1 połączenie! (w trybie nie-interaktywnym nie ma z tym problemu i obsługuje wiele)
konsola 1)

nc localhost 1234
 

output serwera:
new connection: 127.0.0.1
konsola 2)
nc localhost 1234
output serwera

new connection: 127.0.0.1 
new connection: 127.0.0.1

[1]+  Stopped                 ./server.o -i 

W sumie nawet nie wiem jak testować co jest nie tak, bo mam w procesie ojcu wait, odczytuje errno ale nie wypisuje mi nawet tego errno.
Przeczytałem https://www.gnu.org/software/bash/manual/bashref.html#Interactive-Shell-Behavior ale tam nie widze, dlaczego miałby być jakiś problem po zmianie na tryb interaktywny.

Problem 2 (mało istotny)) z bash -i: odpalam
./server.o -i i jak ktoś się do mnie połączy to sigint w konsoli z serwerem nie zabija procesu (ctrl+c) (dopiero jak się rozłącze z serwerem to jestem w stanie zakończyć serwer z tej konsoli, na której jest serwer)

 Server starting on port 1234 using bash interactive mode.
new connection: 127.0.0.1
^C^C^C //3x ctrl +C 

podczas gdy w bashu nieinteraktywnym bez problemu zamykam wszystkie połączenia i kończę program.
Jestem to w stanie zabić z innej konsoli kill -INT PID_server.io //sprawdzony przez ps axf | grep server.o

Mój pełny kod
Makefile:

 
all:
	g++ -std=c++0x -Wall -Werror -pthread -o server.o server.cpp

server.cpp

 
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <pthread.h>
#include <iostream>
#include <fstream>
#include <unistd.h>
#include <set>
#include <vector>
#include <sys/wait.h>
bool interactive;
struct cln {
	int cfd;
	struct sockaddr_in caddr;
};
std::set<int> bashChildrenPIDs;
void SIGINT_handler(int signo)
//przy ctrl+c albo kill -INT PID przerywa polaczenia i sprzata bashe
{
  if (signo == SIGINT){
    printf("received SIGINT - shutting down telnet server\n");
		for(auto bashProcessPIDToKill: bashChildrenPIDs){
			printf("killing bash PID %d\n",bashProcessPIDToKill);
			kill(bashProcessPIDToKill,SIGINT);
		}
		exit(0);
	}
}
void* cthread(void* arg){
	struct cln* c=(struct cln*) arg;
	printf("new connection: %s\n",inet_ntoa((struct in_addr)c->caddr.sin_addr));
	int fork_pid=fork();
	if(fork_pid==-1){
		perror("failed to fork");
	}
	//child
	else if(!fork_pid){
		dup2(c->cfd,0 );
		dup2(c->cfd,1 );
		dup2(c->cfd,2 );
		int error_val;
		if(interactive){
			char *const args[] = { (char*)"bash",(char*)"-i", NULL };//"-i" interaktywna z promptem
			error_val=execvp("bash", args);
		}
		else{
			char *const args[] = { (char *)"bash",NULL };
			error_val=execvp("bash", args);
		}
  	if(error_val){
			exit(error_val);
		}
	}
	//parent
	else if (fork_pid>0){
		bashChildrenPIDs.insert(fork_pid);
		//wiekszosc z tego ifa wzialem z http://stackoverflow.com/a/747819
		int status;
		if (wait(&status) == -1)
    {
      perror("wait()");
    }
    else
    {
			bashChildrenPIDs.erase(fork_pid);
      /* did the child terminate normally? */
      if(WIFEXITED(status))
      {
        printf("%ld exited with return code %d\n",
               (long)fork_pid, WEXITSTATUS(status));
      }
      /* was the child terminated by a signal? */
      else if (WIFSIGNALED(status))
      {
        printf("%ld terminated because it didn't catch signal number %d\n",
               (long)fork_pid, WTERMSIG(status));
      }
			close(c->cfd);
			free(c);
    }
	}

	return NULL;
}

int main(int argc, char** argv){
	//./server.o opcjonalneParametry
	//-p numerWybranegoPrzezCiebiePortu (domyslnie jak uzytkownik nie poda to port 1234)
	//-i (czy ma byc konsola interaktywna)

	std::vector<int> signalsForSigHandlerFunction={SIGINT,SIGTERM};
	for(auto signalToRegister: signalsForSigHandlerFunction)
		if (signal(signalToRegister, SIGINT_handler) == SIG_ERR)
  		printf("\ncan't catch %d\n",signalToRegister);

	int portNum=1234;
	for(int param_num=0;param_num<argc;++param_num){
		if(strcmp(argv[param_num],"-i")==0)
			interactive=true;
		if(strcmp(argv[param_num],"-p")==0)
			portNum=atoi(argv[param_num+1]);
	}
	std::cout<<"Server starting on port "<<portNum<<" using bash ";
	if(!interactive)
		std::cout<<"non-";
	std::cout<<"interactive mode."<<std::endl;
	pthread_t tid;
	int fd=socket(PF_INET,SOCK_STREAM,0);
	int on=1;
	struct sockaddr_in sa;//server addr

	sa.sin_family=PF_INET;
	sa.sin_port=htons(portNum);
	sa.sin_addr.s_addr=INADDR_ANY;
	setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,(char*)&on,sizeof(on));//zeby zwalnial port po zabiciu procesu
	bind(fd,(struct sockaddr*)&sa,sizeof(sa));
	listen(fd,15);
	while(1){
		struct cln* c=(cln*)malloc(sizeof(struct cln));
		int wielkosc_sockaddr=sizeof(c->caddr);
		c->cfd=accept(fd,(struct sockaddr*)&c->caddr,(socklen_t*)&wielkosc_sockaddr);
		pthread_create(&tid,NULL,cthread,c);
		pthread_detach(tid);
	}
	close(fd);
	return 0;
}

krwq napisał(a):

czemu nie SSH?
Taki sobie wybrałem temat na sieciach.

2

Jak dostajesz stopped wydaj komendę fg; pomoże, ale nie tędy droga.
W skrócie: grupa obrywa SIGTTIN; Ponoć ksh jest grzeczniejszy od basha i tak się nie zachowuje, nie sprawdzałem ;)

Żeby mieć ogląd co się dzieje
Odpal w jednym terminalu strace server -i
(ew. do strace'a przekaż jeszcze -D -o nazwa.logu)
w drugim to:
while ( 1 ); do; ps axf | egrep server|bash; sleep 2; clear; ; done

Literatura:
http://unix.stackexchange.com/questions/59793/what-should-interactive-shells-do-in-orphaned-process-groups
http://stackoverflow.com/questions/13718394/what-should-interactive-shells-do-in-orphaned-process-groups
http://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html

Pozdrawiam :)

1

Midnight engineering cholera jasna, noc ma swoją moc ;) Postudiowałem źródła telnetd i tam jest takie cudo o nazwie forkpty, które dla widelca ( ;) ) od razy tworzy pseudoterminal który tenże kontroluje.

#include <pty.h>

dla wywołania gcc -lutil

i sam widołek (zamiast forka, przynajmniej w opcji i ;)):

int master_term_fd;
pid=forkpty(&master_term_fd, NULL, NULL, NULL);

EDIT: A tak poza tematem, to przemyśl koncepcję. Np. te pthready średnio są tu potrzebne, żeby nie powiedzieć groźne; do zmiennej c piszesz w threadach i w mainie. Ratuje Cię tu tak naprawdę blokujący accept w while'u. Dodasz muteks, to przy forkowaniu możesz mieć lipę. No i deskryptor c->fd możesz zamknąć bo zrobieniu dup2 w forku... Ponadto: nie daję głowy, ale na boost::asio nie poszłoby toto sprawniej? Unikaj signala, preferowane jest sigaction...

1 użytkowników online, w tym zalogowanych: 0, gości: 1