736 lines
20 KiB
C
736 lines
20 KiB
C
/* Osmocom CBC (Cell Broacast Centre) */
|
|
|
|
/* (C) 2019-2021 by Harald Welte <laforge@gnumonks.org>
|
|
* All Rights Reserved
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0+
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation; either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
|
|
#include <jansson.h>
|
|
#include <ulfius.h>
|
|
#include <orcania.h>
|
|
|
|
#include <osmocom/core/utils.h>
|
|
#include <osmocom/core/linuxlist.h>
|
|
#include <osmocom/core/sockaddr_str.h>
|
|
|
|
#include <osmocom/gsm/protocol/gsm_48_049.h>
|
|
|
|
#define PREFIX "/api/ecbe/v1"
|
|
|
|
#include <osmocom/cbc/debug.h>
|
|
#include <osmocom/cbc/charset.h>
|
|
#include <osmocom/cbc/cbc_data.h>
|
|
#include <osmocom/cbc/rest_it_op.h>
|
|
|
|
/* get an integer value for field "key" in object "parent" */
|
|
static int json_get_integer(int *out, json_t *parent, const char *key)
|
|
{
|
|
json_t *jtmp;
|
|
|
|
if (!parent || !json_is_object(parent))
|
|
return -ENODEV;
|
|
jtmp = json_object_get(parent, key);
|
|
if (!jtmp)
|
|
return -ENOENT;
|
|
if (!json_is_integer(jtmp))
|
|
return -EINVAL;
|
|
*out = json_integer_value(jtmp);
|
|
return 0;
|
|
}
|
|
|
|
static int json_get_integer_range(int *out, json_t *parent, const char *key, int min, int max)
|
|
{
|
|
int rc, tmp;
|
|
rc = json_get_integer(&tmp, parent, key);
|
|
if (rc < 0)
|
|
return rc;
|
|
if (tmp < min || tmp > max)
|
|
return -ERANGE;
|
|
*out = tmp;
|
|
return 0;
|
|
}
|
|
|
|
/* get a string value for field "key" in object "parent" */
|
|
static const char *json_get_string(json_t *parent, const char *key)
|
|
{
|
|
json_t *jtmp;
|
|
|
|
if (!parent || !json_is_object(parent))
|
|
return NULL;
|
|
jtmp = json_object_get(parent, key);
|
|
if (!jtmp)
|
|
return NULL;
|
|
if (!json_is_string(jtmp))
|
|
return NULL;
|
|
return json_string_value(jtmp);
|
|
}
|
|
|
|
/* geographic scope (part of message_id) as per 3GPP TS 23.041 Section 9.4.1.2 "GS Code" */
|
|
static const struct value_string geo_scope_vals[] = {
|
|
{ 0, "cell_wide_immediate" },
|
|
{ 1, "plmn_wide" },
|
|
{ 2, "lac_sac_tac_wide" },
|
|
{ 3, "cell_wide" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
/* mapping of CBS DCS values for languages in 7bit GSM alphabet to ISO639-1 language codes,
|
|
* as per 3GPP TS 23.038 Section 5 */
|
|
static const struct value_string iso639_1_cbs_dcs_vals[] = {
|
|
{ 0x00, "de" },
|
|
{ 0x01, "en" },
|
|
{ 0x02, "it" },
|
|
{ 0x03, "fr" },
|
|
{ 0x04, "es" },
|
|
{ 0x05, "nl" },
|
|
{ 0x06, "sv" },
|
|
{ 0x07, "da" },
|
|
{ 0x08, "pt" },
|
|
{ 0x09, "fi" },
|
|
{ 0x0a, "no" },
|
|
{ 0x0b, "el" },
|
|
{ 0x0c, "tr" },
|
|
{ 0x0d, "hu" },
|
|
{ 0x0e, "pl" },
|
|
{ 0x20, "cs" },
|
|
{ 0x21, "he" },
|
|
{ 0x22, "ar" },
|
|
{ 0x23, "ru" },
|
|
{ 0x24, "is" },
|
|
{ 0, NULL }
|
|
};
|
|
/* values not expressed in the table above must use "language indication" at the start of the message */
|
|
|
|
/* 3GPP TS 23.041 Section 9.3.24 */
|
|
static const struct value_string ts23041_warning_type_vals[] = {
|
|
{ 0, "earthquake" },
|
|
{ 1, "tsunami" },
|
|
{ 2, "earthquake_and_tsuname" },
|
|
{ 3, "test" },
|
|
{ 4, "other" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
/* parse a smscb.schema.json/warning_type (either encoded or decoded) */
|
|
static int parse_warning_type(json_t *in, const char **errstr)
|
|
{
|
|
json_t *jtmp;
|
|
int i, rc, val;
|
|
|
|
if (!in || !json_is_object(in)) {
|
|
*errstr = "'warning_type' must be object";
|
|
return -EINVAL;
|
|
}
|
|
rc = json_get_integer_range(&i, in, "warning_type_encoded", 0, 255);
|
|
if (rc == 0) {
|
|
val = i;
|
|
} else if (rc == -ENOENT && (jtmp = json_object_get(in, "warning_type_decoded"))) {
|
|
const char *tstr = json_string_value(jtmp);
|
|
if (!tstr) {
|
|
*errstr = "'warning_type_decoded' is not a string";
|
|
return -EINVAL;
|
|
}
|
|
i = get_string_value(ts23041_warning_type_vals, tstr);
|
|
if (i < 0) {
|
|
*errstr = "'warning_type_decoded' is invalid";
|
|
return -EINVAL;
|
|
}
|
|
val = i;
|
|
} else {
|
|
*errstr = "either 'warning_type_encoded' or 'warning_type_decoded' must be present";
|
|
return -EINVAL;
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
/* parse a smscb.schema.json/serial_nr type (either encoded or decoded) */
|
|
static int json2serial_nr(uint16_t *out, json_t *jser_nr, const char **errstr)
|
|
{
|
|
json_t *jtmp;
|
|
int tmp, rc;
|
|
|
|
if (!jser_nr || !json_is_object(jser_nr)) {
|
|
*errstr = "'serial_nr' must be present and an object";
|
|
return -EINVAL;
|
|
}
|
|
rc = json_get_integer_range(&tmp, jser_nr, "serial_nr_encoded", 0, UINT16_MAX);
|
|
if (rc == 0) {
|
|
*out = tmp;
|
|
} else if (rc == -ENOENT && (jtmp = json_object_get(jser_nr, "serial_nr_decoded"))) {
|
|
const char *geo_scope_str;
|
|
int msg_code, upd_nr, geo_scope;
|
|
geo_scope_str = json_get_string(jtmp, "geo_scope");
|
|
if (!geo_scope_str) {
|
|
*errstr = "'geo_scope' is mandatory";
|
|
return -EINVAL;
|
|
}
|
|
geo_scope = get_string_value(geo_scope_vals, geo_scope_str);
|
|
if (geo_scope < 0) {
|
|
*errstr = "'geo_scope' is invalid";
|
|
return -EINVAL;
|
|
}
|
|
rc = json_get_integer_range(&msg_code, jtmp, "msg_code", 0, 1024);
|
|
if (rc < 0) {
|
|
*errstr = "'msg_code' is out of range";
|
|
return rc;
|
|
}
|
|
rc = json_get_integer_range(&upd_nr, jtmp, "update_nr", 0, 15);
|
|
if (rc < 0) {
|
|
*errstr = "'update_nr' is out of range";
|
|
return rc;
|
|
}
|
|
*out = ((geo_scope & 3) << 14) | ((msg_code & 0x3ff) << 4) | (upd_nr & 0xf);
|
|
return 0;
|
|
} else {
|
|
*errstr = "Either 'serial_nr_encoded' or 'serial_nr_decoded' are mandatory";
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* compute the number of pages needed for number of octets */
|
|
static unsigned int pages_from_octets(int n_octets)
|
|
{
|
|
unsigned int n_pages = n_octets / SMSCB_RAW_PAGE_LEN;
|
|
if (n_octets % SMSCB_RAW_PAGE_LEN)
|
|
n_pages++;
|
|
return n_pages;
|
|
}
|
|
|
|
/* parse a smscb.schema.json/payload_decoded type */
|
|
static int parse_payload_decoded(struct smscb_message *out, json_t *jtmp, const char **errstr)
|
|
{
|
|
const char *cset_str, *lang_str, *data_utf8_str;
|
|
int rc, dcs_class = 0;
|
|
|
|
/* character set */
|
|
cset_str = json_get_string(jtmp, "character_set");
|
|
if (!cset_str) {
|
|
*errstr = "Currently 'character_set' is mandatory";
|
|
/* TODO: dynamically decide? */
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* language */
|
|
lang_str = json_get_string(jtmp, "language");
|
|
if (lang_str && strlen(lang_str) > 2) {
|
|
*errstr = "Only two-digit 'language' code is supported";
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* DCS class: if not present, default (0) above will prevail */
|
|
rc = json_get_integer_range(&dcs_class, jtmp, "dcs_class", 0, 3);
|
|
if (rc < 0 && rc != -ENOENT) {
|
|
*errstr = "'dcs_class' out of range";
|
|
return rc;
|
|
}
|
|
|
|
data_utf8_str = json_get_string(jtmp, "data_utf8");
|
|
if (!data_utf8_str) {
|
|
*errstr = "'data_utf8' is mandatory";
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* encode according to character set */
|
|
if (!strcmp(cset_str, "gsm")) {
|
|
if (lang_str) {
|
|
rc = get_string_value(iso639_1_cbs_dcs_vals, lang_str);
|
|
if (rc >= 0)
|
|
out->cbs.dcs = rc;
|
|
else {
|
|
/* TODO: we must encode it in the first 3 characters */
|
|
out->cbs.dcs = 0x0f;
|
|
}
|
|
} else {
|
|
if (json_object_get(jtmp, "dcs_class")) {
|
|
/* user has not specified language but class,
|
|
* express class in DCS */
|
|
out->cbs.dcs = 0xF0 | (dcs_class & 3);
|
|
} else {
|
|
/* user has specified neither language nor class,
|
|
* use general "7 bit alphabet / language unspacified" */
|
|
out->cbs.dcs = 0x0F;
|
|
}
|
|
}
|
|
/* convert from UTF-8 input to GSM 7bit output */
|
|
rc = charset_utf8_to_gsm7((uint8_t *) out->cbs.data, sizeof(out->cbs.data),
|
|
data_utf8_str, strlen(data_utf8_str));
|
|
if (rc > 0) {
|
|
out->cbs.data_user_len = rc;
|
|
out->cbs.num_pages = pages_from_octets(rc);
|
|
}
|
|
} else if (!strcmp(cset_str, "8bit")) {
|
|
/* Determine DCS based on UDH + message class */
|
|
out->cbs.dcs = 0xF4 | (dcs_class & 3);
|
|
/* copy 8bit data over (hex -> binary conversion) */
|
|
rc = osmo_hexparse(data_utf8_str, (uint8_t *)out->cbs.data, sizeof(out->cbs.data));
|
|
if (rc > 0)
|
|
out->cbs.num_pages = pages_from_octets(rc);
|
|
} else if (!strcmp(cset_str, "ucs2")) {
|
|
if (lang_str) {
|
|
/* TODO: we must encode it in the first two octets */
|
|
}
|
|
/* convert from UTF-8 input to UCS2 output */
|
|
rc = charset_utf8_to_ucs2((uint8_t *) out->cbs.data, sizeof(out->cbs.data),
|
|
data_utf8_str, strlen(data_utf8_str));
|
|
if (rc > 0)
|
|
out->cbs.num_pages = pages_from_octets(rc);
|
|
} else {
|
|
*errstr = "Invalid 'character_set'";
|
|
return -EINVAL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* parse a smscb.schema.json/payload type */
|
|
static int json2payload(struct smscb_message *out, json_t *in, const char **errstr)
|
|
{
|
|
json_t *jtmp;
|
|
int rc;
|
|
|
|
if (!in || !json_is_object(in)) {
|
|
*errstr = "'payload' must be JSON object";
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((jtmp = json_object_get(in, "payload_encoded"))) {
|
|
json_t *jpage_arr, *jpage;
|
|
int i, dcs, num_pages, len;
|
|
|
|
out->is_etws = false;
|
|
/* Data Coding Scheme */
|
|
rc = json_get_integer_range(&dcs, jtmp, "dcs", 0, 255);
|
|
if (rc < 0) {
|
|
*errstr = "'dcs' out of range";
|
|
return rc;
|
|
}
|
|
out->cbs.dcs = dcs;
|
|
|
|
/* Array of Pages as hex-strings */
|
|
jpage_arr = json_object_get(jtmp, "pages");
|
|
if (!jpage_arr || !json_is_array(jpage_arr)) {
|
|
*errstr = "'pages' absent or not an array";
|
|
return -EINVAL;
|
|
}
|
|
num_pages = json_array_size(jpage_arr);
|
|
if (num_pages < 1 || num_pages > 15) {
|
|
*errstr = "'pages' array size out of range";
|
|
return -EINVAL;
|
|
}
|
|
out->cbs.num_pages = num_pages;
|
|
out->cbs.data_user_len = 0;
|
|
json_array_foreach(jpage_arr, i, jpage) {
|
|
const char *hexstr;
|
|
if (!json_is_string(jpage)) {
|
|
*errstr = "'pages' array must contain strings";
|
|
return -EINVAL;
|
|
}
|
|
hexstr = json_string_value(jpage);
|
|
if (strlen(hexstr) > 88 * 2) {
|
|
*errstr = "'pages' array must contain strings up to 88 hex nibbles";
|
|
return -EINVAL;
|
|
}
|
|
len = osmo_hexparse(hexstr, out->cbs.data[i], sizeof(out->cbs.data[i]));
|
|
if (len < 0) {
|
|
*errstr = "'pages' array must contain hex strings";
|
|
return -EINVAL;
|
|
}
|
|
out->cbs.data_user_len += len;
|
|
}
|
|
return 0;
|
|
} else if ((jtmp = json_object_get(in, "payload_decoded"))) {
|
|
out->is_etws = false;
|
|
return parse_payload_decoded(out, jtmp, errstr);
|
|
} else if ((jtmp = json_object_get(in, "payload_etws"))) {
|
|
json_t *jwtype, *jtmp2;
|
|
const char *wsecinfo_str;
|
|
|
|
out->is_etws = true;
|
|
|
|
/* Warning Type (value) */
|
|
jwtype = json_object_get(jtmp, "warning_type");
|
|
if (!jwtype) {
|
|
*errstr = "'warning_type' must be object";
|
|
return -EINVAL;
|
|
}
|
|
rc = parse_warning_type(jwtype, errstr);
|
|
if (rc < 0)
|
|
return -EINVAL;
|
|
out->etws.warning_type = rc;
|
|
|
|
/* Emergency User Alert */
|
|
jtmp2 = json_object_get(jtmp, "emergency_user_alert");
|
|
if (jtmp && json_is_true(jtmp2))
|
|
out->etws.user_alert = true;
|
|
else
|
|
out->etws.user_alert = false;
|
|
|
|
/* Popup */
|
|
jtmp2 = json_object_get(jtmp, "popup_on_display");
|
|
if (jtmp && json_is_true(jtmp2))
|
|
out->etws.popup_on_display = true;
|
|
else
|
|
out->etws.popup_on_display = false;
|
|
|
|
/* Warning Security Info */
|
|
wsecinfo_str = json_get_string(jtmp, "warning_sec_info");
|
|
if (wsecinfo_str) {
|
|
if (osmo_hexparse(wsecinfo_str, out->etws.warning_sec_info,
|
|
sizeof(out->etws.warning_sec_info)) < 0) {
|
|
*errstr = "'warnin_sec_info' must be hex string";
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
return 0;
|
|
} else {
|
|
*errstr = "'payload_type_encoded', 'payload_type_decoded' or 'payload_etws' must be present";
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
/* decode a "smscb.schema.json#definitions/smscb_message" */
|
|
static int json2smscb_message(struct smscb_message *out, json_t *in, const char **errstr)
|
|
{
|
|
json_t *jser_nr, *jtmp;
|
|
int msg_id, rc;
|
|
|
|
if (!json_is_object(in)) {
|
|
*errstr = "not a JSON object";
|
|
return -EINVAL;
|
|
}
|
|
|
|
jser_nr = json_object_get(in, "serial_nr");
|
|
if (!jser_nr) {
|
|
*errstr = "serial_nr is mandatory";
|
|
return -EINVAL;
|
|
}
|
|
if (json2serial_nr(&out->serial_nr, jser_nr, errstr) < 0)
|
|
return -EINVAL;
|
|
|
|
rc = json_get_integer_range(&msg_id, in, "message_id", 0, UINT16_MAX);
|
|
if (rc < 0) {
|
|
*errstr = "message_id out of range";
|
|
return -EINVAL;
|
|
}
|
|
out->message_id = msg_id;
|
|
|
|
jtmp = json_object_get(in, "payload");
|
|
if (json2payload(out, jtmp, errstr) < 0)
|
|
return -EINVAL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct value_string category_str_vals[] = {
|
|
{ CBSP_CATEG_NORMAL, "normal" },
|
|
{ CBSP_CATEG_HIGH_PRIO, "high_priority" },
|
|
{ CBSP_CATEG_BACKGROUND, "background" },
|
|
{ 0, NULL }
|
|
};
|
|
|
|
/* decode a "cbc.schema.json#definitions/cbc_message" */
|
|
static int json2cbc_message(struct cbc_message *out, void *ctx, json_t *in, const char **errstr)
|
|
{
|
|
const char *category_str, *cbe_str;
|
|
json_t *jtmp;
|
|
int rc, tmp;
|
|
|
|
if (!json_is_object(in)) {
|
|
*errstr = "CBCMSG must be JSON object";
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* CBE name (M) */
|
|
cbe_str = json_get_string(in, "cbe_name");
|
|
if (!cbe_str) {
|
|
*errstr = "CBCMSG 'cbe_name' is mandatory";
|
|
return -EINVAL;
|
|
}
|
|
out->cbe_name = talloc_strdup(ctx, cbe_str);
|
|
|
|
/* Category (O) */
|
|
category_str = json_get_string(in, "category");
|
|
if (!category_str)
|
|
out->priority = CBSP_CATEG_NORMAL;
|
|
else {
|
|
rc = get_string_value(category_str_vals, category_str);
|
|
if (rc < 0) {
|
|
*errstr = "CBCMSG 'category' unknown";
|
|
return -EINVAL;
|
|
}
|
|
out->priority = rc;
|
|
}
|
|
|
|
/* Repetition Period (O) */
|
|
rc = json_get_integer_range(&tmp, in, "repetition_period", 0, 4095);
|
|
if (rc == 0)
|
|
out->rep_period = tmp;
|
|
else if (rc == -ENOENT){
|
|
*errstr = "CBCMSG 'repetiton_period' is mandatory";
|
|
return rc;
|
|
} else {
|
|
*errstr = "CBCMSG 'repetiton_period' out of range";
|
|
return rc;
|
|
}
|
|
|
|
/* Number of Broadcasts (O) */
|
|
rc = json_get_integer_range(&tmp, in, "num_of_bcast", 0, 65535);
|
|
if (rc == 0)
|
|
out->num_bcast = tmp;
|
|
else if (rc == -ENOENT)
|
|
out->num_bcast = 0; /* unlimited */
|
|
else {
|
|
*errstr = "CBCMSG 'num_of_bcast' out of range";
|
|
return rc;
|
|
}
|
|
|
|
/* Warning Period in seconds (O) */
|
|
rc = json_get_integer_range(&tmp, in, "warning_period_sec", 0, 65535);
|
|
if (rc == 0)
|
|
out->warning_period_sec = tmp;
|
|
else if (rc == -ENOENT)
|
|
out->warning_period_sec = 0xffffffff; /* infinite */
|
|
else {
|
|
*errstr = "CBCMSG 'warning_period_sec' out of range";
|
|
return rc;
|
|
}
|
|
|
|
/* [Geographic] Scope (M) */
|
|
jtmp = json_object_get(in, "scope");
|
|
if (!jtmp) {
|
|
*errstr = "CBCMSG 'scope' is mandatory";
|
|
return -EINVAL;
|
|
}
|
|
|
|
if ((jtmp = json_object_get(jtmp, "scope_plmn"))) {
|
|
out->scope = CBC_MSG_SCOPE_PLMN;
|
|
} else {
|
|
*errstr = "CBCMSG only 'scope_plmn' supported";
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* SMSCB message itself */
|
|
jtmp = json_object_get(in, "smscb_message");
|
|
if (!jtmp) {
|
|
*errstr = "CBCMSG 'smscb_message' is mandatory";
|
|
return -EINVAL;
|
|
}
|
|
rc = json2smscb_message(&out->msg, jtmp, errstr);
|
|
if (rc < 0)
|
|
return rc;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int api_cb_message_post(const struct _u_request *req, struct _u_response *resp, void *user_data)
|
|
{
|
|
struct rest_it_op *riop = talloc_zero(g_cbc, struct rest_it_op);
|
|
const char *errstr = "Unknown";
|
|
json_error_t json_err;
|
|
json_t *json_req = NULL;
|
|
char *jsonstr;
|
|
int rc;
|
|
|
|
if (!riop) {
|
|
LOGP(DREST, LOGL_ERROR, "Out of memory\n");
|
|
ulfius_set_string_body_response(resp, 500, "Out of memory");
|
|
return U_CALLBACK_COMPLETE;
|
|
}
|
|
|
|
riop->operation = REST_IT_OP_MSG_CREATE;
|
|
|
|
json_req = ulfius_get_json_body_request(req, &json_err);
|
|
if (!json_req) {
|
|
errstr = "REST: No JSON Body";
|
|
goto err;
|
|
}
|
|
|
|
char *jsontxt = json_dumps(json_req, 0);
|
|
LOGP(DREST, LOGL_DEBUG, "/message POST: %s\n", jsontxt);
|
|
free(jsontxt);
|
|
|
|
rc = json2cbc_message(&riop->u.create.cbc_msg, riop, json_req, &errstr);
|
|
if (rc < 0)
|
|
goto err;
|
|
|
|
LOGP(DREST, LOGL_DEBUG, "sending as inter-thread op\n");
|
|
/* request message to be added by main thread */
|
|
rc = rest_it_op_send_and_wait(riop);
|
|
if (rc < 0) {
|
|
LOGP(DREST, LOGL_ERROR, "Error %d in inter-thread op\n", rc);
|
|
errstr = "Error in it_queue";
|
|
goto err;
|
|
}
|
|
|
|
json_decref(json_req);
|
|
LOGP(DREST, LOGL_DEBUG, "/message POST -> %u (%s)\n",
|
|
riop->http_result.response_code, riop->http_result.message);
|
|
ulfius_set_string_body_response(resp, riop->http_result.response_code, riop->http_result.message);
|
|
talloc_free(riop);
|
|
return U_CALLBACK_COMPLETE;
|
|
err:
|
|
jsonstr = json_dumps(json_req, 0);
|
|
LOGP(DREST, LOGL_ERROR, "ERROR: %s (%s)\n", errstr, jsonstr);
|
|
free(jsonstr);
|
|
json_decref(json_req);
|
|
talloc_free(riop);
|
|
LOGP(DREST, LOGL_DEBUG, "/message POST -> 400\n");
|
|
ulfius_set_string_body_response(resp, 400, errstr);
|
|
return U_CALLBACK_COMPLETE;
|
|
}
|
|
|
|
static int api_cb_message_del(const struct _u_request *req, struct _u_response *resp, void *user_data)
|
|
{
|
|
const char *message_id_str = u_map_get(req->map_url, "message_id");
|
|
struct rest_it_op *riop = talloc_zero(g_cbc, struct rest_it_op);
|
|
int message_id;
|
|
int status = 404;
|
|
int rc;
|
|
|
|
if (!message_id_str) {
|
|
status = 400;
|
|
goto err;
|
|
}
|
|
message_id = atoi(message_id_str);
|
|
if (message_id < 0 || message_id > 65535) {
|
|
status = 400;
|
|
goto err;
|
|
}
|
|
|
|
if (!riop) {
|
|
status = 500;
|
|
goto err;
|
|
}
|
|
|
|
riop->operation = REST_IT_OP_MSG_DELETE;
|
|
riop->u.del.msg_id = message_id;
|
|
|
|
/* request message to be deleted by main thread */
|
|
rc = rest_it_op_send_and_wait(riop);
|
|
if (rc < 0)
|
|
goto err;
|
|
|
|
LOGP(DREST, LOGL_DEBUG, "/message DELETE(%u) -> %u (%s)\n", message_id,
|
|
riop->http_result.response_code, riop->http_result.message);
|
|
ulfius_set_string_body_response(resp, riop->http_result.response_code, riop->http_result.message);
|
|
talloc_free(riop);
|
|
return U_CALLBACK_COMPLETE;
|
|
err:
|
|
talloc_free(riop);
|
|
ulfius_set_empty_body_response(resp, status);
|
|
return U_CALLBACK_COMPLETE;
|
|
}
|
|
|
|
|
|
static const struct _u_endpoint api_endpoints[] = {
|
|
/* create/update a message */
|
|
{ "POST", PREFIX, "/message", 0, &api_cb_message_post, NULL },
|
|
{ "DELETE", PREFIX, "/message/:message_id", 0, &api_cb_message_del, NULL },
|
|
};
|
|
|
|
static struct _u_instance g_instance;
|
|
#ifdef ULFIUS_MALLOC_NOT_BROKEN
|
|
static void *g_tall_rest;
|
|
static pthread_mutex_t g_tall_rest_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static void *my_o_malloc(size_t sz)
|
|
{
|
|
void *obj;
|
|
pthread_mutex_lock(&g_tall_rest_lock);
|
|
obj = talloc_size(g_tall_rest, sz);
|
|
pthread_mutex_unlock(&g_tall_rest_lock);
|
|
return obj;
|
|
}
|
|
|
|
static void *my_o_realloc(void *obj, size_t sz)
|
|
{
|
|
void *ret;
|
|
pthread_mutex_lock(&g_tall_rest_lock);
|
|
ret = talloc_realloc_size(g_tall_rest, obj, sz);
|
|
pthread_mutex_unlock(&g_tall_rest_lock);
|
|
return ret;
|
|
}
|
|
|
|
static void my_o_free(void *obj)
|
|
{
|
|
pthread_mutex_lock(&g_tall_rest_lock);
|
|
talloc_free(obj);
|
|
pthread_mutex_unlock(&g_tall_rest_lock);
|
|
}
|
|
#endif
|
|
|
|
int rest_api_init(void *ctx, const char *bind_addr, uint16_t port)
|
|
{
|
|
struct osmo_sockaddr_str sastr;
|
|
int i;
|
|
|
|
LOGP(DREST, LOGL_INFO, "Main thread tid: %lu\n", pthread_self());
|
|
|
|
#ifdef ULFIUS_MALLOC_NOT_BROKEN
|
|
/* See https://github.com/babelouest/ulfius/issues/63 */
|
|
g_tall_rest = ctx;
|
|
o_set_alloc_funcs(my_o_malloc, my_o_realloc, my_o_free);
|
|
#endif
|
|
|
|
OSMO_STRLCPY_ARRAY(sastr.ip, bind_addr);
|
|
sastr.port = port;
|
|
|
|
if (strchr(bind_addr, ':')) {
|
|
#if (ULFIUS_VERSION_MAJOR > 2) || (ULFIUS_VERSION_MAJOR == 2) && (ULFIUS_VERSION_MINOR >= 6)
|
|
struct sockaddr_in6 sin6;
|
|
sastr.af = AF_INET6;
|
|
osmo_sockaddr_str_to_sockaddr_in6(&sastr, &sin6);
|
|
if (ulfius_init_instance_ipv6(&g_instance, port, &sin6, U_USE_IPV6, NULL) != U_OK)
|
|
return -1;
|
|
#else
|
|
LOGP(DREST, LOGL_FATAL, "IPv6 requires ulfius version >= 2.6\n");
|
|
return -2;
|
|
#endif
|
|
} else {
|
|
struct sockaddr_in sin;
|
|
sastr.af = AF_INET;
|
|
osmo_sockaddr_str_to_sockaddr_in(&sastr, &sin);
|
|
if (ulfius_init_instance(&g_instance, port, &sin, NULL) != U_OK)
|
|
return -1;
|
|
}
|
|
g_instance.mhd_response_copy_data = 1;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(api_endpoints); i++)
|
|
ulfius_add_endpoint(&g_instance, &api_endpoints[i]);
|
|
|
|
if (ulfius_start_framework(&g_instance) != U_OK) {
|
|
LOGP(DREST, LOGL_FATAL, "Cannot start ECBE REST API at %s:%u\n", bind_addr, port);
|
|
return -1;
|
|
}
|
|
LOGP(DREST, LOGL_NOTICE, "Started ECBE REST API at %s:%u\n", bind_addr, port);
|
|
return 0;
|
|
}
|
|
|
|
void rest_api_fin(void)
|
|
{
|
|
ulfius_stop_framework(&g_instance);
|
|
ulfius_clean_instance(&g_instance);
|
|
}
|