diff --git a/dist/css/custom.css b/dist/css/custom.css
index db8bfa00..4b020f29 100644
--- a/dist/css/custom.css
+++ b/dist/css/custom.css
@@ -33,3 +33,17 @@
width: 140px;
float: left;
}
+
+.webconsole {
+ width:100%;
+ height:100%;
+}
+
+#console {
+ height:500px;
+}
+
+.systemtabcontent {
+ height:100%;
+ min-height:500px;
+}
diff --git a/includes/system.php b/includes/system.php
index e68725de..213e87a8 100755
--- a/includes/system.php
+++ b/includes/system.php
@@ -99,44 +99,67 @@ function DisplaySystem(){
?>
-
+
-
System Information
-
Hostname
-
Pi Revision
-
Uptime
-
Memory Used
-
-
CPU Load
-
-
%
-
+
+
+
+
+
+
+
System Information
+
Hostname
+
Pi Revision
+
Uptime
+
Memory Used
+
+
CPU Load
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
diff --git a/includes/webconsole.php b/includes/webconsole.php
new file mode 100644
index 00000000..101574f5
--- /dev/null
+++ b/includes/webconsole.php
@@ -0,0 +1,841 @@
+ 'password1', 'user2' => 'password2');
+$ACCOUNTS = array();
+
+// Password hash algorithm (password must be hashed)
+// Example: $PASSWORD_HASH_ALGORITHM = 'md5';
+// $PASSWORD_HASH_ALGORITHM = 'sha256';
+$PASSWORD_HASH_ALGORITHM = '';
+
+// Home directory (multi-user mode supported)
+// Example: $HOME_DIRECTORY = '/tmp';
+// $HOME_DIRECTORY = array('user1' => '/home/user1', 'user2' => '/home/user2');
+$HOME_DIRECTORY = '';
+
+// Code below is automatically generated from different components
+// For more information see: https://github.com/nickola/web-console
+//
+// Used components:
+// - jQuery JavaScript Library: https://github.com/jquery/jquery
+// - jQuery Terminal Emulator: https://github.com/jcubic/jquery.terminal
+// - jQuery Mouse Wheel Plugin: https://github.com/brandonaaron/jquery-mousewheel
+// - PHP JSON-RPC 2.0 Server/Client Implementation: https://github.com/sergeyfast/eazy-jsonrpc
+// - Normalize.css: https://github.com/necolas/normalize.css
+?>
+ method
+ */
+ protected $instances = array();
+
+ /**
+ * Decoded Json Request
+ * @var object|array
+ */
+ protected $request;
+
+ /**
+ * Array of Received Calls
+ * @var array
+ */
+ protected $calls = array();
+
+ /**
+ * Array of Responses for Calls
+ * @var array
+ */
+ protected $response = array();
+
+ /**
+ * Has Calls Flag (not notifications)
+ * @var bool
+ */
+ protected $hasCalls = false;
+
+ /**
+ * Is Batch Call in using
+ * @var bool
+ */
+ private $isBatchCall = false;
+
+ /**
+ * Hidden Methods
+ * @var array
+ */
+ protected $hiddenMethods = array(
+ 'execute', '__construct', 'registerinstance'
+ );
+
+ /**
+ * Content Type
+ * @var string
+ */
+ public $ContentType = 'application/json';
+
+ /**
+ * Allow Cross-Domain Requests
+ * @var bool
+ */
+ public $IsXDR = true;
+
+ /**
+ * Max Batch Calls
+ * @var int
+ */
+ public $MaxBatchCalls = 10;
+
+ /**
+ * Error Messages
+ * @var array
+ */
+ protected $errorMessages = array(
+ self::ParseError => 'Parse error',
+ self::InvalidRequest => 'Invalid Request',
+ self::MethodNotFound => 'Method not found',
+ self::InvalidParams => 'Invalid params',
+ self::InternalError => 'Internal error',
+ );
+
+
+ /**
+ * Cached Reflection Methods
+ * @var ReflectionMethod[]
+ */
+ private $reflectionMethods = array();
+
+
+ /**
+ * Validate Request
+ * @return int error
+ */
+ private function getRequest() {
+ $error = null;
+
+ do {
+ if ( array_key_exists( 'REQUEST_METHOD', $_SERVER ) && $_SERVER['REQUEST_METHOD'] != 'POST' ) {
+ $error = self::InvalidRequest;
+ break;
+ };
+
+ $request = !empty( $_GET['rawRequest'] ) ? $_GET['rawRequest'] : file_get_contents( 'php://input' );
+ $this->request = json_decode( $request, false );
+ if ( $this->request === null ) {
+ $error = self::ParseError;
+ break;
+ }
+
+ if ( $this->request === array() ) {
+ $error = self::InvalidRequest;
+ break;
+ }
+
+ // check for batch call
+ if ( is_array( $this->request ) ) {
+ if( count( $this->request ) > $this->MaxBatchCalls ) {
+ $error = self::InvalidRequest;
+ break;
+ }
+
+ $this->calls = $this->request;
+ $this->isBatchCall = true;
+ } else {
+ $this->calls[] = $this->request;
+ }
+ } while ( false );
+
+ return $error;
+ }
+
+
+ /**
+ * Get Error Response
+ * @param int $code
+ * @param mixed $id
+ * @param null $data
+ * @return array
+ */
+ private function getError( $code, $id = null, $data = null ) {
+ return array(
+ 'jsonrpc' => '2.0',
+ 'id' => $id,
+ 'error' => array(
+ 'code' => $code,
+ 'message' => isset( $this->errorMessages[$code] ) ? $this->errorMessages[$code] : $this->errorMessages[self::InternalError],
+ 'data' => $data,
+ ),
+ );
+ }
+
+
+ /**
+ * Check for jsonrpc version and correct method
+ * @param object $call
+ * @return array|null
+ */
+ private function validateCall( $call ) {
+ $result = null;
+ $error = null;
+ $data = null;
+ $id = is_object( $call ) && property_exists( $call, 'id' ) ? $call->id : null;
+ do {
+ if ( !is_object( $call ) ) {
+ $error = self::InvalidRequest;
+ break;
+ }
+
+ // hack for inputEx smd tester
+ if ( property_exists( $call, 'version' ) ) {
+ if ( $call->version == 'json-rpc-2.0' ) {
+ $call->jsonrpc = '2.0';
+ }
+ }
+
+ if ( !property_exists( $call, 'jsonrpc' ) || $call->jsonrpc != '2.0' ) {
+ $error = self::InvalidRequest;
+ break;
+ }
+
+ $fullMethod = property_exists( $call, 'method' ) ? $call->method : '';
+ $methodInfo = explode( '.', $fullMethod, 2 );
+ $namespace = array_key_exists( 1, $methodInfo ) ? $methodInfo[0] : '';
+ $method = $namespace ? $methodInfo[1] : $fullMethod;
+ if ( !$method || !array_key_exists( $namespace, $this->instances ) || !method_exists( $this->instances[$namespace], $method ) || in_array( strtolower( $method ), $this->hiddenMethods ) ) {
+ $error = self::MethodNotFound;
+ break;
+ }
+
+ if ( !array_key_exists( $fullMethod, $this->reflectionMethods ) ) {
+ $this->reflectionMethods[$fullMethod] = new ReflectionMethod( $this->instances[$namespace], $method );
+ }
+
+ /** @var $params array */
+ $params = property_exists( $call, 'params' ) ? $call->params : null;
+ $paramsType = gettype( $params );
+ if ( $params !== null && $paramsType != 'array' && $paramsType != 'object' ) {
+ $error = self::InvalidParams;
+ break;
+ }
+
+ // check parameters
+ switch ( $paramsType ) {
+ case 'array':
+ $totalRequired = 0;
+ // doesn't hold required, null, required sequence of params
+ foreach ( $this->reflectionMethods[$fullMethod]->getParameters() as $param ) {
+ if ( !$param->isDefaultValueAvailable() ) {
+ $totalRequired++;
+ }
+ }
+
+ if ( count( $params ) < $totalRequired ) {
+ $error = self::InvalidParams;
+ $data = sprintf( 'Check numbers of required params (got %d, expected %d)', count( $params ), $totalRequired );
+ }
+ break;
+ case 'object':
+ foreach ( $this->reflectionMethods[$fullMethod]->getParameters() as $param ) {
+ if ( !$param->isDefaultValueAvailable() && !array_key_exists( $param->getName(), $params ) ) {
+ $error = self::InvalidParams;
+ $data = $param->getName() . ' not found';
+
+ break 3;
+ }
+ }
+ break;
+ case 'NULL':
+ if ( $this->reflectionMethods[$fullMethod]->getNumberOfRequiredParameters() > 0 ) {
+ $error = self::InvalidParams;
+ $data = 'Empty required params';
+ break 2;
+ }
+ break;
+ }
+
+ } while ( false );
+
+ if ( $error ) {
+ $result = array( $error, $id, $data );
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Process Call
+ * @param $call
+ * @return array|null
+ */
+ private function processCall( $call ) {
+ $id = property_exists( $call, 'id' ) ? $call->id : null;
+ $params = property_exists( $call, 'params' ) ? $call->params : array();
+ $result = null;
+ $namespace = substr( $call->method, 0, strpos( $call->method, '.' ) );
+
+ try {
+ // set named parameters
+ if ( is_object( $params ) ) {
+ $newParams = array();
+ foreach ( $this->reflectionMethods[$call->method]->getParameters() as $param ) {
+ $paramName = $param->getName();
+ $defaultValue = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null;
+ $newParams[] = property_exists( $params, $paramName ) ? $params->$paramName : $defaultValue;
+ }
+
+ $params = $newParams;
+ }
+
+ // invoke
+ $result = $this->reflectionMethods[$call->method]->invokeArgs( $this->instances[$namespace], $params );
+ } catch ( Exception $e ) {
+ return $this->getError( $e->getCode(), $id, $e->getMessage() );
+ }
+
+ if ( !$id && $id !== 0 ) {
+ return null;
+ }
+
+ return array(
+ 'jsonrpc' => '2.0',
+ 'result' => $result,
+ 'id' => $id,
+ );
+ }
+
+
+ /**
+ * Create new Instance
+ * @param object $instance
+ */
+ public function __construct( $instance = null ) {
+ if ( get_parent_class( $this ) ) {
+ $this->RegisterInstance( $this, '' );
+ } else if ( $instance ) {
+ $this->RegisterInstance( $instance, '' );
+ }
+ }
+
+
+ /**
+ * Register Instance
+ * @param object $instance
+ * @param string $namespace default is empty string
+ * @return $this
+ */
+ public function RegisterInstance( $instance, $namespace = '' ) {
+ $this->instances[$namespace] = $instance;
+ $this->instances[$namespace]->errorMessages = $this->errorMessages;
+
+ return $this;
+ }
+
+
+ /**
+ * Handle Requests
+ */
+ public function Execute() {
+ do {
+ // check for SMD Discovery request
+ if ( array_key_exists( 'smd', $_GET ) ) {
+ $this->response[] = $this->getServiceMap();
+ $this->hasCalls = true;
+ break;
+ }
+
+ $error = $this->getRequest();
+ if ( $error ) {
+ $this->response[] = $this->getError( $error );
+ $this->hasCalls = true;
+ break;
+ }
+
+ foreach ( $this->calls as $call ) {
+ $error = $this->validateCall( $call );
+ if ( $error ) {
+ $this->response[] = $this->getError( $error[0], $error[1], $error[2] );
+ $this->hasCalls = true;
+ } else {
+ $result = $this->processCall( $call );
+ if ( $result ) {
+ $this->response[] = $result;
+ $this->hasCalls = true;
+ }
+ }
+ }
+ } while ( false );
+
+ // flush response
+ if ( $this->hasCalls ) {
+ if ( !$this->isBatchCall ) {
+ $this->response = reset( $this->response );
+ }
+
+ if ( !headers_sent() ) {
+ // Set Content Type
+ if ( $this->ContentType ) {
+ header( 'Content-Type: ' . $this->ContentType );
+ }
+
+ // Allow Cross Domain Requests
+ if ( $this->IsXDR ) {
+ header( 'Access-Control-Allow-Origin: *' );
+ header( 'Access-Control-Allow-Headers: x-requested-with, content-type' );
+ }
+ }
+
+ echo json_encode( $this->response );
+ $this->resetVars();
+ }
+ }
+
+
+ /**
+ * Get Doc Comment
+ * @param $comment
+ * @return string|null
+ */
+ private function getDocDescription( $comment ) {
+ $result = null;
+ if ( preg_match( '/\*\s+([^@]*)\s+/s', $comment, $matches ) ) {
+ $result = str_replace( '*', "\n", trim( trim( $matches[1], '*' ) ) );
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Get Service Map
+ * Maybe not so good realization of auto-discover via doc blocks
+ * @return array
+ */
+ private function getServiceMap() {
+ $result = array(
+ 'transport' => 'POST',
+ 'envelope' => 'JSON-RPC-2.0',
+ 'SMDVersion' => '2.0',
+ 'contentType' => 'application/json',
+ 'target' => !empty( $_SERVER['REQUEST_URI'] ) ? substr( $_SERVER['REQUEST_URI'], 0, strpos( $_SERVER['REQUEST_URI'], '?' ) ) : '',
+ 'services' => array(),
+ 'description' => '',
+ );
+
+ foreach( $this->instances as $namespace => $instance ) {
+ $rc = new ReflectionClass( $instance);
+
+ // Get Class Description
+ if ( $rcDocComment = $this->getDocDescription( $rc->getDocComment() ) ) {
+ $result['description'] .= $rcDocComment . PHP_EOL;
+ }
+
+ foreach ( $rc->getMethods() as $method ) {
+ /** @var ReflectionMethod $method */
+ if ( !$method->isPublic() || in_array( strtolower( $method->getName() ), $this->hiddenMethods ) ) {
+ continue;
+ }
+
+ $methodName = ( $namespace ? $namespace . '.' : '' ) . $method->getName();
+ $docComment = $method->getDocComment();
+
+ $result['services'][$methodName] = array( 'parameters' => array() );
+
+ // set description
+ if ( $rmDocComment = $this->getDocDescription( $docComment ) ) {
+ $result['services'][$methodName]['description'] = $rmDocComment;
+ }
+
+ // @param\s+([^\s]*)\s+([^\s]*)\s*([^\s\*]*)
+ $parsedParams = array();
+ if ( preg_match_all( '/@param\s+([^\s]*)\s+([^\s]*)\s*([^\n\*]*)/', $docComment, $matches ) ) {
+ foreach ( $matches[2] as $number => $name ) {
+ $type = $matches[1][$number];
+ $desc = $matches[3][$number];
+ $name = trim( $name, '$' );
+
+ $param = array( 'type' => $type, 'description' => $desc );
+ $parsedParams[$name] = array_filter( $param );
+ }
+ };
+
+ // process params
+ foreach ( $method->getParameters() as $parameter ) {
+ $name = $parameter->getName();
+ $param = array( 'name' => $name, 'optional' => $parameter->isDefaultValueAvailable() );
+ if ( array_key_exists( $name, $parsedParams ) ) {
+ $param += $parsedParams[$name];
+ }
+
+ if ( $param['optional'] ) {
+ $param['default'] = $parameter->getDefaultValue();
+ }
+
+ $result['services'][$methodName]['parameters'][] = $param;
+ }
+
+ // set return type
+ if ( preg_match( '/@return\s+([^\s]+)\s*([^\n\*]+)/', $docComment, $matches ) ) {
+ $returns = array( 'type' => $matches[1], 'description' => trim( $matches[2] ) );
+ $result['services'][$methodName]['returns'] = array_filter( $returns );
+ }
+ }
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Reset Local Class Vars after Execute
+ */
+ private function resetVars() {
+ $this->response = $this->calls = array();
+ $this->hasCalls = $this->isBatchCall = false;
+ }
+
+ }
+?>
+= 1) ? true : false;
+
+// Utilities
+function is_empty_string($string) {
+ return strlen($string) <= 0;
+}
+
+function is_equal_strings($string1, $string2) {
+ return strcmp($string1, $string2) == 0;
+}
+
+function get_hash($algorithm, $string) {
+ return hash($algorithm, trim((string) $string));
+}
+
+// Command execution
+function execute_command($command) {
+ $descriptors = array(
+ 0 => array('pipe', 'r'), // STDIN
+ 1 => array('pipe', 'w'), // STDOUT
+ 2 => array('pipe', 'w') // STDERR
+ );
+
+ $process = proc_open($command . ' 2>&1', $descriptors, $pipes);
+ if (!is_resource($process)) die("Can't execute command.");
+
+ // Nothing to push to STDIN
+ fclose($pipes[0]);
+
+ $output = stream_get_contents($pipes[1]);
+ fclose($pipes[1]);
+
+ $error = stream_get_contents($pipes[2]);
+ fclose($pipes[2]);
+
+ // All pipes must be closed before "proc_close"
+ $code = proc_close($process);
+
+ return $output;
+}
+
+// Command parsing
+function parse_command($command) {
+ $value = ltrim((string) $command);
+
+ if (!is_empty_string($value)) {
+ $values = explode(' ', $value);
+ $values_total = count($values);
+
+ if ($values_total > 1) {
+ $value = $values[$values_total - 1];
+
+ for ($index = $values_total - 2; $index >= 0; $index--) {
+ $value_item = $values[$index];
+
+ if (substr($value_item, -1) == '\\') $value = $value_item . ' ' . $value;
+ else break;
+ }
+ }
+ }
+
+ return $value;
+}
+
+// RPC Server
+class WebConsoleRPCServer extends BaseJsonRpcServer {
+ protected $home_directory = '';
+
+ private function error($message) {
+ throw new Exception($message);
+ }
+
+ // Authentication
+ private function authenticate_user($user, $password) {
+ $user = trim((string) $user);
+ $password = trim((string) $password);
+
+ if ($user && $password) {
+ global $ACCOUNTS, $PASSWORD_HASH_ALGORITHM;
+
+ if (isset($ACCOUNTS[$user]) && !is_empty_string($ACCOUNTS[$user])) {
+ if ($PASSWORD_HASH_ALGORITHM) $password = get_hash($PASSWORD_HASH_ALGORITHM, $password);
+
+ if (is_equal_strings($password, $ACCOUNTS[$user]))
+ return $user . ':' . get_hash('sha256', $password);
+ }
+ }
+
+ throw new Exception("Incorrect user or password");
+ }
+
+ private function authenticate_token($token) {
+ global $NO_LOGIN;
+ if ($NO_LOGIN) return true;
+
+ $token = trim((string) $token);
+ $token_parts = explode(':', $token, 2);
+
+ if (count($token_parts) == 2) {
+ $user = trim((string) $token_parts[0]);
+ $password_hash = trim((string) $token_parts[1]);
+
+ if ($user && $password_hash) {
+ global $ACCOUNTS;
+
+ if (isset($ACCOUNTS[$user]) && !is_empty_string($ACCOUNTS[$user])) {
+ $real_password_hash = get_hash('sha256', $ACCOUNTS[$user]);
+ if (is_equal_strings($password_hash, $real_password_hash)) return $user;
+ }
+ }
+ }
+
+ throw new Exception("Incorrect user or password");
+ }
+
+ private function get_home_directory($user) {
+ global $HOME_DIRECTORY;
+
+ if (is_string($HOME_DIRECTORY)) {
+ if (!is_empty_string($HOME_DIRECTORY)) return $HOME_DIRECTORY;
+ }
+ else if (is_string($user) && !is_empty_string($user) && isset($HOME_DIRECTORY[$user]) && !is_empty_string($HOME_DIRECTORY[$user]))
+ return $HOME_DIRECTORY[$user];
+
+ return getcwd();
+ }
+
+ // Environment
+ private function get_environment() {
+ $hostname = function_exists('gethostname') ? gethostname() : null;
+ return array('path' => getcwd(), 'hostname' => $hostname);
+ }
+
+ private function set_environment($environment) {
+ $environment = !empty($environment) ? (array) $environment : array();
+ $path = (isset($environment['path']) && !is_empty_string($environment['path'])) ? $environment['path'] : $this->home_directory;
+
+ if (!is_empty_string($path)) {
+ if (is_dir($path)) {
+ if (!@chdir($path)) return array('output' => "Unable to change directory to current working directory, updating current directory",
+ 'environment' => $this->get_environment());
+ }
+ else return array('output' => "Current working directory not found, updating current directory",
+ 'environment' => $this->get_environment());
+ }
+ }
+
+ // Initialization
+ private function initialize($token, $environment) {
+ $user = $this->authenticate_token($token);
+ $this->home_directory = $this->get_home_directory($user);
+ $result = $this->set_environment($environment);
+
+ if ($result) return $result;
+ }
+
+ // Methods
+ public function login($user, $password) {
+ $result = array('token' => $this->authenticate_user($user, $password),
+ 'environment' => $this->get_environment());
+
+ $home_directory = $this->get_home_directory($user);
+ if (!is_empty_string($home_directory)) {
+ if (is_dir($home_directory)) $result['environment']['path'] = $home_directory;
+ else $result['output'] = "Home directory not found: ". $home_directory;
+ }
+
+ return $result;
+ }
+
+ public function cd($token, $environment, $path) {
+ $result = $this->initialize($token, $environment);
+ if ($result) return $result;
+
+ $path = trim((string) $path);
+ if (is_empty_string($path)) $path = $this->home_directory;
+
+ if (!is_empty_string($path)) {
+ if (is_dir($path)) {
+ if (!@chdir($path)) return array('output' => "cd: ". $path . ": Unable to change directory");
+ }
+ else return array('output' => "cd: ". $path . ": No such directory");
+ }
+
+ return array('environment' => $this->get_environment());
+ }
+
+ public function completion($token, $environment, $pattern, $command) {
+ $result = $this->initialize($token, $environment);
+ if ($result) return $result;
+
+ $scan_path = '';
+ $completion_prefix = '';
+ $completion = array();
+
+ if (!empty($pattern)) {
+ if (!is_dir($pattern)) {
+ $pattern = dirname($pattern);
+ if ($pattern == '.') $pattern = '';
+ }
+
+ if (!empty($pattern)) {
+ if (is_dir($pattern)) {
+ $scan_path = $completion_prefix = $pattern;
+ if (substr($completion_prefix, -1) != '/') $completion_prefix .= '/';
+ }
+ }
+ else $scan_path = getcwd();
+ }
+ else $scan_path = getcwd();
+
+ if (!empty($scan_path)) {
+ // Loading directory listing
+ $completion = array_values(array_diff(scandir($scan_path), array('..', '.')));
+ natsort($completion);
+
+ // Prefix
+ if (!empty($completion_prefix) && !empty($completion)) {
+ foreach ($completion as &$value) $value = $completion_prefix . $value;
+ }
+
+ // Pattern
+ if (!empty($pattern) && !empty($completion)) {
+ // For PHP version that does not support anonymous functions (available since PHP 5.3.0)
+ function filter_pattern($value) {
+ global $pattern;
+ return !strncmp($pattern, $value, strlen($pattern));
+ }
+
+ $completion = array_values(array_filter($completion, 'filter_pattern'));
+ }
+ }
+
+ return array('completion' => $completion);
+ }
+
+ public function run($token, $environment, $command) {
+ $result = $this->initialize($token, $environment);
+ if ($result) return $result;
+
+ $output = ($command && !is_empty_string($command)) ? execute_command($command) : '';
+ if ($output && substr($output, -1) == "\n") $output = substr($output, 0, -1);
+
+ return array('output' => $output);
+ }
+}
+
+// Processing request
+if (array_key_exists('REQUEST_METHOD', $_SERVER) && $_SERVER['REQUEST_METHOD'] == 'POST') {
+ $rpc_server = new WebConsoleRPCServer();
+ $rpc_server->Execute();
+}
+else if (!$IS_CONFIGURED) {
+?>
+
+
+
+
+
+ Web Console
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Web Console
+
+
+
+
+
+
+
+
+
+
diff --git a/index.php b/index.php
index 15994036..1c1eb921 100755
--- a/index.php
+++ b/index.php
@@ -13,7 +13,7 @@
* @author Lawrence Yau
* @author Bill Zimmerman
* @license GNU General Public License, version 3 (GPL-3.0)
- * @version 1.2.1
+ * @version 1.2.2
* @link https://github.com/billz/raspap-webgui
* @see http://sirlagz.net/2013/02/08/raspap-webgui/
*/
@@ -127,7 +127,7 @@ $theme_url = 'dist/css/' . $theme;
- RaspAP Wifi Portal v1.2.1
+ RaspAP Wifi Portal v1.2.2
@@ -145,7 +145,7 @@ $theme_url = 'dist/css/' . $theme;
Configure hotspot
- Configure DHCP
+ Configure DHCP Server