Skip to content

Instantly share code, notes, and snippets.

@FelixWolf
Last active December 10, 2023 22:48
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save FelixWolf/e19cc68a27150fbe7150387e90a4b6cf to your computer and use it in GitHub Desktop.
Save FelixWolf/e19cc68a27150fbe7150387e90a4b6cf to your computer and use it in GitHub Desktop.
Fully featured Second Life Redelivery and Vending system

Fully featured Second Life Redelivery and Vending system

This is a tutorial and documentation on how to create a fully featured redelivery and vending system in Second Life. This includes adding support for the Marketplace.

Requirements

For this, you will need the following:

  • Basic programming knowledge
  • Basic security knowledge
  • A scripting language that supports web and database(Python, PHP, Nodejs, etc)
  • A database(Postgresql, MySQL, MongoDB, etc)
  • A place to rez down a LSL server in-world

Gathering your components

It is a good idea to put all your components in a configuration file, this makes referencing them later easier.

I recommend using JSON as a configuration format, however YAML or XML will also suffice. Simply restructure the configuration to your desired format and parse it as needed.

In this configuration, you should store:

  • Your database host/port and login information.
  • Your "Secret key" for your LSL object communication.
  • Your "Secret key" for your Marketplace communication.
  • Your "Secret key" for redelivery signing.

It is important to keep this configuration out of public view(Read: Don't serve it on your web server). It is also recommended to use a different secret key for your LSL communication and marketplace communication.

Marketplace Secret Key

You cannot set your marketplace secret key. You can only obtain one from Linden Lab. If this is leaked, you can always generate a new one.

To obtain your marketplace secret key:

  1. Go to https://marketplace.secondlife.com/
  2. At the top of the page, click "My Marketplace" and choose "Merchant Home"
  3. Click "Store setup" and choose "Automatic Notifications (ANS)"

Your "Secret Key" will change every time you edit the URL, even if it is the same URL.

LSL Communcation Secret Key

You can set this to anything you want, but for security reasons, I recommend something at least 20 bytes in length(or 40 hex characters).

You can use the following command in bash to generate a unique secret key:

echo $(hexdump -n 20 -e '"%08X"' /dev/random)

Setting up the database

Although I recommend storing database in a "rational" sense, it isn't absolutely needed. You can store all the values in the same table, however it does have a chance of getting bloated. Storing data in a rational sense would greatly reduce bloat and increase database spead.

I will be describing the database in a rational sense. If this isn't your style you are free to modify this as you see fit.

Some databases do not allow for a UUID value type. In such case such as MySQL, simply substitute the UUID type for a varchar of 36, or bytes of 16. If you use a bytes of 16, you will need to convert the UUID down to it's raw bytes.

Some databases do not support the "serial" type. This is simply a AUTO INCREMENT data type that increases each time a row is inserted.

Table descriptions are defined in a similar format to SQL. That being, space seperated of the following(in order):

  • Field name
  • Field type
  • Field options
  • Optional comment(starts with --)

Foreign key means it references another table and that table SHOULD have a value mapping to what it is referencing.

Remember to escape your data, even if it is from the marketplace or LSL vending. You SHOULD NOT roll your own escaping function, use the one supplied with your database engine's library.

Residents table

The residents table will store a key and firstname/lastname relation. This allows for lookup on who is who. The table should be described as such:

key uuid PRIMARY KEY NOT NULL
firstname varchar(31) NOT NULL
lastname varchar(31) NOT NULL default 'resident'

You can optionally store additional details about a resident, however fetching such information is the focus of this project.

An example of a optional field would be a "flags" field, which can allow you to define if a resident is banned, or is an administrator.

Inventory table

This is our inventory table. This is what we reference to resolve specific items from redelivery, marketplace IDs, etc. This should ALWAYS contain all your items, even ones not for sale anymore.

id serial PRIMARY KEY
name varchar(255) NOT NULL
description text
image varchar(255)
marketplace_id int
product_id int
inventory_name varchar(63)

We use marketplace_id as our marketplace listing number, and product_id as our product number for our vending machines.

While you can merge the Inventory table with the Products table, I recommend against doing so.

Products table

This is our "products" table. This is what our vending machines in SL will use.

id serial PRIMARY KEY
name varchar(255) NOT NULL
description text
image uuid DEFAULT '32dfd1c8-7ff6-5909-d983-6d4adfb4255d'::uuid
price int DEFAULT 0
permissions int DEFAULT 0 --THIS IS BITWISE!

You can additionally add a flags field to do stuff like automatic refunds.

Marketplace Transactions

This is purely for referencial purpose. It isn't used by redelivery. This can be omitted if so desired.

id bigserial PRIMARY KEY --LL uses a bigint, so we need a big primary key
type varchar(50)
payment_gross varchar(12)
payment_fee varchar(12)
payer uuid NOT NULL --Foreign key with the Residents table
receiver uuid NOT NULL --Foreign key with the Residents table
merchant uuid NOT NULL --Foreign key with the Residents table
marketplace_id serial NOT NULL
marketplace_name varchar(255) NOT NULL
inventory_name varchar(255) NOT NULL
'date' timestamp without time zone DEFAULT CURRENT_DATE

This is useful for receipt lookups if something goes wrong.

Vendor Transactions

This is purely for referencial purpose. It isn't used by redelivery. This can be omitted if so desired.

id serial PRIMARY KEY
payer uuid NOT NULL --Foreign key with the Residents table
receiver uuid NOT NULL --Foreign key with the Residents table
merchant uuid NOT NULL --Foreign key with the Residents table
vendor_id uuid NOT NULL
vendor_location varchar(255) NOT NULL
inventory_name varchar(255) NOT NULL
'date' timestamp without time zone DEFAULT CURRENT_DATE

This is useful for receipt lookups if something goes wrong.

Purchases table

Here is where we store all purchases, these are not directly from the fields of the ANS or vendor response, however, correspond to our internal inventory.

id serial PRIMARY KEY
inventory_id serial --Foreign key with Inventory table
resident uuid NOT NULL --Foreign key with the Residents table
'date' timestamp without time zone DEFAULT CURRENT_DATE

Servers table

We need to know where our redelivery servers are at, so we create a table to keep track of them.

id uuid PRIMARY KEY
url varchar(255)
'date' timestamp without time zone DEFAULT CURRENT_DATE

Web endpoints

We need to be able to receive communications, so lets create our endpoints

GET /ans

We receive marketplace purchases here. We don't have much control over the request, so we do what we have to.

We don't have to return anything, however, responding with "200 OK" as status code should be done.

Marketplace will send the following GET parameters(or search/query parameters):

  • ItemID
  • ReceiverName
  • MerchantName
  • MerchantKey
  • ReceiverKey
  • InventoryName
  • Type
  • Region
  • PaymentFee
  • Currency
  • PaymentGross
  • Location
  • PayerKey
  • TransactionID
  • ItemName
  • PayerName

We should store the results in their appropriate fields of the "Marketplace Purchases" table first.

We can use the ItemID value to look up what they bought by searching the Inventory table by doing: SELECT id FROM Inventory WHERE marketplace_id = %s
Remember to replace the %s with the ItemID!

Using the Inventory ID from the previous query, we can insert the item into the Purchases table, and filling out the rest of the fields with the result from the ANS message.

If we leave it as is, we open ourselves to spoofing attacks!, so it is important that we verify the data is from the marketplace.

We can verify the request is indeed from the marketplace by:

  1. Taking the query string from our CGI server
  2. Removing the "?" if present at the beginning(Optional if you know that your CGI server does not return the "?" at the beginning)
  3. Appending our "Secret Key" we got from the merchant settings to the query string
  4. Hashing it with the SHA1 Algorythm(Be sure it is lower case!)

Example:

# We need to SHA1 hash the data
from hashlib import sha1

# This is our secret key, normally we'd store it in a configuration
ANS_Secret_Key = "00112233445566778899AABBCCDDEEFF"

# Define our dummy parameters
parameters = "?asdf=1&fdsa=2"

# If you know your CGI server doesn't return a "?" in front of this, you can
# skip this check. However it is here for example purposes:
if parameters.startswith("?"):
    # Remove the "?" from the beginning
    parameters = parameters[1:]

# If your hashing algorythm returns it in a UPPER CASE format, remember to
# turn it into LOWER CASE!
test = sha1(parameters + ANS_Secret_Key).hexdigest()

if test == requestHeaders.get("x-ans-verify-hash"):
    print("It is a real purchase!")
else:
    print("Someone is trying to impersonate the marketplace!")

GET /products

This endpoint does not need to be secure, but should return our product listing for vending machines to display.

It is recommended to do pagination as to avoid LSL "Out of Memory" errors. This can be done by adding GET parameters that pass to the LIMIT query. Recommended is both a LIMIT and OFFSET GET parameter.

You can return this as a JSON value, as well as use this as a shut off for your vendors. Simply have the vending machine refresh every so often.

If it returns something other than 200, the vending machine can process that as various codes, such as:

  • 500: Server is having issues
  • 403: Requesting vendor has been shut off
  • 401: Server requires verification

These are just examples, you can add more or not even implement them.

POST /purchase

This endpoint NEEDS TO BE SECURE, this handles your purchases from vending machines. You should use your LSL Secret Key here.

It is recommended to send the following values from SL to this endpoint:

  • Purchaser name(Both first and last name)
  • How much was payed
  • A UNIX timestamp, this can stop replay attacks.
  • Item ID from the database

You SHOULD validate all these fields on the server, even if you already validated them via LSL.

You can get various headers that LSL automatically supplies for additional data and verification:

  • X-SecondLife-Shard
  • X-SecondLife-Region
  • X-SecondLife-Owner-Name
  • X-SecondLife-Owner-Key
  • X-SecondLife-Object-Name
  • X-SecondLife-Object-Key
  • X-SecondLife-Local-Velocity
  • X-SecondLife-Local-Rotation
  • X-SecondLife-Local-Position

Additional headers are detailed here: https://wiki.secondlife.com/wiki/LlHTTPRequest

You should verify the Owner Key. You can verify the region for authorized locations. You MUST verify the data sent.

Recommended way of verifying this is having the LSL script "hash" the request body in the same way the marketplace does, that being:

string verifyHash = llSHA1String( requestBody + SECRET_KEY);

Where you supply this is up to you, it can be a CUSTOM_HEADER, "x-verify-hash", verifyHash, or it can be a query string.

If you put it in the body, MAKE SURE that it is seperatable from the body.

Additionally, NEVER send the SECRET_KEY along with the body.

POST /lslreg

This endpoint MUST BE secure, this handles server registration for redeliveries.

You only need to, at minimum, verify X-SecondLife-Owner-Key. You can optionally use a secret key(other than your existing ones) as verification.

You can also use this to register vendors for added security.

GET /redelivery

This endpoint SHOULD BE secure.
This endpoint returns HTML.\

This is where residents see the redelivery system. You have multiple options on how to do this:

  1. You can supply a redelivery terminal in world that generates a unique URL for the resident.
  2. You can supply a login that asks for a SL username, then send a URL to them in world via llInstantMessage. Be sure to use a Captcha for this method.
  3. You can create a full login system with username and passwords.

If you use a password based system:

  • It is important to encourage the user to NEVER use their SL password.
  • If you store passwords, YOU SHOULD USE A PROPER HASHING ALGORYTHM such as bcrypt, or scrypt.\
  • AVOID using rolling your own hashing system, use something standardized like passlib.

Recommended query parameters are:

  • resident - Resident UUID
  • time - A timestamp, recommended to be unix time modulo'd to 86400. This would keep the URL alive for one day.
  • signature - Authentication token, the resident and time hashed with a secret key.

How you display the data is up to you. You can use javascriptless approach using forms, or use full on javascript with ajax.

All you have to do is remember to send the signature off with the request. It doesn't have to contain a signed version of the product, but should contain the signed version of the resident and time.

Getting the information should first request a listing of Purchases table where resident is equal to the supplied resident query parameter.

You should then take the returned inventory_ids and resolve them.

You can now display the resulting data to the user.

POST /redeliver

This endpoint MUST BE secure.

This is where the resident ends up when they file a redelivery claim on the redelivery page. You should verify that:

  1. They own the item they are requesting
  2. The request is signed

If it all checks out, you can request the inventory_name from the inventory table, and then request a server that has reported it's existance in the past hour from the servers table.

You SHOULD check if the server responded correctly, and if it didn't, skip down to the next until either one responds correctly, or you run out.

If no server responds, you SHOULD put a error message informing the resident to contact you.

Optional endpoints

You can make your life easier by creating a administration panel. I for one prefer to talk with SQL myself, however many users do prefer to use some sort of panel for doing this.

Your administration panel can do various things:

  • Allow for you to redeliver on people's behalf
  • Managing products
  • Managing inventory
  • Schedual updates

LSL implementations

Redelivery / object server

A server is responsible for your in-world stuff, be it redeliveries or sending items to people who purchase stuff from vendors.

Initialization works as follows:

  1. Request a URL using llRequestSecureURL() and wait for the http_request
  2. Store the URL in a variable
  3. Send the variable off to your /lslreg endpoint.

You should have the script either re-initialize or just completely restart when:

  • Region starts
  • URL testing fails

URL testing can be done by either:

  1. Sending a POLL request to the /lslreg endpoint and having the endpoint test the URL.
  2. Requesting the URL from within the LSL script.

Your server should implement the following:

  1. The ability to send inventory off to a resident.
  2. The ability to send a IM off to a resident.

You SHOULD always sign any incoming and outgoing messages with your LSL secret key. Although unlikely, if you accidentally leak your URL, signing prevents spoofing attacks.

Vending machines

WARNING: This involves MONEY TRANSFER, you should take EXTREME CAUTION when dealing with this, and always use fail safes.

Your vending machine should:

  1. Prevent another user from controlling it while someone else is. This can be done by locking it for a specific time and freeing it after inactivity.
  2. Issue refunds using llTransferMoney if your web server does not respond with "200 OK".
  3. Display the item image, price, and L$ cost.

You MUST sign all requests sent to the /purchase endpoint.

You should make your vendor secure against common attacks:

  1. Sitting - This can confuse linksets. It also converts it to a "vehicle".
  2. Spamming - This involves clicking the menu many times to cause too many http request to be sent. This can be fixed by caching.
  3. Payment spoofing - Some modified viewers can send modified L$ payments to vendors, you should always check the money received is the money required.

You SHOULD supply a single button to do payments for each item. This ensures a frustration free experience for the resident. You can do this by using this function: https://wiki.secondlife.com/wiki/LlSetPayPrice

WARNING: The llSetPayPrice function DOES NOT guarentee the resident will be paying that much. Some viewers do not support it, and modified viewers can spoof it as L$0! Always verify the money sent is the money required. In any case, you should refund the resident if it is incorrect.

You can request the item listing from the /products endpoint.

Verifying payment:
When the vending machine is payed, you will receive a money event, simply check the second parameter and compare it to the item price stored in memory.

integer currentItemPrice; //Set this when setting the currently displayed item
state_entry(){
    money(key id, integer amount){
        if(amount != currentItemPrice){
            llSay(0, "You payed me the wrong amount!");
            llInstantMessage(llGetOwner(),
                "Possible spoofing attack from " + (string)id
                + ", payed " + (string)amount
                + ", expected "+(string)currentItemPrice
            );
            llTransferMoney(id, amount);
        }else{
            llSay(0, "Thank you for your purchase!");
            //Do transaction stuff
        }
    }
}

Preventing double purchase:
When a purchase is being processed, you should lock the vendor to prevent double payments. You can outright disable the pay button by swapping state, but this comes with the disadvantage of loosing various events. You should instead lock the vendor by refunding any purchases during a lock.

*Preventing "I accidentally bought what someone else was buying!":* You should confirm the payer is the one who has the lock. If they don't have the lock, you should refund and not process their payment. This also prevents users from griefing others.

Shutting down the vendor in case of error:
It is recommended that if a error does occur, that the vendor locks until the web server is responding again. This will prevent any disgruntled customers.

Handling updates

To handle updates, this can be done in various ways:

  1. Have a script that can be executed from either command line or the web panel. If you do this, you should make sure it doesn't run in the web context. It WILL time out. Instead, run it in a seperate context.
  2. Have a LSL script that requests information from the server and request the server do a redelivery.
  3. Send a group announcement about a update as well as instructions on how to get to the redelivery panel.

In either case, it has the same idea:

  1. Select a row from purchases where item_id is the product to be updated. Limit it by 1, and set the offset to the last ID.
  2. If there is a row response, send a redelivery request.
  3. If there is no row response, stop the script, you reached the end.

Handling double purchases

It is probably best to avoid this on the server side, and instead deal with it on a person by person basis.

In general:

  • If the item is "Copy/No Transfer", you should issue a refund.
  • If the item is "No Copy/Transfer", you can request the item to be given to you if for a refund.
  • If the item is "No Copy/No Transfer", you cannot get reasonable evidence of item deletion, so you do not need to refund. DO NOT ACCEPT REQUESTS TO LOG INTO THE USER'S ACCOUNT TO DELETE! Logging into someone else's account is a violation of the Terms of Service.
  • If the item is "Copy/Transfer", you should not issue a refund.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment