;;;;;;;;;;;;;;;;;;;;;;;; NPSTN Verification Subroutines ;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;; NOTICE: Do not make modifications to this file! ;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;; QUESTIONS: Direct to NPSTN Business Office ;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;; USAGE: Including this in your dialplan alone will NOT help you! ;;;;;;
;;;;;;;;;; You must LEVERAGE the tools below if they are to be useful! ;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Required Variables: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;[globals] ; add to your [globals] dialplan context, if they're not already there
;npstnkey=<your-auth-key> ; NPSTN only: your 32-character NPSTN auth key
;maindisa=5551212 ; NPSTN only: this should be an NPSTN # on your node
;maincnetdisa=15551212 ; C*NET only: this should be a C*NET # on your node

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Optional Variables: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;allowdisathru=YES ; unless NO, implied default YES (allow DISA callers to leave node)
;allowpstnthru=YES ; unless NO, implied default YES (allow PSTN callers to leave node)
;requestverifytokenonipfail=1 ; NPSTN only: unless 1, implied default 0 (request temporary token for downstream node to use in lieu of reverse-IP verification)
;usedefaultblacklistresult=1 ; automatically quarantine nuisance calls

;;;;;;;;;;;;;;;;;;;;;;;;;;; *** PART A: INBOUND *** ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; [from-npstn] (and from-cnet, etc.) should make use of INCOMING verification ;
;;; The FIRST THING that should happen on ANY incoming call is a call to the ;;;;
;;; proper incoming verification subroutine, e.g. npstn-verify, etc. ;;;;;;;;;;;;
;[from-npstn] ; Incoming calls from NPSTN ; *** SAMPLE [from-npstn] context *** ;
;exten => _X!,1,Gosub(npstn-verify,s,1)
;	same => n,Gosub(npstn-blacklist-check,s,1)
;	same => n,GotoIf($[${GOSUB_RETVAL}>=2.5]?reject)
;	same => n,GotoIf($["${clidverif}"="11"]?:accept)
;	same => n,GotoIf($[${GROUP_COUNT(npstnspam@cvs)}<5]?:reject)
;	same => n(accept),Goto(external-users,${EXTEN},1)
;	same => n(reject),Hangup(21)

;;;;;;;;;;;;;;;;;;;;;;;;;;; *** PART B: OUTBOUND *** ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;; [dtnpstn] should look like this, to make use of OUTGOING verification SRs ;;;
;[dtnpstn] ; NPSTN 20190215 NA ; *** SAMPLE [dtnpstn] context *** ;;;;;;;;;;;;;;;
;exten => _X!,1,Set(originalclid=${CALLERID(all)})
;	same => n,Gosub(npstn-out-verify,s,1)
;	same => n,GotoIf($["${GOSUB_RETVAL}"="0"]?npstn-out-blocked,s,1)
;	same => n,Gosub(npstnanifail,check,1) ; ANI FAIL CHECK
;	same => n,ExecIf($["${GOSUB_RETVAL}"="1"]?Hangup(29)) ; ANI FAIL CHECK - prevent switch from ticketing by hanging up with cause code 29, call will get sent to TSP
;	same => n,Gosub(dialnpstn,start,1(${EXTEN},disable,${zipcode},${maindisa}))
;	same => n,Set(CALLERID(all)=${originalclid})
;	same => n,Hangup()

;[dtcnet] ; NPSTN 20210117 NA ; *** SAMPLE [dtcnet] context *** ;;;;;;;;;;;;;;;
;exten => _X!,1,Gosub(cnet-out-verify,${EXTEN},1)
;	same => n,Gosub(dialcnet,start,1(${EXTEN}))
;	same => n,Hangup()

;;;;;;;;;;;;;;;;;;;;;;;;;;; *** Verification SRs *** ;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;; CNAM Subroutines
[disa-rewrite-cnam] ; NPSTN 20201203 NA ; rewrites CNAM if not already tagged
exten => s,1,Set(lowered=${TOLOWER("${CALLERID(name)}")})
	same => n,GotoIf($[${REGEX(" pstn" ${lowered})}=1]?pstn)
	same => n,GotoIf($[${REGEX("via npstn" ${lowered})}=1]?disa)
	same => n,GotoIf($[${REGEX(" gateway" ${lowered})}=1]?disa)
	same => n,GotoIf($[${REGEX(" disa" ${lowered})}=1]?disa) ; This check has been deprecated but is supported for backwards-compatability
	same => n,GotoIf($[${REGEX(" via " ${lowered})}=1]?disa)
	same => n,ExecIf($["${clidverif:-2:1}"="3"]?Set(CALLERID(name)=${CALLERID(name)} via ${clli} US PSTN))
	same => n,ExecIf($["${clidverif:-2:1}"="4"]?Set(CALLERID(name)=${CALLERID(name)} via ${clli} UK PSTN))
	same => n,ExecIf($["${clidverif:-2:1}"="5"]?Set(CALLERID(name)=${CALLERID(name)} via ${clli} AU PSTN))
	same => n,ExecIf($["${clidverif:-2:1}"="6"]?Set(CALLERID(name)=${CALLERID(name)} via ${clli} ${npstncc} PSTN))
	same => n,GotoIf($[${REGEX(" PSTN" "${CALLERID(name)}")}=1]?pstn) ; Did we just rewrite CNAM for PSTN?
	same => n(rewrite),Set(CALLERID(name)=${CALLERID(name)} via ${clli}) ; via ${clli} DISA has been deprecated in favor of via ${clli} (DISA is redundant)
	same => n,Return(0) ;;;;;;;;;;;;;;;;;;;; Return NONE code to [npstn-out-verify]
	same => n(disa),Return(1) ;;;;; Return via CLLI (formerly DISA) code to [npstn-out-verify]
	same => n(pstn),Return(2) ;;;;; Return PSTN code to [npstn-out-verify]

