mirror of
https://github.com/outbackdingo/sysadm.git
synced 2026-01-27 10:20:26 +00:00
- Added text describing the license of these handbooks along with a link. - Fixed trademark replacements for the Server Handbook.
795 lines
22 KiB
ReStructuredText
795 lines
22 KiB
ReStructuredText
.. _Getting Started:
|
|
|
|
Getting Started
|
|
***************
|
|
|
|
Welcome to the SysAdm™ API (application programming interface)
|
|
Reference Guide! SysAdm™ is a complex utility with many integral
|
|
subsystems and classes, and this reference guide will attempt to
|
|
accurately represent the initialization, core features, and intricacies
|
|
of the utility.
|
|
|
|
This handbook will be heavily utilizing the Websocket specification and
|
|
JSON (JavaScript Object Notation) format. For detailed descriptions of
|
|
these items, please refer to their respective websites at
|
|
http://www.websocket.org and http://json.org/.
|
|
|
|
The |sysadm| API Reference Guide is freely available for sharing and
|
|
redistribution under the terms of the
|
|
`Creative Commons Attribution License <https://creativecommons.org/licenses/by/4.0/>`_.
|
|
This means you have permission to copy, distribute, translate, and adapt
|
|
the work as long as you attribute the |lumina| Project as the original
|
|
source of the Handbook.
|
|
|
|
.. _Authentication:
|
|
|
|
Authentication
|
|
==============
|
|
|
|
Once a websocket connection is made to the server, the client needs to
|
|
use the authentication class to authenticate itself to obtain access to
|
|
the SysAdm™ service. Every authentication class request contains the
|
|
following parameters:
|
|
|
|
+----------------+------------+---------------------------------------+
|
|
| **Parameter** | **Value** | **Description** |
|
|
| | | |
|
|
+================+============+=======================================+
|
|
| id | | any unique value for the request; |
|
|
| | | examples include a hash, checksum, |
|
|
| | | or uuid |
|
|
+----------------+------------+---------------------------------------+
|
|
| name | auth | |
|
|
| | | |
|
|
+----------------+------------+---------------------------------------+
|
|
| namespace | rpc | |
|
|
| | | |
|
|
+----------------+------------+---------------------------------------+
|
|
| args | | values vary by type of authentication |
|
|
| | | request |
|
|
+----------------+------------+---------------------------------------+
|
|
|
|
There are three options for server authentication: username and password,
|
|
token authentication, and pre-registered SSL certifications. SSL
|
|
certification requests are handled by both server and bridge, while
|
|
token authentication and username/password requests are server side only.
|
|
|
|
.. tip:: Individual servers can be configured to refuse username and
|
|
password requests altogether.
|
|
|
|
**1. Username and Password:**
|
|
|
|
**WebSocket Request: (Server only)**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "rpc",
|
|
"name" : "auth",
|
|
"id" : "sampleID",
|
|
"args" : {
|
|
"username" : "myuser",
|
|
"password" : "mypassword"
|
|
}
|
|
}
|
|
|
|
.. tip:: When connecting to the localhost, the password field may be
|
|
left empty for non-root user access.
|
|
|
|
**2. Token Authentication: (Server only)**
|
|
|
|
**WebSocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "rpc",
|
|
"name" : "auth_token",
|
|
"id" : "sampleID",
|
|
"args" : {
|
|
"token" : "MySavedAuthToken"
|
|
}
|
|
}
|
|
|
|
Here is an example of using a pre-registered SSL certificate to request
|
|
authentication. Note that this is a two-step process with only a 30
|
|
seconds window of validity, so this is best left to automated systems
|
|
rather than direct user requests.
|
|
|
|
**3.1. SSL Certificate Authentication: (Server and Bridge)**
|
|
|
|
**WebSocket Request (Stage 1 - Initial Request)**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "rpc",
|
|
"name" : "auth_ssl",
|
|
"id" : "sampleID",
|
|
"args" : ""
|
|
}
|
|
|
|
**WebSocket Reply (Stage 1)**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"test_string" : "<some random plaintext string of letters/numbers>"
|
|
},
|
|
"id": "sampleID",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
On receipt of the "test_string", the user-side client must encrypt that
|
|
string with the desired SSL certificate/key combination, then return
|
|
that encrypted string back to the server (Stage 2) within 30 seconds of
|
|
the initial stage 1 reply. The encrypted string should also be
|
|
base64-encoded before insertion into the stage 2 JSON request to ensure
|
|
accurate transport back to the server.
|
|
|
|
**WebSocket Request (Stage 2 - Return Encoded String)**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "rpc",
|
|
"name" : "auth_ssl",
|
|
"id" : "sampleID",
|
|
"args" : {
|
|
"encrypted_string" : "<base64-encoded string>"
|
|
}
|
|
}
|
|
|
|
A successful authentication will provide a similar reply:
|
|
|
|
**WebSocket Reply**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": [
|
|
"SampleAuthenticationToken",
|
|
300
|
|
],
|
|
"id": "sampleID",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
.. note:: The first element of the "args" array is the authentication
|
|
token for later use, while the second element is
|
|
the number of seconds the token will remain valid. The token
|
|
is reset after every successful communication with the
|
|
websocket. In this example, it is set to 5 minutes of
|
|
inactivity before the token is invalidated. The websocket
|
|
server is currently set to close any connection to a client
|
|
after 10 minutes of inactivity.
|
|
|
|
An invalid authentication or a system request after the user session
|
|
has timed out due to inactivity will have a specific message:
|
|
|
|
**WebSocket Reply**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"code": 401,
|
|
"message": "Unauthorized"
|
|
},
|
|
"id": "sampleID",
|
|
"name": "error",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
To clear a pre-saved authentication token, such as signing out:
|
|
|
|
**WebSocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "rpc",
|
|
"name" : "auth_clear",
|
|
"id" : "sampleID",
|
|
"args" : "junk argument"
|
|
}
|
|
|
|
|
|
|
|
**3.2. Alternate SSL Certificate Authentication Initiation (server
|
|
through bridge)**
|
|
|
|
This is an alternate method for stage 1 of the SSL Certificate
|
|
Authentication method. In this case, the initial request has specified
|
|
using a base 64 encoded key, to which the server will respond with its
|
|
own encrypted message. This prevents the bridge from being able to
|
|
decrypt messages between client and server for the duration of the
|
|
connection.
|
|
|
|
**WebSocket Request (Stage 1 - Initial Request)**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "rpc",
|
|
"name" : "auth_ssl",
|
|
"id" : "sampleID",
|
|
"args" : {
|
|
"action" : "auth_ssl",
|
|
"md5_key" : "<base 64 encoded key>"
|
|
}
|
|
}
|
|
|
|
|
|
**WebSocket Reply (Stage 1)**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"test_string" : "<some random plaintext string of letters/numbers>",
|
|
"new_ssl_key" : ["<piece 1>", "<piece 2>", "<continuing>"]
|
|
},
|
|
"id": "sampleID",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
|
|
.. danger:: In the reply above, both values for "test_string" and
|
|
"new_ssl_key" are encrypted with the public SSL key matching
|
|
the md5 sum from the initial request and then base 64
|
|
encoded for transport. All future messages are bulk
|
|
encrypted with the "new_ssl_key", which is a new randomly
|
|
generated private key only known to the server and client.
|
|
For example, the following section {"id", "name",
|
|
"namespace","args"} will now be encrypted with the private
|
|
key in one block prior to transport through the bridge.
|
|
|
|
.. _SSL Certificate Management:
|
|
|
|
SSL Certificate Management
|
|
==========================
|
|
|
|
Several actions are available for managing the SSL certificates used for
|
|
authentication.
|
|
|
|
+---------------+-----------+---------------------------------------------+
|
|
| **Parameter** | **Value** | **Description** |
|
|
| | | |
|
|
+===============+===========+=============================================+
|
|
| id | | Any unique value for the request; examples |
|
|
| | | include a hash, checksum, or uuid. |
|
|
+---------------+-----------+---------------------------------------------+
|
|
| name | settings | |
|
|
| | | |
|
|
+---------------+-----------+---------------------------------------------+
|
|
| namespace | sysadm | |
|
|
| | | |
|
|
+---------------+-----------+---------------------------------------------+
|
|
| action | | Actions include "list_ssl_certs", |
|
|
| | | "register_ssl_cert", and "revoke_ssl_cert". |
|
|
+---------------+-----------+---------------------------------------------+
|
|
|
|
The rest of this section provides examples of the available *actions*
|
|
for each type of request, along with their responses.
|
|
|
|
.. index:: list_ssl_certs, settings
|
|
|
|
.. _List SSL Certificates:
|
|
|
|
List SSL Certificates
|
|
---------------------
|
|
|
|
The "list_ssl_certificates" action lists the known and registered
|
|
certificates. For each certificate, the response includes the username,
|
|
public key, and the certificate's details.
|
|
|
|
**Websocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "example_id",
|
|
"name" : "settings",
|
|
"namespace": "sysadm",
|
|
"args" : {
|
|
"action" : "list_ssl_certificates"
|
|
}
|
|
}
|
|
|
|
**Websocket Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "example_id",
|
|
"name" : "response",
|
|
"namespace": "sysadm",
|
|
"args" : {
|
|
"<user>" : {
|
|
"<key>" : "<certificate_details>"
|
|
}
|
|
}
|
|
}
|
|
|
|
.. note:: The "<key>" value is base64 encoded.
|
|
|
|
.. index:: register_ssl_cert, settings
|
|
|
|
.. _Register a SSL Certificate:
|
|
|
|
Register a SSL Certificate
|
|
--------------------------
|
|
|
|
The "register_ssl_certificate" action registers the specified
|
|
certificate on the server. Once registered, a user is allowed to
|
|
authenticate without a password as long as that same certificate is
|
|
loaded in any future connections. When using this action, The "pub_key"
|
|
needs to match the public key of one of the certificates currently
|
|
loaded into the server/client connection.
|
|
|
|
**Websocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "example_id",
|
|
"name" : "settings",
|
|
"namespace": "sysadm",
|
|
"args" : {
|
|
"action" : "register_ssl_certificate",
|
|
"pub_key" : "<base64key>",
|
|
"nickname" : "<example_name>",
|
|
"email" : "<example_email>"
|
|
}
|
|
}
|
|
|
|
**Websocket Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "example_id",
|
|
"name" : "response",
|
|
"namespace": "sysadm",
|
|
"args" : {}
|
|
}
|
|
|
|
.. note:: The "nickname" and "email" arguments are optional, and may not
|
|
be seen in all responses.
|
|
|
|
.. index:: revoke_ssl_cert, settings
|
|
|
|
.. _Revoke a SSL Certificate:
|
|
|
|
Revoke a SSL Certificate
|
|
------------------------
|
|
|
|
The "revoke_ssl_certificate" action revokes a currently registered
|
|
certificate so that it can no longer be used for authentication. The
|
|
"pub_key" must be specified and must match one of the keys given by the
|
|
"list_ssl_certs" action, but does not need to match any currently loaded
|
|
certificates. The "user" is optional and allows a connection with full
|
|
administrative privileges to revoke a certificate belonging to another
|
|
user.
|
|
|
|
**Websocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "example_id",
|
|
"name" : "settings",
|
|
"namespace": "sysadm",
|
|
"args" : {
|
|
"action" : "revoke_ssl_certificate",
|
|
"pub_key" : "<base64key>",
|
|
"user" : "<example_user>"
|
|
}
|
|
}
|
|
|
|
**Websocket Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "example_id",
|
|
"name" : "response",
|
|
"namespace": "sysadm",
|
|
"args" : {}
|
|
}
|
|
|
|
.. note:: If the current user has full administrative access,
|
|
"list_ssl_certs" will return the registered certificates for
|
|
all users on the system. Otherwise, it will only return the
|
|
certificates for the current user. Similarly,
|
|
"revoke_ssl_cert" may be used to remove certificates
|
|
registered to other users only if the current user/connection
|
|
has full administrative access; otherwise, it may only be used
|
|
to manage the current user's certificates.
|
|
|
|
.. index:: dispatcher, events
|
|
|
|
.. _Dispatcher Subsystem:
|
|
|
|
Dispatcher Subsystem
|
|
====================
|
|
|
|
The dispatcher subsystem is designed for running external utilities or
|
|
scripts in an asynchronous fashion. Any connected client can subscribe
|
|
to per-connection event notifications about dispatcher processes through
|
|
the events system, but only users in the *wheel* group have the
|
|
authority to directly submit new jobs for the dispatcher.
|
|
|
|
.. note:: Other subsystems may also use the dispatcher for long-running
|
|
processes in the background, and these subsystems may allow
|
|
non-wheel group users to perform these tasks as necessary.
|
|
Also, the events namespace does not really translate over to
|
|
REST which was not designed for asyncronous events. For this
|
|
reason, only Websocket examples are used in this section.
|
|
|
|
The format of "dispatcher" event requests is as follows:
|
|
|
|
+---------------+-------------+--------------------------------------+
|
|
| **Parameter** | **Value** | **Description** |
|
|
| | | |
|
|
+===============+=============+======================================+
|
|
| id | | Any unique value for the request, |
|
|
| | | including a hash, checksum, or uuid. |
|
|
+---------------+-------------+--------------------------------------+
|
|
| name | subscribe | use the desired action |
|
|
| | unsubscribe | |
|
|
+---------------+-------------+--------------------------------------+
|
|
| namespace | events | |
|
|
| | | |
|
|
+---------------+-------------+--------------------------------------+
|
|
| args | dispatcher | |
|
|
| | | |
|
|
+---------------+-------------+--------------------------------------+
|
|
|
|
For example, to subscribe to dispatcher events:
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "events",
|
|
"name" : "subscribe",
|
|
"id" : "sampleID",
|
|
"args" : ["dispatcher"]
|
|
}
|
|
|
|
Once subscribed, the requested events will be received as they are
|
|
produced. To unsubscribe from event notifications, repeat the request,
|
|
using "unsubscribe" for the "name". For example, to unsubscribe from
|
|
dispatcher events:
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "events",
|
|
"name" : "unsubscribe",
|
|
"id" : "sampleID",
|
|
"args" : ["dispatcher"]
|
|
}
|
|
|
|
This response indicates that a dispatcher event occurred:
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "events",
|
|
"name" : "event",
|
|
"id" : "",
|
|
"args" : {
|
|
"name" : "dispatcher",
|
|
"args" : "<message>"
|
|
}
|
|
}
|
|
|
|
A "dispatcher" query contains several parameters:
|
|
|
|
+---------------+------------+--------------------------------------+
|
|
| **Parameter** | **Value** | **Description** |
|
|
| | | |
|
|
+===============+============+======================================+
|
|
| id | | Any unique value for the request, |
|
|
| | | including a hash, checksum, or uuid. |
|
|
+---------------+------------+--------------------------------------+
|
|
| name | dispatcher | |
|
|
| | | |
|
|
+---------------+------------+--------------------------------------+
|
|
| namespace | events | |
|
|
| | | |
|
|
+---------------+------------+--------------------------------------+
|
|
| action | | "run" submits process commands |
|
|
| | | |
|
|
+---------------+------------+--------------------------------------+
|
|
|
|
Dispatcher events have the following syntax:
|
|
|
|
**Websocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"namespace" : "events",
|
|
"name" : "dispatcher",
|
|
"id" : "",
|
|
"args" : {
|
|
"cmd_list" : ["/bin/echo something"],
|
|
"log" : "[Running Command: /bin/echo something ]something\n",
|
|
"proc_id" : "procID",
|
|
"success" : "true",
|
|
"time_finished" : "2016-02-02T13:45:13",
|
|
"time_started" : "2016-02-02T13:45:13"
|
|
}
|
|
}
|
|
|
|
Any user within the *wheel* group can use the "run" action to submit a
|
|
new job to the dispatcher:
|
|
|
|
**REST Request**
|
|
|
|
.. code-block:: none
|
|
|
|
PUT /rpc/dispatcher
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"action" : "run",
|
|
"procID2" : [
|
|
"echo chainCmd1",
|
|
"echo chainCmd2"
|
|
],
|
|
"procID1" : "echo sample1"
|
|
}
|
|
|
|
**REST Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"started": [
|
|
"procID1",
|
|
"procID2"
|
|
]
|
|
}
|
|
}
|
|
|
|
**WebSocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"name" : "dispatcher",
|
|
"namespace" : "rpc",
|
|
"id" : "fooid",
|
|
"args" : {
|
|
"procID1" : "echo sample1",
|
|
"procID2" : [
|
|
"echo chainCmd1",
|
|
"echo chainCmd2"
|
|
],
|
|
"action" : "run"
|
|
}
|
|
}
|
|
|
|
**WebSocket Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"started": [
|
|
"procID1",
|
|
"procID2"
|
|
]
|
|
},
|
|
"id": "fooid",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
When submitting a job to the dispatcher, there are several points to
|
|
remember:
|
|
|
|
* Process commands are not the same as shell commands. A dispatcher
|
|
process command uses the syntax "<binary/utility> <list of arguments>",
|
|
similar to a simple shell command. However, complex shell operations
|
|
with pipes or test statements will not function properly within a
|
|
dispatcher process.
|
|
|
|
* There are two types of jobs: a single string entry for simple commands,
|
|
and an array of strings for a chain of commands. A chain of commands
|
|
is treated as a single process, and the commands are run sequentially
|
|
until either a command fails (returns non-0 or the process crashes),
|
|
or until there are no more commands to run.
|
|
|
|
* A chain of commands is useful for multi-step operations but is not
|
|
considered a replacement for a good shell script on the server.
|
|
|
|
.. _Server Subsystems:
|
|
|
|
Server Subsystems
|
|
=================
|
|
|
|
The RPC namespace can be used to get information about SysAdm™ server
|
|
subsystems. This namespace supports the following parameters:
|
|
|
|
+---------------+-----------+------------------------------------------+
|
|
| **Parameter** | **Value** | **Description** |
|
|
| | | |
|
|
+===============+===========+==========================================+
|
|
| id | | Any unique value for the request, |
|
|
| | | including a hash, checksum, or uuid. |
|
|
+---------------+-----------+------------------------------------------+
|
|
| name | | Supported names are "query", "identify", |
|
|
| | | and "list_ssl_checksums" |
|
|
+---------------+-----------+------------------------------------------+
|
|
| namespace | rpc | |
|
|
| | | |
|
|
+---------------+-----------+------------------------------------------+
|
|
| args | | Can be any data. |
|
|
| | | |
|
|
+---------------+-----------+------------------------------------------+
|
|
|
|
The rest of this section provides examples of the available *names* for
|
|
each type of request, along with their responses.
|
|
|
|
.. index:: query, rpc
|
|
|
|
.. _Query Subsystems:
|
|
|
|
Query Subsystems
|
|
----------------
|
|
|
|
An RPC query can be issued to probe all the known subsystems and return
|
|
which ones are currently available and what level of read and write
|
|
access the user has. This subsystem is used only by the server.
|
|
|
|
**REST Request**
|
|
|
|
::
|
|
|
|
PUT /rpc/query
|
|
{
|
|
"junk" : "junk"
|
|
}
|
|
|
|
**REST Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"rpc/dispatcher": "read/write",
|
|
"rpc/syscache": "read",
|
|
"sysadm/lifepreserver": "read/write",
|
|
"sysadm/network": "read/write"
|
|
}
|
|
}
|
|
|
|
**WebSocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"id" : "fooid",
|
|
"name" : "query",
|
|
"namespace" : "rpc",
|
|
"args" : {
|
|
"junk" : "junk"
|
|
}
|
|
}
|
|
|
|
**WebSocket Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"rpc/dispatcher": "read/write",
|
|
"rpc/syscache": "read",
|
|
"sysadm/lifepreserver": "read/write",
|
|
"sysadm/network": "read/write"
|
|
},
|
|
"id": "fooid",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
.. index:: identify, rpc
|
|
|
|
.. _Identify Subsystem:
|
|
|
|
Identify Subsystem
|
|
------------------
|
|
|
|
To identify the type of SysAdm™ system, use :command:`identify`. Possible
|
|
identities are "server", "bridge", and "client," with all three system
|
|
types using this subsystem.
|
|
|
|
**REST Request**
|
|
|
|
::
|
|
|
|
PUT /rpc/identify
|
|
{}
|
|
|
|
**WebSocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args" : {},
|
|
"namespace" : "rpc",
|
|
"id" : "fooid",
|
|
"name" : "identify"
|
|
}
|
|
|
|
**Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"type": "server",
|
|
"hostname": "<hostname>"
|
|
},
|
|
"id": "fooid",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
}
|
|
|
|
.. index:: list_ssl_checksums, rpc
|
|
|
|
.. _List SSL Checksums:
|
|
|
|
List SSL Checksums
|
|
------------------
|
|
|
|
Used by both server and client, :command:`list_ssl_checksums` will list
|
|
the MD5 checksums of all known SSL keys.
|
|
|
|
**REST Request**
|
|
|
|
::
|
|
|
|
PUT /rpc/settings
|
|
{
|
|
"action" : "list_ssl_checksums"
|
|
}
|
|
|
|
**WebSocket Request**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args" : {
|
|
"action" : "list_ssl_checksums"
|
|
},
|
|
"namespace" : "rpc",
|
|
"name" : "settings",
|
|
"id" : "fooid"
|
|
}
|
|
|
|
**Response**
|
|
|
|
.. code-block:: json
|
|
|
|
{
|
|
"args": {
|
|
"md5_keys": [
|
|
"0`H\u0013\r*\u00023\u000bc"
|
|
]
|
|
},
|
|
"id": "fooid",
|
|
"name": "response",
|
|
"namespace": "rpc"
|
|
} |