├── .gitignore ├── LICENSE ├── README.md ├── main.go ├── sip.conf ├── sip.go └── sipconf.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vasily Suvorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sipd 2 | 3 | ## Description 4 | 5 | Sipd - SIP server with registrar functions and PBX features (forwarding, interception and transfer of calls, the calls by short number, voice guidance). 6 | 7 | ## Setting 8 | 9 | Sipd is controlled by only one configuration file sip.conf, which should be in the working directory of the server, and by the flags of the program start. 10 | 11 | Flag | Description 12 | -|- 13 | -a | print about 14 | -b | listen address (default "127.0.0.1:5060") 15 | -c | current country code (default "7") 16 | -cc | current city code (default "495") 17 | -d | The debug mode outputs to the current terminal all SIP messages of the server 18 | -s directory | the server's working directory (default .) 19 | -r | reloads data from sip.conf 20 | -v | outputs name, version of the program, current hostname (realm) 21 | 22 | The sip.conf file is structured text, broken up using headers in square brackets into sections, which in turn describe the parameters of one of the SIP clients served on the server. The header itself is the name of the client. Followed lines in the format key=value are it's parameters. All lines beginning with a semicolon (;) character are comments and are ignored when the configuration is loaded. 23 | 24 | Example of sip.conf file contents: 25 | 26 | ; sip.conf 27 | ; 28 | 29 | [foo] 30 | accountcode=FOOBAR 31 | context=domestic 32 | secret=abc 33 | permit=192.168/16,10/8 34 | ; short phone number 35 | exten=1001 36 | 37 | [bar] 38 | accountcode=FOOBAR 39 | host=192.168.1.129:5060 40 | context=international 41 | secret=cba 42 | permit=192.168.1.129/32 43 | exten=1002 44 | 45 | [siphone1] 46 | accountcode=HOME 47 | dialplan=.+ 48 | secret=12345 49 | exten=1001(2) 50 | 51 | [siphone2] 52 | accountcode=HOME 53 | dialplan=.+ 54 | secret=4321 55 | ; long, short and call forward phone numbers 56 | exten=4999876543#1002=4991234567 57 | 58 | [GATE1] 59 | accountcode=gateways 60 | host=192.168.1.200:5060 61 | exten=9995999 ; gateway prefix when accountcode is gateways 62 | 63 | [GATE2] 64 | accountcode=gateways 65 | host=192.168.1.201:5060 66 | exten=9 67 | 68 | [MEDIAGATE] 69 | accountcode=mediagates 70 | host=192.168.1.100:5070 71 | 72 | ; EOF 73 | 74 | 75 | A section can contains the following keys. 76 | 77 | Key | Description | Default value 78 | -|-|- 79 | accountcode | the client group ID combines them for calls by short numbers and pickups in the limits of this group | 80 | secret | password for this SIP client (its name is indicated in the square brackets of the section header) | 81 | permit | comma separated IP (CIDR) addresses from which the client's requests can arrive | 82 | host | static client IP address (no registration) | 83 | exten | telephone number of the client in the format L#S(N)=F for connecting with him by phone number L or short number S, or transferring the connection, if not available, to another client under the serial number N in the side of the group (accountcode), or forwarding to the phone number F (if redirection is needed "on unavailable", then the symbol ~ is used instead of the symbol =) | 84 | callerid | identifier of the calling channel for substitution, if necessary 85 | dialplan | a standard regular expression (PCRE) that limits the range of called telephone numbers for a given SIP client (the call will take place if the dialed phone number matches this regular expression, otherwise it does not) | .+ 86 | context | It is used when there is no dialplan key and is replaced by it with a value according to the value of this context key (for example, context=em is perceived as dialplan=^7495\d{2,3}$) 87 | 88 | ## Exploitation 89 | 90 | Starting the server from its working directory is performed by the command: 91 | 92 | $ sipd & 93 | 94 | After the changes made in the sip.conf file, you must tell the running SIP server to overload the configuration. This will not affect the current state of the connections served by the server. 95 | 96 | $ sipd -r 97 | 98 | The server stops using the CLI command: 99 | 100 | $ pkill sipd 101 | 102 | ## PBX features 103 | 104 | A registered customer can use the following built-in PBX services by dialing the following command combinations on his phone: 105 | 106 | Command | Service 107 | -|- 108 | *11 | Interception of a call (ring) from a nearby phone with the same accountcode 109 | *72XXXXXXXXXX | Setting call forwarding to a number XXXXXXXXXX 110 | *71XXXXXXXXXX | Setting call forwarding to a number XXXXXXXXXX if the base number will be not available (off, busy etc) 111 | *72 | Check current forwarding. The response will be dictated by the number of forwarding, if the media gateway is configured (silence if not) 112 | *73 | Remove current call forwarding 113 | 114 | ## License 115 | 116 | Sipd server is distributed under license [MIT](https://github.com/gbazil/sipd/blob/master/LICENSE) 117 | 118 | ## Author 119 | 120 | Vasily Suvorov (gbazil) 121 | 122 | --- 123 | [gbazil](https://gbazil.github.io) Moscow 2018 124 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Vasily Suvorov, http://bazil.pro 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "database/sql" 11 | "flag" 12 | "fmt" 13 | _ "github.com/mattn/go-sqlite3" 14 | "math/rand" 15 | "net" 16 | "os" 17 | "regexp" 18 | "strings" 19 | ) 20 | 21 | const ( 22 | appname = "sipd 1.0.0" 23 | copyright = "(c) Vasily Suvorov " 24 | callidsuffix = "7A7" 25 | ) 26 | 27 | const schema = ` 28 | CREATE table IF NOT EXISTS register ( 29 | name TEXT UNIQUE, 30 | secret TEXT, 31 | addr TEXT, 32 | acode TEXT, 33 | exten TEXT, 34 | callerid TEXT, 35 | dialplan TEXT, 36 | acl TEXT 37 | ); 38 | 39 | CREATE table IF NOT EXISTS cdr ( 40 | callid TEXT UNIQUE, 41 | src TEXT, 42 | callerid TEXT, 43 | acode TEXT, 44 | dst TEXT, 45 | request TEXT, 46 | ts1 INTEGER, 47 | ts2 INTEGER, 48 | ts3 INTEGER, 49 | saddr TEXT, 50 | daddr TEXT, 51 | fromtag TEXT, 52 | totag TEXT 53 | ); 54 | 55 | CREATE table IF NOT EXISTS chain ( 56 | key TEXT UNIQUE, 57 | val TEXT 58 | ); 59 | ` 60 | 61 | var bindaddr *string 62 | var conn *net.UDPConn 63 | var db *sql.DB 64 | var debug *bool 65 | 66 | func main() { 67 | // CLI args 68 | about := flag.Bool("a", false, "print about") 69 | basedir := flag.String("s", ".", "base `directory`") 70 | bindaddr = flag.String("b", "127.0.0.1:5060", "listen `address`") 71 | countrycode := flag.String("c", "7", "current country `code`") 72 | citycode := flag.String("cc", "495", "current city `code`") 73 | debug = flag.Bool("d", false, "debug mode") 74 | reload := flag.Bool("r", false, "reload data from sip.conf") 75 | version := flag.Bool("v", false, "print name, version of the program, current hostname") 76 | 77 | flag.Parse() 78 | 79 | realm, _ := os.Hostname() 80 | os.Chdir(*basedir) 81 | 82 | if *about { 83 | fmt.Printf("%s - Light SIP PBX\n%s\n", appname, copyright) 84 | return 85 | } 86 | 87 | if *version { 88 | fmt.Println(appname, "@", realm) 89 | return 90 | } 91 | 92 | db, _ = sql.Open("sqlite3", "sipd.sqlite3") 93 | if _, err := db.Exec(schema); err != nil { 94 | panic(err) 95 | } 96 | 97 | defer db.Close() 98 | 99 | Conf2DB() 100 | if *reload { 101 | return 102 | } 103 | 104 | if listenaddr, err := net.ResolveUDPAddr("udp", *bindaddr); err != nil { 105 | panic(err) 106 | } else if conn, err = net.ListenUDP("udp", listenaddr); err != nil { 107 | panic(err) 108 | } 109 | 110 | defer conn.Close() 111 | 112 | buf := make([]byte, 5000) 113 | via := "SIP/2.0/UDP " + *bindaddr 114 | 115 | for { 116 | n, saddr, err := conn.ReadFromUDP(buf) 117 | if err != nil { 118 | continue 119 | } 120 | 121 | if *debug { 122 | fmt.Printf("<< %s\n%s\n", saddr, buf[:n]) 123 | } 124 | 125 | // echo response 126 | if n < 50 { 127 | conn.WriteToUDP(buf[:n], saddr) 128 | if *debug { 129 | fmt.Printf(">> %s\n%s\n", saddr, buf[:n]) 130 | } 131 | continue 132 | } 133 | 134 | m := make(SIP) // recreate map SIP 135 | m.Parse(string(buf[:n])) 136 | 137 | if m["User-Agent"] != "" { 138 | m["User-Agent"] = appname 139 | } 140 | 141 | switch m.MethodFrom("Request") { 142 | case "REGISTER": 143 | if m["Authorization"] == "" { 144 | m["Request"] = "SIP/2.0 401 Unauthorized" 145 | m["WWW-Authenticate"] = fmt.Sprintf(`Digest algorithm=MD5, realm="%s", nonce="%s"`, realm, m.RandHexString(4)) 146 | } else { 147 | var name, secret, acl string 148 | db.QueryRow("SELECT name, secret, acl FROM register WHERE name = $1", m.ValueFrom("Authorization", "username")).Scan(&name, &secret, &acl) 149 | if secret == "" || !m.CheckIP(saddr.IP, acl) || m.ValueFrom("Authorization", "response") != m.Digest(secret) { 150 | m["Request"] = "SIP/2.0 403 Forbidden" 151 | } else { 152 | m["Request"] = "SIP/2.0 200 OK" 153 | if m["Expires"] == "0" || strings.Contains(m["Contact"], "expires=0") { 154 | db.Exec("UPDATE register SET addr = '' WHERE name = $1", name) 155 | } else { 156 | db.Exec("UPDATE register SET addr = $1 WHERE name = $2", m.AddrFrom("Contact"), name) 157 | } 158 | } 159 | 160 | delete(m, "Authorization") 161 | } 162 | 163 | m.Reply(saddr) 164 | 165 | case "INVITE": 166 | // AAA 167 | var acode, callerid, dialplan, acl string 168 | err = db.QueryRow("SELECT acode, callerid, dialplan, acl FROM register WHERE addr = $1", 169 | m.AddrFrom("Via")).Scan(&acode, &callerid, &dialplan, &acl) 170 | if err != nil || !m.CheckIP(saddr.IP, acl) { 171 | m["Request"] = "SIP/2.0 403 Forbidden" 172 | m.Reply(saddr) 173 | continue 174 | } 175 | 176 | // re-INVITE check 177 | var addr, dst string 178 | db.QueryRow("SELECT saddr FROM cdr WHERE instr($1, callid) AND ts2 IS NOT NULL", m["Call-ID"]).Scan(&addr) 179 | if addr != "" { 180 | if addr == saddr.String() { 181 | db.QueryRow("SELECT daddr, dst FROM cdr WHERE instr($1, callid)", m["Call-ID"]).Scan(&addr, &dst) 182 | m.NameFor("Request", dst) 183 | m.NameFor("To", dst) 184 | } 185 | m["Via"] = via + "," + m["Via"] // shift Via 186 | m.AddrFor("Request", addr) 187 | m.AddrFor("Contact", *bindaddr) 188 | m.Send(addr) 189 | continue 190 | } 191 | 192 | // pickup 193 | if m.NameFrom("Request") == "*11" { 194 | var callid, fromtag, totag string 195 | db.QueryRow("SELECT callid, saddr, cdr.callerid, fromtag, totag FROM cdr, register WHERE daddr = addr AND register.acode = $1 AND ts2 IS NULL", 196 | acode).Scan(&callid, &addr, &dst, &fromtag, &totag) 197 | if addr != "" { 198 | m.NameFor("Request", dst) 199 | m.AddrFor("Request", addr) 200 | m.NameFor("To", dst) 201 | m["Replaces"] = fmt.Sprintf("%s;to-tag=%s;from-tag=%s;early-only", callid, totag, fromtag) // early-only;100rel; 202 | m["Via"] = via + "," + m["Via"] 203 | m.Send(addr) 204 | continue 205 | } 206 | } 207 | 208 | // services 209 | var exten string 210 | if strings.HasPrefix(m.NameFrom("Request"), "*") { 211 | if !m.CheckDialplan(dialplan) { 212 | m["Request"] = "SIP/2.0 503 Service Unavailable" 213 | m.Reply(saddr) 214 | } else { 215 | name := m.NameFrom("Contact") 216 | cmd := m.NameFrom("Request") 217 | db.QueryRow("SELECT exten FROM register WHERE name = $1", name).Scan(&exten) 218 | switch { 219 | case cmd == "*55": 220 | // just music 221 | case cmd == "*73": // delete any call forward 222 | go m.AgiSend() 223 | exten = regexp.MustCompile(`[=~].*`).ReplaceAllString(exten, "") 224 | db.Exec("UPDATE register SET exten = $1 WHERE name = $2", exten, name) 225 | case cmd == "*71" || cmd == "*72": // check call forward 226 | if forward := regexp.MustCompile(`[=~]\d+`).FindString(exten); forward == "" { 227 | m.NameFor("Request", "*73") 228 | } else if forward[0] == '~' { 229 | m.NameFor("Request", "*71"+forward[1:]) 230 | } else if forward[0] == '=' { 231 | m.NameFor("Request", "*72"+forward[1:]) 232 | } 233 | case strings.HasPrefix(cmd, "*71") && len(cmd) == 13: // setup indirect call forward 234 | go m.AgiSend() 235 | exten = regexp.MustCompile(`[=~].*`).ReplaceAllString(exten, "") 236 | db.Exec("UPDATE register SET exten = $1 WHERE name = $2", exten+"~"+cmd[3:], name) 237 | case strings.HasPrefix(cmd, "*72") && len(cmd) == 13: // setup direct call forward 238 | go m.AgiSend() 239 | exten = regexp.MustCompile(`[=~].*`).ReplaceAllString(exten, "") 240 | db.Exec("UPDATE register SET exten = $1 WHERE name = $2", exten+"="+cmd[3:], name) 241 | case strings.HasPrefix(cmd, "*7") || strings.HasPrefix(cmd, "*1"): 242 | m.NameFor("Request", "*70") // wrong forwarding reply 243 | default: 244 | m.NameFor("Request", "*00") // invalid number 245 | } 246 | 247 | db.QueryRow("SELECT addr FROM register WHERE acode = 'mediagates'").Scan(&addr) 248 | if addr == "" { 249 | m["Request"] = "SIP/2.0 410 Gone" 250 | m.Reply(saddr) 251 | } else { 252 | m["Via"] = via + "," + m["Via"] // shift Via 253 | m.AddrFor("Request", addr) 254 | m.AddrFor("Contact", *bindaddr) 255 | m.Send(addr) 256 | } 257 | } 258 | 259 | continue 260 | } 261 | 262 | // to realm or call transfer 263 | var name string 264 | err = db.QueryRow("SELECT name, addr, exten, callerid FROM register WHERE name = $1 OR (length($1) > 4 AND exten LIKE $1 || '%') OR (length($1) == 4 AND acode == $2 AND (exten LIKE $1 || '%' OR exten LIKE '%#' || $1 || '%'))", 265 | m.NameFrom("Request"), acode).Scan(&name, &addr, &exten, &callerid) 266 | if err != sql.ErrNoRows { 267 | // call transfer to any gate with current callerid 268 | if s := regexp.MustCompile(`=\d{10}`).FindString(exten); s != "" { 269 | m.NameFor("Request", "8"+s[1:]) 270 | } else { 271 | if addr != "" { 272 | m["Via"] = via + "," + m["Via"] // shift Via 273 | m.NameFor("Request", name) 274 | m.AddrFor("Request", addr) 275 | m.NameFor("To", name) 276 | m.AddrFor("Contact", *bindaddr) 277 | m.Send(addr) 278 | } else { 279 | m["Request"] = "SIP/2.0 606 Not Acceptable" 280 | m.Reply(saddr) 281 | } 282 | continue 283 | } 284 | } 285 | 286 | // for indirect forwarding 287 | if clid := m.ValueFrom("Request", "clid"); clid != "" { 288 | callerid = clid 289 | } 290 | 291 | // aim by AGI 292 | if callerid == "" { 293 | // `EXEC TRANSFER "SIP/4956600126@83.102.205.31"` 294 | if transfer := regexp.MustCompile(`\d+@[0-9.:]+`).FindString(m.AgiSend()); transfer != "" { 295 | if !strings.Contains(transfer, ":") { 296 | transfer += ":5060" 297 | } 298 | 299 | m["Request"] = "SIP/2.0 302 Moved Temporarily" 300 | m["Contact"] = fmt.Sprintf("Transfer ", transfer) 301 | } else { 302 | m["Request"] = "SIP/2.0 404 Not Found" 303 | } 304 | 305 | m.Reply(saddr) 306 | continue 307 | } 308 | 309 | // to gate 310 | if gw := regexp.MustCompile(`GW\d+$`).FindString(acode); gw != "" { // international496 311 | db.QueryRow("SELECT addr, '666598' FROM register WHERE name = $1", gw).Scan(&addr, &exten) 312 | } else { 313 | var i int 314 | db.QueryRow("SELECT count(*) FROM register WHERE acode = 'gateways' AND exten != ''").Scan(&i) 315 | if i > 0 { 316 | db.QueryRow("SELECT addr, exten FROM register WHERE acode = 'gateways' AND exten != '' LIMIT 1 OFFSET $1", 317 | rand.Intn(i)).Scan(&addr, &exten) 318 | } 319 | } 320 | 321 | if addr == "" { 322 | m["Request"] = "SIP/2.0 404 Not Found" 323 | } else if !m.CheckDialplan(dialplan) { 324 | m["Request"] = "SIP/2.0 403 Forbidden" 325 | } else { 326 | number := m.NameFrom("Request") 327 | // call number in ABC format 328 | if exten != "9" { 329 | number = strings.TrimPrefix(number, "8") // sity legacy 330 | if len(number) > 10 { 331 | number = strings.TrimPrefix(number, "10") // international legacy 332 | } else if len(number) > 9 { 333 | number = *countrycode + number 334 | } else { 335 | number = *countrycode + *citycode + number 336 | } 337 | } 338 | 339 | m["Via"] = via + "," + m["Via"] // shift Via 340 | m.NameFor("Request", exten+number) 341 | m.AddrFor("Request", addr) 342 | m.NameFor("To", exten+number) 343 | m.NameFor("From", callerid) // set callerid 344 | m.AddrFor("Contact", *bindaddr) 345 | m.Send(addr) 346 | continue 347 | } 348 | 349 | m.Reply(saddr) 350 | 351 | case "SIP/2.0": 352 | // unshift self via (self via may be modified by UAC) 353 | m["Via"] = regexp.MustCompile(`^[^,]*,`).ReplaceAllString(m["Via"], "") 354 | 355 | // CDR managementf and indirect forwarding 356 | callid := strings.TrimSuffix(m["Call-ID"], callidsuffix) 357 | if strings.Contains(m["CSeq"], "INVITE") { 358 | switch regexp.MustCompile(`\d{3}`).FindString(m["Request"]) { 359 | case "100": // Trying 360 | db.Exec("INSERT OR IGNORE INTO cdr VALUES ($1, (SELECT name FROM register WHERE addr = $2), $3, (SELECT acode FROM register WHERE addr = $2), $4, $5, strftime('%s', 'now'), NULL, NULL, $6, $7, '', '')", 361 | callid, m.AddrFrom("Via"), m.NameFrom("From"), m.NameFrom("To"), m["Request"], m.AddrFrom("Via"), saddr.String()) 362 | case "180", "183": // Ringing or Session Progress 363 | db.Exec("UPDATE cdr SET request = $1, fromtag = $2, totag = $3, dst = $4, daddr = $5 WHERE callid = $6", 364 | m["Request"], m.ValueFrom("From", "tag"), m.ValueFrom("To", "tag"), m.NameFrom("To"), saddr.String(), callid) 365 | case "200": // Ok 366 | db.Exec("UPDATE cdr SET request = $1, ts2 = strftime('%s', 'now') WHERE callid = $2 AND ts2 IS NULL", m["Request"], callid) 367 | case "480", "486", "603": // Temporarily Unavailable, Busy Here, Decline 368 | var name, exten, callerid string 369 | db.QueryRow("SELECT name, exten, callerid FROM register WHERE addr = $1", saddr.String()).Scan(&name, &exten, &callerid) 370 | if s := regexp.MustCompile(`~\d{10}`).FindString(exten); s != "" { 371 | m["Request"] = "SIP/2.0 302 Moved Temporarily" 372 | m["Contact"] = fmt.Sprintf("", s[1:], *bindaddr, callerid) 373 | } else if db.QueryRow("SELECT val FROM chain WHERE key = $1", name).Scan(&s); s != "" && s != m.NameFrom("From") { 374 | m["Request"] = "SIP/2.0 302 Moved Temporarily" 375 | m["Contact"] = fmt.Sprintf("", s, *bindaddr) 376 | } else { 377 | db.Exec("UPDATE cdr SET request = $1, ts2 = strftime('%s', 'now'), ts3 = strftime('%s', 'now') WHERE callid = $2", m["Request"], callid) 378 | } 379 | default: 380 | db.Exec("UPDATE cdr SET request = $1, ts2 = strftime('%s', 'now'), ts3 = strftime('%s', 'now') WHERE callid = $2", m["Request"], callid) 381 | } 382 | } else if strings.Contains(m["CSeq"], "BYE") && strings.Contains(m["Request"], "200") { 383 | db.Exec("UPDATE cdr SET request = $1, ts3 = strftime('%s', 'now') WHERE callid = $2", m["Request"], callid) 384 | } 385 | 386 | m.AddrFor("Contact", *bindaddr) 387 | m.Send(m.AddrFrom("Via")) 388 | 389 | default: 390 | var addr, dst string 391 | db.QueryRow("SELECT daddr, dst FROM cdr WHERE callid = $1", m["Call-ID"]).Scan(&addr, &dst) 392 | if addr == "" { 393 | if db.QueryRow("SELECT saddr FROM cdr WHERE callid = $1", strings.TrimSuffix(m["Call-ID"], callidsuffix)).Scan(&addr); addr == "" { 394 | continue 395 | } 396 | } else { 397 | m.NameFor("Request", dst) 398 | m.NameFor("To", dst) 399 | if clid := m.ValueFrom("Request", "clid"); clid != "" { 400 | m.NameFor("From", clid) 401 | } 402 | } 403 | 404 | m.AddrFor("Request", addr) 405 | m.AddrFor("Contact", *bindaddr) 406 | m["Via"] = via + "," + m["Via"] // shift Via 407 | m.Send(addr) 408 | 409 | } 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /sip.conf: -------------------------------------------------------------------------------- 1 | ; sip.conf 2 | ; 3 | 4 | [foo] 5 | accountcode=FOOBAR 6 | context=domestic 7 | secret=abc 8 | permit=192.168/16,10/8 9 | ; short phone number 10 | exten=1001 11 | 12 | [bar] 13 | accountcode=FOOBAR 14 | host=192.168.1.129:5060 15 | context=international 16 | secret=cba 17 | permit=192.168.1.129/32 18 | exten=1002 19 | 20 | [siphone1] 21 | accountcode=HOME 22 | dialplan=.+ 23 | secret=12345 24 | exten=1001(2) 25 | 26 | [siphone2] 27 | accountcode=HOME 28 | dialplan=.+ 29 | secret=4321 30 | ; long, short and call forward phone numbers 31 | exten=4999876543#1002=4991234567 32 | 33 | [GATE1] 34 | accountcode=gateways 35 | host=192.168.1.200:5060 36 | exten=9995999 ; gateway prefix when accountcode is gateways 37 | 38 | [GATE2] 39 | accountcode=gateways 40 | host=192.168.1.201:5060 41 | exten=9 42 | 43 | [MEDIAGATE] 44 | accountcode=mediagates 45 | host=192.168.1.100:5070 46 | 47 | ; EOF 48 | -------------------------------------------------------------------------------- /sip.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Vasily Suvorov, http://bazil.pro 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bufio" 11 | "crypto/md5" 12 | "crypto/rand" 13 | "fmt" 14 | "net" 15 | "regexp" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | const ( 21 | NL = "\r\n" 22 | SP = ": " 23 | ) 24 | 25 | type SIP map[string]string 26 | 27 | func (m SIP) Parse(s string) { 28 | var f bool 29 | for i, v := range strings.Split(s, NL) { 30 | if i == 0 { 31 | m["Request"] = v 32 | } else if !f && len(v) > 0 { 33 | pair := strings.Split(v, SP) 34 | if len(m[pair[0]]) > 0 { 35 | m[pair[0]] = m[pair[0]] + "," + pair[1] 36 | } else { 37 | m[pair[0]] = pair[1] 38 | } 39 | } else { 40 | f = true 41 | } 42 | 43 | if f && len(v) > 0 { 44 | m["Content"] += v + NL 45 | } 46 | } 47 | } 48 | 49 | func (m SIP) String() string { 50 | var s string 51 | for k, v := range m { 52 | if k != "Request" && k != "Content" { 53 | if k == "Via" { 54 | for _, vv := range strings.Split(v, ",") { 55 | s += k + SP + vv + NL 56 | } 57 | } else { 58 | s += k + SP + v + NL 59 | } 60 | } 61 | } 62 | 63 | return m["Request"] + NL + s + NL + m["Content"] 64 | } 65 | 66 | func (m SIP) Clone() SIP { 67 | clone := make(SIP) 68 | for key, val := range m { 69 | clone[key] = val 70 | } 71 | 72 | return clone 73 | } 74 | 75 | func (m SIP) MethodFrom(key string) string { 76 | return regexp.MustCompile(`\S*`).FindString(m[key]) 77 | } 78 | 79 | func (m SIP) NameFrom(key string) string { 80 | return strings.TrimPrefix(regexp.MustCompile(`sip:[^@]*`).FindString(m[key]), "sip:") 81 | } 82 | 83 | func (m SIP) NameFor(key, name string) { 84 | if oldname := m.NameFrom(key); oldname != "" { 85 | m[key] = strings.Replace(m[key], "sip:"+oldname, "sip:"+name, 1) 86 | } 87 | } 88 | 89 | func (m SIP) AddrFrom(key string) (addr string) { 90 | if addr = regexp.MustCompile(`\d+\.\d+\.\d+\.\d+:\d+`).FindString(m[key]); addr == "" { 91 | if addr = regexp.MustCompile(`\d+\.\d+\.\d+\.\d+`).FindString(m[key]); addr == "" { 92 | addr = strings.TrimPrefix(regexp.MustCompile(`@[\w.-]*`).FindString(m[key]), "@") 93 | } 94 | } 95 | 96 | return 97 | } 98 | 99 | func (m SIP) AddrFor(key, addr string) { 100 | if s := m.AddrFrom(key); s != "" { 101 | m[key] = strings.Replace(m[key], s, addr, 1) 102 | } 103 | } 104 | 105 | func (m SIP) ValueFrom(key, value string) string { 106 | s := strings.Replace(m[key], `"`, ``, -1) 107 | return strings.TrimPrefix(regexp.MustCompile(value+`=[^ ,;>]*`).FindString(s), value+`=`) 108 | } 109 | 110 | func (m SIP) Reply(addr *net.UDPAddr) { 111 | delete(m, "Allow") 112 | delete(m, "Supported") 113 | 114 | delete(m, "Content") 115 | delete(m, "Content-Type") 116 | m["Content-Length"] = "0" 117 | 118 | pack := []byte(m.String()) 119 | conn.WriteToUDP(pack, addr) 120 | if *debug { 121 | fmt.Printf(">> %s\n%s\n", addr, pack) 122 | } 123 | } 124 | 125 | func (m SIP) Send(addr string) { 126 | if udpaddr, err := net.ResolveUDPAddr("udp", addr); err == nil { 127 | if strings.HasSuffix(m["Call-ID"], callidsuffix) { 128 | m["Call-ID"] = strings.TrimSuffix(m["Call-ID"], callidsuffix) 129 | } else { 130 | m["Call-ID"] += callidsuffix 131 | } 132 | 133 | pack := []byte(m.String()) 134 | conn.WriteToUDP(pack, udpaddr) 135 | if *debug { 136 | fmt.Printf(">> %s\n%s\n", udpaddr, pack) 137 | } 138 | } 139 | } 140 | 141 | func (m SIP) AgiSend() (ret string) { 142 | var conn net.Conn 143 | var uniqueid, addr string 144 | err := db.QueryRow("SELECT callerid, addr FROM register WHERE acode = 'agigates' ORDER BY name LIMIT 1 OFFSET 0").Scan(&uniqueid, &addr) 145 | if err == nil { 146 | conn, err = net.DialTimeout("tcp", addr, time.Second*2) 147 | if err != nil { 148 | db.QueryRow("SELECT callerid, addr FROM register WHERE acode = 'agigates' ORDER BY name LIMIT 1 OFFSET 1").Scan(&uniqueid, &addr) 149 | conn, err = net.DialTimeout("tcp", addr, time.Second*2) 150 | if err != nil { 151 | return 152 | } 153 | } 154 | } 155 | 156 | defer conn.Close() 157 | 158 | s := fmt.Sprintf("agi_channel: SIP/%s\nagi_uniqueid: %s\nagi_dnid: %s\n\n", m.NameFrom("Contact"), uniqueid, m.NameFrom("Request")) 159 | fmt.Fprint(conn, s) 160 | if *debug { 161 | fmt.Println(">>", addr) 162 | fmt.Println(s) 163 | } 164 | 165 | if ret, err = bufio.NewReader(conn).ReadString('\n'); err == nil { 166 | s = "200 result=1\n\n" 167 | fmt.Fprint(conn, s) 168 | if *debug { 169 | fmt.Println("<<", addr) 170 | fmt.Println(ret) 171 | fmt.Println(">>", addr) 172 | fmt.Println(s) 173 | } 174 | } 175 | 176 | return 177 | } 178 | 179 | func (m SIP) Digest(secret string) string { 180 | // HA1=MD5(username:realm:password) HA2=MD5(method:digestURI) response=MD5(HA1:nonce:HA2) 181 | b1 := []byte(m.ValueFrom("Authorization", "username") + ":" + m.ValueFrom("Authorization", "realm") + ":" + secret) 182 | h1 := fmt.Sprintf("%x", md5.Sum(b1)) 183 | 184 | b2 := []byte(m.MethodFrom("Request") + ":" + m.ValueFrom("Authorization", "uri")) 185 | h2 := fmt.Sprintf("%x", md5.Sum(b2)) 186 | 187 | b3 := []byte(h1 + ":" + m.ValueFrom("Authorization", "nonce") + ":" + h2) 188 | return fmt.Sprintf("%x", md5.Sum(b3)) 189 | } 190 | 191 | func (m SIP) CheckIP(addr net.IP, acl string) bool { 192 | for _, cidr := range strings.Split(acl, ",") { 193 | if _, network, err := net.ParseCIDR(cidr); err == nil && network.Contains(addr) { 194 | return true 195 | } 196 | } 197 | 198 | return acl == "" 199 | } 200 | 201 | func (m SIP) CheckDialplan(dialplan string) bool { 202 | if re, err := regexp.Compile(dialplan); err == nil { 203 | return re.MatchString(strings.TrimPrefix(m.NameFrom("Request"), "8")) 204 | } 205 | 206 | return dialplan == "" 207 | } 208 | 209 | func (m SIP) RandHexString(length int) (s string) { 210 | buf := make([]byte, length) 211 | if _, err := rand.Read(buf); err != nil { 212 | return 213 | } 214 | 215 | s = fmt.Sprintf("%x", buf) 216 | 217 | return 218 | } 219 | -------------------------------------------------------------------------------- /sipconf.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2018 Vasily Suvorov, http://bazil.pro 2 | // 3 | // Use of this source code is governed by an MIT-style 4 | // license that can be found in the LICENSE file. 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io/ioutil" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | ) 16 | 17 | func str2arr(str string) []int { 18 | var a []int 19 | 20 | for _, s := range strings.Split(str, ",") { 21 | i, err := strconv.Atoi(s) 22 | if err != nil { 23 | sa := strings.Split(s, "-") 24 | if len(sa) == 2 { 25 | x, err := strconv.Atoi(sa[0]) 26 | if err != nil { 27 | continue 28 | } 29 | 30 | y, err := strconv.Atoi(sa[1]) 31 | if err != nil { 32 | continue 33 | } 34 | 35 | for z := x; z <= y; z++ { 36 | a = append(a, z) 37 | } 38 | } 39 | } else { 40 | a = append(a, i) 41 | } 42 | } 43 | 44 | return a 45 | } 46 | 47 | func Conf2DB() { 48 | buf, err := ioutil.ReadFile("sip.conf") 49 | if err != nil { 50 | return 51 | } 52 | 53 | sqltext := "BEGIN;\nDELETE FROM register;\n" 54 | for _, bulk := range strings.Split(string(buf), "\n[") { 55 | name := strings.TrimSuffix(regexp.MustCompile(`^\S+]`).FindString(bulk), "]") 56 | if name != "" { 57 | if name == "general" { 58 | continue 59 | } 60 | 61 | exten := strings.TrimPrefix(regexp.MustCompile(`(?m)^exten=\S*`).FindString(bulk), "exten=") 62 | acode := strings.TrimPrefix(regexp.MustCompile(`(?m)^accountcode=\S*`).FindString(bulk), "accountcode=") 63 | secret := strings.TrimPrefix(regexp.MustCompile(`(?m)^secret=\S*`).FindString(bulk), "secret=") 64 | acl := strings.TrimPrefix(regexp.MustCompile(`(?m)^acl=\S*`).FindString(bulk), "acl=") 65 | 66 | addr := strings.TrimPrefix(regexp.MustCompile(`(?m)^host=[0-9.:]*`).FindString(bulk), "host=") 67 | if addr == "" { 68 | // live user registered addr 69 | db.QueryRow("SELECT addr FROM register WHERE name = $1 AND secret = $2", name, secret).Scan(&addr) 70 | } else { 71 | if !strings.Contains(addr, ":") { 72 | addr += ":5060" 73 | } 74 | } 75 | 76 | callerid := strings.TrimPrefix(regexp.MustCompile(`(?m)^callerid=.*$`).FindString(bulk), "callerid=") 77 | if d10 := regexp.MustCompile(`\d{10}`).FindString(callerid); d10 != "" { 78 | callerid = d10 79 | } 80 | 81 | dialplan := strings.TrimPrefix(regexp.MustCompile(`(?m)^dialplan=\S*`).FindString(bulk), "dialplan=") 82 | if dialplan == "" { 83 | switch strings.TrimPrefix(regexp.MustCompile(`(?m)^context=\S*`).FindString(bulk), "context=") { 84 | case "domestic": 85 | dialplan = `^.{2,10}$|^\*.{2,12}$` // 10 R CCC 1234567 = 13 86 | case "mobile": 87 | dialplan = `^9.|^49[59]|^800|^.{2,3}$` 88 | case "local": 89 | dialplan = `^49[59]|^800|^.{2,3}$` 90 | case "trush", "em": 91 | dialplan = `^.{2,3}$` 92 | } 93 | } 94 | 95 | sqltext += fmt.Sprintf("INSERT INTO register VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s');\n", 96 | name, secret, addr, acode, exten, callerid, dialplan, acl) 97 | } 98 | } 99 | 100 | var final string 101 | if _, err := db.Exec(sqltext); err != nil { 102 | final = "ROLLBACK;\n" 103 | } else { 104 | final = "COMMIT\n" 105 | } 106 | 107 | db.Exec(final) 108 | 109 | if *debug { 110 | fmt.Println(sqltext + final) 111 | } 112 | 113 | // compose indirect call forward chains (serial call) 114 | sqltext = "DELETE FROM chain;\n" 115 | rows, _ := db.Query("SELECT name, acode, exten FROM register ORDER BY acode, name") 116 | for rows.Next() { 117 | var name, acode, exten string 118 | if err := rows.Scan(&name, &acode, &exten); err != nil { 119 | continue 120 | } 121 | 122 | if serial := regexp.MustCompile(`\([^)]+`).FindString(exten); serial != "" { 123 | for _, i := range str2arr(strings.TrimPrefix(serial, "(")) { 124 | key := name 125 | err := db.QueryRow("SELECT name FROM register WHERE acode = $1 ORDER BY acode, name LIMIT 1 OFFSET $2 - 1", acode, i).Scan(&name) 126 | if err == nil { 127 | sqltext += fmt.Sprintf("REPLACE INTO chain VALUES ('%s', '%s');\n", key, name) 128 | } 129 | } 130 | } 131 | } 132 | 133 | db.Exec(sqltext) 134 | if *debug { 135 | fmt.Println(sqltext) 136 | } 137 | } 138 | --------------------------------------------------------------------------------