/* * Copyright (c) 2017 Cisco and/or its affiliates. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * NB: binds to all interfaces on the listen port, which might be a security issue. * * The CLI runs as an event managed listener. The Api here creates, starts, stops, and destroys it. * * The CLI is a user interface to the programmatic interface in metis_Configuration.h * */ #include #include #include #include #include #include #include #include #include #include "metis_CommandLineInterface.h" struct metis_command_line_interface { MetisForwarder *metis; PARCEventSocket *listener; PARCArrayList *openSessions; uint16_t port; }; typedef struct metis_cli_session { MetisCommandLineInterface *parentCli; MetisSocketType clientSocket; struct sockaddr *clientAddress; int clientAddressLength; PARCEventQueue *streamBuffer; bool doingTheRightThing; } _MetisCommandLineInterface_Session; struct metis_cli_command; typedef struct metis_cli_command _MetisCommandLineInterface_Command; struct metis_cli_command { char *text; char *helpDescription; void (*func)(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params); }; static void _metisCommandLineInterface_ListenerCallback(MetisSocketType client_socket, struct sockaddr *client_addr, int socklen, void *user_data); static _MetisCommandLineInterface_Session *metisCliSession_Create(MetisCommandLineInterface *cli, MetisSocketType client_socket, struct sockaddr *client_addr, int socklen); static void _metisCliSession_Destory(_MetisCommandLineInterface_Session **cliSessionPtr); static void _metisCliSession_ReadCallback(PARCEventQueue *event, PARCEventType type, void *cliSessionVoid); static void _metisCliSession_EventCallback(PARCEventQueue *event, PARCEventQueueEventType what, void *cliSessionVoid); static bool _metisCliSession_ProcessCommand(_MetisCommandLineInterface_Session *session, char *cmdline); static void _metisCliSession_DisplayMotd(_MetisCommandLineInterface_Session *session); static void _metisCliSession_DisplayPrompt(_MetisCommandLineInterface_Session *session); // used by PARCArrayList static void _session_VoidDestroyer(void **cliSessionVoidPtr) { _MetisCommandLineInterface_Session **cliSessionPtr = (_MetisCommandLineInterface_Session **) cliSessionVoidPtr; (*cliSessionPtr)->doingTheRightThing = true; _metisCliSession_Destory(cliSessionPtr); } // ==================================================================================== static void _cmd_Help(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params); static void _cmd_Show(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params); static void _cmd_Exit(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params); static void _cmd_Tunnel(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params); static void _cmd_Version(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params); /** * @typedef _cliCommands * The commands, their short help, and their function pointer * @constant <#name#> <#description#> * List must be terminated with a NULL entry * * Example: * @code * <#example#> * @endcode */ static _MetisCommandLineInterface_Command _cliCommands[] = { { "exit", "Ends the session", _cmd_Exit }, { "help", "Displays the help menu", _cmd_Help }, { "show", "Displays state", _cmd_Show }, { "tunnel", "manage tunnels", _cmd_Tunnel }, { "ver", "Forwarder version", _cmd_Version }, { NULL, NULL, NULL } }; // ==================================================================================== MetisCommandLineInterface * metisCommandLineInterface_Create(MetisForwarder *metis, uint16_t port) { MetisCommandLineInterface *cli = parcMemory_AllocateAndClear(sizeof(MetisCommandLineInterface)); assertNotNull(cli, "parcMemory_AllocateAndClear(%zu) returned NULL", sizeof(MetisCommandLineInterface)); cli->port = port; cli->listener = NULL; cli->metis = metis; cli->openSessions = parcArrayList_Create(_session_VoidDestroyer); return cli; } void metisCommandLineInterface_Start(MetisCommandLineInterface *cli) { // listen address struct sockaddr_in6 addr6; memset(&addr6, 0, sizeof(addr6)); addr6.sin6_family = PF_INET6; addr6.sin6_port = htons(cli->port); MetisDispatcher *dispatcher = metisForwarder_GetDispatcher(cli->metis); PARCEventSocket *listener = metisDispatcher_CreateListener(dispatcher, _metisCommandLineInterface_ListenerCallback, cli, -1, (struct sockaddr *) &addr6, sizeof(addr6)); assertNotNull(listener, "Got null listener"); cli->listener = listener; } void metisCommandLineInterface_Destroy(MetisCommandLineInterface **cliPtr) { assertNotNull(cliPtr, "Parameter must be non-null double pointer"); assertNotNull(*cliPtr, "Parameter must dereference to non-null pointer"); MetisCommandLineInterface *cli = *cliPtr; parcArrayList_Destroy(&cli->openSessions); if (cli->listener) { MetisDispatcher *dispatcher = metisForwarder_GetDispatcher(cli->metis); metisDispatcher_DestroyListener(dispatcher, &(cli->listener)); } parcMemory_Deallocate((void **) &cli); *cliPtr = NULL; } /** * Creates a client-specific session * * <#Discussion#> * * @param <#param1#> * @return <#return#> * * Example: * @code * <#example#> * @endcode */ static _MetisCommandLineInterface_Session * metisCliSession_Create(MetisCommandLineInterface *cli, MetisSocketType clientSocket, struct sockaddr *clientAddress, int clientAddressLength) { _MetisCommandLineInterface_Session *session = parcMemory_AllocateAndClear(sizeof(_MetisCommandLineInterface_Session)); assertNotNull(session, "parcMemory_AllocateAndClear(%zu) returned NULL", sizeof(_MetisCommandLineInterface_Session)); session->parentCli = cli; session->clientAddress = parcMemory_Allocate(clientAddressLength); assertNotNull(session->clientAddress, "parcMemory_Allocate(%d) returned NULL", clientAddressLength); session->clientAddressLength = clientAddressLength; session->clientSocket = clientSocket; memcpy(session->clientAddress, clientAddress, clientAddressLength); MetisDispatcher *dispatcher = metisForwarder_GetDispatcher(cli->metis); PARCEventScheduler *eventBase = metisDispatcher_GetEventScheduler(dispatcher); session->streamBuffer = parcEventQueue_Create(eventBase, clientSocket, PARCEventQueueOption_CloseOnFree | PARCEventQueueOption_DeferCallbacks); parcEventQueue_SetCallbacks(session->streamBuffer, _metisCliSession_ReadCallback, NULL, _metisCliSession_EventCallback, session); parcEventQueue_Enable(session->streamBuffer, PARCEventType_Read); return session; } /** * SHOULD ONLY BE CALLED FROM ARRAYLIST * * Do not call this on your own!! It should only be called when an * item is removed from the cli->openSessions array list. * * Will close the tcp session and free memory. * * @param <#param1#> * * Example: * @code * <#example#> * @endcode */ static void _metisCliSession_Destory(_MetisCommandLineInterface_Session **cliSessionPtr) { assertNotNull(cliSessionPtr, "Parameter must be non-null double pointer"); assertNotNull(*cliSessionPtr, "Parameter must dereference to non-null pointer"); _MetisCommandLineInterface_Session *session = *cliSessionPtr; assertTrue(session->doingTheRightThing, "Ha! caught you! You called Destroy outside the PARCArrayList"); parcEventQueue_Destroy(&(session->streamBuffer)); parcMemory_Deallocate((void **) &(session->clientAddress)); parcMemory_Deallocate((void **) &session); *cliSessionPtr = NULL; } /** * Called on a new connection to the server socket * * Will allocate a new _MetisCommandLineInterface_Session and put it in the * server's PARCArrayList * * @param <#param1#> * @return <#return#> * * Example: * @code * <#example#> * @endcode */ static void _metisCommandLineInterface_ListenerCallback(MetisSocketType client_socket, struct sockaddr *client_addr, int socklen, void *user_data) { MetisCommandLineInterface *cli = (MetisCommandLineInterface *) user_data; _MetisCommandLineInterface_Session *session = metisCliSession_Create(cli, client_socket, client_addr, socklen); parcArrayList_Add(cli->openSessions, session); _metisCliSession_DisplayMotd(session); _metisCliSession_DisplayPrompt(session); } static void _metisCliSession_ReadCallback(PARCEventQueue *event, PARCEventType type, void *cliSessionVoid) { assertTrue(type == PARCEventType_Read, "Illegal type: expected read event, got %d\n", type); _MetisCommandLineInterface_Session *session = (_MetisCommandLineInterface_Session *) cliSessionVoid; PARCEventBuffer *input = parcEventBuffer_GetQueueBufferInput(event); while (parcEventBuffer_GetLength(input) > 0) { size_t readLength = 0; char *cmdline = parcEventBuffer_ReadLine(input, &readLength); if (cmdline == NULL) { // we have run out of input, we're done parcEventBuffer_Destroy(&input); return; } // we have a whole command line bool success = _metisCliSession_ProcessCommand(session, cmdline); parcEventBuffer_FreeLine(input, &cmdline); if (!success) { // the session is dead parcEventBuffer_Destroy(&input); return; } _metisCliSession_DisplayPrompt(session); } parcEventBuffer_Destroy(&input); } static void _metisCommandLineInterface_RemoveSession(MetisCommandLineInterface *cli, _MetisCommandLineInterface_Session *session) { size_t length = parcArrayList_Size(cli->openSessions); for (size_t i = 0; i < length; i++) { _MetisCommandLineInterface_Session *x = parcArrayList_Get(cli->openSessions, i); if (x == session) { // removing from list will call the session destroyer parcArrayList_RemoveAndDestroyAtIndex(cli->openSessions, i); return; } } assertTrue(0, "Illegal state: did not find a session in openSessions %p", (void *) session); } static void _metisCliSession_EventCallback(PARCEventQueue *event, PARCEventQueueEventType what, void *cliSessionVoid) { _MetisCommandLineInterface_Session *session = (_MetisCommandLineInterface_Session *) cliSessionVoid; if (what & PARCEventQueueEventType_Error) { MetisCommandLineInterface *cli = session->parentCli; _metisCommandLineInterface_RemoveSession(cli, session); } } static void _metisCliSession_DisplayMotd(_MetisCommandLineInterface_Session *session) { parcEventQueue_Printf(session->streamBuffer, "Metis Forwarder CLI\n"); parcEventQueue_Printf(session->streamBuffer, "Copyright (c) 2017 Cisco and/or its affiliates.\n\n"); parcEventQueue_Flush(session->streamBuffer, PARCEventType_Write); } static void _metisCliSession_DisplayPrompt(_MetisCommandLineInterface_Session *session) { parcEventQueue_Printf(session->streamBuffer, "metis> "); parcEventQueue_Flush(session->streamBuffer, PARCEventType_Write); } /** * Process commands until there's not a whole line (upto CRLF) * * <#Discussion#> * * @param <#param1#> * @return false if the session died, true if its still going * * Example: * @code * <#example#> * @endcode */ static bool _metisCliSession_ProcessCommand(_MetisCommandLineInterface_Session *session, char *cmdline) { // parse out the first word. The NULL goes in after a space or tab. // "cmd" will be the string prior to the NULL, and "cmdline" will be what's after the NULL. char *cmd = strsep(&cmdline, " \t"); // there's a secret command for unit testing if (strcasecmp(cmd, "~~") == 0) { parcEventQueue_Printf(session->streamBuffer, "success: %s\n", cmdline); return true; } for (int i = 0; _cliCommands[i].text != NULL; i++) { if (strcasecmp(_cliCommands[i].text, cmd) == 0) { _cliCommands[i].func(session, &_cliCommands[i], cmdline); if (_cliCommands[i].func == _cmd_Exit) { return false; } return true; } } // could not find command, print the help menu parcEventQueue_Printf(session->streamBuffer, "Unrecognized command: %s, displaying help menu\n", cmdline); _cmd_Help(session, NULL, NULL); return true; } static void _cmd_Help(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params) { for (int i = 0; _cliCommands[i].text != NULL; i++) { parcEventQueue_Printf(session->streamBuffer, "%-8s %s\n", _cliCommands[i].text, _cliCommands[i].helpDescription); } } static void _cmd_Show(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params) { parcEventQueue_Printf(session->streamBuffer, "not implemented\n"); } static void _cmd_Tunnel(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params) { parcEventQueue_Printf(session->streamBuffer, "not implemented\n"); } static void _cmd_Exit(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params) { parcEventQueue_Printf(session->streamBuffer, "Exiting session, goodby\n\n"); parcEventQueue_Flush(session->streamBuffer, PARCEventType_Write); _metisCommandLineInterface_RemoveSession(session->parentCli, session); } static void _cmd_Version(_MetisCommandLineInterface_Session *session, _MetisCommandLineInterface_Command *command, const char *params) { PARCJSON *versionJson = metisConfiguration_GetVersion(metisForwarder_GetConfiguration(session->parentCli->metis)); char *str = parcJSON_ToString(versionJson); parcEventQueue_Printf(session->streamBuffer, "%s", str); parcMemory_Deallocate((void **) &str); parcJSON_Release(&versionJson); }