diff --git a/CHANGELOG.md b/CHANGELOG.md index 17e274c3..1f7d70c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ [//]: # (END_SECTION HEADER) [//]: # (START_SECTION COMMITS +7287564593dd788bdfc5e3472cca0bd89b84a4bc d8ac84aa08a89a604320df81870c18c734f8fdf0 3893533afbfaf9d5aedb02b45688ac94920cbd4f f8bfe45b3cc64e49d3decc0adb7a10f493ff22a0 @@ -1969,6 +1970,32 @@ a72121b9551921aa3dced32d943c6034ba318f82 ce6c5aac0db5476dc496c34388e4f9ce2c4b86e5 b46b1e64f06f448bde78b98e3ae8228ce5f96067 END_SECTION COMMITS) +[//]: # (START_SECTION 7287564593dd788bdfc5e3472cca0bd89b84a4bc) +### Improved DNS Hostname Resolution And Caching + +> Commit: [7287564593dd788bdfc5e3472cca0bd89b84a4bc](https://github.com/dOpensource/dsiprouter/commit/7287564593dd788bdfc5e3472cca0bd89b84a4bc) +> Date: Thu, 1 Jul 2021 12:25:12 -0400 +> Author: Tyler Moore (tmoore@goflyball.com) +> Committer: Tyler Moore (tmoore@goflyball.com) +> Signed: Tyler Moore (devopsec) + + +- Resolves [#325](https://github.com/dOpensource/dsiprouter/issues/325) +- implement new caching system via cronjob +- update dr_gateways DNS names to resolve to all available IP's +- update uacreg DNS names to resolve to all available IP's +- update DNS names every 5 minutes +- update backend to transparently access/store JSON in description/tag fields +- update all other tables to use new schema for JSON storage +- move local address to cron updated entry in address table +- add FLT_INTERNAL flag for internal use addresses +- add/update a few utility functions to `dsip_lib.sh` +- update default imports to use new JSON structure + + +--- + +[//]: # (END_SECTION 7287564593dd788bdfc5e3472cca0bd89b84a4bc) [//]: # (START_SECTION d8ac84aa08a89a604320df81870c18c734f8fdf0) ### Fix Bug In Commit 9e7949a diff --git a/dsiprouter.sh b/dsiprouter.sh index 36589b29..1a2a2a8d 100755 --- a/dsiprouter.sh +++ b/dsiprouter.sh @@ -67,6 +67,7 @@ setStaticScriptSettings() { FLT_CARRIER=8 FLT_PBX=9 FLT_MSTEAMS=22 + FLT_INTERNAL=20 FLT_OUTBOUND=8000 FLT_INBOUND=9000 FLT_LCR_MIN=10000 @@ -514,6 +515,14 @@ export -f reconfigureMysqlSystemdService # generate dynamic python config settings on install function configurePythonSettings { + setConfigAttrib 'FLT_CARRIER' "$FLT_CARRIER" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_PBX' "$FLT_PBX" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_MSTEAMS' "$FLT_MSTEAMS" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_INTERNAL' "$FLT_INTERNAL" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_OUTBOUND' "$FLT_OUTBOUND" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_INBOUND' "$FLT_INBOUND" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_LCR_MIN' "$FLT_LCR_MIN" ${DSIP_CONFIG_FILE} + setConfigAttrib 'FLT_FWD_MIN' "$FLT_FWD_MIN" ${DSIP_CONFIG_FILE} setConfigAttrib 'KAM_KAMCMD_PATH' "$(type -p kamcmd)" ${DSIP_CONFIG_FILE} -q setConfigAttrib 'KAM_CFG_PATH' "$SYSTEM_KAMAILIO_CONFIG_FILE" ${DSIP_CONFIG_FILE} -q setConfigAttrib 'RTP_CFG_PATH' "$SYSTEM_RTPENGINE_CONFIG_FILE" ${DSIP_CONFIG_FILE} -q @@ -649,19 +658,19 @@ function updateKamailioConfig { # update kamailio config file if (( $DEBUG == 1 )); then - enableKamailioConfigAttrib 'WITH_DEBUG' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_DEBUG' ${DSIP_KAMAILIO_CONFIG_FILE} else - disableKamailioConfigAttrib 'WITH_DEBUG' ${DSIP_KAMAILIO_CONFIG_FILE} + disableKamailioConfigFeature 'WITH_DEBUG' ${DSIP_KAMAILIO_CONFIG_FILE} fi if (( $SERVERNAT == 1 )); then - enableKamailioConfigAttrib 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} else - disableKamailioConfigAttrib 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} + disableKamailioConfigFeature 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} fi if [[ -n "$KAM_HOMER_HOST" ]]; then - enableKamailioConfigAttrib 'WITH_HOMER' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_HOMER' ${DSIP_KAMAILIO_CONFIG_FILE} else - disableKamailioConfigAttrib 'WITH_HOMER' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_HOMER' ${DSIP_KAMAILIO_CONFIG_FILE} fi setKamailioConfigSubstdef 'DSIP_ID' "${DSIP_ID}" ${DSIP_KAMAILIO_CONFIG_FILE} setKamailioConfigSubstdef 'DSIP_VERSION' "${DSIP_VERSION}" ${DSIP_KAMAILIO_CONFIG_FILE} @@ -675,6 +684,14 @@ function updateKamailioConfig { setKamailioConfigSubstdef 'DMQ_PORT' "${KAM_DMQ_PORT}" ${DSIP_KAMAILIO_CONFIG_FILE} setKamailioConfigSubstdef 'HEP_PORT' "${KAM_HEP_PORT}" ${DSIP_KAMAILIO_CONFIG_FILE} setKamailioConfigSubstdef 'HOMER_HOST' "${KAM_HOMER_HOST}" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_CARRIER' "$FLT_CARRIER" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_PBX' "$FLT_PBX" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_MSTEAMS' "$FLT_MSTEAMS" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_INTERNAL' "$FLT_INTERNAL" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_OUTBOUND' "$FLT_OUTBOUND" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_INBOUND' "$FLT_INBOUND" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_LCR_MIN' "$FLT_LCR_MIN" ${DSIP_KAMAILIO_CONFIG_FILE} + setKamailioConfigDef 'FLT_FWD_MIN' "$FLT_FWD_MIN" ${DSIP_KAMAILIO_CONFIG_FILE} setKamailioConfigGlobal 'server.api_server' "${DSIP_API_BASEURL}" ${DSIP_KAMAILIO_CONFIG_FILE} setKamailioConfigGlobal 'server.api_token' "${DSIP_API_TOKEN}" ${DSIP_KAMAILIO_CONFIG_FILE} setKamailioConfigGlobal 'server.role' "${ROLE}" ${DSIP_KAMAILIO_CONFIG_FILE} @@ -700,7 +717,7 @@ function updateKamailioConfig { # note: the '@' symbol must be escaped in perl regex if printf '%s' "$KAM_DB_HOST" | grep -q -oP '(\[.*\]|.*,.*)'; then # db connection is clustered - enableKamailioConfigAttrib 'WITH_DBCLUSTER' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_DBCLUSTER' ${DSIP_KAMAILIO_CONFIG_FILE} # TODO: support different type/user/pass/port/name per connection # TODO: support multiple clusters @@ -837,6 +854,121 @@ function updateCACertsDir { chmod 640 ${DSIP_CERTS_DIR}/ca/cert.*.pem } +# Update dynamic addresses in address table +function updateDynamicAddresses { + local OLD_ADDR_IDS=( ) NEW_ADDR_IDS=( ) NEW_ADDR_LIST=( ) SQL_STATEMENTS=( ) + lcaol OLD_ADDR_ID="" UAC_ID="" SIP_ADDR="" INSERT_TEMPLATE="" + + # update resolved IP address entries for dr_gateways entries using DNS addresses + # - dr_gateways to address mapping: dr_gateways.description['addr_list'] -> address.id + while IFS= read -r ROW; do + # split up the fields + OLD_ADDR_IDS=( $(cut -d $'\t' -f 1 <<<"$ROW") ) + SIP_ADDR=$(cut -d $'\t' -f 2 <<<"$ROW") + + # get other address fields (should be same for the list of addresses) + INSERT_TEMPLATE=$( + mysql -sNA --user="$KAM_DB_USER" --password="$KAM_DB_PASS" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" $KAM_DB_NAME \ + -e "SELECT * FROM address WHERE id IN (${OLD_ADDR_IDS[@]}) LIMIT 1;" | + awk -F $'\t' '{printf "INSERT INTO address VALUES(NULL,%s,RESOLVED_ADDR,%s,%s,%s);",$2,$4,$5,$6}' + ) + + # delete old addresses and create new ones + SQL_STATEMENTS+=("DELETE FROM address WHERE id IN (${OLD_ADDR_IDS[@]});") + for ADDR in $(hostToIP -a "$SIP_ADDR"); do + NEW_ADDR_LIST+=("$ADDR") + SQL_STATEMENTS+=( $(perl -e "\$addr='$ADDR';" -pe 's%RESOLVED_ADDR%${addr}%' <<<"$INSERT_TEMPLATE" 2>/dev/null) ) + done + + # run sql statements as a transaction and check for errors + sqlAsTransaction --user="$MYSQL_ROOT_USERNAME" --password="$MYSQL_ROOT_PASSWORD" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" --db="$KAM_DB_NAME" "${SQL_STATEMENTS[@]}" + (( $? != 0 )) && { printerr 'Failed updating gateway associated addresses'; cleanupAndExit 1; } + + # update address list + NEW_ADDR_IDS=( $( + mysql -sNA --user="$KAM_DB_USER" --password="$KAM_DB_PASS" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" $KAM_DB_NAME \ + -e "SELECT id FROM address WHERE ip_addr IN ($(joinwith '' ',' '' ${NEW_ADDR_LIST[@]});" + ) ) + + # run sql statements as a transaction and check for errors + sqlAsTransaction --user="$MYSQL_ROOT_USERNAME" --password="$MYSQL_ROOT_PASSWORD" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" --db="$KAM_DB_NAME" \ + "UPDATE dr_gateways SET description = JSON_REPLACE(description, '$.addr_list', '[$(joinwith '' ',' '' ${NEW_ADDR_IDS[@]})]');" + (( $? != 0 )) && { printerr 'Failed updating gateway associated addresses'; cleanupAndExit 1; } + + done < <( + mysql -sNA --user="$KAM_DB_USER" --password="$KAM_DB_PASS" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" $KAM_DB_NAME <<-'EOF' +SELECT REGEXP_REPLACE(JSON_EXTRACT(description, '$.addr_list'), '\\[([^\\[\\]]+)\\]', '\\1') AS addr_list, address AS sip_addr +FROM dr_gateways +WHERE JSON_EXISTS(description, '$.addr_list') + AND REGEXP_REPLACE(address, + '^' + '(?:(?P[a-zA-Z]+):/?/?)?' + '(?:(?P[a-zA-Z0-9\\-_.]+(?::.+)?)(?:@))?' + '(?:' + '(?:\\[?(?P(?:[0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,7}:|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:(?:(?::[0-9a-fA-F]{1,4}){1,6})|:(?:(?::[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(?::[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(?:ffff(?::0{1,4}){0,1}:){0,1}(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])|(?:[0-9a-fA-F]{1,4}:){1,4}:(?:(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(?:25[0-5]|(?:2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\]?)|' + '(?P(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\\.(?:[0-9]{1,2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5]))|' + '(?P(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9_][a-zA-Z0-9\\-_]*[a-zA-Z0-9])\\.)*(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9]))' + ')' + '(?::(?P[1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5]))?' + '(?P/[^\\s?;]*)?' + '(?:(?:[?;])(?P.*))?' + '$', + '\\5') <> ''; +EOF +) + + # reset vars + OLD_ADDR_IDS=( ) NEW_ADDR_IDS=( ) NEW_ADDR_LIST=( ) SQL_STATEMENTS=( ) + OLD_ADDR_ID="" UAC_ID="" SIP_ADDR="" INSERT_TEMPLATE="" + + # update resolved IP address entries for dr_gateways and uacreg entries using DNS addresses + # - dr_gateways to address mapping: dr_gateways.description['addr_list'] -> address.id + # - uacreg to address mapping: uacreg.l_uuid -> dr_gw_lists.id, dr_gw_lists.description['name']+'-uac' -> address.tag['name'] + while IFS= read -r ROW; do + # split up the fields + OLD_ADDR_ID=$(cut -d $'\t' -f 1 <<<"$ROW") + UAC_ID=$(cut -d $'\t' -f 2 <<<"$ROW") + + # get other address fields (will make new address associated to uac entry) + INSERT_TEMPLATE=$( + mysql -sNA --user="$KAM_DB_USER" --password="$KAM_DB_PASS" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" $KAM_DB_NAME \ + -e "SELECT * FROM address WHERE id='$OLD_ADDR_ID' LIMIT 1;" | + awk -F $'\t' '{printf "INSERT INTO address VALUES(NULL,%s,RESOLVED_ADDR,%s,%s,%s);",$2,$4,$5,$6}' + ) + + # delete old addresses and create new ones + SQL_STATEMENTS+=("DELETE FROM address WHERE id='$OLD_ADDR_ID';") + for ADDR in $(hostToIP -a "$SIP_ADDR"); do + NEW_ADDR_LIST+=("$ADDR") + SQL_STATEMENTS+=( $(perl -e "\$addr='$ADDR';" -pe 's%RESOLVED_ADDR%${addr}%' <<<"$INSERT_TEMPLATE" 2>/dev/null) ) + done + + # update address list + NEW_ADDR_IDS=( $( + mysql -sNA --user="$KAM_DB_USER" --password="$KAM_DB_PASS" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" $KAM_DB_NAME \ + -e "SELECT id FROM address WHERE ip_addr IN ($(joinwith '' ',' '' ${NEW_ADDR_LIST[@]});" + ) ) + done < <( + mysql -sNA --user="$KAM_DB_USER" --password="$KAM_DB_PASS" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" $KAM_DB_NAME <<-'EOF' +SELECT t1.id AS uac_id, t3.id AS addr_id +FROM uacreg t1 + JOIN dr_gw_lists t2 ON t1.l_uuid = t2.id + JOIN address t3 ON CONCAT(JSON_UNQUOTE(JSON_EXTRACT(t2.description, '$.name')), '-uac') = + JSON_UNQUOTE(JSON_EXTRACT(t3.tag, '$.name')); +EOF +) + + # run sql statements after collecting them all, as a transaction and check for errors + sqlAsTransaction --user="$MYSQL_ROOT_USERNAME" --password="$MYSQL_ROOT_PASSWORD" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" --db="$KAM_DB_NAME" "${SQL_STATEMENTS[@]}" + (( $? != 0 )) && { printerr 'Failed updating uac auth associated addresses'; cleanupAndExit 1; } + + # update local addresses that may change as network changes + local INTERNAL_NET_PREFIX=$(echo -n "$INTERNAL_NET" | cut -d '/' -f 2) + sqlAsTransaction --user="$MYSQL_ROOT_USERNAME" --password="$MYSQL_ROOT_PASSWORD" --host="$KAM_DB_HOST" --port="$KAM_DB_PORT" --db="$KAM_DB_NAME" \ + "UPDATE address set ip_addr='$INTERNAL_IP', mask='$INTERNAL_NET_PREFIX' WHERE JSON_UNQUOTE(JSON_EXTRACT(tag, '$.name')) = 'dsip-internal';" + (( $? != 0 )) && { printerr 'Failed updating internal dynamic addresses'; cleanupAndExit 1; } +} + function generateKamailioConfig { # copy of template kamailio configuration to dsiprouter system config dir cp -f ${DSIP_KAMAILIO_CONFIG_DIR}/kamailio_dsiprouter.cfg ${DSIP_KAMAILIO_CONFIG_FILE} @@ -857,9 +989,9 @@ function generateKamailioConfig { # non-module features to enable if (( ${WITH_LCR} == 1 )); then - enableKamailioConfigAttrib 'WITH_LCR' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_LCR' ${DSIP_KAMAILIO_CONFIG_FILE} else - disableKamailioConfigAttrib 'WITH_LCR' ${DSIP_KAMAILIO_CONFIG_FILE} + disableKamailioConfigFeature 'WITH_LCR' ${DSIP_KAMAILIO_CONFIG_FILE} fi # Backup kamcfg and link the dsiprouter kamcfg @@ -911,14 +1043,30 @@ function configureKamailioDB { mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/address.sql + # Update schema for carrierroute table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/carrierroute.sql + + # Update schema for carrierfailureroute table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/carrierfailureroute.sql + # Update schema for dispatcher table mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/dispatcher.sql + # Update schema for domainpolicy table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/domainpolicy.sql + # Update schema for dr_gateways table mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/dr_gateways.sql + # Update schema for dr_groups table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/dr_groups.sql + # Update schema for dr_gw_lists table mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/dr_gw_lists.sql @@ -927,10 +1075,26 @@ function configureKamailioDB { mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/dr_rules.sql + # Update schema for globalblacklist table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/globalblacklist.sql + + # Update schema for lcr_gw table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/lcr_gw.sql + + # Update schema for speed_dial table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/speed_dial.sql + # Update schema for subscribers table mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/subscriber.sql + # Update schema for trusted table + mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ + < ${DSIP_DEFAULTS_DIR}/trusted.sql + # Install schema for custom LCR logic mysql -s -N --user="$ROOT_DB_USER" --password="$ROOT_DB_PASS" --host="${KAM_DB_HOST}" --port="${KAM_DB_PORT}" $KAM_DB_NAME \ < ${DSIP_DEFAULTS_DIR}/dsip_lcr.sql @@ -995,13 +1159,14 @@ function configureKamailioDB { # import default carriers and outbound routes mkdir -p /tmp/defaults + local INTERNAL_NET_PREFIX=$(echo -n "$INTERNAL_NET" | cut -d '/' -f 2) # generate defaults subbing in dynamic values cp -f ${DSIP_DEFAULTS_DIR}/dr_gw_lists.csv /tmp/defaults/dr_gw_lists.csv - sed "s/FLT_CARRIER/$FLT_CARRIER/g; s/FLT_PBX/$FLT_PBX/g; s/FLT_MSTEAMS/$FLT_MSTEAMS/g" \ + sed "s/FLT_CARRIER/$FLT_CARRIER/g; s/FLT_PBX/$FLT_PBX/g; s/FLT_MSTEAMS/$FLT_MSTEAMS/g; s/FLT_INTERNAL/$FLT_INTERNAL/g; s/INTERNAL_IP/$INTERNAL_IP/g; s/INTERNAL_NET_PREFIX/$INTERNAL_NET_PREFIX/g;" \ ${DSIP_DEFAULTS_DIR}/address.csv > /tmp/defaults/address.csv - sed "s/FLT_CARRIER/$FLT_CARRIER/g; s/FLT_PBX/$FLT_PBX/g; s/FLT_MSTEAMS/$FLT_MSTEAMS/g" \ + sed "s/FLT_CARRIER/$FLT_CARRIER/g; s/FLT_PBX/$FLT_PBX/g; s/FLT_MSTEAMS/$FLT_MSTEAMS/g; s/FLT_INTERNAL/$FLT_INTERNAL/g;" \ ${DSIP_DEFAULTS_DIR}/dr_gateways.csv > /tmp/defaults/dr_gateways.csv - sed "s/FLT_OUTBOUND/$FLT_OUTBOUND/g; s/FLT_INBOUND/$FLT_INBOUND/g" \ + sed "s/FLT_OUTBOUND/$FLT_OUTBOUND/g; s/FLT_INBOUND/$FLT_INBOUND/g;" \ ${DSIP_DEFAULTS_DIR}/dr_rules.csv > /tmp/defaults/dr_rules.csv # import default carriers @@ -1020,14 +1185,14 @@ function configureKamailioDB { # TODO: deprecated since CLI command is being deprecated function enableSERVERNAT { - enableKamailioConfigAttrib 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} printwarn "SERVERNAT is enabled - Restarting Kamailio is required" printwarn "You can restart it by executing: systemctl restart kamailio" } # TODO: deprecated since CLI command is being deprecated function disableSERVERNAT { - disableKamailioConfigAttrib 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} + disableKamailioConfigFeature 'WITH_SERVERNAT' ${DSIP_KAMAILIO_CONFIG_FILE} printwarn "SERVERNAT is disabled - Restarting Kamailio is required" printdbg "You can restart it by executing: systemctl restart kamailio" @@ -1117,7 +1282,7 @@ configureSystemRepos() { printerr 'Could not configure system repositories' cleanupAndExit 1 elif (( $? >= 100 )); then - printwarn 'Some issue(s) with system repositories' + printwarn 'Some issue(s) with system repositories' else printdbg 'System repositories configured successfully' touch ${DSIP_SYSTEM_CONFIG_DIR}/.reposconfigured @@ -1199,11 +1364,11 @@ function installRTPEngine { ${DSIP_PROJECT_DIR}/rtpengine/${DISTRO}/install.sh install ret=$? if (( $ret == 0 )); then - enableKamailioConfigAttrib 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE} systemctl restart kamailio printdbg "configuring RTPEngine service" elif (( $ret == 2 )); then - enableKamailioConfigAttrib 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE} printwarn "RTPEngine install waiting on reboot" cleanupAndExit 0 else @@ -1237,7 +1402,7 @@ function uninstallRTPEngine { if (( $? == 0 )); then if [ -f "${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled" ]; then - disableKamailioConfigAttrib 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE} + disableKamailioConfigFeature 'WITH_RTPENGINE' ${DSIP_KAMAILIO_CONFIG_FILE} systemctl restart kamailio fi else @@ -1513,6 +1678,8 @@ function installKamailio { generateKamailioConfig updateKamailioConfig updateKamailioStartup + # update dynamic addresses every 5 minutes + cronAppend "*/5 * * * * ${DSIP_PROJECT_DIR}/dsiprouter.sh updatedynamicaddrs" else printerr "kamailio install failed" cleanupAndExit 1 @@ -1553,6 +1720,9 @@ function uninstallKamailio { removeInitCmd "dsiprouter.sh updatekamconfig" removeDependsOnInit "kamailio.service" + # remove cronjobs for kamailio + cronRemove "${DSIP_PROJECT_DIR}/dsiprouter.sh updatedynamicaddrs" + # Remove the hidden installed file, which denotes if it's installed or not rm -f ${DSIP_SYSTEM_CONFIG_DIR}/.kamailioinstalled @@ -2108,7 +2278,7 @@ function setCredentials { cleanupAndExit 1 fi - # update non-encrypte settings locally and gather statements for updating DB + # update non-encrypted settings locally and gather statements for updating DB if [[ -n "${SET_DSIP_GUI_USER}" ]]; then SQL_STATEMENTS+=("update kamailio.dsip_settings set DSIP_USERNAME='${SET_DSIP_GUI_USER}' where DSIP_ID=${DSIP_ID};") setConfigAttrib 'DSIP_USERNAME' "$SET_DSIP_GUI_USER" ${DSIP_CONFIG_FILE} -q @@ -3891,10 +4061,32 @@ function processCMD { ;; # internal command, generates CA dir from CA bundle file updatecacertsdir) - # update dnsmasq config + # update cacerts config RUN_COMMANDS+=(updateCACertsDir) shift + while (( $# > 0 )); do + OPT="$1" + case $OPT in + -debug) + export DEBUG=1 + set -x + shift + ;; + *) # fail on unknown option + printerr "Invalid option [$OPT] for command [$ARG]" + usageOptions + cleanupAndExit 1 + shift + ;; + esac + done + ;; + updatedynamicaddrs) + # update resolved DNS addresses + RUN_COMMANDS+=(updateDynamicAddresses) + shift + while (( $# > 0 )); do OPT="$1" case $OPT in diff --git a/dsiprouter/dsip_lib.sh b/dsiprouter/dsip_lib.sh index f6f69542..f99bed4f 100755 --- a/dsiprouter/dsip_lib.sh +++ b/dsiprouter/dsip_lib.sh @@ -170,25 +170,25 @@ function decryptConfigAttrib() { } export -f decryptConfigAttrib -# $1 == attribute name +# $1 == feature name # $2 == kamailio config file -function enableKamailioConfigAttrib() { +function enableKamailioConfigFeature() { local NAME="$1" local CONFIG_FILE="$2" sed -i -r -e "s~#+(!(define|trydef|redefine)[[:space:]]? $NAME)~#\1~g" ${CONFIG_FILE} } -export -f enableKamailioConfigAttrib +export -f enableKamailioConfigFeature -# $1 == attribute name +# $1 == feature name # $2 == kamailio config file -function disableKamailioConfigAttrib() { +function disableKamailioConfigFeature() { local NAME="$1" local CONFIG_FILE="$2" sed -i -r -e "s~#+(!(define|trydef|redefine)[[:space:]]? $NAME)~##\1~g" ${CONFIG_FILE} } -export -f disableKamailioConfigAttrib +export -f disableKamailioConfigFeature # $1 == name of defined url to change # $2 == value to change url to @@ -204,6 +204,24 @@ function setKamailioConfigDburl() { } export -f setKamailioConfigDburl +# $1 == name of define to change +# $2 == +# $3 == kamailio config file +# $4 == -q (quote as string) +function setKamailioConfigDef() { + local NAME="$1" + local VALUE="$2" + local CONFIG_FILE="$3" + + if [[ "$4" == "-q" ]]; then + VALUE='"'"${VALUE}"'"' + fi + + perl -e "\$name='${NAME}'; \$value='${VALUE}';" \ + -i -pe 's%(#+\!)(define|trydef|redefine)([ \t]+${name}[ \t]+).*%\1\2\3${value}%g' ${CONFIG_FILE} +} +export -f setKamailioConfigDef + # $1 == name of substdef to change # $2 == value to change substdef to # $3 == kamailio config file @@ -388,6 +406,8 @@ export -f ipv6Test # notes: prints internal ip, or empty string if not available # notes: tries ipv4 first then ipv6 function getInternalIP() { + local IPV6_ENABLED=${IPV6_ENABLED:-0} + local IP=$(ip -4 route get $GOOGLE_DNS_IPV4 2>/dev/null | head -1 | grep -oP 'src \K([^\s]+)') if (( ${IPV6_ENABLED} == 1 )) && [[ -z "$IP" ]]; then IP=$(ip -6 route get $GOOGLE_DNS_IPV6 2>/dev/null | head -1 | grep -oP 'src \K([^\s]+)') @@ -470,11 +490,14 @@ export -f getInternalFQDN # notes: will use EXTERNAL_IP if available or look it up dynamically # notes: tries ipv4 first then ipv6 function getExternalFQDN() { + local IPV6_ENABLED=${IPV6_ENABLED:-0} + local EXTERNAL_IP=${EXTERNAL_IP:-$(getExternalIP)} local EXTERNAL_FQDN=$(dig @${GOOGLE_DNS_IPV4} +short -x ${EXTERNAL_IP} 2>/dev/null | head -1 | sed 's/\.$//') if (( ${IPV6_ENABLED} == 1 )) && [[ -z "$EXTERNAL_FQDN" ]]; then EXTERNAL_FQDN=$(dig @${GOOGLE_DNS_IPV6} +short -x ${EXTERNAL_IP} 2>/dev/null | head -1 | sed 's/\.$//') fi + printf '%s' "$EXTERNAL_FQDN" } export -f getExternalFQDN @@ -483,6 +506,7 @@ export -f getExternalFQDN # notes: prints internal CIDR address, or empty string if not available # notes: tries ipv4 first then ipv6 function getInternalCIDR() { + local IPV6_ENABLED=${IPV6_ENABLED:-0} local PREFIX_LEN="" DEF_IFACE="" local IP=$(ip -4 route get $GOOGLE_DNS_IPV4 2>/dev/null | head -1 | grep -oP 'src \K([^\s]+)') @@ -505,6 +529,26 @@ function getInternalCIDR() { } export -f getInternalCIDR +# $1 == host to resolve +# $2 == -a (return all resolved IPs) +# output: IP address(es) of host +function hostToIP() { + local IPV6_ENABLED=${IPV6_ENABLED:-0} + local HOST="$1" + + local IP_ADDR=$(dig @${GOOGLE_DNS_IPV4} +short A ${HOST} 2>/dev/null) + if (( ${IPV6_ENABLED} == 1 )) && [[ -z "$EXTERNAL_FQDN" ]]; then + IP_ADDR=$(dig @${GOOGLE_DNS_IPV6} +short AAAA ${HOST} 2>/dev/null | head -1 | sed 's/\.$//') + fi + + if [[ "$2" == "-a" ]]; then + echo -n "$IP_ADDR" + else + echo -n "$IP_ADDR" | head -1 + fi +} +export -f hostToIP + # $1 == cmd as executed in systemd (by ExecStart=) # notes: take precaution when adding long running functions as they will block startup in boot order # notes: adding init commands on an AMI instance must not be long running processes, otherwise they will fail diff --git a/gui/database/__init__.py b/gui/database/__init__.py index a46612fe..7176a579 100644 --- a/gui/database/__init__.py +++ b/gui/database/__init__.py @@ -1,19 +1,24 @@ # make sure the generated source files are imported instead of the template ones import sys + +import sqlalchemy.dialects.mysql + sys.path.insert(0, '/etc/dsiprouter/gui') -import os +import os, json from enum import Enum from datetime import datetime, timedelta -from sqlalchemy import create_engine, MetaData, Table, Column, String +from sqlalchemy import create_engine, MetaData, Table, Column, String, exc as sql_exceptions +from sqlalchemy.sql import operators as sql_op from sqlalchemy.orm import mapper, sessionmaker, scoped_session -from sqlalchemy import exc as sql_exceptions +from sqlalchemy.ext.mutable import MutableDict +from sqlalchemy.types import TypeDecorator, VARCHAR +from sqlalchemy.dialects.mysql import JSON as MysqlJSON import settings -from shared import IO, debugException, dictToStrFields +from shared import IO, debugException from util.networking import safeUriToHost, safeFormatSipUri from util.security import AES_CTR - if settings.KAM_DB_TYPE == "mysql": try: import MySQLdb as db_driver @@ -30,6 +35,177 @@ debugException(ex) raise + +class JSON(MysqlJSON, TypeDecorator): + impl = VARCHAR + cache_ok = True + + def __init__(self, length, *args, **kwargs): + self.__class__.impl = VARCHAR(length) + super().__init__(*args, **kwargs) + + def coerce_compared_value(self, op, value): + if op in (sql_op.like_op, sql_op.not_like_op): + return String() + else: + return self + + def process_bind_param(self, value, dialect): + if value is None: + return '{}' + else: + return json.dumps(value) + + def process_result_value(self, value, dialect): + if value is None: + return {} + else: + return json.loads(value) + +MutableDict.associate_with(JSON) + +# TODO: remove after validation of above implementation +# class hybrid_dict_property(hybrid_property): +# def __init__( +# self, fget, fset=None, fdel=None, +# fgetitem=None, fsetitem=None, fdelitem=None, +# expr=None, custom_comparator=None, update_expr=None, +# item_expr=None, item_custom_comparator=None, item_update_expr=None, +# ): +# self.fgetitem = fgetitem +# self.fsetitem = fsetitem +# self.fdelitem = fdelitem +# self.item_expr = item_expr +# self.item_custom_comparator = item_custom_comparator +# self.item_update_expr = item_update_expr +# super().__init__( +# fget, fset=fset, fdel=fdel, +# expr=expr, custom_comparator=custom_comparator, update_expr=update_expr, +# ) +# +# def __getitem__(self, instance, key): +# if instance is None: +# return self.fget(instance)[key] +# else: +# return self.fgetitem(instance) +# +# def __setitem__(self, instance, key, val): +# if self.fsetitem is None: +# tmp = self.fget(instance) +# tmp[key] = val +# return self.fset(instance, tmp) +# else: +# self.fsetitem(instance, key, val) +# +# def __delitem__(self, instance, key): +# if self.fdelitem is None: +# tmp = self.fget(instance) +# del (tmp[key]) +# self.fset(instance, tmp) +# else: +# self.fdelitem(instance, key) +# +# def get_item(self, fgetitem): +# return self._copy(fgetitem=fgetitem) +# +# def set_item(self, fsetitem): +# return self._copy(fsetitem=fsetitem) +# +# def delete_item(self, fdelitem): +# return self._copy(fdelitem=fdelitem) +# +# def item_expression(self, expr): +# return self._copy(item_expr=expr) +# +# def item_comparator(self, comparator): +# return self._copy(item_custom_comparator=comparator) +# +# def item_update_expression(self, meth): +# return self._copy(item_update_expr=meth) +# +# @memoized_property +# def _item_expr_comparator(self): +# if self.item_custom_comparator is not None: +# return self._get_item_comparator(self.item_custom_comparator) +# elif self.item_expr is not None: +# return self._get_item_expr(self.item_expr) +# else: +# return self._get_item_expr(self.fgetitem) +# +# def _get_item_expr(self, expr): +# def _item_expr(cls): +# return ExprComparator(cls, expr(cls), self) +# +# update_wrapper(_item_expr, expr) +# +# return self._get_item_comparator(_item_expr) +# +# def _get_item_comparator(self, comparator): +# proxy_attr = attributes.create_proxied_attribute(self) +# +# def item_expr_comparator(owner): +# return proxy_attr( +# owner, +# self.__name__, +# self, +# comparator(owner), +# doc=comparator.__doc__ or self.__doc__, +# ) +# +# return item_expr_comparator +# +# class ObjectWithDescription(object): +# """ +# Handles description field setter/getter for JSON data +# """ +# +# @hybrid_dict_property +# def description(self): +# return strFieldsToDict(self._description) +# +# @description.setter +# def description(self, val): +# self._description = dictToStrFields(val) +# +# @description.deleter +# def description(self): +# self._description = '{}' +# +# @description.get_item +# def description(self, key): +# return strFieldsToDict(self._description)[key] +# +# @description.set_item +# def description(self, key, val): +# tmp = strFieldsToDict(self._description) +# tmp[key] = val +# self._description = dictToStrFields(tmp) +# +# @description.delete_item +# def description(self, key): +# tmp = strFieldsToDict(self._description) +# del(tmp[key]) +# self._description = tmp +# +# @description.expression +# def description(cls): +# return sql_func.JSON_UNQUOTE(cls._description) +# +# @description.update_expression +# def description(cls, val): +# return [(cls._description, dictToStrFields(val))] +# +# @description.item_expression +# def description(cls, key): +# return sql_func.JSON_UNQUOTE(sql_func.JSON_EXTRACT(cls._description, '$.{}'.format(key))) +# +# @description.item_update_expression +# def description(cls, key, val): +# tmp = strFieldsToDict(cls._description) +# tmp[key] = val +# return [(cls._description, dictToStrFields(tmp))] + + class Gateways(object): """ Schema for dr_gateways table\n @@ -38,19 +214,19 @@ class Gateways(object): The address field can be a full SIP URI, partial URI, or only host; where host portion is an IP or FQDN """ - def __init__(self, name, address, strip, prefix, type, gwgroup=None, addr_id=None, attrs=''): - description = {"name": name} + def __init__(self, name, address, strip, prefix, type, gwgroup=None, addr_list=None, attrs=''): + desc_fields = {'name': name} if gwgroup is not None: - description["gwgroup"] = str(gwgroup) - if addr_id is not None: - description['addr_id'] = str(addr_id) + desc_fields['gwgroup'] = gwgroup + if addr_list is not None: + desc_fields['addr_list'] = addr_list self.type = type self.address = address self.strip = strip self.pri_prefix = prefix self.attrs = attrs - self.description = dictToStrFields(description) + self.description = desc_fields pass @@ -61,9 +237,11 @@ class GatewayGroups(object): """ def __init__(self, name, gwlist=[], type=settings.FLT_CARRIER): - self.description = "name:{},type:{}".format(name, type) + self.description = {'name': name, 'type': type} self.gwlist = ",".join(str(gw) for gw in gwlist) + #Column('description', JSON, nullable=False, server_default='{}') + pass class Address(object): @@ -75,15 +253,15 @@ class Address(object): """ def __init__(self, name, ip_addr, mask, type, gwgroup=None, port=0): - tag = {"name": name} + tag_fields = {"name": name} if gwgroup is not None: - tag["gwgroup"] = str(gwgroup) + tag_fields["gwgroup"] = gwgroup self.grp = type self.ip_addr = ip_addr self.mask = mask self.port = port - self.tag = dictToStrFields(tag) + self.tag = tag_fields pass @@ -95,7 +273,7 @@ class InboundMapping(object): gwname = Column(String) - def __init__(self, groupid, prefix, gwlist, description=''): + def __init__(self, groupid, prefix, gwlist, description={}): self.groupid = groupid self.prefix = prefix self.gwlist = gwlist @@ -111,7 +289,7 @@ class OutboundRoutes(object): Documentation: `dr_rules table `_ """ - def __init__(self, groupid, prefix, timerec, priority, routeid, gwlist, description): + def __init__(self, groupid, prefix, timerec, priority, routeid, gwlist, description={}): self.groupid = groupid self.prefix = prefix self.timerec = timerec @@ -127,7 +305,7 @@ class CustomRouting(object): Schema for dr_custom_rules table\n """ - def __init__(self, locality, ppm, description): + def __init__(self, locality, ppm, description={}): self.locality = locality self.ppm = ppm self.description = description @@ -232,8 +410,8 @@ class dSIPLeases(object): def __init__(self, gwid, sid, ttl): self.gwid = gwid self.sid = sid - t = datetime.now() + timedelta(seconds=ttl) - self.expiration = t.strftime('%Y-%m-%d %H:%M:%S') + now = datetime.now() + timedelta(seconds=ttl) + self.expiration = now.strftime('%Y-%m-%d %H:%M:%S') pass @@ -328,7 +506,6 @@ class dSIPCertificates(object): """ def __init__(self, domain, type, email, cert, key): - self.domain = domain self.type = type self.email = email @@ -343,12 +520,10 @@ class dSIPDNIDEnrichment(object): """ def __init__(self, dnid, country_code='', routing_number='', rule_name=''): - description = {'name': rule_name} - self.dnid = dnid self.country_code = country_code self.routing_number = routing_number - self.description = dictToStrFields(description) + self.description = {'name': rule_name} pass @@ -430,7 +605,7 @@ class Dispatcher(object): Documentation: `dispatcher table `_ """ - def __init__(self, setid, destination, flags=None, priority=None, attrs=None, description=''): + def __init__(self, setid, destination, flags=None, priority=None, attrs=None, description={}): self.setid = setid self.destination = safeFormatSipUri(destination) self.flags = flags @@ -528,6 +703,7 @@ def createSessionMaker(): :return: SessionMaker() object """ + # create session loader and db engine if 'SessionLoader' in globals(): return globals()['SessionLoader'] if not 'db_engine' in globals(): @@ -535,32 +711,49 @@ def createSessionMaker(): else: db_engine = globals()['db_engine'] + # create metadata based on engine metadata = MetaData(db_engine) - dr_gateways = Table('dr_gateways', metadata, autoload=True) - address = Table('address', metadata, autoload=True) - outboundroutes = Table('dr_rules', metadata, autoload=True) - inboundmapping = Table('dr_rules', metadata, autoload=True) - subscriber = Table('subscriber', metadata, autoload=True) - dsip_domain_mapping = Table('dsip_domain_mapping', metadata, autoload=True) - dsip_multidomain_mapping = Table('dsip_multidomain_mapping', metadata, autoload=True) - # fusionpbx_mappings = Table('dsip_fusionpbx_mappings', metadata, autoload=True) - dsip_lcr = Table('dsip_lcr', metadata, autoload=True) - uacreg = Table('uacreg', metadata, autoload=True) - dr_gw_lists = Table('dr_gw_lists', metadata, autoload=True) + # create table definitions reflected from engine + # - override column definitions here if needed + address = Table('address', metadata, + Column('tag', JSON(255), nullable=False, server_default='{}'), + autoload=True) + dr_custom_rules = Table('dr_custom_rules', metadata, + Column('description', JSON(255), nullable=False, server_default='{}'), + autoload=True) + dr_gateways = Table('dr_gateways', metadata, + Column('description', JSON(255), nullable=False, server_default='{}'), + autoload=True) # dr_groups = Table('dr_groups', metadata, autoload=True) + dr_gw_lists = Table('dr_gw_lists', metadata, + Column('description', JSON(255), nullable=False, server_default='{}'), + autoload=True) + dr_rules = Table('dr_rules', metadata, + Column('description', JSON(255), nullable=False, server_default='{}'), + autoload=True, extend_existing=True) + dispatcher = Table('dispatcher', metadata, + Column('description', JSON(255), nullable=False, server_default='{}'), + autoload=True) domain = Table('domain', metadata, autoload=True) domain_attrs = Table('domain_attrs', metadata, autoload=True) - dispatcher = Table('dispatcher', metadata, autoload=True) - dsip_endpoint_lease = Table('dsip_endpoint_lease', metadata, autoload=True) - dsip_maintmode = Table('dsip_maintmode', metadata, autoload=True) dsip_calllimit = Table('dsip_calllimit', metadata, autoload=True) - dsip_notification = Table('dsip_notification', metadata, autoload=True) - dsip_hardfwd = Table('dsip_hardfwd', metadata, autoload=True) - dsip_failfwd = Table('dsip_failfwd', metadata, autoload=True) dsip_cdrinfo = Table('dsip_cdrinfo', metadata, autoload=True) dsip_certificates = Table('dsip_certificates', metadata, autoload=True) - dsip_dnid_enrichment = Table('dsip_dnid_enrich_lnp', metadata, autoload=True) + dsip_dnid_enrichment = Table('dsip_dnid_enrich_lnp', metadata, + Column('description', JSON(255), nullable=False, server_default='{}'), + autoload=True) + dsip_domain_mapping = Table('dsip_domain_mapping', metadata, autoload=True) + dsip_endpoint_lease = Table('dsip_endpoint_lease', metadata, autoload=True) + dsip_failfwd = Table('dsip_failfwd', metadata, autoload=True) + # dsip_fusionpbx_mappings = Table('dsip_fusionpbx_mappings', metadata, autoload=True) + dsip_hardfwd = Table('dsip_hardfwd', metadata, autoload=True) + dsip_lcr = Table('dsip_lcr', metadata, autoload=True) + dsip_maintmode = Table('dsip_maintmode', metadata, autoload=True) + dsip_multidomain_mapping = Table('dsip_multidomain_mapping', metadata, autoload=True) + dsip_notification = Table('dsip_notification', metadata, autoload=True) + subscriber = Table('subscriber', metadata, autoload=True) + uacreg = Table('uacreg', metadata, autoload=True) # dr_gw_lists_alias = select([ # dr_gw_lists.c.id.label("drlist_id"), @@ -571,14 +764,15 @@ def createSessionMaker(): # dr_gw_lists_alias.c.drlist_id == dr_groups.c.id, # dr_gw_lists_alias.c.drlist_description == dr_groups.c.description) + # map table definitions to class definitions mapper(Gateways, dr_gateways) mapper(Address, address) - mapper(InboundMapping, inboundmapping) - mapper(OutboundRoutes, outboundroutes) + mapper(InboundMapping, dr_rules) + mapper(OutboundRoutes, dr_rules) mapper(dSIPDomainMapping, dsip_domain_mapping) mapper(dSIPMultiDomainMapping, dsip_multidomain_mapping) mapper(Subscribers, subscriber) - # mapper(CustomRouting, customrouting) + mapper(CustomRouting, dr_custom_rules) mapper(dSIPLCR, dsip_lcr) mapper(UAC, uacreg) mapper(GatewayGroups, dr_gw_lists) @@ -600,6 +794,7 @@ def createSessionMaker(): # 'description': [dr_groups.c.description, dr_gw_lists_alias.c.drlist_description], # }) + # bind and return scoped session loadSession = scoped_session(sessionmaker(bind=db_engine)) return loadSession diff --git a/gui/dsiprouter.py b/gui/dsiprouter.py index 0c9ad90a..3f5644b0 100755 --- a/gui/dsiprouter.py +++ b/gui/dsiprouter.py @@ -16,16 +16,16 @@ from flask_script import Manager, Server from flask_wtf.csrf import CSRFProtect from itsdangerous import URLSafeTimedSerializer -from sqlalchemy import func, exc as sql_exceptions +from sqlalchemy import exc as sql_exceptions, func as sql_func from sqlalchemy.orm import load_only from werkzeug import exceptions as http_exceptions from werkzeug.utils import secure_filename from werkzeug.middleware.proxy_fix import ProxyFix from sysloginit import initSyslogLogger from shared import updateConfig, getCustomRoutes, debugException, debugEndpoint, \ - stripDictVals, strFieldsToDict, dictToStrFields, allowed_file, showError, IO, objToDict, StatusCodes + stripDictVals, allowed_file, showError, IO, objToDict, StatusCodes from util.networking import getInternalIP, getExternalIP, safeUriToHost, safeFormatSipUri, safeStripPort, getInternalCIDR, \ - ipToHost, hostToIP, getFQDN, getHostname + ipToHost, hostToIP, isValidIP, getFQDN, getHostname from database import db_engine, SessionLoader, DummySession, Gateways, Address, InboundMapping, OutboundRoutes, Subscribers, \ dSIPLCR, UAC, GatewayGroups, Domain, DomainAttrs, dSIPDomainMapping, dSIPMultiDomainMapping, Dispatcher, dSIPMaintModes, \ dSIPCallLimits, dSIPHardFwd, dSIPFailFwd @@ -243,19 +243,21 @@ def displayCarrierGroups(gwgroup=None): db = SessionLoader() - typeFilter = "%type:{}%".format(settings.FLT_CARRIER) - # res must be a list() if gwgroup is not None and gwgroup != "": res = [db.query(GatewayGroups).outerjoin(UAC, GatewayGroups.id == UAC.l_uuid).add_columns( GatewayGroups.id, GatewayGroups.gwlist, GatewayGroups.description, - UAC.r_username, UAC.auth_password, UAC.r_domain, UAC.auth_username, UAC.realm, UAC.auth_proxy).filter( - GatewayGroups.id == gwgroup).first()] + UAC.r_username, UAC.auth_password, UAC.r_domain, UAC.auth_username, UAC.realm, UAC.auth_proxy + ).filter( + GatewayGroups.id == gwgroup + ).first()] else: res = db.query(GatewayGroups).outerjoin(UAC, GatewayGroups.id == UAC.l_uuid).add_columns( GatewayGroups.id, GatewayGroups.gwlist, GatewayGroups.description, - UAC.r_username, UAC.auth_password, UAC.r_domain, UAC.auth_username, UAC.realm, UAC.auth_proxy).filter( - GatewayGroups.description.like(typeFilter)) + UAC.r_username, UAC.auth_password, UAC.r_domain, UAC.auth_username, UAC.realm, UAC.auth_proxy + ).filter( + GatewayGroups.description['type'] == settings.FLT_CARRIER + ).all() return render_template('carriergroups.html', rows=res) @@ -315,6 +317,13 @@ def addUpdateCarrierGroups(): auth_domain = safeUriToHost(auth_domain) if auth_domain is None: raise http_exceptions.BadRequest("Auth domain hostname/address is malformed") + auth_host = safeStripPort(auth_domain) + if not isValidIP(auth_host): + auth_host_addr_list = hostToIP(auth_host, only_first=False) + if auth_host_addr_list is None: + raise http_exceptions.BadRequest("Auth domain hostname/address is malformed") + else: + auth_host_addr_list = [auth_host] if len(auth_proxy) == 0: auth_proxy = auth_domain auth_proxy = safeFormatSipUri(auth_proxy, default_user=r_username) @@ -334,49 +343,53 @@ def addUpdateCarrierGroups(): # Add auth_domain(aka registration server) to the gateway list if authtype == "userpwd": + addr_name = name + "-uac" Uacreg = UAC(gwgroup, r_username, auth_password, realm=auth_realm, auth_username=auth_username, auth_proxy=auth_proxy, local_domain=settings.EXTERNAL_IP_ADDR, remote_domain=auth_domain) - Addr = Address(name + "-uac", auth_domain, 32, settings.FLT_CARRIER, gwgroup=gwgroup) db.add(Uacreg) - db.add(Addr) + for auth_host in auth_host_addr_list: + Addr = Address(addr_name, auth_host, 32, settings.FLT_CARRIER, gwgroup=gwgroup) + db.add(Addr) # Updating else: # config form if len(new_name) > 0: Gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroup).first() - gwgroup_fields = strFieldsToDict(Gwgroup.description) - old_name = gwgroup_fields['name'] - gwgroup_fields['name'] = new_name - Gwgroup.description = dictToStrFields(gwgroup_fields) + old_name = Gwgroup.description['name'] + Gwgroup.description['name'] = new_name - Addr = db.query(Address).filter(Address.tag.contains("name:{}-uac".format(old_name))).first() - if Addr is not None: - addr_fields = strFieldsToDict(Addr.tag) - addr_fields['name'] = 'name:{}-uac'.format(new_name) - Addr.tag = dictToStrFields(addr_fields) + addr_name = new_name + "-uac" + addr_old_name = old_name + "-uac" + for Addr in db.query(Address).filter(Address.tag['name'] == addr_old_name).all(): + Addr.tag['name'] = addr_name # auth form else: + addr_name = name + "-uac" + if authtype == "userpwd": # update uacreg if exists, otherwise create if not db.query(UAC).filter(UAC.l_uuid == gwgroup).update( {'l_username': r_username, 'r_username': r_username, 'auth_username': auth_username, 'auth_password': auth_password, 'r_domain': auth_domain, 'realm': auth_realm, - 'auth_proxy': auth_proxy, 'flags': UAC.FLAGS.REG_ENABLED.value}, synchronize_session=False): + 'auth_proxy': auth_proxy, 'flags': UAC.FLAGS.REG_ENABLED.value}, synchronize_session=False + ): Uacreg = UAC(gwgroup, r_username, auth_password, realm=auth_realm, auth_username=auth_username, auth_proxy=auth_proxy, local_domain=settings.EXTERNAL_IP_ADDR, remote_domain=auth_domain) db.add(Uacreg) - # update address if exists, otherwise create - if not db.query(Address).filter(Address.tag.contains("name:{}-uac".format(name))).update( - {'ip_addr': auth_domain}, synchronize_session=False): - Addr = Address(name + "-uac", auth_domain, 32, settings.FLT_CARRIER, gwgroup=gwgroup) + # delete old addresses + db.query(Address).filter(Address.tag['name'] == addr_name).delete(synchronize_session=False) + + # create new addresses + for auth_host in auth_host_addr_list: + Addr = Address(addr_name, auth_host, 32, settings.FLT_CARRIER, gwgroup=gwgroup) db.add(Addr) else: # delete uacreg and address if they exist db.query(UAC).filter(UAC.l_uuid == gwgroup).delete(synchronize_session=False) - db.query(Address).filter(Address.tag.contains("name:{}-uac".format(name))).delete(synchronize_session=False) + db.query(Address).filter(Address.tag['name'] == addr_name).delete(synchronize_session=False) db.commit() globals.reload_required = True @@ -424,8 +437,7 @@ def deleteCarrierGroups(): gwgroup = form['gwgroup'] gwlist = form['gwlist'] if 'gwlist' in form else '' - - Addrs = db.query(Address).filter(Address.tag.contains("gwgroup:{}".format(gwgroup))) + Addrs = db.query(Address).filter(Address.tag['gwgroup'] == gwgroup) Gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroup) gwgroup_row = Gwgroup.first() if gwgroup_row is not None: @@ -499,7 +511,7 @@ def displayCarriers(gwid=None, gwgroup=None, newgwid=None): gateway_rules = {} for rule in rules: if str(gwid) in filter(None, rule.gwlist.split(',')): - gateway_rules[rule.ruleid] = strFieldsToDict(rule.description)['name'] + gateway_rules[rule.ruleid] = rule.description['name'] carrier_rules.append(json.dumps(gateway_rules, separators=(',', ':'))) # get carriers by carrier group @@ -514,7 +526,7 @@ def displayCarriers(gwid=None, gwgroup=None, newgwid=None): gateway_rules = {} for rule in rules: if gateway_id in filter(None, rule.gwlist.split(',')): - gateway_rules[rule.ruleid] = strFieldsToDict(rule.description)['name'] + gateway_rules[rule.ruleid] = rule.description['name'] carrier_rules.append(json.dumps(gateway_rules, separators=(',', ':'))) # get all carriers @@ -525,7 +537,7 @@ def displayCarriers(gwid=None, gwgroup=None, newgwid=None): gateway_rules = {} for rule in rules: if str(gateway.gwid) in filter(None, rule.gwlist.split(',')): - gateway_rules[rule.ruleid] = strFieldsToDict(rule.description)['name'] + gateway_rules[rule.ruleid] = rule.description['name'] carrier_rules.append(json.dumps(gateway_rules, separators=(',', ':'))) return render_template('carriers.html', rows=carriers, routes=carrier_rules, gwgroup=gwgroup, new_gwid=newgwid, @@ -579,22 +591,34 @@ def addUpdateCarriers(): strip = form['strip'] if len(form['strip']) > 0 else '0' prefix = form['prefix'] if len(form['prefix']) > 0 else '' + # validate required args if len(hostname) == 0: raise http_exceptions.BadRequest("Carrier hostname/address is required") - + # properly format the SIP uri sip_addr = safeUriToHost(hostname, default_port=5060) if sip_addr is None: raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") - host_addr = safeStripPort(sip_addr) + # resolve all IP's for this hostname + host = safeStripPort(sip_addr) + if not isValidIP(host): + host_addr_list = hostToIP(host, only_first=False) + if host_addr_list is None: + raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") + else: + host_addr_list = [host] + addr_id_list = [] # Adding if len(gwid) <= 0: if len(gwgroup) > 0: - Addr = Address(name, host_addr, 32, settings.FLT_CARRIER, gwgroup=gwgroup) - db.add(Addr) - db.flush() + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_CARRIER, gwgroup=gwgroup) + db.add(Addr) + db.flush() + addr_id_list.append(Addr.id) - Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_CARRIER, gwgroup=gwgroup, addr_id=Addr.id) + Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_CARRIER, gwgroup=gwgroup, + addr_list=addr_id_list) db.add(Gateway) db.flush() @@ -606,11 +630,13 @@ def addUpdateCarriers(): Gatewaygroup.gwlist = ','.join(gwlist) else: - Addr = Address(name, host_addr, 32, settings.FLT_CARRIER) - db.add(Addr) - db.flush() + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_CARRIER) + db.add(Addr) + db.flush() + addr_id_list.append(Addr.id) - Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_CARRIER, addr_id=Addr.id) + Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_CARRIER, addr_list=addr_id_list) db.add(Gateway) # Updating @@ -620,41 +646,28 @@ def addUpdateCarriers(): Gateway.strip = strip Gateway.pri_prefix = prefix - gw_fields = strFieldsToDict(Gateway.description) - gw_fields['name'] = name + Gateway.description['name'] = name if len(gwgroup) <= 0: - gw_fields['gwgroup'] = gwgroup - - # if address exists update - address_exists = False - if 'addr_id' in gw_fields and len(gw_fields['addr_id']) > 0: - Addr = db.query(Address).filter(Address.id == gw_fields['addr_id']).first() - - # if entry is non existent handle in next block - if Addr is not None: - address_exists = True + Gateway.description['gwgroup'] = gwgroup - Addr.ip_addr = host_addr - addr_fields = strFieldsToDict(Addr.tag) - addr_fields['name'] = name - - if len(gwgroup) > 0: - addr_fields['gwgroup'] = gwgroup - Addr.tag = dictToStrFields(addr_fields) - - # otherwise create the address - if not address_exists: - if len(gwgroup) > 0: - Addr = Address(name, host_addr, 32, settings.FLT_CARRIER, gwgroup=gwgroup) - else: - Addr = Address(name, host_addr, 32, settings.FLT_CARRIER) + # delete old addresses + if 'addr_list' in Gateway.description and len(Gateway.description['addr_list']) > 0: + db.query(Address).filter(Address.id.in_(Gateway.description['addr_list'])).delete( + synchronize_session=False) + # create new addresses + if len(gwgroup) > 0: + gwgroup_field = gwgroup + else: + gwgroup_field = None + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_CARRIER, gwgroup=gwgroup_field) db.add(Addr) db.flush() - gw_fields['addr_id'] = str(Addr.id) + addr_id_list.append(Addr.id) - # gw_fields may be updated above so set after - Gateway.description = dictToStrFields(gw_fields) + # update description fields + Gateway.description['addr_list'] = host_addr_list db.commit() globals.reload_required = True @@ -706,16 +719,14 @@ def deleteCarriers(): Gateway = db.query(Gateways).filter(Gateways.gwid == gwid) Gateway_Row = Gateway.first() - gw_fields = strFieldsToDict(Gateway_Row.description) # find associated gwgroup if not provided if len(gwgroup) <= 0: - gwgroup = gw_fields['gwgroup'] if 'gwgroup' in gw_fields else '' + gwgroup = Gateway_Row.description['gwgroup'] if 'gwgroup' in Gateway_Row.description else '' # remove associated address if exists - if 'addr_id' in gw_fields: - Addr = db.query(Address).filter(Address.id == gw_fields['addr_id']) - Addr.delete(synchronize_session=False) + if 'addr_list' in Gateway_Row.description: + db.query(Address).filter(Address.id.in_(Gateway_Row.description['addr_list'])).delete(synchronize_session=False) # grab any related carrier groups Gatewaygroups = db.execute('SELECT * FROM dr_gw_lists WHERE FIND_IN_SET({}, dr_gw_lists.gwlist)'.format(gwid)) @@ -928,14 +939,14 @@ def deletePBX(): gwid = form['gwid'] name = form['name'] + uac_addr_name = "uac-{}".format(name) gateway = db.query(Gateways).filter(Gateways.gwid == gwid) + db.query(Address).filter(Address.id.in_(gateway.first().description['addr_list'])).delete(synchronize_session=False) + db.query(Address).filter(Address.tag['name'] == uac_addr_name).delete(synchronize_session=False) gateway.delete(synchronize_session=False) - address = db.query(Address).filter(Address.tag.contains("name:{}".format(name))) - address.delete(synchronize_session=False) subscriber = db.query(Subscribers).filter(Subscribers.rpid == gwid) subscriber.delete(synchronize_session=False) - address.delete(synchronize_session=False) domainmultimapping = db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwid) res = domainmultimapping.options(load_only("domain_list", "attr_list")).first() @@ -998,9 +1009,7 @@ def displayInboundMapping(): db = SessionLoader() - endpoint_filter = "%type:{}%".format(settings.FLT_PBX) - carrier_filter = "%type:{}%".format(settings.FLT_CARRIER) - + # TODO: optimize query and indexes, replace raw query with ORM query or stored procedure res = db.execute("""select * from ( select r.ruleid, r.groupid, r.prefix, r.gwlist, r.description as rule_description, g.id as gwgroupid, g.description as gwgroup_description from dr_rules as r left join dr_gw_lists as g on g.id = REPLACE(r.gwlist, '#', '') where r.groupid = {flt_inbound} ) as t1 left join ( @@ -1013,14 +1022,18 @@ def displayInboundMapping(): select ff.dr_ruleid as ff_ruleid, ff.dr_groupid as ff_groupid, ff.did as ff_fwddid, NULL as ff_gwgroupid from dsip_failfwd as ff left join dr_rules as r on ff.dr_ruleid = r.ruleid where ff.dr_groupid = {flt_outbound} ) as t3 on t1.ruleid = t3.ff_ruleid""".format(flt_inbound=settings.FLT_INBOUND, flt_outbound=settings.FLT_OUTBOUND)) - epgroups = db.query(GatewayGroups).filter(GatewayGroups.description.like(endpoint_filter)).all() + epgroups = db.query(GatewayGroups).filter( + GatewayGroups.description['type'] == settings.FLT_PBX + ).all() gwgroups = db.query(GatewayGroups).filter( - (GatewayGroups.description.like(endpoint_filter)) | (GatewayGroups.description.like(carrier_filter))).all() + (GatewayGroups.description['type'] == settings.FLT_PBX) | + (GatewayGroups.description['type'] == settings.FLT_CARRIER) + ).all() # sort endpoint groups by name - epgroups.sort(key=lambda x: strFieldsToDict(x.description)['name'].lower()) + epgroups.sort(key=lambda x: x.description['name'].lower()) # sort gateway groups by type then by name - gwgroups.sort(key=lambda x: (strFieldsToDict(x.description)['type'], strFieldsToDict(x.description)['name'].lower())) + gwgroups.sort(key=lambda x: (x.description['type'], x.description['name'].lower())) dids = [] if len(settings.FLOWROUTE_ACCESS_KEY) > 0 and len(settings.FLOWROUTE_SECRET_KEY) > 0: @@ -1078,7 +1091,7 @@ def addUpdateInboundMapping(): ruleid = form['ruleid'] if 'ruleid' in form else None gwgroupid = form['gwgroupid'] if 'gwgroupid' in form and form['gwgroupid'] != "0" else '' prefix = form['prefix'] if 'prefix' in form else '' - description = 'name:{}'.format(form['rulename']) if 'rulename' in form else '' + description = {'name':form['rulename']} if 'rulename' in form else {} hardfwd_enabled = int(form['hardfwd_enabled']) hf_gwgroupid = form['hf_gwgroupid'] if 'hf_gwgroupid' in form and form['hf_gwgroupid'] != "0" else '' hf_groupid = form['hf_groupid'] if 'hf_groupid' in form else '' @@ -1104,24 +1117,19 @@ def addUpdateInboundMapping(): raise http_exceptions.BadRequest("Duplicate DID's are not allowed") if "lb_" in gwgroupid: - x = gwgroupid.split("_"); + x = gwgroupid.split("_") gwgroupid = x[1] dispatcher_id = x[2].zfill(4) # Create a gateway - Gateway = Gateways("drouting_to_dispatcher", "localhost",0, dispatcher_id, settings.FLT_PBX, gwgroup=gwgroupid) + Gateway = Gateways("drouting_to_dispatcher", "localhost", 0, dispatcher_id, settings.FLT_PBX, gwgroup=gwgroupid) db.add(Gateway) db.flush() - Addr = Address("myself", settings.INTERNAL_IP_ADDR, 32,1, gwgroup=gwgroupid) - db.add(Addr) - db.flush() - # Define an Inbound Mapping that maps to the newly created gateway gwlist = Gateway.gwid IMap = InboundMapping(settings.FLT_INBOUND, prefix, gwlist, description) db.add(IMap) - db.flush() db.commit() return displayInboundMapping() @@ -1193,28 +1201,25 @@ def addUpdateInboundMapping(): inserts = [] if "lb_" in gwgroupid: - x = gwgroupid.split("_"); + x = gwgroupid.split("_") gwgroupid = x[1] dispatcher_id = x[2].zfill(4) # Create a gateway - Gateway = db.query(Gateways).filter(Gateways.description.like(gwgroupid) & Gateways.description.like("lb:{}".format(dispatcher_id))).first() + Gateway = db.query(Gateways).filter( + (Gateways.description['gwgroupid'] == gwgroupid) & + (Gateways.description['lb'] == dispatcher_id) + ).first() if Gateway: - fields = strFieldsToDict(Gateway.description) - fields['lb'] = dispatcher_id - fields['gwgroup'] = gwgroupid - Gateway.update({'prefix':dispatcher_id,'description': dictToStrFields(fields)}) + Gateway.description['prefix'] = dispatcher_id else: - Gateway = Gateways("drouting_to_dispatcher", "localhost",0, dispatcher_id, settings.FLT_PBX, gwgroup=gwgroupid) + Gateway = Gateways("drouting_to_dispatcher", "localhost", 0, dispatcher_id, settings.FLT_PBX, + gwgroup=gwgroupid) + Gateway.description['lb'] = dispatcher_id + Gateway.description['gwgroup'] = gwgroupid db.add(Gateway) db.flush() - Addr = db.query(Address).filter(Address.ip_addr == settings.INTERNAL_IP_ADDR).first() - if Addr is None: - Addr = Address("myself", settings.INTERNAL_IP_ADDR, 32, 1, gwgroup=gwgroupid) - db.add(Addr) - db.flush() - # Assign Gateway id to the gateway list gwlist = Gateway.gwid @@ -1262,7 +1267,7 @@ def addUpdateInboundMapping(): if len(hf_gwgroupid) > 0: gwlist = '#{}'.format(hf_gwgroupid) db.query(InboundMapping).filter(InboundMapping.groupid == hf_groupid).update( - {'prefix': '', 'gwlist': gwlist, 'description': 'name:Hard Forward from {} to DID {}'.format(prefix, hf_fwddid)}, + {'prefix': '', 'gwlist': gwlist, 'description': {'name':'Hard Forward from {} to DID {}'.format(prefix, hf_fwddid)}}, synchronize_session=False) else: hf_rule = db.query(InboundMapping).filter( @@ -1323,7 +1328,7 @@ def addUpdateInboundMapping(): else: fwdgroupid = int(lastfwd.groupid) + 1 - ffwd = InboundMapping(fwdgroupid, '', gwlist, 'name:Failover Forward from {} to DID {}'.format(prefix, ff_fwddid)) + ffwd = InboundMapping(fwdgroupid, '', gwlist, {'name':'Failover Forward from {} to DID {}'.format(prefix, ff_fwddid)}) inserts.append(ffwd) # if no gwgroup selected we dont need dr_rules we set dr_groupid to FLT_OUTBOUND and we create htable else: @@ -1343,7 +1348,7 @@ def addUpdateInboundMapping(): if len(ff_gwgroupid) > 0: gwlist = '#{}'.format(ff_gwgroupid) db.query(InboundMapping).filter(InboundMapping.groupid == ff_groupid).update( - {'prefix': '', 'gwlist': gwlist, 'description': 'name:Failover Forward from {} to DID {}'.format(prefix, ff_fwddid)}, + {'prefix': '', 'gwlist': gwlist, 'description': {'name':'Failover Forward from {} to DID {}'.format(prefix, ff_fwddid)}}, synchronize_session=False) else: ff_rule = db.query(InboundMapping).filter( @@ -1510,9 +1515,9 @@ def processInboundMappingImport(filename, override_gwgroupid, name, db): else: gwgroupid = '#{}'.format(row[1]) if '#' not in row[1] else row[1] if len(row) > 1: - description = 'name:{}'.format(row[2]) + description = {'name': row[2]} else: - description = 'name:{}'.format(name) + description = {'name': name} IMap = InboundMapping(settings.FLT_INBOUND, prefix, gwgroupid, description) db.add(IMap) @@ -1681,23 +1686,23 @@ def displayOutboundRoutes(): db = SessionLoader() - carrier_filter = "%type:{}%".format(settings.FLT_CARRIER) - rows = db.query(OutboundRoutes).filter( (OutboundRoutes.groupid == settings.FLT_OUTBOUND) | ((OutboundRoutes.groupid >= settings.FLT_LCR_MIN) & (OutboundRoutes.groupid < settings.FLT_FWD_MIN))).outerjoin( dSIPLCR, dSIPLCR.dr_groupid == OutboundRoutes.groupid).outerjoin( - GatewayGroups, func.REPLACE(OutboundRoutes.gwlist, '#', '') == GatewayGroups.id).add_columns( + GatewayGroups, sql_func.REPLACE(OutboundRoutes.gwlist, '#', '') == GatewayGroups.id).add_columns( dSIPLCR.from_prefix, dSIPLCR.cost, dSIPLCR.dr_groupid, OutboundRoutes.ruleid, - OutboundRoutes.prefix, OutboundRoutes.routeid, func.REPLACE(OutboundRoutes.gwlist, '#', '').label('gwgroupid'), + OutboundRoutes.prefix, OutboundRoutes.routeid, sql_func.REPLACE(OutboundRoutes.gwlist, '#', '').label('gwgroupid'), OutboundRoutes.timerec, OutboundRoutes.priority, OutboundRoutes.description, GatewayGroups.description.label('gwgroup_description'), GatewayGroups.gwlist) - cgroups = db.query(GatewayGroups).filter(GatewayGroups.description.like(carrier_filter)).all() + cgroups = db.query(GatewayGroups).filter( + GatewayGroups.description['type'] == settings.FLT_CARRIER + ).all() # sort carrier groups by name - cgroups.sort(key=lambda x: strFieldsToDict(x.description)['name'].lower()) + cgroups.sort(key=lambda x: x.description['name'].lower()) teleblock = {} teleblock["gw_enabled"] = settings.TELEBLOCK_GW_ENABLED @@ -1761,7 +1766,7 @@ def addUpateOutboundRoutes(): priority = int(form['priority']) if 'priority' in form and len(form['priority']) > 0 else 0 routeid = form['routeid'] if 'routeid' in form else '' gwgroupid = form['gwgroupid'] if 'gwgroupid' in form and form['gwgroupid'] != "0" else '' - description = 'name:{}'.format(form['name']) if 'name' in form else '' + description = {'name': form['name']} if 'name' in form else {} pattern = None gwlist = '#{}'.format(gwgroupid) if len(gwgroupid) > 0 else '' @@ -1774,8 +1779,12 @@ def addUpateOutboundRoutes(): print("from_prefix: {}".format(from_prefix)) # Grab the lastest groupid and increment - mlcr = db.query(dSIPLCR).filter((dSIPLCR.dr_groupid >= settings.FLT_LCR_MIN) & - (dSIPLCR.dr_groupid < settings.FLT_FWD_MIN)).order_by(dSIPLCR.dr_groupid.desc()).first() + mlcr = db.query(dSIPLCR).filter( + (dSIPLCR.dr_groupid >= settings.FLT_LCR_MIN) & + (dSIPLCR.dr_groupid < settings.FLT_FWD_MIN) + ).order_by( + dSIPLCR.dr_groupid.desc() + ).first() db.commit() # Start LCR routes with a groupid in settings (default is 10000) @@ -1830,8 +1839,12 @@ def addUpateOutboundRoutes(): # Adding a From prefix to an existing To elif prefix is not None and groupid == settings.FLT_OUTBOUND: # Create a new groupid - mlcr = db.query(dSIPLCR).filter((dSIPLCR.dr_groupid >= settings.FLT_LCR_MIN) & - (dSIPLCR.dr_groupid < settings.FLT_FWD_MIN)).order_by(dSIPLCR.dr_groupid.desc()).first() + mlcr = db.query(dSIPLCR).filter( + (dSIPLCR.dr_groupid >= settings.FLT_LCR_MIN) & + (dSIPLCR.dr_groupid < settings.FLT_FWD_MIN) + ).order_by( + dSIPLCR.dr_groupid.desc() + ).first() # Start LCR routes with a groupid set in settings (default is 10000) if mlcr is None: @@ -2042,18 +2055,6 @@ def noneFilter(list): else: return list -def attrFilter(list, field): - if list is None: - return "" - if ":" in list: - d = dict(item.split(":") for item in list.split(",")) - try: - return d[field] - except: - return - else: - return list - def domainTypeFilter(list): if list is None: return "Unknown" @@ -2272,7 +2273,6 @@ def initApp(flask_app): flask_app.config['TIMED_SERIALIZER'] = URLSafeTimedSerializer(flask_app.config["SECRET_KEY"]) # Add jinja2 filters - flask_app.jinja_env.filters["attrFilter"] = attrFilter flask_app.jinja_env.filters["yesOrNoFilter"] = yesOrNoFilter flask_app.jinja_env.filters["noneFilter"] = noneFilter flask_app.jinja_env.filters["imgFilter"] = imgFilter diff --git a/gui/modules/api/api_routes.py b/gui/modules/api/api_routes.py index 66d52b7c..02afb5e3 100644 --- a/gui/modules/api/api_routes.py +++ b/gui/modules/api/api_routes.py @@ -1,22 +1,21 @@ # make sure the generated source files are imported instead of the template ones import sys + sys.path.insert(0, '/etc/dsiprouter/gui') -import os, time, json, random, subprocess, requests, csv, base64, codecs, re, OpenSSL -import urllib.parse as parse +import os, time, json, random, subprocess, requests, csv, base64, codecs, re, dns.resolver from contextlib import closing from collections import OrderedDict from datetime import datetime from flask import Blueprint, jsonify, render_template, request, session, send_file, Response -from sqlalchemy import exc as sql_exceptions, and_,or_ +from sqlalchemy import exc as sql_exceptions, func as sql_func, and_, or_ from werkzeug import exceptions as http_exceptions from werkzeug.utils import secure_filename from database import SessionLoader, DummySession, Address, dSIPNotification, Domain, DomainAttrs, dSIPDomainMapping, \ dSIPMultiDomainMapping, Dispatcher, Gateways, GatewayGroups, Subscribers, dSIPLeases, dSIPMaintModes, \ dSIPCallLimits, InboundMapping, dSIPCDRInfo, dSIPCertificates, Dispatcher, dSIPDNIDEnrichment -from shared import allowed_file, dictToStrFields, isCertValid, rowToDict, showApiError, \ - debugEndpoint, StatusCodes, strFieldsToDict, IO, getRequestData -from util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort +from shared import allowed_file, isCertValid, rowToDict, showApiError, debugEndpoint, StatusCodes, getRequestData +from util.networking import getExternalIP, hostToIP, safeUriToHost, safeStripPort, isValidIP from util.notifications import sendEmail from util.security import AES_CTR, urandomChars, EasyCrypto, api_security from util.file_handling import isValidFile, change_owner @@ -39,7 +38,6 @@ # we need to abstract out common code between gui and api and standardize routes! - @api.route("/api/v1/kamailio/stats", methods=['GET']) @api_security def getKamailioStats(): @@ -65,6 +63,7 @@ def getKamailioStats(): except Exception as ex: return showApiError(ex) + @api.route("/api/v1/kamailio/health", methods=['GET']) def getSystemHealth(): # defaults.. keep data returned separate from returned metadata @@ -79,7 +78,7 @@ def getSystemHealth(): jsonrpc_payload = {"jsonrpc": "2.0", "method": "tm.stats", "id": 1} r = requests.get('http://127.0.0.1:5060/api/kamailio', json=jsonrpc_payload) if r: - #health_data.['version'] = "5.3.4." + # health_data.['version'] = "5.3.4." http_status = r.status_code if http_status >= 400: response_payload['msg'] = 'Server is down' @@ -94,7 +93,6 @@ def getSystemHealth(): return jsonify(response_payload), http_status - @api.route("/api/v1/kamailio/reload", methods=['GET']) @api_security def reloadKamailio(): @@ -129,7 +127,8 @@ def reloadKamailio(): {'method': 'htable.reload', 'jsonrpc': '2.0', 'id': 1, 'params': ["inbound_failfwd"]}, {'method': 'htable.reload', 'jsonrpc': '2.0', 'id': 1, 'params': ["inbound_prefixmap"]}, {'method': 'uac.reg_reload', 'jsonrpc': '2.0', 'id': 1}, - {'method': 'cfg.sets', 'jsonrpc': '2.0', 'id': 1, 'params': ['teleblock', 'gw_enabled', str(settings.TELEBLOCK_GW_ENABLED)]}, + {'method': 'cfg.sets', 'jsonrpc': '2.0', 'id': 1, + 'params': ['teleblock', 'gw_enabled', str(settings.TELEBLOCK_GW_ENABLED)]}, {'method': 'cfg.sets', 'jsonrpc': '2.0', 'id': 1, 'params': ['server', 'role', settings.ROLE]}, {'method': 'cfg.sets', 'jsonrpc': '2.0', 'id': 1, 'params': ['server', 'api_server', dsip_api_url]}, {'method': 'cfg.sets', 'jsonrpc': '2.0', 'id': 1, 'params': ['server', 'api_token', dsip_api_token]} @@ -201,7 +200,7 @@ def getEndpointLease(): # Convert TTL to Seconds r = re.compile('\d*m|M') if r.match(ttl): - ttl = 60 * int(ttl[0:(len(ttl)-1)]) + ttl = 60 * int(ttl[0:(len(ttl) - 1)]) # Generate some values rand_num = random.randint(1, 200) @@ -384,8 +383,7 @@ def handleInboundMapping(): if res is not None: data = rowToDict(res) data = {'ruleid': data['ruleid'], 'did': data['prefix'], - 'name': strFieldsToDict(data['description'])['name'] if 'name' in strFieldsToDict( - data['description']) else '', + 'name': data['description']['name'] if 'name' in data['description'] else '', 'servers': data['gwlist'].split(',')} payload['data'].append(data) payload['msg'] = 'Rule Found' @@ -401,8 +399,7 @@ def handleInboundMapping(): if res is not None: data = rowToDict(res) data = {'ruleid': data['ruleid'], 'did': data['prefix'], - 'name': strFieldsToDict(data['description'])['name'] if 'name' in strFieldsToDict( - data['description']) else '', + 'name': data['description']['name'] if 'name' in data['description'] else '', 'servers': data['gwlist'].split(',')} payload['data'].append(data) payload['msg'] = 'DID Found' @@ -416,8 +413,7 @@ def handleInboundMapping(): for row in res: data = rowToDict(row) data = {'ruleid': data['ruleid'], 'did': data['prefix'], - 'name': strFieldsToDict(data['description'])['name'] if 'name' in strFieldsToDict( - data['description']) else '', + 'name': data['description']['name'] if 'name' in data['description'] else '', 'servers': data['gwlist'].split(',')} payload['data'].append(data) payload['msg'] = 'Rules Found' @@ -459,7 +455,7 @@ def handleInboundMapping(): gwlist = ','.join(data['servers']) prefix = data['did'] - description = 'name:{}'.format(data['name']) if 'name' in data else '' + description = {'name': data['name']} if 'name' in data else {} # don't allow duplicate entries if db.query(InboundMapping).filter(InboundMapping.prefix == prefix).filter( @@ -513,7 +509,7 @@ def handleInboundMapping(): ','.join(settings.DID_PREFIX_ALLOWED_CHARS))) updates['prefix'] = data['did'] if 'name' in data: - updates['description'] = 'name:{}'.format(data['name']) + updates['description'] = {'name': data['name']} # update single rule by ruleid rule_id = request.args.get('ruleid') @@ -645,9 +641,9 @@ def handleNotificationRequest(): # customize message based on type gwid = data.pop('gwid', None) gw_row = db.query(Gateways).filter(Gateways.gwid == gwid).first() if gwid is not None else None - gw_name = strFieldsToDict(gw_row.description)['name'] if gw_row is not None else '' + gw_name = gw_row.description['name'] if gw_row is not None else '' gwgroup_row = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first() - gwgroup_name = strFieldsToDict(gwgroup_row.description)['name'] if gwgroup_row is not None else '' + gwgroup_name = gwgroup_row.description['name'] if gwgroup_row is not None else '' if notif_type == dSIPNotification.FLAGS.TYPE_OVERLIMIT.value: data['html_body'] = ( '' @@ -713,13 +709,13 @@ def deleteEndpointGroup(gwgroupid): subscriber.delete(synchronize_session=False) typeFilter = "%gwgroup:{}%".format(gwgroupid) - endpoints = db.query(Gateways).filter(Gateways.description.like(typeFilter)) + endpoints = db.query(Gateways).filter(Gateways.description['gwgroup'] == gwgroupid) if endpoints is not None: address_ids = [] for endpoint in endpoints: - description_dict = strFieldsToDict(endpoint.description) - if 'addr_id' in description_dict: - address_ids.append(description_dict['addr_id']) + description_dict = endpoint.description + if 'addr_list' in description_dict: + address_ids.extend(description_dict['addr_list']) db.query(Address).filter(Address.id.in_(address_ids)).delete(synchronize_session=False) endpoints.delete(synchronize_session=False) @@ -736,18 +732,19 @@ def deleteEndpointGroup(gwgroupid): domainmapping = db.query(dSIPMultiDomainMapping).filter(dSIPMultiDomainMapping.pbx_id == gwgroupid) if domainmapping.count() > 0: - # Get list of all domains managed by the endpoint group + # Get list of all domains managed by the endpoint group query = "select distinct did from domain_attrs where name = 'created_by' and value='{}'".format(gwgroupid); domain_attrs = db.execute(query) - did_list=[] - did_list_string="" + did_list = [] + did_list_string = "" for did in domain_attrs: did_list_string = did_list_string + "," + "'" + did[0] + "'" if did_list_string[0][0] == ",": did_list_string = did_list_string[1:] # Delete all domains - query = "delete from domain where did in (select did from domain_attrs where name = 'created_by' and value='{}')".format(gwgroupid); + query = "delete from domain where did in (select did from domain_attrs where name = 'created_by' and value='{}')".format( + gwgroupid); db.execute(query) # Delete all domains_attrs @@ -757,7 +754,8 @@ def deleteEndpointGroup(gwgroupid): # Delete domain mapping, which will stop the fusionpbx sync domainmapping.delete(synchronize_session=False) - dispatcher = db.query(Dispatcher).filter(or_(Dispatcher.setid == gwgroupid, Dispatcher.setid == int(gwgroupid) + 1000)) + dispatcher = db.query(Dispatcher).filter( + or_(Dispatcher.setid == gwgroupid, Dispatcher.setid == int(gwgroupid) + 1000)) if dispatcher is not None: dispatcher.delete(synchronize_session=False) @@ -792,7 +790,7 @@ def getEndpointGroup(gwgroupid): endpointgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first() if endpointgroup is not None: - gwgroup_data['name'] = strFieldsToDict(endpointgroup.description)['name'] + gwgroup_data['name'] = endpointgroup.description['name'] else: raise http_exceptions.NotFound("Endpoint Group Does Not Exist") @@ -833,7 +831,7 @@ def getEndpointGroup(gwgroupid): ep = {} ep['gwid'] = endpoint.gwid ep['hostname'] = endpoint.address - ep['description'] = strFieldsToDict(endpoint.description)['name'] + ep['description'] = endpoint.description['name'] if dispatcher: ep['weight'] = weightList[endpoint.address] if endpoint.address in weightList else '' else: @@ -901,16 +899,13 @@ def listEndpointGroups(): db = SessionLoader() - endpointgroups = db.query(GatewayGroups).filter(GatewayGroups.description.like(typeFilter)).all() + endpointgroups = db.query(GatewayGroups).filter(GatewayGroups.description['type'] == settings.FLT_PBX).all() for endpointgroup in endpointgroups: - # Grap the description field, which is comma seperated key/value pair - fields = strFieldsToDict(endpointgroup.description) - # append summary of endpoint group data response_payload['data'].append({ 'gwgroupid': endpointgroup.id, - 'name': fields['name'], + 'name': endpointgroup.description['name'], 'gwlist': endpointgroup.gwlist }) @@ -1055,9 +1050,7 @@ def updateEndpointGroups(gwgroupid=None): if Gwgroup is None: raise http_exceptions.NotFound('gwgroup does not exist') gwgroup_data['gwgroupid'] = gwgroupid - gwgroup_desc_dict = strFieldsToDict(Gwgroup.description) - gwgroup_desc_dict['name'] = request_payload['name'] if 'name' in request_payload else '' - Gwgroup.description = dictToStrFields(gwgroup_desc_dict) + Gwgroup.description['name'] = request_payload['name'] if 'name' in request_payload else '' db.flush() # Update concurrent call limit @@ -1136,10 +1129,11 @@ def updateEndpointGroups(gwgroupid=None): # Update endpoints # Get List of existing endpoints # If endpoint sent over is not in the existing endpoint list then remove it - gwgroup_filter = "%gwgroup:{}%".format(gwgroupid_str) current_endpoints_lut = { - x.gwid: {'address': x.address, 'description_dict': strFieldsToDict(x.description)} \ - for x in db.query(Gateways).filter(Gateways.description.like(gwgroup_filter)).all() + x.gwid: {'address': x.address, 'description_dict': x.description} \ + for x in db.query(Gateways).filter( + Gateways.description['gwgroup'] == gwgroupid + ).all() } updated_endpoints = request_payload['endpoints'] if "endpoints" in request_payload else [] unprocessed_endpoints_lut = {} @@ -1165,30 +1159,38 @@ def updateEndpointGroups(gwgroupid=None): if authtype == "ip": # for ip auth we must create address records for the endpoint host_addr = safeStripPort(sip_addr) - host_ip = hostToIP(host_addr) - - Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid) - db.add(Addr) - db.flush() + if not isValidIP(host_addr): + host_addr_list = hostToIP(host_addr, only_first=False) + if host_addr_list is None: + raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") + else: + host_addr_list = [host_addr] + host_addr_id_list = [] + + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_PBX, gwgroup=gwgroupid) + db.add(Addr) + db.flush() + host_addr_id_list.append(Addr.id) Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, - addr_id=Addr.id) + addr_list=host_addr_id_list) else: # we know this a new endpoint so we don't have to check for any address records here Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid) - # Create dispatcher group with the set id being the gateway group id - dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight),description=name) + dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight), + description={'name': name}) db.add(dispatcher) # Create dispatcher for FusionPBX external interface if FusionPBX feature is enabled if int(fusionpbxenabled) > 0: - sip_addr_external = safeUriToHost(hostname, default_port=5080) + sip_addr_external = safeUriToHost(hostname, default_port=5080) # Add 1000 to the gwgroupid so that the setid for the FusionPBX external interface is 1000 apart - dispatcher = Dispatcher(setid=gwgroupid+1000, destination=sip_addr_external, attrs="weight={}".format(weight),description=name) + dispatcher = Dispatcher(setid=gwgroupid + 1000, destination=sip_addr_external, + attrs="weight={}".format(weight), description={'name': name}) db.add(dispatcher) - db.add(Gateway) db.flush() gwlist.append(Gateway.gwid) @@ -1226,25 +1228,37 @@ def updateEndpointGroups(gwgroupid=None): if authtype == "ip": # for ip auth we must create address records for the endpoint host_addr = safeStripPort(sip_addr) - host_ip = hostToIP(host_addr) + if not isValidIP(host_addr): + host_addr_list = hostToIP(host_addr, only_first=False) + if host_addr_list is None: + raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") + else: + host_addr_list = [host_addr] + host_addr_id_list = [] + + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_PBX, gwgroup=gwgroupid) + db.add(Addr) + db.flush() + host_addr_id_list.append(Addr.id) + Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, + addr_list=host_addr_id_list) - Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid) - db.add(Addr) - db.flush() - Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, addr_id=Addr.id) else: # we know this a new endpoint so we don't have to check for any address records here Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid) # Create dispatcher group with the set id being the gateway group id - dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight),description=name) + dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight), + description={'name': name}) db.add(dispatcher) # Create dispatcher for FusionPBX external interface if FusionPBX feature is enabled if int(fusionpbxenabled) > 0: - sip_addr_external = safeUriToHost(safeStripPort(hostname), default_port=5080) + sip_addr_external = safeUriToHost(safeStripPort(hostname), default_port=5080) # Add 1000 to the gwgroupid so that the setid for the FusionPBX external interface is 1000 apart - dispatcher = Dispatcher(setid=gwgroupid+1000, destination=sip_addr_external, attrs="weight={}".format(weight),description=name) + dispatcher = Dispatcher(setid=gwgroupid + 1000, destination=sip_addr_external, + attrs="weight={}".format(weight), description={'name': name}) db.add(dispatcher) @@ -1274,102 +1288,106 @@ def updateEndpointGroups(gwgroupid=None): if authtype == "ip": # for ip auth we must create address records for the endpoint host_addr = safeStripPort(sip_addr) - host_ip = hostToIP(host_addr) - - # if address exists update, otherwise create it - address_exists = False - if 'addr_id' in endpoint_fields and len(endpoint_fields['addr_id']) > 0: - Addr = db.query(Address).filter(Address.id == endpoint_fields['addr_id']).first() - - if Addr is not None: - address_exists = True - - Addr.ip_addr = host_ip - addr_fields = strFieldsToDict(Addr.tag) - addr_fields['name'] = name - addr_fields['gwgroup'] = gwgroupid_str - Addr.tag = dictToStrFields(addr_fields) + if not isValidIP(host_addr): + host_addr_list = hostToIP(host_addr, only_first=False) + if host_addr_list is None: + raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") + else: + host_addr_list = [host_addr] + host_addr_id_list = [] - if not address_exists: - Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid) + # delete old addresses + if 'addr_list' in endpoint_fields and len(endpoint_fields['addr_list']) > 0: + db.query(Address).filter(Address.id.in_(endpoint_fields['addr_list'])).delete( + synchronize_session=False) + # create new addresses + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_PBX, gwgroup=gwgroupid) db.add(Addr) db.flush() - endpoint_fields['addr_id'] = str(Addr.id) + host_addr_id_list.append(Addr.id) + + # update address list if endpoint has one + if 'addr_list' in endpoint_fields: + endpoint_fields['addr_list'] = host_addr_id_list + else: # if not using ip auth make sure we delete any old address records for the endpoint - if 'addr_id' in endpoint_fields and len(endpoint_fields['addr_id']) > 0: - Addr = db.query(Address).filter(Address.id == endpoint_fields['addr_id']) - if Addr is not None: - Addr.delete(synchronize_session=False) + if 'addr_list' in endpoint_fields and len(endpoint_fields['addr_list']) > 0: + db.query(Address).filter(Address.id.in_(endpoint_fields['addr_list'])).delete( + synchronize_session=False) # remove addr_id field from endpoint description - endpoint_fields.pop("addr_id", None) + endpoint_fields.pop("addr_list", None) # update the endpoint db.query(Gateways).filter(Gateways.gwid == gwid).update( - {"description": dictToStrFields(endpoint_fields), "address": sip_addr, "strip": strip, + {"description": endpoint_fields, "address": sip_addr, "strip": strip, "pri_prefix": prefix}, synchronize_session=False) # update the weight DispatcherEntry = db.query(Dispatcher).filter( (Dispatcher.setid == gwgroupid) & (Dispatcher.destination == "sip:{}".format(sip_addr))).first() - #if weight is None or len(weight) == 0: + # if weight is None or len(weight) == 0: # if DispatcherEntry is not None: # db.delete(DispatcherEntry) - #elif weight: + # elif weight: if DispatcherEntry is not None: db.query(Dispatcher).filter( (Dispatcher.setid == gwgroupid) & (Dispatcher.destination == "sip:{}".format(sip_addr))).update( {"attrs": "weight={}".format(weight)}, synchronize_session=False) else: dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight), - description=name) + description={'name': name}) db.add(dispatcher) if int(fusionpbxenabled) > 0: # update the weight for the external load balancer dispatcher set - sip_addr_external = safeUriToHost(safeStripPort(hostname), default_port=5080) - DispatcherEntry = db.query(Dispatcher).filter((Dispatcher.setid == int(gwgroupid) + 1000) & (Dispatcher.destination == "sip:{}".format(sip_addr_external))).first() + sip_addr_external = safeUriToHost(safeStripPort(hostname), default_port=5080) + DispatcherEntry = db.query(Dispatcher).filter((Dispatcher.setid == int(gwgroupid) + 1000) & ( + Dispatcher.destination == "sip:{}".format(sip_addr_external))).first() if weight is None or len(weight) == 0: if DispatcherEntry is not None: db.delete(DispatcherEntry) elif weight: if DispatcherEntry is not None: - db.query(Dispatcher).filter((Dispatcher.setid == int(gwgroupid) + 1000) & (Dispatcher.destination == "sip:{}".format(sip_addr_external))).update({"attrs":"weight={}".format(weight)},synchronize_session=False) + db.query(Dispatcher).filter((Dispatcher.setid == int(gwgroupid) + 1000) & ( + Dispatcher.destination == "sip:{}".format(sip_addr_external))).update( + {"attrs": "weight={}".format(weight)}, synchronize_session=False) else: - #sip_addr_external = safeUriToHost(hostname, default_port=5080) - #dispatcher = Dispatcher(setid=gwgroupid + 1000, destination=sip_addr_external, attrs="weight={}".format(weight),description=name) - #db.add(dispatcher) + # sip_addr_external = safeUriToHost(hostname, default_port=5080) + # dispatcher = Dispatcher(setid=gwgroupid + 1000, destination=sip_addr_external, attrs="weight={}".format(weight),description={'name': name}) + # db.add(dispatcher) pass gwlist.append(gwid) # conditional endpoints to delete # we also cleanup house here in case of stray entries - del_gateways = db.query(Gateways).filter(and_( \ - Gateways.gwid.in_(del_gwids), \ - Gateways.address != "localhost" \ - )) - del_gateways_cleanup = db.query(Gateways).filter(and_( \ - Gateways.description.like(gwgroup_filter), \ - Gateways.gwid.notin_(gwlist), \ - Gateways.address != "localhost" \ - )) + del_gateways = db.query(Gateways).filter(and_( + Gateways.gwid.in_(del_gwids), + Gateways.address != "localhost" + )) + del_gateways_cleanup = db.query(Gateways).filter(and_( + Gateways.description['gwgroup'] == gwgroupid, + Gateways.gwid.notin_(gwlist), + Gateways.address != "localhost" + )) # make sure we delete any associated address entries del_addr_ids = [] for gateway in del_gateways.union(del_gateways_cleanup): - description_dict = strFieldsToDict(gateway.description) - if 'addr_id' in description_dict: - del_addr_ids.append(description_dict['addr_id']) + description_dict = gateway.description + if 'addr_list' in description_dict: + del_addr_ids.extend(description_dict['addr_list']) db.query(Address).filter(Address.id.in_(del_addr_ids)).delete(synchronize_session=False) - # delete the dispatcher entries that correspond to the endpoints/gateways that was Deleted - del_addrs = [] + # delete the dispatcher entries that correspond to the endpoints/gateways that was Deleted + del_addr_ids = [] for gateway in del_gateways.union(del_gateways_cleanup): - del_addrs.append("sip:{}".format(gateway.address)) - db.query(Dispatcher).filter(and_(Dispatcher.setid == gwgroupid, Dispatcher.destination.in_(del_addrs))).delete(synchronize_session=False) - - + del_addr_ids.append("sip:{}".format(gateway.address)) + db.query(Dispatcher).filter( + Dispatcher.setid == gwgroupid & Dispatcher.destination.in_(del_addr_ids) + ).delete(synchronize_session=False) del_gateways.delete(synchronize_session=False) del_gateways_cleanup.delete(synchronize_session=False) @@ -1446,7 +1464,7 @@ def updateEndpointGroups(gwgroupid=None): if not deleteTaggedCronjob(gwgroupid): raise Exception('Crontab entry could not be deleted') - # Update FusionPBX + # Update FusionPBX # Convert fusionpbxenabled variable to int if isinstance(fusionpbxenabled, str): @@ -1555,7 +1573,7 @@ def addEndpointGroups(data=None, endpointGroupType=None, domain=None): """ db = DummySession() - fusionpbxenabled=0 + fusionpbxenabled = 0 # use a whitelist to avoid possible buffer overflow vulns or crashes VALID_REQUEST_DATA_ARGS = { @@ -1709,31 +1727,40 @@ def addEndpointGroups(data=None, endpointGroupType=None, domain=None): raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") if authtype == "ip": + # for ip auth we must create address records for the endpoint host_addr = safeStripPort(sip_addr) - host_ip = hostToIP(host_addr) + if not isValidIP(host_addr): + host_addr_list = hostToIP(host_addr, only_first=False) + if host_addr_list is None: + raise http_exceptions.BadRequest("Endpoint hostname/address is malformed") + else: + host_addr_list = [host_addr] + host_addr_id_list = [] - Addr = Address(name, host_ip, 32, settings.FLT_PBX, gwgroup=gwgroupid) - db.add(Addr) - db.flush() - Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, addr_id=Addr.id, - attrs=attrs) + for host_addr in host_addr_list: + Addr = Address(name, host_addr, 32, settings.FLT_PBX, gwgroup=gwgroupid) + db.add(Addr) + db.flush() + host_addr_id_list.append(Addr.id) + Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, + addr_list=host_addr_id_list, attrs=attrs) else: Gateway = Gateways(name, sip_addr, strip, prefix, settings.FLT_PBX, gwgroup=gwgroupid, attrs=attrs) - # Create dispatcher group with the set id being the gateway group id - dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight),description=name) + dispatcher = Dispatcher(setid=gwgroupid, destination=sip_addr, attrs="weight={}".format(weight), + description={'name': name}) db.add(dispatcher) # Create dispatcher for FusionPBX external interface if FusionPBX feature is enabled if fusionpbxenabled: - sip_addr_external = safeUriToHost(hostname, default_port=5080) + sip_addr_external = safeUriToHost(hostname, default_port=5080) # Add 1000 to the gwgroupid so that the setid for the FusionPBX external interface is 1000 apart - dispatcher = Dispatcher(setid=gwgroupid+1000, destination=sip_addr_external, attrs="weight={}".format(weight),description=name) + dispatcher = Dispatcher(setid=gwgroupid + 1000, destination=sip_addr_external, + attrs="weight={}".format(weight), description={'name': name}) db.add(dispatcher) - db.add(Gateway) db.flush() gwlist.append(Gateway.gwid) @@ -1745,14 +1772,14 @@ def addEndpointGroups(data=None, endpointGroupType=None, domain=None): # Update Gateway group with the Dispatcher ID. It's denoted with the LB field gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first() if gwgroup is not None: - fields = strFieldsToDict(gwgroup.description) + fields = gwgroup.description fields['lb'] = gwgroupid if fusionpbxenabled: fields['lb_ext'] = gwgroupid + 1000 # Update the GatewayGroup with the lists of gateways db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).update( - {'gwlist': gwlist_str, 'description': dictToStrFields(fields)}, synchronize_session=False) + {'gwlist': gwlist_str, 'description': fields}, synchronize_session=False) # Setup notifications if 'notifications' in request_payload: @@ -1855,7 +1882,7 @@ def getNumberEnrichment(rule_id=None): 'dnid': rule.dnid, 'country_code': rule.country_code, 'routing_number': rule.routing_number, - 'rule_name': strFieldsToDict(rule.description)['name'] + 'rule_name': rule.description['name'] }) else: raise http_exceptions.NotFound("Enrichment Rule Does Not Exist") @@ -1869,7 +1896,7 @@ def getNumberEnrichment(rule_id=None): 'dnid': rule.dnid, 'country_code': rule.country_code, 'routing_number': rule.routing_number, - 'rule_name': strFieldsToDict(rule.description)['name'] + 'rule_name': rule.description['name'] }) response_payload['msg'] = 'Enrichment Rule(s) found' @@ -2105,7 +2132,7 @@ def updateNumberEnrichment(rule_id=None, request_payload=None): else: rule_id = rule_data.pop('rule_id') description = {'name': rule_data.pop('rule_name')} - rule_data['description'] = dictToStrFields(description) + rule_data['description'] = description if not db.query(dSIPDNIDEnrichment).filter(dSIPDNIDEnrichment.id == rule_id).update( rule_data, synchronize_session=False): @@ -2420,7 +2447,7 @@ def fetchNumberEnrichment(request_payload=None): 'dnid': rule.dnid, 'country_code': rule.country_code, 'routing_number': rule.routing_number, - 'rule_name': strFieldsToDict(rule.description)['name'] + 'rule_name': rule.description['name'] }) globals.reload_required = True @@ -2467,38 +2494,38 @@ def generateCDRS(gwgroupid, type=None, email=False, dtfilter=datetime.min, cdrfi gwgroup = db.query(GatewayGroups).filter(GatewayGroups.id == gwgroupid).first() if gwgroup is not None: - gwgroupName = strFieldsToDict(gwgroup.description)['name'] + gwgroupName = gwgroup.description['name'] else: response_payload['status'] = "0" response_payload['message'] = "Endpont group doesn't exist" return jsonify(response_payload) if len(cdrfilter) > 0: - query = ( - """SELECT t1.cdr_id, t1.call_start_time, t1.duration AS call_duration, t1.calltype AS call_direction, - t2.id AS src_gwgroupid, substring_index(substring_index(t2.description, 'name:', -1), ',', 1) AS src_gwgroupname, - t3.id AS dst_gwgroupid, substring_index(substring_index(t3.description, 'name:', -1), ',', 1) AS dst_gwgroupname, + query = (""" + SELECT t1.cdr_id, t1.call_start_time, t1.duration AS call_duration, t1.calltype AS call_direction, + t2.id AS src_gwgroupid, JSON_EXTRACT(t2.description, '$.name') AS src_gwgroupname, + t3.id AS dst_gwgroupid, JSON_EXTRACT(t3.description, '$.name') AS dst_gwgroupname, t1.src_username, t1.dst_username, t1.src_ip AS src_address, t1.dst_domain AS dst_address, t1.sip_call_id AS call_id FROM cdrs t1 JOIN dr_gw_lists t2 ON (t1.src_gwgroupid = t2.id) JOIN dr_gw_lists t3 ON (t1.dst_gwgroupid = t3.id) WHERE (t2.id = '{gwgroupid}' OR t3.id = '{gwgroupid}') AND t1.call_start_time >= '{dtfilter}' AND t1.cdr_id IN ({cdrfilter}) - ORDER BY t1.call_start_time DESC;""" - ).format(gwgroupid=gwgroupid, dtfilter=dtfilter, cdrfilter=cdrfilter) + ORDER BY t1.call_start_time DESC; + """).format(gwgroupid=gwgroupid, dtfilter=dtfilter, cdrfilter=cdrfilter) else: - query = ( - """SELECT t1.cdr_id, t1.call_start_time, t1.duration AS call_duration, t1.calltype AS call_direction, - t2.id AS src_gwgroupid, substring_index(substring_index(t2.description, 'name:', -1), ',', 1) AS src_gwgroupname, - t3.id AS dst_gwgroupid, substring_index(substring_index(t3.description, 'name:', -1), ',', 1) AS dst_gwgroupname, + query = (""" + SELECT t1.cdr_id, t1.call_start_time, t1.duration AS call_duration, t1.calltype AS call_direction, + t2.id AS src_gwgroupid, JSON_EXTRACT(t2.description, '$.name') AS src_gwgroupname, + t3.id AS dst_gwgroupid, JSON_EXTRACT(t3.description, '$.name') AS dst_gwgroupname, t1.src_username, t1.dst_username, t1.src_ip AS src_address, t1.dst_domain AS dst_address, t1.sip_call_id AS call_id FROM cdrs t1 JOIN dr_gw_lists t2 ON (t1.src_gwgroupid = t2.id) JOIN dr_gw_lists t3 ON (t1.dst_gwgroupid = t3.id) WHERE (t2.id = '{gwgroupid}' OR t3.id = '{gwgroupid}') AND t1.call_start_time >= '{dtfilter}' - ORDER BY t1.call_start_time DESC;""" - ).format(gwgroupid=gwgroupid, dtfilter=dtfilter) + ORDER BY t1.call_start_time DESC; + """).format(gwgroupid=gwgroupid, dtfilter=dtfilter) - rows = db.execute(query) + rows = db.execute(query) cdrs = [] for row in rows: data = {} @@ -2798,18 +2825,17 @@ def getOptionMessageStatus(domain): if not response: return False - try: # Loop thru each record in the dispatcher list - for record in range(0,len(records)): + for record in range(0, len(records)): sets = records[record] # Loop thru each set for set in sets: - #print("{},{}".format(sets[set]['ID'],domain)) + # print("{},{}".format(sets[set]['ID'],domain)) # Loop thru each target targets = sets[set]['TARGETS'] # Loop thru each destination within a target - for dest in range(0,len(targets)): + for dest in range(0, len(targets)): # Grab the destination body and flags # The Body contains the domain name of the destionation dest_body = format(targets[dest]['DEST']['ATTRS']['BODY']) @@ -2845,8 +2871,8 @@ def testConnectivity(domain): test_data['hostname_check'] = True # Try again, but use Google DNS resolver if the check fails with local DNS else: - # Does the IP address of this server resolve to the domain - import dns.resolver + # TODO: dnsmasq should handle forwarding to external resolvers + # this code shouldn't be needed # Get the IP address of the domain from Google DNS resolver = dns.resolver.Resolver() diff --git a/gui/modules/dnid_enrichment/dnid_enrichment.sql b/gui/modules/dnid_enrichment/dnid_enrichment.sql index 731ae17b..bf0b939f 100644 --- a/gui/modules/dnid_enrichment/dnid_enrichment.sql +++ b/gui/modules/dnid_enrichment/dnid_enrichment.sql @@ -6,7 +6,7 @@ CREATE TABLE dsip_dnid_enrich_lnp ( dnid varchar(64) NOT NULL, country_code varchar(64) NOT NULL DEFAULT '', routing_number varchar(64) NOT NULL DEFAULT '', - description varchar(128) NOT NULL DEFAULT '', + description varchar(255) NOT NULL DEFAULT '{}', PRIMARY KEY (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; diff --git a/gui/modules/dnid_enrichment/install.sh b/gui/modules/dnid_enrichment/install.sh index 9b082e50..238d3372 100755 --- a/gui/modules/dnid_enrichment/install.sh +++ b/gui/modules/dnid_enrichment/install.sh @@ -34,7 +34,7 @@ function installSQL { function install { installSQL - enableKamailioConfigAttrib 'WITH_DNID_LNP_ENRICHMENT' ${DSIP_KAMAILIO_CONFIG_FILE} + enableKamailioConfigFeature 'WITH_DNID_LNP_ENRICHMENT' ${DSIP_KAMAILIO_CONFIG_FILE} systemctl restart kamailio if systemctl is-active --quiet kamailio; then @@ -47,7 +47,7 @@ function install { } function uninstall { - disableKamailioConfigAttrib 'WITH_DNID_LNP_ENRICHMENT' ${DSIP_KAMAILIO_CONFIG_FILE} + disableKamailioConfigFeature 'WITH_DNID_LNP_ENRICHMENT' ${DSIP_KAMAILIO_CONFIG_FILE} systemctl restart kamailio if systemctl is-active --quiet kamailio; then diff --git a/gui/modules/domain/domain_routes.py b/gui/modules/domain/domain_routes.py index 19e4ccc4..0d36eccf 100644 --- a/gui/modules/domain/domain_routes.py +++ b/gui/modules/domain/domain_routes.py @@ -1,5 +1,4 @@ -from flask import Blueprint, session, render_template -from flask import Flask, render_template, request, redirect, abort, flash, session, url_for, send_from_directory +from flask import Blueprint, session from sqlalchemy import case, func, exc as sql_exceptions from werkzeug import exceptions as http_exceptions from database import SessionLoader, DummySession, Domain, DomainAttrs, dSIPDomainMapping, dSIPMultiDomainMapping, Dispatcher, Gateways, Address @@ -7,7 +6,6 @@ from shared import * import settings import globals -import re domains = Blueprint('domains', __name__) @@ -83,7 +81,9 @@ def addDomain(domain, authtype, pbxs, notes, db): else: socket_addr = settings.EXTERNAL_IP_ADDR - dispatcher = Dispatcher(setid=PBXDomain.id, destination=endpoint, attrs="socket=tls:{}:5061;ping_from=sip:{}".format(socket_addr,domain),description='msteam_endpoint:{}'.format(endpoint)) + dispatcher = Dispatcher(setid=PBXDomain.id, destination=endpoint, + attrs="socket=tls:{}:5061;ping_from=sip:{}".format(socket_addr,domain), + description={'msteam_endpoint': endpoint}) db.add(dispatcher) db.add(PBXDomainAttr1) @@ -108,7 +108,7 @@ def addDomain(domain, authtype, pbxs, notes, db): endpointGroup = {"name":domain,"endpoints":None} endpoints = [] for hostname in msteams_dns_endpoints: - endpoints.append({"hostname":hostname,"description":"msteams_endpoint","maintmode":False}); + endpoints.append({"hostname": hostname,"description":"msteams_endpoint","maintmode":False}); endpointGroup['endpoints'] = endpoints addEndpointGroups(endpointGroup,"msteams",domain) @@ -127,7 +127,8 @@ def addDomain(domain, authtype, pbxs, notes, db): # Create entry in dispatcher and set dispatcher_set_id in domain_attrs PBXDomainAttr8 = DomainAttrs(did=domain, name='dispatcher_set_id', value=PBXDomain.id) for pbx_id in pbx_list: - dispatcher = Dispatcher(setid=PBXDomain.id, destination=gatewayIdToIP(pbx_id, db), description='pbx_id:{}'.format(pbx_id)) + dispatcher = Dispatcher(setid=PBXDomain.id, destination=gatewayIdToIP(pbx_id, db), + description={'pbx_id': pbx_id}) db.add(dispatcher) db.add(PBXDomainAttr1) diff --git a/gui/settings.py b/gui/settings.py index 682ecc67..53467eca 100644 --- a/gui/settings.py +++ b/gui/settings.py @@ -83,13 +83,14 @@ SQLALCHEMY_SQL_DEBUG = False # These constants shouldn't be modified -# FLT_CARRIER/FLT_PBX: type in dr_gateways table -# FLT_MSTEAMS: type in dr_gateways table +# FLT_CARRIER/FLT_PBX: type in dr_gateways/address table +# FLT_MSTEAMS/FLT_INTERNAL: type in dr_gateways/address table # FLT_OUTBOUND/FLT_INBOUND: groupid in dr_rules table # FLT_LCR_MIN/FLT_FWD_MIN: range of groupid in dr_rules table FLT_CARRIER = 8 FLT_PBX = 9 FLT_MSTEAMS = 22 +FLT_INTERNAL = 20 FLT_OUTBOUND = 8000 FLT_INBOUND = 9000 FLT_LCR_MIN = 10000 diff --git a/gui/shared.py b/gui/shared.py index c93e285f..dd2632c5 100644 --- a/gui/shared.py +++ b/gui/shared.py @@ -77,15 +77,10 @@ def rowToDict(row): return d def strFieldsToDict(fields_str): - fields = {} - for field in fields_str.split(','): - if ':' in field: - tmp = field.split(':', 1) - fields[tmp[0]] = tmp[1] - return fields + return json.loads(fields_str) def dictToStrFields(fields_dict): - return ','.join("{}:{}".format(k, v) for k, v in fields_dict.items()) + return json.dumps(fields_dict) def updateConfig(config_obj, field_dict, hot_reload=False): """ diff --git a/gui/templates/carriergroups.html b/gui/templates/carriergroups.html index 5f596c26..fce64edf 100644 --- a/gui/templates/carriergroups.html +++ b/gui/templates/carriergroups.html @@ -44,7 +44,7 @@

List of Carrier Groups

{{ row.id|noneFilter() }} - {{ row.description|attrFilter('name')|noneFilter() }} + {{ row.description['name']|noneFilter() }} {{ row.gwlist|noneFilter() }} {{ row.r_username|noneFilter() }} {{ row.r_username|noneFilter() }} diff --git a/gui/templates/carriers.html b/gui/templates/carriers.html index 67425247..2872ffd5 100644 --- a/gui/templates/carriers.html +++ b/gui/templates/carriers.html @@ -27,7 +27,7 @@ {% endif %} {{ row.gwid }} - {{ row.description|attrFilter('name') }} + {{ row.description['name'] }} {{ row.address }} {{ row.strip }} {{ row.pri_prefix }} diff --git a/gui/templates/inboundmapping.html b/gui/templates/inboundmapping.html index 089d7ade..675bb80a 100644 --- a/gui/templates/inboundmapping.html +++ b/gui/templates/inboundmapping.html @@ -58,12 +58,12 @@

List of Inbound Routes

Load Balancing Group {% else %} {% if row.gwlist.split(',')|length > 1 %} - {{ row.gwgroup_description|attrFilter('name') }} +1 + {{ row.gwgroup_description['name'] }} +1 {% else %} - {{ row.gwgroup_description|attrFilter('name') }} + {{ row.gwgroup_description['name'] }} {% endif %} {% endif %} - {{ row.rule_description|attrFilter('name') }} + {{ row.rule_description['name'] }} {{ row.gwlist }}

@@ -140,12 +140,12 @@

@@ -191,7 +191,7 @@ @@ -237,7 +237,7 @@ @@ -295,12 +295,12 @@ @@ -346,7 +346,7 @@ @@ -392,7 +392,7 @@ @@ -472,7 +472,7 @@ @@ -516,7 +516,7 @@ @@ -562,7 +562,7 @@ diff --git a/gui/templates/outboundroutes.html b/gui/templates/outboundroutes.html index c4a489d9..e38d2286 100644 --- a/gui/templates/outboundroutes.html +++ b/gui/templates/outboundroutes.html @@ -47,8 +47,8 @@

List of Outbound Routes

{{ row.priority }} {{ row.routeid }} {{ row.gwgroupid }} - {{ row.gwgroup_description|attrFilter('name') }} - {{ row.description|attrFilter('name') }} + {{ row.gwgroup_description['name'] }} + {{ row.description['name'] }}