[cnamsearch] ; NPSTN 20201203 NA ; replaces the old, inefficient [cnamsearch] ; no longer actively used, but supported for backwards-compatability (significantly more performant)
exten => s,1,Return(${REGEX(${ARG2} ${ARG1})})

;;; Outgoing verification
[npstn-out-verify] ; NPSTN 20190212 NA ; set IAX flags and modifies CNAM if necessary
exten => _X!,1,Goto(s,1) ; needed for backwards-compatability
exten => s,1,NoOp()
	same => n,ExecIf($[${LEN(${FILTER(0-1,${forcesecurechannel})})}=1]?Set(CHANNEL(secure_bridge_signaling)=${FILTER(0-1,${forcesecurechannel})})) ; Secure Calling
	same => n,ExecIf($[${LEN(${FILTER(0-1,${forcesecurechannel})})}=1]?Set(CHANNEL(secure_bridge_media)=${FILTER(0-1,${forcesecurechannel})})) ; Secure Calling
	same => n,ExecIf($[${LEN(${FILTER(0-1,${forcesecurechannel})})}=1]?Set(IAXVAR(forcesecurechannel)=${FILTER(0-1,${forcesecurechannel})})) ; Secure Calling
	same => n,ExecIf($[${LEN(${FILTER(A-D0,${autovonprioritydigit})})}=1]?Set(IAXVAR(npstnmlpp)=${FILTER(A-D0,${autovonprioritydigit})})) ; MLPP/AUTOVON
	same => n,ExecIf($[${LEN(${FILTER(0-9,${isup-oli})})}=2]?Set(IAXVAR(isup-oli)=${FILTER(0-9,${isup-oli})})) ; ANI II
	same => n,GosubIf($[${LEN(${clidverif})}>0|"${requestverifytokenonipfail}"="1"]?npstn-set-flags,s,1)
	same => n,Set(IAXVAR(DISAVIA)=${maindisa}) ; Lets other nodes know where this call came from so they can verify this NPSTN node if needed (duplicate needed in case npstn-set-flags not called)
	same => n,GosubIf($[${LEN(${clidverif})}>0]?disa-rewrite-cnam,s,1)
	same => n,Gosub(npstn-out-allow,s,1(${GOSUB_RETVAL}))
	same => n,Return(${GOSUB_RETVAL}) ; Return 1 to complete outgoing call, 0 to divert to intercept

[cnet-out-verify] ; NPSTN 20210117 NA ; set IAX flags and modifies CNAM if necessary
exten => s,1,Set(IAXVAR(DISAVIA)=${maincnetdisa}) ; Lets other nodes know where this call came from so they can verify this C*NET node if necessary
	same => n,GosubIf($[${LEN(${clidverif})}>0]?cnet-set-flags,s,1)
	same => n,GosubIf($[${LEN(${clidverif})}>0]?disa-rewrite-cnam,s,1)
	same => n,Gosub(npstn-out-allow,s,1(${GOSUB_RETVAL})) ; optional, and not NPSTN-specific
	same => n,Return(${GOSUB_RETVAL}) ; Return 1 to complete outgoing call, 0 to divert to intercept

[npstn-set-flags]
exten => s,1,Set(IAXVAR(clidverif)=${clidverif})
	same => n,Set(IAXVAR(DISAVIA)=${maindisa}) ; Lets other nodes know where this call came from so they can verify this NPSTN node if necessary
	same => n,GotoIf($["${requestverifytokenonipfail}"="1"]?:done)
	same => n,Set(IAXVAR(npstnverifytoken)=${SHELL(curl "https://crtc.npstn.us/api/v1/verification/temptoken.php?auth=${npstnkey}&disavia=${maindisa}")})
	same => n(done),Return()

[cnet-set-flags]
exten => s,1,Set(IAXVAR(clidverif)=${clidverif})
	same => n,Set(IAXVAR(DISAVIA)=${maincnetdisa}) ; Lets other nodes know where this call came from so they can verify this C*NET node if necessary
	same => n,Return()

[npstn-blacklist-check]
exten => s,1,Return(${FILTER(0-9\x2E,${SHELL(curl "https://npstn.us/api/v1/blacklist/?get&auth=${npstnkey}&cvs=${clidverif}&number=${CALLERID(num)}")})})

[npstn-out-allow] ; NPSTN 20190215 NA ; determine whether outgoing call should be allowed to complete
exten => s,1,ExecIf($["${usedefaultblacklistresult}"="1"]?Gosub(npstn-blacklist-check,s,1):Goto(reg))
	same => n,GotoIf($["${GOSUB_RETVAL}"=""]?term)
	same => n,GotoIf($[${GOSUB_RETVAL}>=2]?term)
	same => n(reg),Set(cnamcode=${ARG1}) ; 0=CNAM just modified,1=CNAM received tagged as via DISA,2=CNAM received tagged as via PSTN
	same => n,GotoIf($["${cnamcode}"="1"]?disa)
	same => n,GotoIf($["${cnamcode}"="2"]?disa)
	same => n,Return(1) ; if cnamcode=0 (CNAM just modified), allow outgoing call to complete regardless
	same => n(disa),GotoIf($["${allowdisathru}"="NO"]?term) ; allowdisathru global var. must be set to YES/NO per NPSTN Docs
	same => n,Return(1) ; if cnamcode=1 (CNAM received as via DISA), allow outgoing call if global var. allowdisathru=YES
	same => n(pstn),GotoIf($["${allowpstnthru}"="NO"]?term) ; allowpstnthru global var. must be set to YES/NO per NPSTN Docs
	same => n(term),Return(0) ; call should not be allowed to leave this switch

