Compare commits
	
		
			No commits in common. "53cc2a08ce05941a1321d0aab0bcc1bc713299b1" and "4c6087fe9db057e6f93fa79118ffcd4ac0cd90dd" have entirely different histories.
		
	
	
		
			53cc2a08ce
			...
			4c6087fe9d
		
	
		
					 4 changed files with 133 additions and 141 deletions
				
			
		|  | @ -12,7 +12,7 @@ endif() | ||||||
| project ("shell-server") | project ("shell-server") | ||||||
| 
 | 
 | ||||||
| # Добавьте источник в исполняемый файл этого проекта. | # Добавьте источник в исполняемый файл этого проекта. | ||||||
| add_executable (shell-server "main.c" "server.c" "server.h" "client.c" "client.h") | add_executable (shell-server "main.c" "server.c" "server.h") | ||||||
| 
 | 
 | ||||||
| if (CMAKE_VERSION VERSION_GREATER 3.12) | if (CMAKE_VERSION VERSION_GREATER 3.12) | ||||||
|   set_property(TARGET shell-server PROPERTY CXX_STANDARD 20) |   set_property(TARGET shell-server PROPERTY CXX_STANDARD 20) | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								main.c
									
										
									
									
									
								
							
							
						
						
									
										35
									
								
								main.c
									
										
									
									
									
								
							|  | @ -3,38 +3,31 @@ | ||||||
| 
 | 
 | ||||||
| #include "server.h" | #include "server.h" | ||||||
| 
 | 
 | ||||||
