Difference between revisions of "User:Darien Caldwell/HTTP-DNS"

From Second Life Wiki
Jump to navigation Jump to search
(Created page with "{{void-box |title=Dynamic DNS service for HTTP-IN via Google App Engine: |content= Once it's installed you can start using it: http://yourappname.appspot.com/?type=add&name=[NAM…")
 
m
 
(30 intermediate revisions by 2 users not shown)
Line 1: Line 1:
{{void-box
{{void-box
|title=Dynamic DNS service for HTTP-IN via Google App Engine:
|title=Dynamic DNS service for HTTP-IN via Google App Engine:
|content=
NOTE: While the original version of this code did not provide true DNS service, it has since been updated to do so. Please read the original thread to understand the evolution of the code. A lot of good information and workarounds are also provided in the old thread!
http://forums-archive.secondlife.com/54/33/323981/1.html
One of the big issues with HTTP-IN is the fact that the URLs are dynamic and can change under many conditions. Many have balked at the requirements of having an external server to manage a Dynamic DNS service to keep track of these ever changing URLs. So here's a free solution that anyone can use, and expand with their needs.
Using this DNS, you can have servers register their URL 'service' and allow other in-world (and out of world) applications discover the URL for this service in a consistent manner. The Google App Engine is free and offers resource limits which should be able to support small to mid range applications. Of course, for a small fee you can purchase more resources from Google as well.
(Resouce limits: http://developers.google.com/appengine/docs/quotas )
This is the basics, and has no verification or encryption, but should be fairly secure as long as you don't go around giving your service URL to people. Feel free to add on to it as you like. The code is released to the Public Domain. Enjoy.
NOTE: As of March 8th, 2013, Google has announced the depreciation of the Python 2.5 Runtime. I have updated the files here to use the newer Pythonn 2.7 Runtime. If you use this service, and have it on Python 2.5, you should consider migrating to 2.7  As well, To use Python 2.7, you must migrate from the Master/Slave datastore to the HRD (High Replication Datastore).
Migration Info here:
Migrating to HRD: https://developers.google.com/appengine/docs/adminconsole/migration
Migrating to Python 2.7: https://developers.google.com/appengine/docs/python/python25/migrate27
}}
{{void-box
|title=Setup and Installation
|content=
*First, download the files here:
http://home.comcast.net/~volfin/lsl-dns.zip
Or, you can find the contents of the zip file here:
http://wiki.secondlife.com/w/index.php?title=User:Darien_Caldwell/HTTP-DNS%20Code
Now, what to do with these files:
*Go to http://developers.google.com/appengine/ to sign up for a free account. Or if you already have a GMail or other Google account, use that.
*Go here to install Python 2.7 (if you don't already have it)
http://www.python.org/download/releases/2.7.5/  (Note that GAE updated to Python 2.7.5 on July 16th, 2013. other versions of 2.7.x can be used, but may lack newer features)
*Download and install the App Engine SDK for Python:
http://developers.google.com/appengine/downloads
The SDK will be installed into a subdirectory called 'google_appengine'.
*Create a directory under the 'google_appengine' directory called 'lsl-dns'
*Place the files from the zip file (dns.py and app.yaml) into this new directory.
*Edit the app.yaml file to change the name of the application to the name you specified when you signed up for the service (at https://appengine.google.com/ ):
application: YOUR-APP-NAME-HERE <-- Change this
*From the 'google_appengine' directory (where the SDK was installed), run this command from the console (DOS Prompt):
appcfg.py update lsl-dns/
This will install the application.
*You're done.
}}
{{void-box
|title=The API Calls
|content=
|content=
Once it's installed you can start using it:
Once it's installed you can start using it:


http://yourappname.appspot.com/?type=add&name=[NAME]&url=[URL]
*http://''yourappname''.appspot.com/?type=add&name=[NAME]&url=[URL]
Adds a new service named [NAME] with the HTTP-IN url [URL].  
Adds a new service named [NAME] with the HTTP-IN url [URL].  
The URL must be converted to a string using llEscapeURL() first.
The URL must be converted to a string using llEscapeURL() first.
Line 10: Line 76:
  already exists.  
  already exists.  


http://yourappname.appspot.com/?type=remove&name=[NAME]
*http://''yourappname''.appspot.com/?type=remove&name=[NAME]
Removes the given service named [NAME].  
Removes the given service named [NAME].  
Returns 'Removed' if successful, or 'None' if the service wasn't found.
Returns 'Removed' if successful, or 'None' if the service wasn't found.


http://yourappname.appspot.com/?type=update&name=[NAME]&url=[URL]
*http://''yourappname''.appspot.com/?type=update&name=[NAME]&url=[URL]
Updates the service named [NAME] with the HTTP-IN url [URL].
Updates the service named [NAME] with the HTTP-IN url [URL].
  The URL must be converted to a string using llEscapeURL() first.
  The URL must be converted to a string using llEscapeURL() first.
Line 20: Line 86:
  If the service doesn't exist, a new service is added and 'Added' is returned.
  If the service doesn't exist, a new service is added and 'Added' is returned.


http://yourappname.appspot.com/?type=retrieve&name=[NAME]
*http://''yourappname''.appspot.com/?type=retrieve&name=[NAME]
Retrieves the url of the given service named [NAME].  
Retrieves the url of the given service named [NAME].  
Returns the URL if the service is found, or 'None' if the service wasn't found.
Returns the URL if the service is found, or 'None' if the service wasn't found.
  the returned URL must be converted to a URL using llUnescapeURL()
  the returned URL must be converted to a URL using llUnescapeURL()


http://yourappname.appspot.com/?type=list
*http://''yourappname''.appspot.com/?type=list
Lists the available services currently stored.  
Lists the available services currently stored.  
Returns a Comma seperated list of service names, ending with the word 'END'.
Returns a Comma seperated list of service names, ending with the word 'END'.
  if no services are defined, 'Empty' is returned.
  if no services are defined, 'Empty' is returned.


}}
{{void-box
|title=Optimizing to stay in the Free Quota
|content= Google App Engine is designed for high scalability, so it will try to use as much resources as it can to keep up with the load put on it. However, for someone in the free Quota, you may not want it to do that. :) So, to stay in the free quota you have to sacrifice some scalability and latency. But for SL this is generally okay to do.
The thing to do is make some simple changes to some Application configuration settings.  Logging into your application dashboard (https://appengine.google.com/), on the left hand menu find Application Settings under the "Administration" subheading, and go there. Then change the settings shown below to match what you see here:
[[File:app_settings.jpg|Application Instance Settings]]
}}
{{void-box
|title=Using the DNS URL
|content=
Once a service has been entered, you can use the serivce name in the URL to do automatic redirection.
Example:
You establish a service named 'Intro1'
The URL for the service is:
http://''sim3015.aditi.lindenlab.com'':12046/cap/3ff4f3f2-ea08-76c1-cef6-a22b4a573a7c
By Navigating to http://''yourappname''.appspot.com/Intro1
The page will be automatically redirected to the service URL:
http://''sim3015.aditi.lindenlab.com'':12046/cap/3ff4f3f2-ea08-76c1-cef6-a22b4a573a7c
If the service name used in the URL is invalid, 'None' is returned.
}}
{{void-box
|title=Example LSL Script
|content=
<source lang="lsl2">
string url = "";
string service_name = "Intro1";
key URL_KEY;
setup()
{
    llReleaseURL(url);
    URL_KEY=llRequestURL();
}
default
{
    state_entry()
    {
        llSetObjectName("HTTP Server");
        setup();
    }
   
    on_rez(integer n)
    {
        setup();
    }
    changed(integer c)
    {
        if (c & (CHANGED_REGION | CHANGED_REGION_START | CHANGED_TELEPORT) )
        {
            setup();
        }
    }
    //Response to our HTTPRequest
    http_response(key id, integer status, list meta, string body)
    {
       
        if (status == 415) // The remote server did reply to your request but the Content-Type of the reply (such as XML, JSON, Atom, RSS, PLS)
                          // is not recognised by the LL server and so is not passed back to the script.
                          // You can assume that 415 means the server heard your request and did reply.
        {
            llInstantMessage(llGetOwner(),"Special Error Status 415 Encountered.");
            return;
        }
        else if (status == 499) // Besides the usual HTTP status codes, SL implements a special status code 499.
                                // This code isn't generated by the remote web server but by SL's servers, it can indicate:
                                // Request timeout (60 seconds)  --  SSL failure  --  A space was present in the url (escape your URL with llEscapeURL).
        {
            llInstantMessage(llGetOwner(),"Special Error Status 499 Encountered.");
            return;
        }
        else if (status == 502)  // The proxy server received an invalid response from an upstream server.
                                // This error occurs when you send an llHTTPRequest to an object in-world, and it does not reply with an llHTTPResponse.
        {
            llInstantMessage(llGetOwner(),"Special Error Status 502 Encountered.");
            return;
        }
       
        if (body=="Updated") llOwnerSay("DNS updated with Server Address.");
        else  llOwnerSay("Response code:"+(string)status+" Body:"+body);  // Some other arbitrary response
    }
    http_request(key id, string method, string body)
    {
        if (id==URL_KEY)
        {
            if (method == URL_REQUEST_GRANTED)
            {
                // the '/' must be added to work around a quirk with LL's HTTP-in URLS (see forum thread)
                url = body+"/";
                                // DON'T FORGET TO CHANGE 'yourappname' to the name of your specific App!!!
                llHTTPRequest("http://yourappname.appspot.com/?type=update&name=" + service_name + "&url=" + llEscapeURL(url),[],"");
            }
            else
            {
                url="";
                llOwnerSay("Error: HTTP-IN URL Request Denied.");
            }
        }
        else
        {
            // Some other Request
            llOwnerSay("Got:"+body);
            string pathInfoHeader = llGetHTTPHeader(id, "x-path-info");
            llOwnerSay("Path:"+pathInfoHeader);
            string query = llGetHTTPHeader(id, "x-query-string");
            llOwnerSay("Query:"+query);
        }
       
        // should always respond to prevent timeouts.
        llHTTPResponse(id,200,"ACK");
    }
}
</source>
}}
{{void-box
|title=Advanced Version
|content= There is an advanced version that has a very basic admin function, password protection, and verbose logging. Thanks goes to [[User:CrystalShard Foo|Crystalshard Foo]] for contributing her time to work with me on this.
The code can be found here: 
  http://wiki.secondlife.com/w/index.php?title=User:Darien_Caldwell/Advanced%20HTTP-DNS%20Code
}}
{{void-box
|title=Advanced API Calls
|content=
The API is much the same, but there are several new *optional* parameters that can be used.
*wpass: Write protect password. If an entry has a write protect password set, it can't be updated or deleted without specificying this password.
*rpass: Read Protect password. If an entry has a read protect password set, it can't be retrieved without specificying this password. It also can't be called via a DNS url.
*hidden: Hidden status. If an entry is marked as hidden, it will not show up in the Entry List. 'list'ing the services with the admin password included will show hidden entries.
*pass: A generic password parameter, takes the place of rpass, wpass, or both in various situations. If whien adding a service through an update, you specifiy 'pass', it will be set as the read and write passwords. specifying 'rpass' or 'wpass' will override this.
*admin: Admin Password. The hard-coded Admin password allows global overriding of some actions, regardless of the entry password set. If an Admin password is set, only Add requests that include the admin password will be accepted. 'admin' can also be used to override update,remove,retrieve,and list.
The Admin password is hard coded in the dns.py file:
<source lang="python">
        admin_password = "adminpassword"        # This password enables you to have unrestricted access to the DNS
</source>
*http://''yourappname''.appspot.com/?type=add&name=[NAME]&url=[URL]&hidden=[0/1]&admin=[ADMIN PASS]&wpass=[WRITE PASS]&rpass=[READ PASS]
Adds a new service named [NAME] with the HTTP-IN url [URL].
Can be 'hidden', 'wpass' protected, and/or 'rpass' protected.
The URL must be converted to a string using llEscapeURL() first.
if the URL is added, returns the response 'Added', or 'Found' if the service already exists.
*http://''yourappname''.appspot.com/?type=remove&name=[NAME]&admin=[ADMIN PASS]&pass=[WRITE PASS]
Removes the given service named [NAME].
If 'wpass' was set during creation/update, must be specificed as 'pass'. or overridden by admin=[ADMIN PASS]
Returns 'Removed' if successful, or 'None' if the service wasn't found.
*http://''yourappname''.appspot.com/?type=update&name=[NAME]&url=[URL]&hidden=[0/1]&admin=[ADMIN PASS]&wpass=[WRITE PASS]&rpass=[READ PASS]
Updates the service named [NAME] with the HTTP-IN url [URL].
Can be 'hidden', 'wpass' protected, and/or 'rpass' protected.
If 'wpass' was set during previous creation/update, must be specificed as 'pass', or overridden by admin=[ADMIN PASS]
The URL must be converted to a string using llEscapeURL() first.
If the URL is updated the response 'Updated' is returned.
If the service doesn't exist, a new service is added and 'Added' is returned.
*http://''yourappname''.appspot.com/?type=retrieve&name=[NAME]&pass=[READ PASS]&admin=[ADMIN PASS]
Retrieves the url of the given service named [NAME].
If 'rpass' was set during previous creation/update, must be specificed as 'pass', or overridden by admin=[ADMIN PASS]
Returns the URL if the service is found, or 'None' if the service wasn't found.
the returned URL must be converted to a URL using llUnescapeURL()
*http://''yourappname''.appspot.com/?type=list&admin=[ADMIN PASS]
Lists the available services currently stored.
Will only show entries not marked as 'hidden', Unless &admin=[ADMIN PASS] is included. then all are shown.
Returns a Comma seperated list of service names, ending with the word 'END'.
If no services are defined, 'Empty' is returned.
Enjoy :)
}}
}}

Latest revision as of 08:38, 29 August 2015

Dynamic DNS service for HTTP-IN via Google App Engine:

NOTE: While the original version of this code did not provide true DNS service, it has since been updated to do so. Please read the original thread to understand the evolution of the code. A lot of good information and workarounds are also provided in the old thread!

http://forums-archive.secondlife.com/54/33/323981/1.html

One of the big issues with HTTP-IN is the fact that the URLs are dynamic and can change under many conditions. Many have balked at the requirements of having an external server to manage a Dynamic DNS service to keep track of these ever changing URLs. So here's a free solution that anyone can use, and expand with their needs.

Using this DNS, you can have servers register their URL 'service' and allow other in-world (and out of world) applications discover the URL for this service in a consistent manner. The Google App Engine is free and offers resource limits which should be able to support small to mid range applications. Of course, for a small fee you can purchase more resources from Google as well. (Resouce limits: http://developers.google.com/appengine/docs/quotas )


This is the basics, and has no verification or encryption, but should be fairly secure as long as you don't go around giving your service URL to people. Feel free to add on to it as you like. The code is released to the Public Domain. Enjoy.

NOTE: As of March 8th, 2013, Google has announced the depreciation of the Python 2.5 Runtime. I have updated the files here to use the newer Pythonn 2.7 Runtime. If you use this service, and have it on Python 2.5, you should consider migrating to 2.7 As well, To use Python 2.7, you must migrate from the Master/Slave datastore to the HRD (High Replication Datastore).

Migration Info here:

Migrating to HRD: https://developers.google.com/appengine/docs/adminconsole/migration

Migrating to Python 2.7: https://developers.google.com/appengine/docs/python/python25/migrate27

Setup and Installation

  • First, download the files here:
http://home.comcast.net/~volfin/lsl-dns.zip

Or, you can find the contents of the zip file here:

http://wiki.secondlife.com/w/index.php?title=User:Darien_Caldwell/HTTP-DNS%20Code

Now, what to do with these files:

  • Go here to install Python 2.7 (if you don't already have it)
http://www.python.org/download/releases/2.7.5/  (Note that GAE updated to Python 2.7.5 on July 16th, 2013. other versions of 2.7.x can be used, but may lack newer features)
  • Download and install the App Engine SDK for Python:
http://developers.google.com/appengine/downloads

The SDK will be installed into a subdirectory called 'google_appengine'.

  • Create a directory under the 'google_appengine' directory called 'lsl-dns'
  • Place the files from the zip file (dns.py and app.yaml) into this new directory.
  • Edit the app.yaml file to change the name of the application to the name you specified when you signed up for the service (at https://appengine.google.com/ ):
application: YOUR-APP-NAME-HERE <-- Change this
  • From the 'google_appengine' directory (where the SDK was installed), run this command from the console (DOS Prompt):
appcfg.py update lsl-dns/

This will install the application.

  • You're done.

The API Calls

Once it's installed you can start using it:

  • http://yourappname.appspot.com/?type=add&name=[NAME]&url=[URL]

Adds a new service named [NAME] with the HTTP-IN url [URL]. The URL must be converted to a string using llEscapeURL() first.

if the URL is added, returns the response 'Added', or 'Found' if the service
already exists. 
  • http://yourappname.appspot.com/?type=remove&name=[NAME]

Removes the given service named [NAME].

Returns 'Removed' if successful, or 'None' if the service wasn't found.
  • http://yourappname.appspot.com/?type=update&name=[NAME]&url=[URL]

Updates the service named [NAME] with the HTTP-IN url [URL].

The URL must be converted to a string using llEscapeURL() first.
If the URL is updated the response 'Updated' is returned.
If the service doesn't exist, a new service is added and 'Added' is returned.
  • http://yourappname.appspot.com/?type=retrieve&name=[NAME]

Retrieves the url of the given service named [NAME].

Returns the URL if the service is found, or 'None' if the service wasn't found.
the returned URL must be converted to a URL using llUnescapeURL()
  • http://yourappname.appspot.com/?type=list

Lists the available services currently stored.

Returns a Comma seperated list of service names, ending with the word 'END'.
if no services are defined, 'Empty' is returned.

Optimizing to stay in the Free Quota

Google App Engine is designed for high scalability, so it will try to use as much resources as it can to keep up with the load put on it. However, for someone in the free Quota, you may not want it to do that. :) So, to stay in the free quota you have to sacrifice some scalability and latency. But for SL this is generally okay to do.

The thing to do is make some simple changes to some Application configuration settings. Logging into your application dashboard (https://appengine.google.com/), on the left hand menu find Application Settings under the "Administration" subheading, and go there. Then change the settings shown below to match what you see here:

Application Instance Settings

Using the DNS URL

Once a service has been entered, you can use the serivce name in the URL to do automatic redirection.

Example:

You establish a service named 'Intro1'

The URL for the service is: http://sim3015.aditi.lindenlab.com:12046/cap/3ff4f3f2-ea08-76c1-cef6-a22b4a573a7c

By Navigating to http://yourappname.appspot.com/Intro1 The page will be automatically redirected to the service URL:

http://sim3015.aditi.lindenlab.com:12046/cap/3ff4f3f2-ea08-76c1-cef6-a22b4a573a7c

If the service name used in the URL is invalid, 'None' is returned.

Example LSL Script

string url = "";
string service_name = "Intro1";
key URL_KEY;


setup()
{
    llReleaseURL(url);
    URL_KEY=llRequestURL();
}

default
{
    state_entry()
    {
        llSetObjectName("HTTP Server");
        setup();
    }
    
    on_rez(integer n)
    {
        setup();
    }

    changed(integer c)
    {
        if (c & (CHANGED_REGION | CHANGED_REGION_START | CHANGED_TELEPORT) )
        {
            setup();
        }
    }

    //Response to our HTTPRequest
    http_response(key id, integer status, list meta, string body)
    {
        
        if (status == 415) // The remote server did reply to your request but the Content-Type of the reply (such as XML, JSON, Atom, RSS, PLS) 
                           // is not recognised by the LL server and so is not passed back to the script. 
                           // You can assume that 415 means the server heard your request and did reply. 
        {
            llInstantMessage(llGetOwner(),"Special Error Status 415 Encountered.");
            return;
        }
        else if (status == 499) // Besides the usual HTTP status codes, SL implements a special status code 499. 
                                // This code isn't generated by the remote web server but by SL's servers, it can indicate: 
                                // Request timeout (60 seconds)   --  SSL failure  --  A space was present in the url (escape your URL with llEscapeURL). 
        {
            llInstantMessage(llGetOwner(),"Special Error Status 499 Encountered.");
            return;
        }
        else if (status == 502)  // The proxy server received an invalid response from an upstream server. 
                                 // This error occurs when you send an llHTTPRequest to an object in-world, and it does not reply with an llHTTPResponse. 
        {
            llInstantMessage(llGetOwner(),"Special Error Status 502 Encountered.");
            return;
        }
        
        if (body=="Updated") llOwnerSay("DNS updated with Server Address.");
        else  llOwnerSay("Response code:"+(string)status+" Body:"+body);  // Some other arbitrary response

    }

    http_request(key id, string method, string body)
    {
        if (id==URL_KEY)
        {
            if (method == URL_REQUEST_GRANTED)
            {
                // the '/' must be added to work around a quirk with LL's HTTP-in URLS (see forum thread)
                url = body+"/"; 
                                // DON'T FORGET TO CHANGE 'yourappname' to the name of your specific App!!!
                llHTTPRequest("http://yourappname.appspot.com/?type=update&name=" + service_name + "&url=" + llEscapeURL(url),[],"");
            }
            else
            {
                url="";
                llOwnerSay("Error: HTTP-IN URL Request Denied.");
            }
        }
        else
        {
            // Some other Request
            llOwnerSay("Got:"+body);
            string pathInfoHeader = llGetHTTPHeader(id, "x-path-info");
            llOwnerSay("Path:"+pathInfoHeader);
            string query = llGetHTTPHeader(id, "x-query-string");
            llOwnerSay("Query:"+query);
        }
        
        // should always respond to prevent timeouts.
        llHTTPResponse(id,200,"ACK"); 
    }
}

Advanced Version

There is an advanced version that has a very basic admin function, password protection, and verbose logging. Thanks goes to Crystalshard Foo for contributing her time to work with me on this.

The code can be found here:

http://wiki.secondlife.com/w/index.php?title=User:Darien_Caldwell/Advanced%20HTTP-DNS%20Code

Advanced API Calls

The API is much the same, but there are several new *optional* parameters that can be used.

  • wpass: Write protect password. If an entry has a write protect password set, it can't be updated or deleted without specificying this password.
  • rpass: Read Protect password. If an entry has a read protect password set, it can't be retrieved without specificying this password. It also can't be called via a DNS url.
  • hidden: Hidden status. If an entry is marked as hidden, it will not show up in the Entry List. 'list'ing the services with the admin password included will show hidden entries.
  • pass: A generic password parameter, takes the place of rpass, wpass, or both in various situations. If whien adding a service through an update, you specifiy 'pass', it will be set as the read and write passwords. specifying 'rpass' or 'wpass' will override this.
  • admin: Admin Password. The hard-coded Admin password allows global overriding of some actions, regardless of the entry password set. If an Admin password is set, only Add requests that include the admin password will be accepted. 'admin' can also be used to override update,remove,retrieve,and list.
The Admin password is hard coded in the dns.py file:
        admin_password = "adminpassword"        # This password enables you to have unrestricted access to the DNS
  • http://yourappname.appspot.com/?type=add&name=[NAME]&url=[URL]&hidden=[0/1]&admin=[ADMIN PASS]&wpass=[WRITE PASS]&rpass=[READ PASS]

Adds a new service named [NAME] with the HTTP-IN url [URL]. Can be 'hidden', 'wpass' protected, and/or 'rpass' protected. The URL must be converted to a string using llEscapeURL() first.

if the URL is added, returns the response 'Added', or 'Found' if the service already exists. 
  • http://yourappname.appspot.com/?type=remove&name=[NAME]&admin=[ADMIN PASS]&pass=[WRITE PASS]

Removes the given service named [NAME]. If 'wpass' was set during creation/update, must be specificed as 'pass'. or overridden by admin=[ADMIN PASS]

Returns 'Removed' if successful, or 'None' if the service wasn't found.
  • http://yourappname.appspot.com/?type=update&name=[NAME]&url=[URL]&hidden=[0/1]&admin=[ADMIN PASS]&wpass=[WRITE PASS]&rpass=[READ PASS]

Updates the service named [NAME] with the HTTP-IN url [URL]. Can be 'hidden', 'wpass' protected, and/or 'rpass' protected. If 'wpass' was set during previous creation/update, must be specificed as 'pass', or overridden by admin=[ADMIN PASS]

The URL must be converted to a string using llEscapeURL() first.
If the URL is updated the response 'Updated' is returned.
If the service doesn't exist, a new service is added and 'Added' is returned.
  • http://yourappname.appspot.com/?type=retrieve&name=[NAME]&pass=[READ PASS]&admin=[ADMIN PASS]

Retrieves the url of the given service named [NAME]. If 'rpass' was set during previous creation/update, must be specificed as 'pass', or overridden by admin=[ADMIN PASS]

Returns the URL if the service is found, or 'None' if the service wasn't found.
the returned URL must be converted to a URL using llUnescapeURL()
  • http://yourappname.appspot.com/?type=list&admin=[ADMIN PASS]

Lists the available services currently stored. Will only show entries not marked as 'hidden', Unless &admin=[ADMIN PASS] is included. then all are shown.

Returns a Comma seperated list of service names, ending with the word 'END'.
If no services are defined, 'Empty' is returned.
Enjoy :)