[npstn-out-blocked] ; NPSTN 20190215 NA ; destination for calls that are not allowed to leave this node ; updated 20201214 to check for existence
exten => s,1,ExecIf($[${DIALPLAN_EXISTS(invalidincoming,s,1)}]?Goto(invalidincoming,s,1):PlayTones(congestion))
	same => n,Wait(5)
	same => n,Hangup(21)

[lookupchan] ; NPSTN 20190316 NA ; analyzes lookup request and returns 0 if it is not a supported channel ; ARG1 = lookup
exten => s,1,ExecIf($["${TOUPPER(${ARG1:0:3})}"="SIP"]?Return(2)) ; don't further check the destination if it's SIP
	same => n,GotoIf($[${REGEX("^[iI][aA][xX]2\/[a-zA-Z0-9:_\-]+@([a-zA-Z0-9_\-]+\.)+[a-zA-Z0-9_\-]+\/[0-9]+$" ${ARG1})}=1]?:bad) ; above ensures there is at least 1 "." in the host (that it isn't a reference to a local peer with username/password) ; updated 20210125 to include : to allow for IAX2 passwords
	same => n,ExecIf($["${TOUPPER(${ARG1:0:4})}"="IAX2"]?Return(1)) ; allowed channel technologies are IAX, SIP, MGCP, and H323
	same => n,ExecIf($["${TOUPPER(${ARG1:0:4})}"="MGCP"]?Return(3))
	same => n,ExecIf($["${TOUPPER(${ARG1:0:4})}"="H323"]?Return(4))
	same => n(bad),Return(0)

[lookupipcheck] ; NPSTN 20201006 NA ; returns 0 if an IP address is public ; ARG1 = lookup
exten => s,1,Gosub(isitip,s,1(${ARG1}))
	same => n,Set(ARRAY(iaxisip,ip)=${GOSUB_RETVAL})
	same => n,GotoIf($[${iaxisip}=1]?checkip)
	same => n,Gosub(fqdn2ip,s,1(${ARG1}))
	same => n,Set(ARRAY(ds1,ip)=${GOSUB_RETVAL})
	same => n(checkip),Gosub(isprivateip4,s,1(${ip}))
	same => n,Return(${GOSUB_RETVAL})

[isprivateip4] ; NPSTN 20201006 NA ; RETURN -1 = loopback, 1 = private, 0 = public ; ARG1 = IP
exten => s,1,GotoIf($["${ARG1}"="127.0.0.1"]?loopback)
	same => n,GotoIf($["${ARG1:0:3}"="10."]?private) ; Class A private
	same => n,GotoIf($["${ARG1:0:8}"="192.168."]?private) ; Class C private
	same => n,GotoIf($["${ARG1:0:8}"="169.254."]?private) ; APIPA
	same => n,GotoIf($["${ARG1:0:4}"="172."]?classb) ; Class B
	same => n,Goto(public)
	same => n(public),Return(0)
	same => n(loopback),Return(-1)
	same => n(classb),GotoIf($["${ARG1:6:1}"="."]?:public)
	same => n,GotoIf($[${ARG1:4:2}<16]?public)
	same => n,GotoIf($[${ARG1:4:2}>31]?public)
	same => n,Goto(private)
	same => n(private),Return(1)

;;; Incoming verification
[pstn-us-verify] ; NPSTN 20190212 NA ; to be called immediately on any incoming call from a US PSTN DID
exten => s,1,Set(CALLERID(num)=${FILTER(0-9A-Za-z,${CALLERID(num)})})
	same => n,Gosub(anonymous-check,s,1)
	same => n,GotoIf($[${GOSUB_RETVAL}=0]?anonymous:checklen)
	same => n(checklen),GotoIf($[${LEN(${FILTER(0-9,${CALLERID(num)})})}<10]?invalid)
	same => n,Gosub(pstn-us-format-lookup,${FILTER(0-9,${CALLERID(num)}):-10},1)
	same => n,GotoIf($["${GOSUB_RETVAL}"="0"]?invalid)
	same => n(valid),Set(__clidverif=30)
	same => n,Goto(done)
	same => n(invalid),Set(__clidverif=31)
	same => n,Goto(done)
	same => n(anonymous),Set(__clidverif=32)
	same => n,Goto(done)
	same => n(done),Return()

[pstn-us-format-lookup] ; NPSTN 20210109 NA
exten => _NXXNXXXXXX,1,Return(1)
exten => _X!,1,Return(0)
exten => _[01]XXXXXXXXX,1,Return(0)
exten => _XXX[01]XXXXXX,1,Return(0)
exten => _[2-79]00NXXXXXX,1,Return(0)
exten => _[2-79]22NXXXXXX,1,Return(0)
exten => _[2-79]33NXXXXXX,1,Return(0)
exten => _[2-79]44NXXXXXX,1,Return(0)
exten => _[2-79]55NXXXXXX,1,Return(0)
exten => _[2-79]66NXXXXXX,1,Return(0)
exten => _[2-79]77NXXXXXX,1,Return(0)
exten => _[2-79]88NXXXXXX,1,Return(0)
exten => _[2-79]99NXXXXXX,1,Return(0)
exten => _X11XXXXXXX,1,Return(0)

[pstn-uk-verify] ; NPSTN 20190212 NA ; to be called immediately on any incoming call from a UK PSTN DID
exten => s,1,Set(CALLERID(num)=${FILTER(0-9A-Za-z,${CALLERID(num)})})
	same => n,Gosub(anonymous-check,s,1)
	same => n,GotoIf($[${GOSUB_RETVAL}=0]?anonymous:checklen)
	same => n(checklen),GotoIf($[${LEN(${CALLERID(num)})}<8]?invalid) ; minimum acceptable UK CLID length assumed to be 8
	same => n(valid),Set(__clidverif=40)
	same => n,Goto(done)
	same => n(invalid),Set(__clidverif=41)
	same => n,Goto(done)
	same => n(anonymous),Set(__clidverif=42)
	same => n,Goto(done)
	same => n(done),Return()

[pstn-au-verify] ; NPSTN 20200122 NA ; to be called immediately on any incoming call from a AU PSTN DID
exten => s,1,Set(CALLERID(num)=${FILTER(0-9A-Za-z,${CALLERID(num)})})
	same => n,Gosub(anonymous-check,s,1)
	same => n,GotoIf($[${GOSUB_RETVAL}=0]?anonymous:checklen)
	same => n(checklen),GotoIf($[${LEN(${CALLERID(num)})}<10]?invalid) ; minimum acceptable AU CLID is 10
	same => n,GotoIf($["${CALLERID(num):-9:1}"="6"]?invalid) ; AU numbers don't start with 06 or 09 or 01
	same => n,GotoIf($["${CALLERID(num):-9:1}"="9"]?invalid)
	same => n,GotoIf($["${CALLERID(num):-9:1}"="1"]?invalid)
	same => n,GotoIf($["${CALLERID(num):-10:1}"="0"]?valid:invalid) ; AU numbers start with a 0 (10th last digit)
	same => n(valid),Set(__clidverif=50)
	same => n,Goto(done)
	same => n(invalid),Set(__clidverif=51)
	same => n,Goto(done)
	same => n(anonymous),Set(__clidverif=52)
	same => n,Goto(done)
	same => n(done),Return()

; NOTICE: IF USING THIS SUBROUTINE ANYWHERE!: you MUST define a global variable named npstncc which contains your 2-character country code!
[pstn-other-verify] ; NPSTN 20200122 NA ; to be called immediately on any incoming call from a PSTN DID outside the US, UK, or AU
exten => s,1,Set(CALLERID(num)=${FILTER(0-9A-Za-z,${CALLERID(num)})})
	same => n,Gosub(anonymous-check,s,1)
	same => n,GotoIf($[${GOSUB_RETVAL}=0]?anonymous:checklen)
	same => n(checklen),GotoIf($[${LEN(${CALLERID(num)})}<8]?invalid) ; minimum acceptable CLID length, in general, assumed to be 7
	same => n(valid),Set(__clidverif=60)
	same => n,Goto(done)
	same => n(invalid),Set(__clidverif=61)
	same => n,Goto(done)
	same => n(anonymous),Set(__clidverif=62)
	same => n,Goto(done)
	same => n(done),Return()

[anonymous-check] ; NPSTN 20190212 NA ; returns 0 if the CLID or CNAM is likely private/anonymous/withheld/blocked/restricted/etc. ; otherwise returns 1
exten => s,1,Set(LOCAL(cnam)=${FILTER(0-9A-Za-z\x20,${CALLERID(name)}):0:9}) ; prevent erroneous "s being used in the comparison
	same => n,Set(LOCAL(cnum)=${CALLERID(num)})
	same => n,GotoIf($["${TOLOWER(${cnam}):0:9}"="anonymous"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnum}):0:9}"="anonymous"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnam}):0:7}"="private"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnum}):0:7}"="private"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnam}):0:10}"="restricted"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnum}):0:10}"="restricted"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnam}):0:8}"="withheld"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnum}):0:8}"="withheld"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnam}):0:8}"="unknown"]?anon)
	same => n,GotoIf($["${TOLOWER(${cnum}):0:8}"="unknown"]?anon)
	same => n,GotoIf($["${FILTER(0-9,${cnum})}"=""]?anon)
	same => n,Return(1)
	same => n(anon),Return(0)

[cnet-verify] ; NPSTN 20190212 NA ; to be called immediately on all [from-cnet] calls
exten => _X!,1,Goto(s,1) ; backwards-compatability
exten => s,1,Set(CALLERID(num)=${FILTER(0-9A-Za-z,${CALLERID(num)})})
	same => n,Set(LOCAL(fromnum)=${IAXVAR(DISAVIA)}) ; this should be a C*NET network # on the upstream node, with full country code + number
	same => n,Set(LOCAL(clidverified)=${IAXVAR(clidverif)})
	same => n,GotoIf($[${LEN(${clidverified})}=2]?verifynode) ; Should the node reporting the caller's verification status be trusted? (Confirm it's a valid C*NET node telling us, not a rogue server)
	same => n,Gosub(cnet-verifyclid,${EXTEN},1(${CALLERID(num)}))
	same => n,Set(LOCAL(valid)=${GOSUB_RETVAL})
	same => n,GotoIf($[${valid}=1]?valid:invalid)
	same => n(verifynode),Gosub(cnet-verifyclid,s,1(${fromnum})) ; Verify the C*NET node through which the C*NET call is passing
	same => n,Set(LOCAL(valid)=${GOSUB_RETVAL})
	same => n,GotoIf($[${valid}=1]?judgedvalid:judgedinvalid)
	same => n(valid),Set(__clidverif=20)
	same => n,Return()
	same => n(invalid),Set(__clidverif=21)
	same => n,Set(GROUP(cvs)=cnetspam)
	same => n,Return()
	same => n(judgedvalid),Set(__clidverif=${clidverified})
	same => n,Return()
	same => n(judgedinvalid),Set(__clidverif=29)
	same => n,Return()