| int _tmain(int argc, char *argv[]) { | void PrintHelpMessage() { | ||||||
|  |     printf("Usage error: I know 3 arguments \n\t\"install\"\n\t\"delete\"\n\t\"app\"\n"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | int _tmain(int argc, TCHAR *argv[]) { | ||||||
|  |     printf("\n->Start of parent execution.\n"); | ||||||
|     if (argc == 2) { |     if (argc == 2) { | ||||||
|         if (lstrcmpi(argv[1], TEXT("-s")) == 0) { |         if (lstrcmpi(argv[1], TEXT("-c")) == 0) { | ||||||
|  |             return 0; | ||||||
|  |         } | ||||||
|  |         else if (lstrcmpi(argv[1], TEXT("-s")) == 0) { | ||||||
|             StartShellServer(); |             StartShellServer(); | ||||||
|             return 0; |             return 0; | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             goto help_message; |             PrintHelpMessage(); | ||||||
|  |             return -1; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|     else if (argc == 3) { |  | ||||||
|         if (lstrcmpi(argv[1], TEXT("-c")) == 0) { |  | ||||||
|             StartShellClient(argv[2]); |  | ||||||
|             return 0; |  | ||||||
|         } |  | ||||||
|         // TODO: implement service
 |  | ||||||
|         else if (lstrcmpi(argv[1], TEXT("-s")) == 0 && lstrcmpi(argv[2], TEXT("-service")) == 0) { |  | ||||||
|             // CreateService
 |  | ||||||
|             return 0; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             goto help_message; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     else { |     else { | ||||||
|         goto help_message; |         PrintHelpMessage(); | ||||||
|  |         return -1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return 0; |     return 0; | ||||||
| help_message: |  | ||||||
|     printf("Wrong usage\nUsage: %s [-c {remote ip} | -s [-service]]\n", argv[0]); |  | ||||||
|     return -1; |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										225
									
								
								server.c
									
										
									
									
									
								
							
							
						
						
									
										225
									
								
								server.c
									
										
									
									
									
								
							|  | @ -1,32 +1,77 @@ | ||||||
| #include "server.h" | #include "server.h" | ||||||
| 
 | 
 | ||||||
| #define BUFSIZE 4096 | #define BUFSIZE 4096 | ||||||
|  | #define DEFAULT_PORT "50113" | ||||||
| #define DEFAULT_BUFLEN 512 | #define DEFAULT_BUFLEN 512 | ||||||
| 
 | 
 | ||||||
| typedef struct PipeThreadInfo_t { | // Handles for child process STDIN and STDOUT
 | ||||||
|     HANDLE Pipe; | HANDLE g_hChildStd_IN_Rd = NULL; | ||||||
|     SOCKET ClientSocket; | HANDLE g_hChildStd_IN_Wr = NULL; | ||||||
| } PipeThreadInfo; | HANDLE g_hChildStd_OUT_Rd = NULL; | ||||||
|  | HANDLE g_hChildStd_OUT_Wr = NULL; | ||||||
| 
 | 
 | ||||||
| void CreateChildProcess(HANDLE g_hChildStd_IN_Rd, HANDLE g_hChildStd_OUT_Wr); | SOCKET ClientSocket = INVALID_SOCKET; | ||||||
| DWORD WINAPI WorkWithClient(LPVOID lpParam); | 
 | ||||||
| DWORD WINAPI WriteToPipe(LPVOID lpParam); | //HANDLE g_hInputFile = NULL;
 | ||||||
| DWORD WINAPI ReadFromPipe(LPVOID lpParam); |  | ||||||
| void ErrorExit(PCTSTR); |  | ||||||
| 
 | 
 | ||||||
| void StartShellServer() { | void StartShellServer() { | ||||||
|     printf("\n->Start of shell server execution.\n"); |     printf("\n->Start of shell server execution.\n"); | ||||||
| 
 | 
 | ||||||
|  |     CreatePipes(); | ||||||
|  |     CreateSocket(); | ||||||
|  | 
 | ||||||
|  |     // Create the child process. 
 | ||||||
|  |     CreateChildProcess(); | ||||||
|  | 
 | ||||||
|  |     // TODO: split into threads
 | ||||||
|  |     WriteToPipe(); | ||||||
|  |     // Read from pipe that is the standard output for child process. 
 | ||||||
|  |     ReadFromPipe(); | ||||||
|  | 
 | ||||||
|  |     printf("\n->End of shell server execution.\n"); | ||||||
|  | 
 | ||||||
|  |     // The remaining open handles are cleaned up when this process terminates. 
 | ||||||
|  |     // To avoid resource leaks in a larger application, close handles explicitly. 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void CreatePipes() { | ||||||
|  |     SECURITY_ATTRIBUTES saAttr; | ||||||
|  | 
 | ||||||
|  |     // Set the bInheritHandle flag so pipe handles are inherited. 
 | ||||||
|  | 
 | ||||||
|  |     saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); | ||||||
|  |     saAttr.bInheritHandle = TRUE; | ||||||
|  |     saAttr.lpSecurityDescriptor = NULL; | ||||||
|  | 
 | ||||||
|  |     // Create a pipe for the child process's STDOUT. 
 | ||||||
|  | 
 | ||||||
|  |     if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0)) | ||||||
|  |         ErrorExit(TEXT("StdoutRd CreatePipe")); | ||||||
|  | 
 | ||||||
|  |     // Ensure the read handle to the pipe for STDOUT is not inherited.
 | ||||||
|  | 
 | ||||||
|  |     if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) | ||||||
|  |         ErrorExit(TEXT("Stdout SetHandleInformation")); | ||||||
|  | 
 | ||||||
|  |     // Create a pipe for the child process's STDIN. 
 | ||||||
|  | 
 | ||||||
|  |     if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0)) | ||||||
|  |         ErrorExit(TEXT("Stdin CreatePipe")); | ||||||
|  | 
 | ||||||
|  |     // Ensure the write handle to the pipe for STDIN is not inherited. 
 | ||||||
|  | 
 | ||||||
|  |     if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0)) | ||||||
|  |         ErrorExit(TEXT("Stdin SetHandleInformation")); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: to review
 | ||||||
|  | void CreateSocket() { | ||||||
|     WSADATA wsaData; |     WSADATA wsaData; | ||||||
|     struct addrinfo* result = NULL, * ptr = NULL, hints; |     struct addrinfo* result = NULL, * ptr = NULL, hints; | ||||||
|     int iResult; |  | ||||||
| 
 |  | ||||||
|     SOCKET ClientSocket = INVALID_SOCKET; |  | ||||||
|     SOCKET ListenSocket = INVALID_SOCKET; |     SOCKET ListenSocket = INVALID_SOCKET; | ||||||
| 
 | 
 | ||||||
|     /*
 |     int iResult; | ||||||
|     * Initialize listening socket |  | ||||||
|     */ |  | ||||||
| 
 | 
 | ||||||
|     // Initialize Winsock
 |     // Initialize Winsock
 | ||||||
|     iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); |     iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); | ||||||
|  | @ -80,87 +125,22 @@ void StartShellServer() { | ||||||
|         return 1; |         return 1; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /*
 |     // Accepting a connection
 | ||||||
|     * Process client connection |     ClientSocket = INVALID_SOCKET; | ||||||
|     */ |  | ||||||
|     for (;;) { |  | ||||||
|         // Accepting a client connection
 |  | ||||||
|         ClientSocket = INVALID_SOCKET; |  | ||||||
| 
 | 
 | ||||||
|         ClientSocket = accept(ListenSocket, NULL, NULL); |     // Accept a client socket
 | ||||||
|         if (ClientSocket == INVALID_SOCKET) { |     ClientSocket = accept(ListenSocket, NULL, NULL); | ||||||
|             printf("accept failed: %d\n", WSAGetLastError()); |     if (ClientSocket == INVALID_SOCKET) { | ||||||
|             break; |         printf("accept failed: %d\n", WSAGetLastError()); | ||||||
|         } |         closesocket(ListenSocket); | ||||||
|         printf("Client connected\n"); |         WSACleanup(); | ||||||
|         // Create separate thread to process client connection
 |         return 1; | ||||||
|         CreateThread(NULL, 0, WorkWithClient, ClientSocket, 0, NULL); |  | ||||||
|     } |     } | ||||||
| 
 |     // No longer needed
 | ||||||
|     /*
 |  | ||||||
|     * Finalization |  | ||||||
|     */ |  | ||||||
|     // TODO: properly close all handles
 |  | ||||||
|     closesocket(ListenSocket); |     closesocket(ListenSocket); | ||||||
|     WSACleanup(); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| DWORD WINAPI WorkWithClient(LPVOID lpParam) { | void CreateChildProcess() | ||||||
|     /*
 |  | ||||||
|     * Client variables |  | ||||||
|     */ |  | ||||||
|     // Pipe handles for child STDIN/STDOUT
 |  | ||||||
|     HANDLE g_hChildStd_IN_Rd = NULL; |  | ||||||
|     HANDLE g_hChildStd_IN_Wr = NULL; |  | ||||||
|     HANDLE g_hChildStd_OUT_Rd = NULL; |  | ||||||
|     HANDLE g_hChildStd_OUT_Wr = NULL; |  | ||||||
| 
 |  | ||||||
|     SECURITY_ATTRIBUTES saAttr; |  | ||||||
|     HANDLE PipeThreads[2]; |  | ||||||
| 
 |  | ||||||
|     SOCKET ClientSocket = (SOCKET)lpParam; |  | ||||||
| 
 |  | ||||||
|     /*
 |  | ||||||
|     * Initialize pipes |  | ||||||
|     */ |  | ||||||
|     // Set the bInheritHandle flag so pipe handles are inherited. 
 |  | ||||||
|     saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); |  | ||||||
|     saAttr.bInheritHandle = TRUE; |  | ||||||
|     saAttr.lpSecurityDescriptor = NULL; |  | ||||||
| 
 |  | ||||||
|     // Create a pipe for the child process's STDOUT. 
 |  | ||||||
|     if (!CreatePipe(&g_hChildStd_OUT_Rd, &g_hChildStd_OUT_Wr, &saAttr, 0)) |  | ||||||
|         ErrorExit(TEXT("StdoutRd CreatePipe")); |  | ||||||
| 
 |  | ||||||
|     // Ensure the read handle to the pipe for STDOUT is not inherited.
 |  | ||||||
|     if (!SetHandleInformation(g_hChildStd_OUT_Rd, HANDLE_FLAG_INHERIT, 0)) |  | ||||||
|         ErrorExit(TEXT("Stdout SetHandleInformation")); |  | ||||||
| 
 |  | ||||||
|     // Create a pipe for the child process's STDIN. 
 |  | ||||||
|     if (!CreatePipe(&g_hChildStd_IN_Rd, &g_hChildStd_IN_Wr, &saAttr, 0)) |  | ||||||
|         ErrorExit(TEXT("Stdin CreatePipe")); |  | ||||||
| 
 |  | ||||||
|     // Ensure the write handle to the pipe for STDIN is not inherited. 
 |  | ||||||
|     if (!SetHandleInformation(g_hChildStd_IN_Wr, HANDLE_FLAG_INHERIT, 0)) |  | ||||||
|         ErrorExit(TEXT("Stdin SetHandleInformation")); |  | ||||||
| 
 |  | ||||||
|     /*
 |  | ||||||
|     * Create child process cmd.exe |  | ||||||
|     */ |  | ||||||
|     CreateChildProcess(g_hChildStd_IN_Rd, g_hChildStd_OUT_Wr); |  | ||||||
| 
 |  | ||||||
|     // Create threads for STDIN/STDOUT
 |  | ||||||
|     PipeThreadInfo WriteThreadInfo = { g_hChildStd_IN_Wr, ClientSocket }; |  | ||||||
|     PipeThreadInfo ReadThreadInfo = { g_hChildStd_OUT_Rd, ClientSocket }; |  | ||||||
|     PipeThreads[0] = CreateThread(NULL, 0, WriteToPipe, &WriteThreadInfo, 0, NULL); |  | ||||||
|     PipeThreads[1] = CreateThread(NULL, 0, ReadFromPipe, &ReadThreadInfo, 0, NULL); |  | ||||||
| 
 |  | ||||||
|     WaitForMultipleObjects(2, PipeThreads, TRUE, INFINITE); |  | ||||||
| 
 |  | ||||||
|     printf("\n->Client disconnected.\n"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| void CreateChildProcess(HANDLE g_hChildStd_IN_Rd, HANDLE g_hChildStd_OUT_Wr) |  | ||||||
| // Create a child process that uses the previously created pipes for STDIN and STDOUT.
 | // Create a child process that uses the previously created pipes for STDIN and STDOUT.
 | ||||||
| { | { | ||||||
|     TCHAR szCmdline[] = TEXT("C:\\Windows\\System32\\cmd.exe"); |     TCHAR szCmdline[] = TEXT("C:\\Windows\\System32\\cmd.exe"); | ||||||
|  | @ -215,31 +195,36 @@ void CreateChildProcess(HANDLE g_hChildStd_IN_Rd, HANDLE g_hChildStd_OUT_Wr) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| DWORD WINAPI WriteToPipe(LPVOID lpParam) | void WriteToPipe(void) | ||||||
|  | 
 | ||||||
|  | // Read from a file and write its contents to the pipe for the child's STDIN.
 | ||||||
|  | // Stop when there is no more data. 
 | ||||||
| { | { | ||||||
|     DWORD dwRead, dwWritten; |     DWORD dwRead, dwWritten; | ||||||
|  |     //CHAR chBuf[BUFSIZE] = "dir\r\n";
 | ||||||
|     BOOL bSuccess = FALSE; |     BOOL bSuccess = FALSE; | ||||||
|     int i = 0; |     int i = 0; | ||||||
| 
 | 
 | ||||||
|     SOCKET ClientSocket = ((PipeThreadInfo*)lpParam)->ClientSocket; |  | ||||||
|     HANDLE g_hChildStd_IN_Wr = ((PipeThreadInfo*)lpParam)->Pipe; |  | ||||||
| 
 |  | ||||||
|     char recvbuf[DEFAULT_BUFLEN]; |     char recvbuf[DEFAULT_BUFLEN]; | ||||||
|     int iResult, iSendResult; |     int iResult, iSendResult; | ||||||
|     int recvbuflen = DEFAULT_BUFLEN; |     int recvbuflen = DEFAULT_BUFLEN; | ||||||
|      |      | ||||||
|     // Change codepage to UTF-8
 |  | ||||||
|     //CHAR chcpCommand[] = "chcp 65001\n";
 |  | ||||||
|     //bSuccess = WriteFile(g_hChildStd_IN_Wr, chcpCommand, strlen(chcpCommand), &dwWritten, NULL);
 |  | ||||||
|     //if (!bSuccess) goto WriteToPipe_end;
 |  | ||||||
| 
 | 
 | ||||||
|     do |     do | ||||||
|     { |     { | ||||||
|         iResult = recv(ClientSocket, recvbuf, recvbuflen, 0); |         iResult = recv(ClientSocket, recvbuf, recvbuflen, 0); | ||||||
|         if (iResult > 0) { |         if (iResult > 0) { | ||||||
|             bSuccess = WriteFile(g_hChildStd_IN_Wr, recvbuf, iResult, &dwWritten, NULL); |             printf("Bytes received: %d\n", iResult); | ||||||
|             if (!bSuccess) break; | 
 | ||||||
|             printf("Received command: %s\n"); |             // Echo the buffer back to the sender
 | ||||||
|  |             iSendResult = send(ClientSocket, recvbuf, iResult, 0); | ||||||
|  |             if (iSendResult == SOCKET_ERROR) { | ||||||
|  |                 printf("send failed: %d\n", WSAGetLastError()); | ||||||
|  |                 closesocket(ClientSocket); | ||||||
|  |                 WSACleanup(); | ||||||
|  |                 return 1; | ||||||
|  |             } | ||||||
|  |             printf("Bytes sent: %d\n", iSendResult); | ||||||
|         } |         } | ||||||
|         else if (iResult == 0) |         else if (iResult == 0) | ||||||
|             printf("Connection closing...\n"); |             printf("Connection closing...\n"); | ||||||
|  | @ -249,34 +234,40 @@ DWORD WINAPI WriteToPipe(LPVOID lpParam) | ||||||
|             WSACleanup(); |             WSACleanup(); | ||||||
|             return 1; |             return 1; | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         //dwRead = strlen(chBuf) + 1;
 | ||||||
|  |         bSuccess = WriteFile(g_hChildStd_IN_Wr, recvbuf, iResult, &dwWritten, NULL); | ||||||
|  |         if (!bSuccess) break; | ||||||
|  |         //break;
 | ||||||
|  |         //dwRead = strlen(chBuf) + 1;
 | ||||||
|  |         //bSuccess = WriteFile(g_hChildStd_IN_Wr, chBuf, dwRead, &dwWritten, NULL);
 | ||||||
|  |         //if (!bSuccess) break;
 | ||||||
|  |         ReadFromPipe(); | ||||||
|     } while (iResult > 0); |     } while (iResult > 0); | ||||||
| 
 | 
 | ||||||
| WriteToPipe_end: |     // Close the pipe handle so the child process stops reading. 
 | ||||||
|     // Closing STDIN => cmd.exe exit
 | 
 | ||||||
|     if (!CloseHandle(g_hChildStd_IN_Wr)) |     if (!CloseHandle(g_hChildStd_IN_Wr)) | ||||||
|         ErrorExit(TEXT("StdInWr CloseHandle")); |         ErrorExit(TEXT("StdInWr CloseHandle")); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| DWORD WINAPI ReadFromPipe(LPVOID lpParam) | void ReadFromPipe(void) | ||||||
| { |  | ||||||
|     DWORD dwRead; |  | ||||||
|     CHAR chBuf[BUFSIZE + 1]; |  | ||||||
| 
 | 
 | ||||||
|  | // Read output from the child process's pipe for STDOUT
 | ||||||
|  | // and write to the parent process's pipe for STDOUT. 
 | ||||||
|  | // Stop when there is no more data. 
 | ||||||
|  | { | ||||||
|  |     DWORD dwRead, dwWritten; | ||||||
|  |     CHAR chBuf[BUFSIZE]; | ||||||
|     BOOL bSuccess = FALSE; |     BOOL bSuccess = FALSE; | ||||||
|     //HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
 |     HANDLE hParentStdOut = GetStdHandle(STD_OUTPUT_HANDLE); | ||||||
|     SOCKET ClientSocket = ((PipeThreadInfo*)lpParam)->ClientSocket; |  | ||||||
|     HANDLE g_hChildStd_OUT_Rd = ((PipeThreadInfo*)lpParam)->Pipe; |  | ||||||
| 
 | 
 | ||||||
|     int iSendResult; |     int iSendResult; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     for (;;) |     for (;;) | ||||||
|     { |     { | ||||||
|         bSuccess = ReadFile(g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL); |         bSuccess = ReadFile(g_hChildStd_OUT_Rd, chBuf, BUFSIZE, &dwRead, NULL); | ||||||
|         if (!bSuccess || dwRead == 0) break; |         if (!bSuccess || dwRead == 0) break; | ||||||
|         printf("STDOUT: %s\n", chBuf); |  | ||||||
|         |  | ||||||
|         chBuf[dwRead] = '\0'; |  | ||||||
| 
 | 
 | ||||||
|         iSendResult = send(ClientSocket, chBuf, dwRead, 0); |         iSendResult = send(ClientSocket, chBuf, dwRead, 0); | ||||||
|         if (iSendResult == SOCKET_ERROR) { |         if (iSendResult == SOCKET_ERROR) { | ||||||
|  | @ -285,7 +276,11 @@ DWORD WINAPI ReadFromPipe(LPVOID lpParam) | ||||||
|             WSACleanup(); |             WSACleanup(); | ||||||
|             return 1; |             return 1; | ||||||
|         } |         } | ||||||
|  |         // Write to console
 | ||||||
|  |         bSuccess = WriteFile(hParentStdOut, chBuf, | ||||||
|  |             dwRead, &dwWritten, NULL); | ||||||
|         if (!bSuccess) break; |         if (!bSuccess) break; | ||||||
|  |         break; | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										8
									
								
								server.h
									
										
									
									
									
								
							
							
						
						
									
										8
									
								
								server.h
									
										
									
									
									
								
							|  | @ -12,7 +12,11 @@ | ||||||
| #include <winsock2.h> | #include <winsock2.h> | ||||||
| #include <ws2tcpip.h> | #include <ws2tcpip.h> | ||||||
| 
 | 
 | ||||||
| #define DEFAULT_PORT "50113" |  | ||||||
| 
 |  | ||||||
| // Defined functions
 | // Defined functions
 | ||||||
| void StartShellServer(); | void StartShellServer(); | ||||||
|  | void CreatePipes(); | ||||||
|  | void CreateChildProcess(void); | ||||||
|  | void WriteToPipe(void); | ||||||
|  | void ReadFromPipe(void); | ||||||
|  | void ErrorExit(PCTSTR); | ||||||
|  | void CreateSocket(); | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue