Arma 3: WebAPI
Topic · Markdown · JSON · original wiki page
WebAPI allows to use HTTP, Websocket and other REST APIs.
All web requests return handles, these behave like a Promise. It can be waited on to receive the first result or failure (note special behavior for Websockets which have multiple results).
These handles are not Serializable, they behave like Control/Display references. When loading a savegame all stored handles will be null, and they cannot be transferred over network.
Note that all HTTP Requests (including Websockets connection establish) are checked using a CORS Preflight check.
The API provides a pre-defined domain named "armaserver" which points to the Server the player is playing on.
http://armaserver:8080/test
will send the request to the server, using the same address that the player used to connect to the server (note this will fail if player joined via Steam Datagram relay (ugh)). In singleplayer and on the server, this points to localhost.
Initiating Requests
Command syntax is:
private _handle = webRequest HASHMAP;
The structure of the HashMap depends on the request type.
{
type: STRING, // "http"|"websocket"|"mdns"|"send"|"close"|"flush"
// "http"/"websocket"/"mdns" only
callbackContext: ANY // Context passed to all internal callbacks as _thisArgs
// "http"/"mdns" only
onCompleted: CODE // Function called with params ["_request", "_result"]. On success or failure.
// "http"/"websocket" only. For websocket these are only used for initial connection establishment
url: STRING, // URL to connect to, must be http/https for "http" type, or ws/wss for "websocket" type
origin: STRING, // Optional. Identifier of sender, like "ACEMOD" or "arma://ACEMOD", will be passed as "Origin" in preflight request
headers: HASHMAP<STRING, STRING>, // Optional. Extra headers to add to the request. Note the list of not allowed headers.
verifySSL: BOOL, // Optional, Default true. verify SSL/TLS certificate for https/wss connections
debug: BOOL, // Optional default false. Print verbose debug logging into RPT, only available if the -debug commandline parameter is set
// "http" only
method: STRING, // Optional, default GET. "GET"|"POST"|"PUT"|"DELETE"|"CUSTOM"|...
postData: STRING, // Data to send as body of request, only valid with "POST"/"CUSTOM" methods
// "websocket" only
onWebsocketEstablished: CODE, // Function called once Websocket connection is established and ready to send/receive data.
// params ["_request", "_data"].
// Returns the headers and result the server returned to the CONNECT invocation
onWebsocketLost: CODE, // Function called when initial connection failed, or connection was lost.
// params ["_request", "_data"].
// _data contains error message.
onWebsocketDataReceived: CODE, // Function called with params ["_request", "_data"].
// _data is a string containing the TEXT content that was transmitted.
// "send"/"close" only
connection: HANDLE, // Handle to a websocket session
// "send" only
data: STRING, // Data to send over the websocket connection
// "close" only
statusCode: SCALAR, // Optional, default 1000. Websocket status code to send in close message.
// See https://www.rfc-editor.org/info/rfc6455/#section-5.5.1
reason: STRING // Optional, message to send with close command to the server
}
- All callbacks are called in Unscheduled Environment.
- The "http", "websocket", "mdns" types establish new requests.
- The "send", "info", "close" types are used on established connections.
HTTP
Enforces that Origin header is set (and cannot be overwritten by headers parameter).
The Origin is always arma://-prefixed. The default Origin is arma://null
Automatically passes an Accept-Encoding header specifying deflate, gzip, br, zstd
Automatically sets the User-Agent header to "BIGameEngine/2.22.154051" (The current game version)
The following headers are blocked and cannot be set:
- Sec-*
- Proxy-*
- Host
- Accept-Encoding
- Content-Length
- User-Agent
- Origin
- Cookie
- Cookie2
- Connection
- Keep-Alive
- Proxy-Authorization
- Proxy-Authenticate
- TE
- Trailer
- Transfer-Encoding
- Via
- Upgrade
- Expect
- Referer
spawn {
private _handle = webRequest #{
"type": "http",
//"method": "GET", "GET" is default
"origin": "test",
"url": "https://myip.wtf/text"
};
private _result = waitUntil _handle; // suspend until its ready
_result params ["_request", "_result"];
systemChat format ["My IP is: %1", _result get "body"];
};
Results can be retrieved/awaited by either specifying the "onCompleted" callback, or using the returned _handle, which is a Promise.
The result structure depends on whether the request was successful or failed.
Success means that the target webserver could be reached and returned a response, it might still be an error response returned by the server, for that check the httpCode.
A successful request results in a HashMap of following structure:
{
httpCode: SCALAR, // the numeric HTTP result code
body: STRING, // the Content returned by the request
responseHeaders: HASHMAP<STRING, STRING>, // the headers provided in the response
}
Maximum length of body is the same as maximum string length.
A failed request results in a HashMap of following structure:
{
error: STRING, // Error message explaining what failed
}
Failed requests generally only happen on Internal errors, Preflight failures or Timeouts.
Note that starting due to security reasons, some error messages are less detailed if the -debug command line parameter is not set.
Example messages:
Without -debug parameter:
All Preflight failures: {"error":"Request error"}
Preflight successful but server did not respond to actual request: {"error":"Operation timed out after 30003 milliseconds with 0 bytes received"}
With -debug parameter:
{"error":"Preflight failed: Reason: Did not find method in CORS header 'Access-Control-Allow-Methods'"}
{"error":"Preflight failed: Reason: CORS header 'Access-Control-Allow-Origin' missing"}
Domain does not exist
{"error":"Preflight failed: Could not resolve host: doesnotexist.cim, CurlError: 6"}
Server does not allow OPTIONS method, required for Preflight check
{"error":"Preflight failed: Reason: CORS request got result HTTP 405"}
{"error":"Operation timed out after 30003 milliseconds with 0 bytes received"}
Server not reachable, so Preflight check also failed:
{"error":"Preflight failed: Connection timed out after 5000 milliseconds"}
Certificate error:
{"error":"Preflight failed: Certificate validation failed"}
Websocket
For the initial "CONNECT" request, the same limitations/requirements as the HTTP request apply. The onWebsocketEstablished/onWebsocketLost eventhandler parameters also match what a HTTP request would be.
private _handle = webRequest #{
"type": "websocket",
"origin": "test",
"url": "wss://localhost:7082/websocket/relay",
"onWebsocketEstablished": {
GResult = _this select 1;
systemChat str _this;
},
"onWebsocketLost": {
GResultLoss = _this select 1;
systemChat str _this;
},
"onWebsocketDataReceived": {
GLastMessage = _this select 1;
systemChat str _this;
},
"callbackContext": "hello world context",
"verifySSL": false
};
GHandle = _handle;
Websockets need to use the callbacks. The handle is also a Promise, but it will only be resolved once, on the first Established/Lost. After the connection is established, the handle cannot be used to receive data or notice Connection Loss events.
Unlike HTTP Requests, the returned handle MUST be stored, once the Handle goes out of scope (and is not stored elsewhere), the connection is closed.
The max length of a websocket message is also the max SQF string length.
When the server sends a larger message, the client will cut the connection with WS status 1009;
the message will not be forwarded to script and the onWebsocketLost callback will be called with an error message.
Note that handling fragmented messages (one message split into multiple "Continuation" frames, which must be re-assembled by receiver) is inefficient. Prefer having messages in a large single frame instead.
There are currently no Websocket extensions supported by the engine. RFC 7692 may be supplied at a later point (ref https://github.com/curl/curl/discussions/16065)
When the server closes the connection, the close status code and message, are forwarded inside the error message for onWebsocketLost.
To send a message:
webRequest #{
"type": "send",
"connection": _handle,
"data": "Message to Send"
};
To close the connection:
webRequest #{
"type": "close",
"connection": _handle,
"statusCode": SCALAR, // Optional, default 1000. Websocket status code to send in close message. See https://www.rfc-editor.org/info/rfc6455/#section-5.5.1
"reason": STRING // Optional, message to send with close command to the server
};
mDNS
A mutual DNS request will look up the name "_arma3web._tcp.local" on the local network. It can be used to find Arma web services that advertise themselves.
This can be used to find services that run on non-static port assignments.
mDNS cannot error, it always results in Hashmap<SERVICE_RECORD> (That means a hashmap, of string:hashmap pairs) One service can have multiple SRV records (multiple ports) but usually only have one. The SRV records can have different hostnames, but usually only have one. Thus the ADDR hashmap usually only has one entry.
mDNS requests have a timeout of 10 seconds. They wait for services to announce themselves, if within 10 seconds it did not receive a single announcement, it aborts. That means the result will be empty.
SERVICE_RECORD
{
SRV: Array<SRV_Record>,
TXT: Array<STRING>,
ADDR: Hashmap<STRING, Array<STRING>> // Network addresses, corresponding to the SRV records name
}
SRV_RECORD:
{
name: STRING,
priority: SCALAR,
weight: SCALAR,
port: SCALAR,
}
Example:
{
"IAmAServiceName._arma3web._tcp.local": {
"TXT": [
"OhSoMany=Things",
"txtvers=1"
],
"ADDR": {
"The service decides its hostname": [
"127.0.0.1",
"[::1]"
]
},
"SRV": [
{
"priority": 0,
"port": 1337,
"name": "The service decides its hostname",
"weight": 0
}
]
},
"ASPNetSample._arma3web._tcp.local": {
"TXT": [
"version=1.0",
"status=running",
"path=/api/v1",
"txtvers=1"
],
"ADDR": {
"SecondaryService.local": [
"[::1]"
],
"ASPNetSample_a3web.local": [
"172.18.144.1",
"192.168.73.1",
"172.18.48.1",
"192.168.11.1",
"172.22.208.1",
"172.27.160.1",
"172.23.16.1"
]
},
"SRV": [
{
"priority": 0,
"port": 1338,
"name": "ASPNetSample_a3web.local",
"weight": 0
},
{
"priority": 0,
"port": 7082,
"name": "ASPNetSample_a3web.local",
"weight": 0
}
]
}
}
Sample for how to announce mDNS services (in C#): https://github.com/dedmen/ArmaWebRequestTests/blob/master/mDNSHostedService.cs
Preflight
Every web request executes a CORS preflight request, to check if the Webserver allows us to request from it. See https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
The preflight result is cached, cache expiry respects the "Access-Control-Max-Age" response header of the webserver.
If no such header is provided, the default is 60 seconds.
The maximum allowed age is 600 seconds (10 minutes), the minimum is 5 seconds.
The cache will also only store 64 URLs, and will evict the least recently used entry if full.
If a previously unknown METHOD is used on a known URL, a new preflight is sent, the allowed methods result is inserted into the cached entry and its expiry time is extended.
The Preflight "Origin" that must be allowed, is decided by the script issuing the request, but it uses a "arma://" protocol. Like "arma://ACEMod"
The returned "Access-Control-Allow-Origin" should either be "arma://" (Note that "" is not allowed) or exactly the same as the "Origin" request header.
Note that the FULL URL, is the cache key for the preflight request, that means using query parameters in the URL, will create new Preflight requests. Thus it is highly discouraged too use query parameters, use POST or QUERY requests instead.
Access-Control-Request-Headers / Access-Control-Allow-Headers are not checked.
If -debug parameter is not set, there can be a maximum of 5 preflight failures (where the server actually replies, timeout or failing to reach the server does not count), after that the whole domain is blacklisted until game restart.
The preflight cache can be cleared, but only if -debug is set:
webRequest #{ type: "flush" };