[cnet-verifyclid] ; NPSTN 20190218 NA
exten => s,1,NoOp()
	same => n(ewr),Gosub(cnet-reverse-iax,s,1(${ARG1}))
	same => n,Set(ARRAY(calleriax,cnumvalid)=${GOSUB_RETVAL})
	same => n,GotoIf($[${cnumvalid}=1]?:invalid)
	same => n,GotoIf($["${calleriax}"=""]?invalid)
	same => n,Gosub(isitip,s,1(${calleriax}))
	same => n,Set(ARRAY(iaxisip,calleriplookup)=${GOSUB_RETVAL})
	same => n,GotoIf($[${iaxisip}=1]?getpeer)
	same => n,Gosub(fqdn2ip,s,1(${calleriax}))
	same => n,Set(ARRAY(calleriaxip,calleriplookup)=${GOSUB_RETVAL})
	same => n(getpeer),Gosub(getpeerip,s,1)
	same => n,Set(ipactual=${GOSUB_RETVAL})
	;same => n,GotoIf($[${ipactual}=1]?:invalid) ; commented out to allow for cnet-exceptions
	same => n,GotoIf($[${LEN(${FILTER(0-9,${CALLERID(num)})})}>0]?ipcomp)
	same => n,Gosub(cnet-exceptions,${FILTER(0-9,${CALLERID(num)})},1(${ipactual}))
	same => n,Set(match=${GOSUB_RETVAL})
	same => n,GotoIf($["${match}"="1"}]?done)
	same => n(ipcomp),Gosub(ipcomp,s,1(${calleriplookup},${ipactual}))
	same => n,Set(match=${GOSUB_RETVAL})
	same => n(done),Return(${match})
	same => n(invalid),Return(0)

[cnet-exceptions]
exten => _1488XXXX,1,Gosub(ipcomp,s,1(199.199.196.82,${ARG1}))
	same => n,Return(${GOSUB_RETVAL})
exten => _1596XXXX,1,Gosub(ipcomp,s,1(45.32.212.173,${ARG1}))
	same => n,Return(${GOSUB_RETVAL})
include => noexception

[noexception]
exten => _[A-Za-z]!,1,Return(0)
exten => _X!,1,Return(0)

[npstn-verify] ; NPSTN 20190212 NA ; to be called immediately on all [from-npstn] calls, updated 20210518 to save FQDN from npstn-verifyclid
exten => _X!,1,Goto(s,1) ; backwards-compatability
exten => s,1,Set(CALLERID(num)=${FILTER(0-9A-Za-z,${CALLERID(num)})})
	same => n,Set(LOCAL(fromnum)=${IAXVAR(DISAVIA)}) ; this should be a number on the upstream node
	same => n,Set(LOCAL(clidverified)=${IAXVAR(clidverif)})
	same => n,GotoIf($[${LEN(${clidverified})}=2]?verifynode) ; Should the node reporting the caller's verification status be trusted? (Confirm it's a valid NPSTN node telling us, not a rogue server)
	same => n,Gosub(npstn-verifyclid,s,1(${CALLERID(num)}))	; Allows callers dialing through DISAs to still maintain verification status
	same => n,Set(ARRAY(LOCAL(valid),LOCAL(fqdn))=${GOSUB_RETVAL}) ; Now, not just any server can set IAXVAR(clidverif) to spoof verification status (only a valid NPSTN node could do this)
	same => n,GotoIf($[${valid}=1]?valid)
	same => n(tokenverify),ExecIf($["${IAXVAR(npstnverifytoken)}"=""]?GotoIf($[${LEN(${clidverified})}=2]?judgedinvalid:invalid)) ; failed reverse IP check, fallback to token verification if available
	same => n,Set(LOCAL(tokenresult)=${SHELL(curl "https://crtc.npstn.us/api/v1/verification/temptoken.php?auth=${npstnkey}&token=${IAXVAR(npstnverifytoken)}")})
	same => n,Set(IAXVAR(npstnverifytoken)=) ; clear variable
	same => n,ExecIf($[$[${LEN(${disavia})}=7]&$["${fromnum}"="${tokenresult}"]]?GotoIf($[${LEN(${clidverified})}=2]?judgedvalid:valid):GotoIf($[${LEN(${clidverified})}=2]?judgedinvalid:invalid))
	same => n(verifynode),Gosub(npstn-verifyclid,s,1(${fromnum})) ; Verify the NPSTN node through which the NPSTN call is passing
	same => n,Set(ARRAY(LOCAL(valid),LOCAL(fqdn))=${GOSUB_RETVAL})
	same => n,GotoIf($[${valid}=1]?judgedvalid)
	same => n,Goto(tokenverify)
	same => n(valid),Set(__clidverif=10)
	same => n,Gosub(npstn-rsa-in-prefetch,s,1(${fqdn}))
	same => n,Goto(writeflags)
	same => n(invalid),Set(__clidverif=11)
	same => n,Set(GROUP(cvs)=npstnspam)
	same => n,Goto(writeflags)
	same => n(judgedvalid),Set(__clidverif=${FILTER(0-9,${clidverified})})
	same => n,Gosub(npstn-rsa-in-prefetch,s,1(${fqdn}))
	same => n,Goto(writeflags)
	same => n(judgedinvalid),Set(__clidverif=19)
	same => n,Goto(writeflags)
	same => n(writeflags),NoOp()
	same => n,ExecIf($[${LEN(${FILTER(A-D0,${IAXVAR(npstnmlpp)})})}=1]?Set(__autovonprioritydigit=${FILTER(A-D0,${IAXVAR(npstnmlpp)})})) ; MLPP/AUTOVON
	same => n,ExecIf($[${LEN(${FILTER(0-1,${IAXVAR(forcesecurechannel)})})}=1]?Set(__forcesecurechannel=${FILTER(0-1,${IAXVAR(forcesecurechannel)})})) ; Secure Calling
	same => n,ExecIf($[${LEN(${FILTER(0-9,${IAXVAR(isup-oli)})})}=2]?Set(__isup-oli=${FILTER(0-9,${IAXVAR(isup-oli)})})) ; ANI II
	same => n,Return()

[npstn-rsa-in-prefetch] ; NPSTN 20210518 ; RSA public key prefetcher: check if a public key for this FQDN exists, redownloading it at most often every 12 hours
exten => s,1,Set(LOCAL(fqdn)=${FILTER(A-Za-z0-9\x2D\x2E\x3A,${ARG1})}) ; sanitize FQDN: allow alphanumeric, hyphens, periods, and colons only (FQDNs/IPv4/IPv6)
	same => n,Set(LOCAL(inkey)=npstn-rsa-${FILTER(A-Za-z0-9\x2D\x2E,${ARG1})}) ; filename cannot contain the colon, because that is the delimeter
	same => n,Set(files=${FILTER(0-9,${SHELL(find /var/lib/asterisk/keys/ -name '*${fqdn}*' | wc -l)})})
	same => n,GotoIf($["${files}"="0"]?prefetch) ; no public key exists for this server, try to fetch
	same => n,GotoIf($[${STAT(e,/var/lib/asterisk/keys/${inkey}.pub)}]?:prefetch) ; check each time if key doesn't exist, because it could have just been added
	same => n,GotoIf($[$[$[${EPOCH}-${STAT(C,/var/lib/asterisk/keys/${inkey}.pub)}]>43200]|$[$[${EPOCH}-${STAT(M,/var/lib/asterisk/keys/${inkey}.pub)}]>43200]]?prefetch) ; if file changed/modified > 12 hrs. ago, refetch
	same => n,Return() ; key exists and we're not going to fetch it again right now
	same => n(prefetch),System(wget "https://crtc.npstn.us/api/v1/rsa/?auth=${npstnkey}&fqdn=${fqdn}" -O /var/lib/asterisk/keys/${inkey}.pub) ; this will save a file, regardless of whether response was 200 or 404
	; key exists now (though it may or may not be legitimate. Dummy public keys are a good thing, actually. It prevents us from trying to fetch each time and lets us know no key exists right now.)
	same => n,GotoIf($[${STAT(s,/var/lib/asterisk/keys/${inkey}.pub)}=0]?done) ; if we downloaded a dummy key, no reason to modify configuration, so we're done
	same => n,Set(LOCAL(wordcount)=${FILTER(0-9,${SHELL(grep "inkeys" "/etc/asterisk/iax-rsa-npstn-in.conf" | grep -e "=${inkey}" -e ":${inkey}:" -e ":${inkey} " | wc -l)})})
	same => n,GotoIf($["${wordcount}"=""|"${wordcount}"="0"]?:done) ; key name already listed in inkeys?
	same => n,Set(LOCAL(inkeys)=${FILTER(A-Za-z0-9\x2D\x2E\x3A,${SHELL(grep "inkeys" "/etc/asterisk/iax-rsa-npstn-in.conf" | cut -d'\;' -f 1 | cut -d'=' -f 2)})}) ; new key, so get list of current keys
	same => n,ExecIf($["${inkeys}"!=""]?System(sed -i 's/${inkeys}/${inkeys}:${inkey}/' /etc/asterisk/iax-rsa-npstn-in.conf)) ; append key to list of inkeys 
	same => n(done),ExecIf($[${IFMODULE(app_reload.so)}]?Reload(iax2):System(asterisk -rx "iax2 reload")) ; reload IAX2 and crypto keys.
	same => n,ExecIf($[${IFMODULE(app_reload.so)}]?Reload(res_crypto):System(asterisk -rx "module reload res_crypto"))
	same => n,Return() ; note that this subroutine does not flush out stale or obsolete keys over time, but periodically cleaning this file and the keys is okay.

[npstn-verifyclid] ; NPSTN 20190212 NA, updated 20210518 to return FQDN as well
exten => _X!,1,Goto(s,1) ; backwards-compatability
exten => s,1,Set(LOCAL(cnum)=${ARG1})
	same => n,Gosub(npstn-reverse-iax,s,1(${cnum}))
	same => n,Set(ARRAY(iaxlookup,cnumvalid)=${GOSUB_RETVAL})
	same => n,GotoIf($["${cnumvalid}"="1"]?:invalid)
	same => n,GotoIf($["${iaxlookup}"=""]?invalid)
	same => n,Gosub(isitip,s,1(${iaxlookup}))
	same => n,Set(ARRAY(LOCAL(iaxisip),LOCAL(iplookup))=${GOSUB_RETVAL})
	same => n,GotoIf($[${iaxisip}=1]?getpeer)
	same => n(convert),Gosub(fqdn2ip,s,1(${iaxlookup}))
	same => n,Set(ARRAY(LOCAL(iaxip),LOCAL(iplookup))=${GOSUB_RETVAL})
	same => n(getpeer),Gosub(getpeerip,s,1)
	same => n,GotoIf($[${GOSUB_RETVAL}=0]?invalid)
	same => n,Gosub(ipcomp,s,1(${iplookup},${GOSUB_RETVAL}))
	same => n,GotoIf($[${GOSUB_RETVAL}=1]?valid:invalid)
	same => n(invalid),Return(0,${fqdn})
	same => n(valid),Return(1,${CUT(CUT(iaxlookup,@,2),/,1)})

[npstn-get-number-ip] ; NPSTN 20201213 NA ; Returns IP address associated with a VALID NPSTN # (ARG1)
exten => s,1,Set(iaxuri=${SHELL(curl "https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=${ARG1}")})
	same => n,ExecIf($["${iaxuri}"=""]?Return()) ; invalid lookup
	same => n,Gosub(isitip,s,1(${iaxuri}))
	same => n,Set(ARRAY(iaxisip,iaxiplookup)=${GOSUB_RETVAL})
	same => n,GotoIf($[${iaxisip}=1]?return)
	same => n,Gosub(fqdn2ip,s,1(${iaxuri}))
	same => n,Set(ARRAY(iaxip,iaxiplookup)=${GOSUB_RETVAL})
	same => n(return),Return(${iaxiplookup})

[npstn-reverse-iax] ; NPSTN 20190212 NA ; based on npstn-ewr-fetch
exten => s,1,Set(nocid=${SHELL(curl "https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=")})
	same => n,GotoIf($["${nocid:0:5}"!="IAX2/"]?failover) ; temporary CRTC issue
	same => n,Set(badcid=${SHELL(curl "https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=442072221234")})
	same => n,Set(rtcr=${SHELL(curl "https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=${ARG1}")})
	same => n,Set(cv=${IF($[$["${rtcr}"="${nocid}"]|$["${rtcr}"="${badcid}"]]?0:1)})
	same => n,ExecIf($[$[${cv}=1]&$["${rtcr:0:5}"="IAX2/"]&$[${LEN(${ARG1})}=7]]?Set(DB(npstnlookups/${ARG1})=${rtcr})) ; cache the incoming NPSTN #
	same => n,GotoIf($[$[${LEN(${FILTER(0-9,${IAXVAR(DISAVIA)})})}=7]&$[${DB_EXISTS(npstnlookups/${FILTER(0-9,${IAXVAR(DISAVIA)})})}!=1]]?:ret)
	same => n,Set(node=${SHELL(curl "https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=${FILTER(0-9,${IAXVAR(DISAVIA)})}")})
	same => n,ExecIf($[$[${cv}=1]&$["${badcid:0:5}"="IAX2/"]]?Set(DB(npstnlookups/${FILTER(0-9,${IAXVAR(DISAVIA)})})=${node})) ; cache the incoming node
	same => n(ret),Return(${rtcr},${cv})
	same => n(failover),ExecIf($[${DB_EXISTS(npstnlookups/${ARG1})}=1]?Return(${DB(npstnlookups/${ARG1})},1)) ; exact # in cache?
	same => n,ExecIf($[$[${LEN(${FILTER(0-9,${IAXVAR(DISAVIA)})})}=7]&$[${DB_EXISTS(npstnlookups/${FILTER(0-9,${IAXVAR(DISAVIA)})})}=1]]?Return(${DB(npstnlookups/${FILTER(0-9,${IAXVAR(DISAVIA)})})},1)) ; node in cache?
	same => n,Return(,0) ; give up

[cktslookup] ; NPSTN 20190218 NA ; ARG1 = full international number to lookup on C*NET
exten => s,1,Set(ENUM=${ENUMLOOKUP(+${ARG1},ALL,,1,std.ckts.info)})
	same => n,GotoIf($[${LEN(${ENUM})}=0]?fail)
	same => n,GotoIf($[${ENUM:0:3}=iax]?iax)
	same => n,GotoIf($[${ENUM:0:3}=sip]?sip)
	same => n,GotoIf($[${ENUM:0:4}=h323]?h323)
	same => n,Goto(fail)
	same => n(iax),Set(DIALSTR=IAX2/${ENUM:5})
	same => n,Goto(done)
	same => n(sip),Set(DIALSTR=SIP/${ENUM:4})
	same => n,Goto(done)
	same => n(h323),Set(DIALSTR=H323/${ENUM:5})
	same => n(done),Return(${DIALSTR})
	same => n(fail),Return(${ENUM})

[cnet-reverse-iax] ; NPSTN 20190212 NA ; based on npstn-ewr-fetch
exten => s,1,Set(nocid=CNET_NUMBER_NOT_VALID)})
	same => n,Set(badcid=)})
	same => n,Gosub(cktslookup,s,1(${ARG1}))
	same => n,Set(rtcr=${GOSUB_RETVAL})
	same => n,Set(cv=${IF($[$["${rtcr}"="${nocid}"]|$["${rtcr}"="${badcid}"]]?0:1)})
	same => n,Return(${rtcr},${cv})

[isitip]    ;20181116 BJC - ARG1 is a iaxuri - if ARG1 contains an IP address, returns dnf=1 otherwise dnf=0  
exten => s,1,Set(oc1=${ISNULL(${FILTER(0-9,${CUT(CUT(CUT(ARG1,/,2),@,2),.,1)})})})    ;slice iaxuri and evaluate octet1
	same => n,Set(oc2=${ISNULL(${FILTER(0-9,${CUT(CUT(CUT(ARG1,/,2),@,2),.,2)})})})            ;slice iaxuri and evaluate octet2
	same => n,Set(oc3=${ISNULL(${FILTER(0-9,${CUT(CUT(CUT(ARG1,/,2),@,2),.,3)})})})            ;slice iaxuri and evaluate octet3
	same => n,Set(oc4=${ISNULL(${FILTER(0-9,${CUT(CUT(CUT(ARG1,/,2),@,2),.,4)})})})            ;slice iaxuri and evaluate octet4
	same => n,Set(dnf=${IF($[${oc1}|${oc2}|${oc3}|${oc4}]=0?0:1)})                            ;OR sum octets
	same => n,Set(ip=${IF($["${dnf}"="1"]?${CUT(CUT(ARG1,@,2),/,1)}:0)})                    ;IP address
	same => n,Return(${dnf},${ip})                                                            ;return IP flag & IP  

[fqdn2ip]    ;20181114 BJC - ARG1 is iaxuri containing an fqdn, returns iaxuri containing ip address and ip address if host exists else returns Unknown Host and fqdn
exten => s,1,Set(ARRAY(ds1,ds2,ds3)=${CUT(ARG1,@,1)},${CUT(CUT(ARG1,@,2),/,1)},${CUT(CUT(ARG1,@,2),/,2)})        ;extract fqdn to {DS2}
	same => n,Set(ds2=${FILTER(0-9.,${SHELL(dig ${CUT(ds2,:,1)} +noall +short | sed -n '$'p)})})                   ;convert fqdn to IP - modified 20210125 NA to support : for non-4569 ports
	same => n,GotoIf($["${ds2}"=""]?nn)                                                                            ;unknown host?
	same => n,Return(${ds1}@${ds2}/${ds3},${ds2})                                                                  ;return converted iaxuri & IP address
	same => n(nn),Return(Unknown Host,${ds2})                                                                      ;Unknown Host - no IP address

[getpeerip]    ;20181114 BJC / 20190223 NA - returns peerip for IAX2/SIP/H323/MGCP channel or 0 for all other channel technologies
exten => s,1,Set(ct=${CHANNEL(channeltype)})            ;get technology
	same => n,GotoIf($["${ct}"="IAX2"]?setpip)            ;incoming NPSTN calls are IAX2 or one of the other 3 technologies, any other technology must be local/test channels
	same => n,GotoIf($["${ct}"="SIP"]?setpip)
	same => n,GotoIf($["${ct}"="H323"]?setpip)
	same => n,GotoIf($["${ct}"="MGCP"]?setpip)
	same => n,Goto(z)
	same => n(setpip),Set(pip=${CHANNEL(peerip)})                    ;get peer IP
	same => n,Return(${pip})                                ;return peer IP
	same => n(z),Return(0)                                    ;return 0 for local channels

[ipcomp]    ;20181114 BJC - ARG1 and ARG2 are IP addresses - returns cpf=1 if ARG1 & ARG2 coincidence otherwise cpf=0
exten => s,1,ExecIf($[${LEN(${ARG1})}=0|${LEN(${ARG2})}=0]?Return(0)) ; added 20210125 NA to prevent dialplan warnings if empty argument(s) provided
	same => n,Set(cpf=${IF($[${FILTER(0-9,${ARG1})}=${FILTER(0-9,${ARG2})}]?1:0)})    ;compare ip addresses
	same => n,Return(${cpf})                                                           ;return compare flag

;;;;;;;;;;;;;;;;;;; UNUSED (INCLUDED FOR REFERENCE ONLY) ;;;;;;;;;;;;;;;;;;;;;;;
[npstn-ewr-fetch]    ;20181114 BJC - ARG1 is ${CALLERID(num)}, ARG2 is ${EXTEN} - returns caller and callee iaxuris and caller validity, "ewr"=each way route
exten => _X!,1,Set(nocid=${CURL(https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=)})    ;Get fault code for ""<>                                    
	same => n,Set(badcid=${CURL(https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=442072221234)})    ;Get fault code    for non-NPSTN extension
	same => n,Set(rtcr=${CURL(https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=${ARG1})})    ;Get route to caller   ;store caller iaxuri
	same => n,Set(rtce=${CURL(https://crtc.npstn.us/api/v1/?auth=${npstnkey}&lookup=${ARG2})})     ;Get route to callee   ;store callee iaxuri
	same => n,Set(cv=${IF($[$["${rtcr}"="${nocid}"]|$["${rtcr}"="${badcid}"]]?1:0)})    ;check for valid NPSTN CID
	same => n,Return(${rtcr},${rtce},${cv})                                                                            ;return variables

[cidcheck]    ;20181211 BJC ARG1=CALLERID(name) ARG2=CALLERID(num) Returns 1 for acceptable CALLERID, 0 otherwise
exten => _X!,1,Set(ARRAY(PCNA,PCNU,ANON,ASTK,ITNL,WHLD)="${ARG1}","${FILTER(0-9,${ARG2})}","anonymous","asterisk","international","withheld")    ; populate variables
	same => n,GotoIf($[$["${TOLOWER(${PCNA})}"="${ANON}"]|["${TOLOWER(${PCNA})}"="${ASTK}"]|["${TOLOWER(${PCNA})}"=""]]?rj)             ; 1st cid check-CNAM
	same => n,GotoIf($[$["${TOLOWER(${PCNA})}"="${ITNL}"]|["${TOLOWER(${PCNA})}"="${WHLD}"]|["${TOLOWER(${PCNA})}"=""]]?rj)             ; 1st cid check-CNAM
	same => n,GotoIf($[$["${TOLOWER(${PCNU})}"="${ANON}"]|["${TOLOWER(${PCNU})}"="${ASTK}"]|["${TOLOWER(${PCNU})}"=""]]?rj)                ; 1st cid check-CNUM
	same => n,GotoIf($[$["${TOLOWER(${PCNU})}"="${ITNL}"]|["${TOLOWER(${PCNU})}"="${WHLD}"]|["${TOLOWER(${PCNU})}"=""]]?rj)             ; 1st cid check-CNUM
	same => n,Return(1)                                                                ; Accept cid
	same => n(rj),Return(0)                                                            ; Reject cid