# SIPI Lua Interface

SIPI has an embedded [LUA](http://www.lua.org) interpreter. LUA is a simple script language that was developped specifically to be embedded into applications. For example the games [minecraft](https://www.minecraft.net) and [World of Warcraft](https://worldofwarcraft.com/de-de/) make extensive use of LUA scripting for customization and programming extensions.

Each HTTP request to SIPI invokes a recent, independent lua-instance (Version 5.3.5). Therefore, LUA may be used in the following contexts:

- Preflight function
- Embedded in HTML pages
- RESTful services using the SIPI routing

Each lua-instance in SIPI includes additional SIPI-specific information:

- global variables about the SIPI configuration
- information about the current HTTP request
- SIPI specific functions for
- processing the request and send back information
- getting image information and transforming images
- querying and changing the SIPI runtime configuration (e.g. the cache)

In general, the SIPI LUA function make use that a Lua function's return value may consist of more than one element (see [Multiple Results](http://www.lua.org/pil/5.3.html)):

Sipi provides the [LuaRocks](https://luarocks.org/) package manager which must be used in the context of SIPI.

*The Lua interpreter in Sipi runs in a multithreaded environment: each request runs in its own thread and has its own Lua interpreter. Therefore, only Lua packages that are known to be thread-safe may be used!*

## Preflight function

It is possible to define a LUA pre-flight function for *IIIF*-requests and independently one for *file*-requests (indicated by a */file* postfix in the URL). Both are optional and are best located in the init-script (see [configuration options](https://sipi.io/guide/sipi/#setup-of-sipi-directories) of SIPI). It is executed after the incoming HTTP request data has been processed but before an action to respond to the request has been taken. It should be noted that the pre-flight script is only executed for IIIF-specific requests (either using the IIIF URL-syntax or the */file* postfix). All other HTTP requests are being directed to "normal" HTTP-server part of SIPI. These can utilize the lua functionality by embedding LUA commands within the HTML.

### IIIF preflight function

The IIIF preflight function must have the name **pre_flight** with the following signature:

```
function pre_flight(prefix,identifier,cookie)

    return "allow", filepath
end
```

The preflight function takes 3 parameter:

- `prefix`: This is the prefix that is given on the IIIF url [mandatory]\
  *http(s)://{server}/**{prefix}**/{id}/{region}/{size}/{rotation}/{quality}.{format}*\
  Please note that the prefix may contain several "/" that can be used as path to the repository file
- `identifier`: The image identifier (which must not correspond to an actual filename in the media files repositoy of the SIPI IIIF server) [mandatory]
- `cookie`: A cookie containing authorization information. Usually the cookie contains a Json Web Token [optional]

The pre-flight function must return at least 2 parameters:

- `permission`: A string or a table indication the permission to read the image. In a simple case it's either the string `"allow"` or `"deny"`.\
  To allow more flexibility, the following permission tables are supported:
  - Restricted access with watermark. The watermark must be a TIFF file with a single 8-bit channel (gray value image). For example:\
    `{ type = 'restrict', watermark = './wm/mywatermark.tif' }`
  - Restricted access with size limitation. The size must be a [IIIF size expression](https://iiif.io/api/image/3.0/#42-size). For example:\
    `{ type = 'restrict', size='!256,256' }`
  - SIPI also supports the [IIIF Authentification API](https://iiif.io/api/auth/1.0/). See section IIIF Authentification on how to implement this feature in the pre-flight function.
- `filepath`: The path to the master image file in the media files repository. This path can be assembled using the `prefix` and `identifier` using any additional information (e.g. accessing a database or using the LUA restful client)

The most simple working pre-flight looks as follows assuming that the `identifier`is the name of the master image file in the repository and the `prefix` is the path:

```
function pre_flight(prefix, identifier, cookie)
    if config.prefix_as_path then
        filepath = config.imgroot .. '/' .. prefix .. '/' .. identifier
    else
        filepath = config.imgroot .. '/' .. identifier
    end
    return 'allow', filepath
end
```

Above example preflight function allows all files to be served without restriction.

#### More complex example of preflight function

The following example uses some SIPI lua funtions to access an authorization server to check if the user (identified by a cookie) is allowed to see the specific image. We are using [Json Web Tokens](https://jwt.io) (JWT) which are supported by SIPI specific LUA functions. Please note that the SIPI JTW-functions support an arbitrary payload that has not to follow the JWT recommendations. In order to encode, the JWT_ALG_HS256 is beeing used together with the key that is defined in the SIPI configuration as [jwt_secret](https://sipi.io/guide/sipi/#jwt-secret).

```
function pre_flight(prefix, identifier, cookie) 
    --
    -- make up the file path
    --
    if config.prefix_as_path then
        filepath = config.imgroot .. '/' .. prefix .. '/' .. identifier
    else
        filepath = config.imgroot .. '/' .. identifier
    end
    --
    -- we need a cookie containing the user information that will be
    -- sent to the authorization server. In this
    -- example, the content does not follow the JWT rules
    -- (which is possible to pack any table into a JWT encoded token)
    --
    if cookie then
        --
        -- we decode the cookie in order to get a table of key/value pairs
        --
        success, userinfo = server.decode_jwt(cookie)
        if not success then
            return 'deny', filepath
        end
        --
        -- prepare the RESTful call to the authorization server
        --

        -- add the image identifier to the info table:
        userinfo["imgid"] = identifier
        -- encode the userinfo to a JWT-like token:
        local new_cookie = server.generate_jwt(userinfo) 
        local url = 'http://auth.institution.org/api/getauth/' .. identifier
        local auth_information = { Cookie = new_cookie }
        --
        -- make the HTTP request with a timeout of 500 ms
        --
        success, result = server.http('GET', url, auth_information, 500) 
        if success then
            --
            -- we got a response from the server
            --
            success, response_json = server.json_to_table(result.body)
            if success then  -- everything OK
                return {
                            type = response_json.type,
                            restriction = response_json.restriction
                        }, filepath
            else
                return 'deny', filepath
            end
        else
            return 'deny', filepath
        end
    else
        return 'deny', filepath
    end
end
```

Above example assumes that the cookie data is a string that contains encrypted user data from a table (key/value pair). Jason Web Token. This token is decoded and the information about the image to be displayed is added. Then the information is encoded as a new token that ist transmitted to the RESTful interface of the authentification server. The answer is assumed to be json containing information about the type ('allow', 'deny', 'restrict') and the restriction settings. The pre-flight function uses the following SIPI-specific LUA global variables and function:

- [config.imgroot](#configimgroot): (Global variable) Root directory of the image repository.
- [server.http()](#serverhttp): (Function) Used to create a RESTful GET request.
- [server.generate_jwt()](#servergenerate_jwt): (Function) Create a new JWT token from a key/value table.
- [server.json_to_table()](#serverjson_to_table): (function) Convert a JSON into a LUA table.

### File preflight function

An URL in the form `http(s)://{server}/{prefix}/{identifier}/file` serves the given file as binary object (including propere mimetype in the header etc.). The file has to reside in the directory tree defined for IIIF requests. In these cases, a preflight script name `file_pre_flight` is being called if defined. Its signature is as follows:

```
function file_pre_flight(filepath, cookie)

end
```

A simple example allowing access only to the file *"unit/test.csv"* would be:

```
function file_pre_flight(filepath, cookie)
    if filepath == "./images/unit/test.csv" then
        return "allow", filepath
    else
        return "deny", ""
    end
end
```

This script would deny all other file access and the SIPI IIIF server responds with a `401 Unauthorized` error.

## LUA embedded in HTML

The HTTP server that is included in SIPI can serve any type of file which are just transfered as is to the client. However, if a file has an extension of `.elua`, it is assumed to be a HTML file with embedded LUA code. ALL SIPI-specific LUA functions and global variables are available.

Embedding works with the special tag `<lua>` and `</lua>`. All text between the opening and closing tag is interpreted as LUA code. SIPI provides an extra LUA function to output data to the client ([server.print](#serverprint)). Thus, dynamic, server-generated HTML may be created. A sample page that displays some information about the server configuration and client info could like follows:

```
<html>
    <head>
        <title>SIPI Configuration Info</title>
    </head>
    <body>
    <h1>SIPI Configuration Info</h1>
    <h2>Configuration variables</h2>
    <table>
        <tr>
            <td>imgroot</td>
            <td>:</td>
            <td><lua>server.print(config.imgroot)</lua></td>
        </tr>
        <tr>
            <td>docroot</td>
            <td>:</td>
            <td><lua>server.print(server.docroot)</lua></td>
        </tr>
        <tr>
            <td>hostname</td>
            <td>:</td>
            <td><lua>server.print(config.hostname)</lua></td>
        </tr>
        <tr>
            <td>scriptdir</td>
            <td>:</td>
            <td><lua>server.print(config.scriptdir)</lua></td>
        </tr>
        <tr>
            <td>cachedir</td>
            <td>:</td>
            <td><lua>server.print(config.cache_dir)</lua></td>
        </tr>
        <tr>
            <td>tmpdir</td>
            <td>:</td>
            <td><lua>server.print(config.tmpdir)</lua></td>
        </tr>
        <tr>
            <td>port</td>
            <td>:</td>
            <td><lua>server.print(config.port)</lua></td>
        </tr>
        <lua>
            if server.has_openssl then
                server.print('<tr><td>SSL port</td><td>:</td><td>' ..
                             config.sslport .. '</td></tr>')
            end
        </lua>
        <tr>
            <td>number of threads:</td>
            <td>:</td>
            <td><lua>server.print(config.n_threads)</lua></td>
        </tr>
        <tr>
            <td>maximal post size:</td>
            <td>:</td>
            <td><lua>server.print(config.max_post_size)</lua></td>
        </tr>
    </table>
    <h2>Client information</h2>
    <table>
        <tr>
            <td>Host in request</td>
            <td>:</td>
            <td><lua>server.print(server.host)</lua></td>
        </tr>
        <tr>
            <td>IP of client</td>
            <td>:</td>
            <td><lua>server.print(server.client_ip)</lua></td>
        </tr>
        <tr>
            <td>URL path</td>
            <td>:</td>
            <td><lua>server.print(server.uri)</lua></td>
            </tr>
    </table>

    <p>Important Note: "IP of client" and "Host in request" may
       indicate the information of a proxy and notof the actual
       client!</p>
    <h2>Request Header Information</h2>
    <table>
        <lua>
            for key, val in pairs(server.header) do
                server.print('<tr><td>' .. key ..
                             '</td><td>:</td><td>' .. val .
                             '</td></tr>')
            end
        </lua>
    </table>
    </body>
</html>
```

### Embedded LUA and enforcing SSL

The supplied example initialization file offers a LUA function that enforces the use of a SSL encryption page proteced by a user name and password. It is used as follows by adding the following code *before the `<html>` opening tag*:

```
<lua>
    if server.secure then
        protocol = 'https://'
    else
        protocol = 'http://'
    end

    success,token = authorize_page('admin.sipi.org',
                                   'administrator',
                                    extecteduser, expectedPassword)
    if not success then
        return
    end
</lua>
```

where `expectedUser` and `extectedPassword` are the user/password combination the user is expected to enter.

### File uploads to SIPI

The SIPI specific LUA function allow the upload of files using POST requests with `multipart/form-data` content. The global variable `server.uploads` contains\`the information about the uploads. The following variables and function help to deal with uploads:

- [server.uploads](#serveruploads) : information about the files in the upload request.
- [server.copyTmpfile](#servercopytmpfile) : copies a fie from the upload location to the destination directory.

In addition the file system functions that SIPI provides may be used.

See the scripts `upload.elua` and `do-upload.elua` in the server directory, and `upload.lua` in the scripts directory for a working example.

## RESTful API and custom routes

Custom routes to implement a RESTful API can be defined in Sipi's configuration file using the `routes` configuration variable. For example:

```
routes = {
    {
        method = 'GET',
        route = '/status',
        script = 'get_repository_status.lua'
    },
    {
        method = 'POST',
        route = '/make_thumbnail',
        script = 'make_image_thumbnail.lua'
    }
}
```

Sipi looks for these scripts in the directory specified by `scriptdir` in its configuration file. The first route that matches the beginning of the requested URL path will be used.

## IIIF Authentication API 1.0 in SIPI

The `pre_flight` function is also responsible for activating the IIIF Auth API. In order to do so, the pre_flight script returns a table that contains all necessary information. For details about the IIIF Authentication API 1.0 see the [IIIF documentation](https://iiif.io/api/auth/1.0/). The following fields have to be returned by the `pre_flight`-function as LUA-table:

- `type`: String giving the type. Valid are:\
  `"login"`, `"clickthrough"`, `""kiosk"` or `"external"`.
- `cookieUrl`: URL where to get a valid IIIF Auth cookie for this service.
- `tokenUrl`: URL where to get a valid IIIF Auth token for this service.
- `confirmLabel`: Label to display in confirmation box.
- `description`: Description for login window.
- `failureDescription`: Information, if login fails.
- `failureHeader`: Header for failure window.
- `header`: Header of login window
- `label`: Label of the login window

In addition, the filepath has to be returns. A full response may look as follows:

```
return {
   type = 'login',
    cookieUrl = 'https://localhost/iiif-cookie.html',
    tokenUrl = 'https://localhost/iiif-token.php',
    confirmLabel =  'Login to SIPI',
    description = 'This Example requires a demo login!',
    failureDescription = '<a href="http://example.org/policy">Access Policy</a>',
    failureHeader = 'Authentication Failed',
    header = 'Please Log In',
    label = 'Login to SIPI',
}, filepath
```

SIPI will use this information returned by the `pre_flight` function to return the appropriate responses to the client requests based on the IIIF Authentication API 1.0. Check for support of the IIIF Authentication API 1.0 for [mirador](https://projectmirador.org) and [universalviewer](https://universalviewer.io), both applications which suppport the IIIF standards.

## SIPI variables available to Lua scripts

There are many globally accessible LUA variables made available which reflext the configuration of SIPI and the state of the server and request. This variables a read only and created for every request.

### SIPI configuration variables

This variables are defined ither in the configuration file if SIPI, in environemt variables at startup or as command line option when starting the server.

#### config.hostname

```
config.hostname
```

The hostname SIPI is configures to run on (see [hostname](https://sipi.io/guide/sipi/#hostname) in configuration description).

#### config.port

```
config.port
```

Portnumber where the SIPI server listens (see [serverport](https://sipi.io/guide/sipi/#portnum) in configuration description).

#### config.sslport

```
config.sslport
```

Portnumber for SSL connections of SIPI (see [sslport](https://sipi.io/guide/sipi/#sslport) in configuration description).

#### config.imgroot

```
config.imgroot
```

Root directory for IIIF-served images (see [imgroot](https://sipi.io/guide/sipi/#imgroot) in configuration description).

#### config.docroot

```
config.docroot
```

Root directory for WEB-Server (see [docroot](https://sipi.io/guide/sipi/#docroot) in configuration description).

#### config.max_temp_file_age

```
config.max_temp_file_age
```

maximum age of temporary files (see [max_temp_file_age](https://sipi.io/guide/sipi/#maxtmpfileage) in configuration description).

#### config.prefix_as_path

```
config.prefix_as_path`
```

`true` if the prefix should be used as path info (see [prefix_as_path](https://sipi.io/guide/sipi/#prefixaspath) in configuration description).

#### config.init_script

```
config.init_script
```

Path to initialization script (see [initscript](https://sipi.io/guide/sipi/#scriptinit) in configuration description).

#### config.scriptdir

```
config.scriptdir
```

Path to script directory. (see [scriptdir](https://sipi.io/guide/sipi/#scriptdir) in configuration description).

#### config.cache_dir

```
config.cache_dir
```

Path to cache directory for iIIF served images. (see [cachedir](https://sipi.io/guide/sipi/#cachedir) in configuration description).

#### config.cache_size

```
config.cache_size
```

Maximal size of cache (see [cachesize](https://sipi.io/guide/sipi/#cachesize) in configuration description).

#### config.cache_n_files

```
config.cache_n_files
```

Maximal number of files in cache. (see [cache_nfiles](https://sipi.io/guide/sipi/#cachenfiles) in configuration description).

#### config.cache_hysteresis

```
config.cache_hysteresis
```

Amount of data to be purged if cache reaches maximum size. (see [cache_hysteresis](https://sipi.io/guide/sipi/#hysteresis) in configuration description).

#### config.jpeg_quality

```
config.jpeg_quality
```

Unfortunately, the IIIF Image API does not allow to give a JPEG quality (=compression) on the IIIF URL. SIPI allows to configure the compression quality system wide with this parameter. Allowed values are in he range [1..100] where 1 the worst quality (and highest compression factor = smallest file size) and 100 the highest quality (with lowest compression factor = biggest file size). Please note that SIPI is not able to provide lossless compression for JPEG files. (see [jpeg_quality](https://sipi.io/guide/sipi/#jpegquality) in configuration description).

#### config.keep_alive

```
config.keep_alive
```

Maximal keep-alive time for HTTP requests that ask for a keep-alive connection. (see [keep_alive](https://sipi.io/guide/sipi/#keepalive) in configuration description).

#### config.thumb_size

```
config.thumb_size
```

Default thumbnail image size. (see [thumb_size](https://sipi.io/guide/sipi/#thumbsize) in configuration description).

#### config.n_threads

```
config.n_threads
```

Number of worker threads SIPI uses. (see [nthreads](https://sipi.io/guide/sipi/#nthreads) in configuration description).

#### config.max_post_size

```
config.max_post_size
```

Maximal size of POST data allowed (see [max_post_size](https://sipi.io/guide/sipi/#maxpostsize) in configuration description).

#### config.tmpdir

```
config.tmpdir
```

Temporary directory to store uploads. (see [tmpdir](https://sipi.io/guide/sipi/#tmpdir) in configuration description).

#### config.ssl/\_certificate

```
config.ssl_certificate
```

Path to the SSL certificate that SIPI uses. (see [ssl_certificate](https://sipi.io/guide/sipi/#sslcertificate) in configuration description).

#### config.ssl/\_key

```
config.ssl_key
```

Path to the SSL key that SIPI uses. (see [ssl_key](https://sipi.io/guide/sipi/#sslkey) in configuration description).

#### config.logfile

```
config.logfile
```

Name of the logfile. SIPI is currently using the built-in logger which logs to stdout and the logfile name is ignored. (see [logfile](https://sipi.io/guide/sipi/#logfile) in configuration description).

#### config.loglevel

```
config.loglevel
```

Indicates what should be logged. The variable contains a integer that corresponds to the syslog level. (see [loglevel](https://sipi.io/guide/sipi/#loglevel) in configuration description).

#### config.adminuser

```
config.adminuser
```

Name of admin user. (see [user](https://sipi.io/guide/sipi/#configuration-of-administrator-access) in configuration description).

#### config.password

```
config.password
```

Password (plain text, not encrypted) of admin user (*use with caution*)! (see [password](https://sipi.io/guide/sipi/#configuration-of-administrator-access) in configuration description).

### SIPI Server Variables

Sipi server variables are dependent on the incoming request and are created by SIPI automatically for each request.

#### server.method

```
server.method
```

The HTTP request method. Is one of `OPTIONS`, `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `TRACE`, `CONNECT` or `OTHER`.

#### server.has_openssl

```
server.has_openssl
```

`true` if OpenSSL is available. This variable is determined compilation time. Usually SSL should be included, but SIPI can be compiled without SSL support. There is no option in the configuration file for this.

#### server.secure

```
server.secure
```

`true` if the connection was made over HTTPS using SSL.

#### server.host

```
server.host
```

The hostname of the Sipi server that was used in the request.

#### server.client_ip

```
server.client_ip
```

The IPv4 or IPv6 address of the client connecting to Sipi.

#### server.client_port

```
server.client_port
```

The port number of the client socket.

#### server.uri

```
server.uri
```

The URL path used to access Sipi (does not include the hostname).

#### server.header

```
server.header
```

A table containing all the HTTP request headers(in lowercase).

#### server.cookies

```
server.cookies
```

A table of the cookies that were sent with the request.

#### server.get

```
server.get
```

A table of GET request parameters.

#### server.post

```
server.post
```

A table of POST or PUT request parameters.

#### server.request

```
server.request
```

All request parameters.

#### server.content

```
server.content
```

If the request had a body, the variable contains the body data. Otherwise it's `nil`.

#### server.content_type

```
server.content_type
```

Returns the content type of the request. If there is no type or no content, this variable is `nil`.

#### server.uploads

```
server.uploads
```

This is an array of upload parameters, one per file. Each one is a table containing:

- `fieldname`: the name of the form field.
- `origname`: the original filename.
- `tmpname`: a temporary path to the uploaded file.
- `mimetype`: the MIME type of the uploaded file as provided by the browser.
- `filesize`: the size of uploaded file in bytes.

The upload can be accessed as follows:

```
for index, value in pairs(server.uploads) do
    --
    -- copy the uploaded file to the image repository using the original name
    --
    server.copyTmpfile(index, config.imgdir .. '/' .. value["origname"])
end
```

### Knora-specific variables

The development of SIPI came out of the need to have a flexible, high performance IIIF server for the Swiss National research infrastructure [Data and Service Center for the Humanities](https://dasch.swiss) (DaSCH). The aim of the DaSCH is to guarantee long-term accessibility of research data from the Humanities. The operates a specialized platform [Knora](https://knora.org). The following variables are for internal use only.

#### config.knora_path

```
config.knora_path
```

Path to knora REST API (only for SIPI used with Knora)

#### config.knora_port

```
config.knora_port
```

Port that the Knora API uses

## SIPI functions available to Lua scripts

Sipi provides the following functions that can be called from Lua scripts. Each function returns two values. The first value is `true` if the operation succeeded, `false` otherwise. If the operation succeeded, the second value is the result of the operation, otherwise it is an error message.

### SIPI Connection Functions

These LUA function alter the way the HTTP connection is handled.

#### server.setBuffer

```
success, errmsg = server.setBuffer([bufsize][,incsize])
```

Activates the the connection buffer. Optionally the buffer size and increment size can be given. Returns `true, nil` on success or `false, errormsg` on failure.

#### server.sendHeader

```
success, errormsg = server.sendHeader(key, value)
```

Sets an HTTP response header. Returns `true, nil` on success or `false, errormsg` on failure.

#### server.sendCookie

```
success, errormsg = server.sendCookie(key, value [, options-table])
```

Sets a cookie in the HTTP response. Returns `true, nil` on success or `false, errormsg` on failure. The optional `options-table` is a Lua table containing the following keys:

- `path`
- `domain`
- `expires` (value in seconds)
- `secure` (boolean)
- `http_only` (boolean)

#### server.sendStatus

```
server.sendStatus(code)
```

Sends an HTTP status code. This function is always successful and returns nothing.

#### server.print

```
success, errormsg = server.print(values)
```

Prints variables and/or strings over the HTTP connection to the client that originated the request. Returns `true, nil` on success or `false, errormsg` on failure.

#### server.requireAuth

```
success, table = server.requireAuth()
```

This function retrieves HTTP authentication data that was supplied after sending a `'WWW-Authenticate'`-header (e.g. by issuing a the following commands to enter the HTTP login dialog:

```
    server.setBuffer()
    server.sendStatus(401);
    server.sendHeader('WWW-Authenticate', 'Basic realm="Sipi"')
```

It returns `true, table` on success or `false, errormsg` on failure. The result of the authorization is returned as table with the following elements:

- `status`: Either `BASIC`, `BEARER`, `NOAUTH` (no authorization header) or `ERROR`
- `username`: A string containing the supplied username (only existing if stats is `BASIC`)
- `password`: A string containing the supplied password (only existing if stats is `BASIC`)
- `token`: A string containing the raw token information (only if status `BEARER`)
- `message`: A string containing the error message (only if status `ERROR`)

Example:

```
success, auth = server.requireAuth()
if not success then
    server.sendStatus(501)
    server.print("Error in getting authentication scheme!")
    return -1
end

if auth.status == 'BASIC' then
    --
    -- everything OK, let's create the token for further
    -- calls and ad it to a cookie
    --
    if auth.username == config.adminuser and
       auth.password == config.password then
        tokendata = {
            iss = "sipi.unibas.ch",
            aud = "knora.org",
            user = auth.username
        }
        success, token = server.generate_jwt(tokendata)
        if not success then
            server.sendStatus(501)
            server.print("Could not generate JWT!")
            return -1
        end
        success, errormsg = server.sendCookie('sipi',
                                              token,
                                              {path = '/', expires = 3600})
        if not success then
            server.sendStatus(501)
            server.print("Couldn't send cookie with JWT!")
            return -1
        end
    else
        server.sendStatus(401)
        server.sendHeader('WWW-Authenticate', 'Basic realm="Sipi"')
        server.print("Wrong credentials!")
        return -1
    end
elseif auth.status == 'BEARER' then
    success, jwt = server.decode_jwt(auth.token)
    if not success then
        server.sendStatus(501)
        server.print("Couldn't deocde JWT!")
        return -1
    end
    if (jwt.iss ~= 'sipi.unibas.ch') or
       (jwt.aud ~= 'knora.org') or
       (jwt.user ~= config.adminuser) then
        server.sendStatus(401)
        server.sendHeader('WWW-Authenticate', 'Basic realm="Sipi"')
        return -1
    end
elseif auth.status == 'NOAUTH' then
    server.setBuffer()
    server.sendStatus(401);
    server.sendHeader('WWW-Authenticate', 'Basic realm="Sipi"')
    return -1
else
    server.status(401)
    server.sendHeader('WWW-Authenticate', 'Basic realm="Sipi"')
    return -1
end
```

### SIPI File System Function

These functions offer tools to manipuale files and directories, and to gather file information.

#### server.fs.ftype

```
success, filetype = server.fs.ftype(filepath)
```

Checks the filetype of a given filepath. Returns either `true, filetype` (with filetype one of `"FILE"`, `"DIRECTORY"`, `"CHARDEV"`, `"BLOCKDEV"`, `"LINK"`, `"SOCKET"` or `"UNKNOWN"`) or `false, errormsg`.

#### server.fs.modtime

```
success, modtime = server.fs.modtime(filepath)
```

Retrieves the last modification date of a file in seconds since epoch UTC. Returns either `true`, `modtime` or `false`, `errormsg`.

#### server.fs.is_readable

```
success, readable = server.fs.is_readable(filepath)
```

Checks if a file is readable. Returns `true, readable` (boolean) on success or `false, errormsg` on failure.

#### server.fs.is_writeable

```
success, writeable = server.fs.is_writeable(filepath)
```

Checks if a file is writeable. Returns `true, writeable` (boolean) on success or `false, errormsg` on failure.

#### server.fs.is_executable

```
success, errormsg = server.fs.is_executable(filepath)
```

Checks if a file is executable. Returns `true, executable` (boolean) on success or `false, errormsg` on failure.

#### server.fs.exists

```
success, exists = server.fs.exists(filepath)
```

Checks if a file exists. Checks if a file exists. Returns `true, exists` (boolean) on success or `false, errormsg` on failure.

#### server.fs.unlink

```
success, errormsg = server.fs.unlink(filename)
```

Deletes a file from the file system. The file must exist and the user must have write access. Returns `true, nil` on success or `false, errormsg` on failure.

#### server.fs.mkdir

```
success, errormsg = server.fs.mkdir(dirname, [tonumber('0755', 8)])
```

Creates a new directory, optionally with the specified permissions. Returns `true, nil` on success or `false, errormsg` on failure.

#### server.fs.rmdir

```
success, errormsg = server.fs.rmdir(dirname)
```

Deletes a directory. Returns `true, nil` on success or `false, errormsg` on failure.

#### server.fs.getcwd

```
success, curdir = server.fs.getcwd()
```

Gets the current working directory. Returns `true, current_dir` on success or `false, errormsg` on failure.

#### server.fs.readdir

```
success, filenames = server.fs.readdir(dirname)
```

Gets the names of the files in a directory, not including `.` and `..`. Returns `true, table` on success or `false, errormsg` on failure.

#### server.fs.chdir

```
success, oldir = server.fs.chdir(newdir)
```

Change working directory. Returns `true, olddir` on success or `false, errormsg` on failure.

#### server.fs.copyFile

```
success, errormsg = server.fs.copyFile(source, destination)
```

Copies a file from source to destination. Returns `true, nil`on success or `false, errormsg` on failure.

#### server.fs.moveFile

```
success, errormsg = server.fs.moveFile(from, to)
```

Moves a file. The move connot cross filesystem boundaries! `true, nil`on success or `false, errormsg` on failure.

### Other Helper Function

#### server.http

```
success, result = server.http(method, "http://server.domain[:port]/path/file" [, header] [, timeout])
```

Performs an HTTP request using curl. Currently implements only GET requests. Parameters:

- `method`: The HTTP request method. Currently must be `"GET"`.
- `url`: The HTTP URL.
- `header`: An optional table of key-value pairs representing HTTP request headers.
- `timeout`: An optional number of milliseconds until the connection times out.

Authentication is not yet supported.

The result is a table:

```
result = {
    status_code = value -- HTTP status code returned
    erromsg = "error description" -- only if success is false
    header = {
        name = value [, name = value, ...]
    },
    certificate = { -- only if HTTPS connection
        subject = value,
        issuer = value
    },
    body = data,
    duration = milliseconds
}
```

Example:

```
success, result = server.http("GET", "http://www.salsah.org/api/resources/1", 100)

if (result.success) then
   server.print("<table>")
   server.print("<tr><th>Field</th><th>Value</th></tr>")
   for k,v in pairs(server.header) do
       server.print("<tr><td>", k, "</td><td>", v, "</td></tr>")
   end
   server.print("</table><hr/>")

   server.print("Duration: ", result.duration, " ms<br/><hr/>")
   server.print("Body:<br/>", result.body)
else
   server.print("ERROR: ", result.errmsg)
end
```

#### server.table_to_json

```
success, jsonstr = server.table\_to\_json(table)
```

Converts a (nested) Lua table to a JSON string. Returns `true, jsonstr` on success or `false, errormsg` on failure.

#### server.json_to_table

```
success, table = server.json_to_table(jsonstr)
```

Converts a JSON string to a (nested) Lua table. Returns `true, table` on success or `false, errormsg` on failure.

#### server.generate_jwt

```
success, token = server.generate_jwt(table)
```

Generates a [JSON Web Token](https://jwt.io/) (JWT) with the supplied table as payload. Returns `true, token` on success or `false, errormsg` on failure. The internal may contain arbitrary keys and/or may contains the JWT claims as follows. (The type `IntDate` is a number of seconds since 1970-01-01T0:0:0Z):

- `iss` (string => StringOrURI) OPT: principal that issued the JWT.
- `exp` (number => IntDate) OPT: expiration time on or after which the token MUST NOT be accepted for processing.
- `nbf` (number => IntDate) OPT: identifies the time before which the token MUST NOT be accepted for processing.
- `iat` (number => IntDate) OPT: identifies the time at which the JWT was issued.
- `aud` (string => StringOrURI) OPT: identifies the audience that the JWT is intended for. The audience value is a string, typically the base address of the resource being accessed, such as `https://contoso.com`.
- `prn` (string => StringOrURI) OPT: identifies the subject of the JWT.
- `jti` (string => String) OPT: provides a unique identifier for the JWT.

#### server.decode_jwt

```
success, table = server.decode_jwt(token)
```

Decodes a [JSON Web Token](https://jwt.io/) (JWT) and returns its content as table. Returns `true, table` on success or `false, errormsg` on failure.

#### server.parse_mimetype

```
success, mimetype = server.parse_mimetype(str)
```

Parses a mimtype HTTP header string and returns a pair containing the actual mimetype and the charset used (if available). It returns `true, pair` with pair as mimetype and charset on success, `false, errormsg` on failure.

#### server.file_mimetype

```
success, table = server.file_mimetype(path)
success, table = server.file_mimetype(index)
```

Determines the mimetype of a file. The *first* form is used if the file path is known. The *second* form can be used for uploads by passing the upload file index. It returns `true, table` on success or `false, errormsg` on failure. The table has 2 members: - `mimetype` - `charset`

#### server.file_mimeconsistency

```
success, is_consistent = server.file_mimeconsistency(path)
success, is_consistent = server.file_mimeconsistency(index)
```

Checks if the file extension and the mimetype determined by the magic of the file is consistent. The *first* form requires a path (including the filename with extension), the *second* can be used for checking uploads by passing the file index. It returns `true, is_consistent` on success or `false, errormsg` in case of an error. `is_consistent` is true if the mimetype corresponds to the file extension.

#### server.copyTmpfile

```
success, errormsg = server.copyTmpfile(from, to)
```

Sipi saves each uploaded file in a temporary location (given by the config variable `tmpdir`) and deletes it after the request has been served. This function is used to copy the file to another location where it can be retrieved later. Returns `true, nil` on success or `false, errormsg` on failure.

Parameters:

- `from`: an index (integer value) of array server.uploads.
- `target`: destination path

#### server.systime

```
systime = server.systime()
```

Returns the current system time on the server in seconds since epoch.

#### server.log

```
server.log(message, loglevel)
```

Writes a message to the built-in logger. Severity levels are:

- `server.loglevel.LOG_EMERG`
- `server.loglevel.LOG_ALERT`
- `server.loglevel.LOG_CRIT`
- `server.loglevel.LOG_ERR`
- `server.loglevel.LOG_WARNING`
- `server.loglevel.LOG_NOTICE`
- `server.loglevel.LOG_INFO`
- `server.loglevel.LOG_DEBUG`

#### server.uuid

```
success, uuid = server.uuid()
```

Generates a random UUID version 4 identifier in canonical form, as described in [RFC 4122](https://tools.ietf.org/html/rfc4122). Returns `true, uuid` on success or `false, errormsg` on failure.

#### server.uuid62

```
success, uuid62 = server.uuid62()
```

Generates a Base62-encoded UUID. Returns `true, uuid62` on success or `false, errormsg` on failure.

#### server.uuid_to_base62

```
success, uuid62 = server.uuid_to_base62(uuid)
```

Converts a canonical UUID string to a Base62-encoded UUID. Returns `true, uuid62` on success or `false, errormsg` on failure.

#### server.base62_to_uuid

```
success, uuid = server.base62_to_uuid(uuid62)
```

Converts a Base62-encoded UUID to canonical form. Returns `true, uuid` on success or `false, errormsg` on failure.

## Cache Management Functions

The following functions are available for managing the SIPI image cache from Lua scripts.

#### cache.size

```
success, size = cache.size()
```

Returns the current total size of cached files in bytes. Returns `nil` if no cache is configured.

#### cache.max_size

```
success, max = cache.max_size()
```

Returns the configured maximum cache size limit in bytes.

#### cache.nfiles

```
success, count = cache.nfiles()
```

Returns the current number of files in the cache.

#### cache.max_nfiles

```
success, max = cache.max_nfiles()
```

Returns the configured maximum number of files allowed in cache.

#### cache.path

```
path = cache.path()
```

Returns the filesystem path to the cache directory, or `nil` if no cache is configured.

#### cache.filelist

```
filelist = cache.filelist([sortmethod])
```

Returns a table of cached files with metadata. The optional `sortmethod` parameter controls sorting:

- `"AT_ASC"` — sort by access time, ascending
- `"AT_DESC"` — sort by access time, descending
- `"FS_ASC"` — sort by file size, ascending
- `"FS_DESC"` — sort by file size, descending

Each entry in the returned table contains:

| Field         | Type    | Description                                |
| ------------- | ------- | ------------------------------------------ |
| `canonical`   | string  | Canonical cache key                        |
| `origpath`    | string  | Original file path                         |
| `cachepath`   | string  | Cache file path                            |
| `size`        | integer | File size in bytes                         |
| `last_access` | string  | Last access time (`"YYYY-MM-DD HH:MM:SS"`) |

Returns `nil` if no cache is configured.

#### cache.delete

```
success = cache.delete(canonical)
```

Deletes a specific cached file by its canonical key. Returns `true` on success, `false` otherwise.

#### cache.purge

```
count = cache.purge()
```

Purges cache entries based on configured purge criteria (LRU). Returns the number of files purged, or `nil` if no cache is configured.

## Filesystem Helper Functions

The `server.fs` table provides filesystem operations. All functions return `(true, result)` on success or `(false, error_message)` on failure.

#### server.fs.exists

```
success, exists = server.fs.exists(filepath)
```

Check if a file or directory exists. Returns `true/false` for the existence check.

#### server.fs.ftype

```
success, filetype = server.fs.ftype(filepath)
```

Get the type of a path. Returns one of: `"FILE"`, `"DIRECTORY"`, `"CHARDEV"`, `"BLOCKDEV"`, `"LINK"`, `"FIFO"`, `"SOCKET"`, `"UNKNOWN"`.

#### server.fs.modtime

```
success, timestamp = server.fs.modtime(filepath)
```

Get the modification time of a file as a Unix timestamp (seconds since epoch).

#### server.fs.readdir

```
success, filenames = server.fs.readdir(dirpath)
```

List all files and directories in a directory. Returns a Lua table of filenames (excludes `.` and `..`).

#### server.fs.is_readable

```
success, readable = server.fs.is_readable(filepath)
```

Check if a file is readable by the current process.

#### server.fs.is_writeable

```
success, writeable = server.fs.is_writeable(filepath)
```

Check if a file is writable by the current process.

#### server.fs.is_executable

```
success, executable = server.fs.is_executable(filepath)
```

Check if a file is executable by the current process.

#### server.fs.unlink

```
success, errmsg = server.fs.unlink(filepath)
```

Delete a file from the filesystem.

#### server.fs.mkdir

```
success, errmsg = server.fs.mkdir(dirname, mode)
```

Create a new directory. `mode` is a Unix permission integer (e.g., `tonumber('0755', 8)`).

#### server.fs.rmdir

```
success, errmsg = server.fs.rmdir(dirname)
```

Remove an empty directory.

#### server.fs.getcwd

```
success, cwd = server.fs.getcwd()
```

Get the current working directory.

#### server.fs.chdir

```
success, old_dir = server.fs.chdir(newdir)
```

Change the current working directory. Returns the previous working directory on success.

#### server.fs.copyFile

```
success, errmsg = server.fs.copyFile(source, target)
```

Copy a file from source to target.

#### server.fs.moveFile

```
success, errmsg = server.fs.moveFile(source, target)
```

Move/rename a file. `source` can be a file path (string) or an uploaded file index (integer, 1-based).

## Server Request Properties

The following read-only properties are available on the `server` table within request handlers:

| Property              | Type    | Description                                                                |
| --------------------- | ------- | -------------------------------------------------------------------------- |
| `server.method`       | string  | HTTP method: `"GET"`, `"POST"`, `"PUT"`, `"DELETE"`, `"HEAD"`, `"OPTIONS"` |
| `server.uri`          | string  | The complete request URI/path                                              |
| `server.host`         | string  | The Host header value                                                      |
| `server.client_ip`    | string  | Client's IP address                                                        |
| `server.client_port`  | integer | Client's port number                                                       |
| `server.secure`       | boolean | Whether the connection is HTTPS                                            |
| `server.has_openssl`  | boolean | Whether OpenSSL is available                                               |
| `server.route`        | string  | The matched route (if using routing)                                       |
| `server.content`      | string  | Raw POST/PUT body content                                                  |
| `server.content_type` | string  | Content-Type header value                                                  |
| `server.docroot`      | string  | Document root path                                                         |

### Request Data Tables

| Property         | Type  | Description                             |
| ---------------- | ----- | --------------------------------------- |
| `server.header`  | table | HTTP request headers (name-value pairs) |
| `server.cookies` | table | Cookie name-value pairs                 |
| `server.get`     | table | URL query parameters                    |
| `server.post`    | table | POST form parameters                    |
| `server.request` | table | Path parameters                         |

### Uploaded Files

`server.uploads` is a table of uploaded files (1-based indexing). Each entry contains:

| Field       | Type    | Description                    |
| ----------- | ------- | ------------------------------ |
| `fieldname` | string  | Form field name                |
| `origname`  | string  | Original filename              |
| `tmpname`   | string  | Temporary file path on server  |
| `mimetype`  | string  | MIME type of the uploaded file |
| `filesize`  | integer | File size in bytes             |

## Additional Server Functions

#### server.setBuffer

```
success, errmsg = server.setBuffer([bufsize], [incsize])
```

Enable response buffering with optional buffer size and increment size (in bytes).

#### server.sendCookie

```
success, errmsg = server.sendCookie(name, value [, options])
```

Set a cookie in the HTTP response. The optional `options` table can contain:

| Key         | Type    | Description                      |
| ----------- | ------- | -------------------------------- |
| `path`      | string  | Cookie path                      |
| `domain`    | string  | Cookie domain                    |
| `expires`   | integer | Expiration (seconds since epoch) |
| `secure`    | boolean | Secure flag                      |
| `http_only` | boolean | HTTP-only flag                   |

#### server.requireAuth

```
auth = server.requireAuth()
```

Parse authentication information from the request. Returns a table with:

| Field      | Type   | Description                                     |
| ---------- | ------ | ----------------------------------------------- |
| `status`   | string | `"NOAUTH"`, `"BASIC"`, `"BEARER"`, or `"ERROR"` |
| `username` | string | Username (BASIC auth only)                      |
| `password` | string | Password (BASIC auth only)                      |
| `token`    | string | Bearer token (BEARER auth only)                 |
| `message`  | string | Error message (ERROR status only)               |

## Utility Functions

#### helper.filename_hash

```
success, hashed_path = helper.filename_hash(filename)
```

Convert a filename into a hashed filesystem path, using SIPI's internal hash algorithm for cache file organization.

## Installing Lua modules

To install Lua modules that can be used in Lua scripts, use `local/bin/luarocks`. Make sure that the location where the modules are stored is in the Lua package path, which is printed by local/bin/lurocks path. The Lua paths will be used by the Lua interpreter when loading modules in a script with `require` (see [Using LuaRocks to install packages in the current directory](http://leafo.net/guides/customizing-the-luarocks-tree.html)).

For example, using `local/bin/luarocks install --local package`, the package will be installed in `~/.luarocks/`. To include this path in the Lua's interpreter package search path, you can use an environment variable. Running `local/bin/luarocks path` outputs the code you can use to do so. Alternatively, you can build the package path at the beginning of a Lua file by setting `package.path` and `package.cpath` (see [Running scripts with packages](http://leafo.net/guides/customizing-the-luarocks-tree.html#the-install-locations/using-a-custom-directory/quick-guide/running-scripts-with-packages)).
