├── reg.png ├── README.md └── sip client.go /reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/framsc/sip-client-in-golang/HEAD/reg.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SIP client in Go 2 | 3 | SIP 相关的软件很多 https://en.wikipedia.org/wiki/List_of_SIP_software, 很多是开源的。我没去读源代码,只是看了下pjsip、osip、eXosip 的API,发现pjsip封装的很好,文档也齐全,osip偏底层,用起来十分不方便,所以eXosip就出现了,它不仅封装了osip,还作了扩展,虽然号称提供了 high-level API,用起来还是没pjsip方便,文档也不完善,只有一个 Doxygen 自动生成的文档。不过用别人的东西总是感觉理解不深刻,如果想真正理解就得自己开发一个。 4 | 5 | ## 结构 6 | 7 | 目前只实现了 SIP 协议中 REGISTER 过程: 8 | 9 | ![wireshark](reg.png) 10 | 11 | 这是软件运行时 Wireshark 抓到的包。 12 | 13 | 发出的包用 sprintf 生成的,收到的包用正则表达解析,然后按SIP协议的要求反馈。代码很简单,不到200行(包括注释),主要是 golang 的net、regex package 强大。 14 | 15 | ## 为什么选择 golang? 16 | 17 | 1. 网络编程十分方便,只要 Dial, Listen, Read Write 就行,比C语言 socket, bind, listen, send, recv ... 方便. 但是 Dial 可以 Dial("tcp") 和 Dial("udp"), Liten却不是Listen("tcp") 和 Listen("udp"), 而是 Listen(叫 ListenTCP 对称一些,也能忍,可以它就是Listen) 和 ListenUDP,看来还是很别扭。 18 | 2. golang 有 Google 作靠山。 19 | 20 | 另外我还写了个 C 语言版的 client: 21 | https://github.com/frams163com/sip_client 22 | 23 | go 貌似没有很强大的 SIP 软件,考虑到 go 可以直接调用 C library,做了个exosip封装,还在完善中。。。 24 | 25 | 26 | 做完这个就可以学习媒体部分了。 27 | 28 | -------------------------------------------------------------------------------- /sip client.go: -------------------------------------------------------------------------------- 1 | // 小驼峰命名用来表示自定义变量, 并且只在这个package内使用,大驼峰命名的变量package外可用 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "regexp" 8 | "net" 9 | "strings" 10 | "strconv" 11 | "crypto/md5" 12 | ) 13 | // 14 | /* 15 | WWW-Authenticate = "WWW-Authenticate" HCOLON challenge 16 | challenge = ("Digest" LWS digest-cln *(COMMA digest-cln)) 17 | / other-challenge 18 | other-challenge = auth-scheme LWS auth-param 19 | *(COMMA auth-param) 20 | digest-cln = realm / domain / nonce 21 | / opaque / stale / algorithm 22 | / qop-options / auth-param 23 | realm = "realm" EQUAL realm-value 24 | realm-value = quoted-string 25 | domain = "domain" EQUAL LDQUOT URI 26 | *( 1*SP URI ) RDQUOT 27 | URI = absoluteURI / abs-path 28 | nonce = "nonce" EQUAL nonce-value 29 | nonce-value = quoted-string 30 | opaque = "opaque" EQUAL quoted-string 31 | stale = "stale" EQUAL ( "true" / "false" ) 32 | algorithm = "algorithm" EQUAL ( "MD5" / "MD5-sess" 33 | / token ) 34 | qop-options = "qop" EQUAL LDQUOT qop-value 35 | *("," qop-value) RDQUOT 36 | qop-value = "auth" / "auth-int" / token 37 | */ 38 | // 401 Unauthorized message 中 WWW-Authenticate 字段 39 | // belle-sip 直接用 ANTLAR 生成 parser, osip 没有依赖 parser generator 40 | // 从 BNF 语法中可以看到它属于正则语法, 用正则表达式就能搞定 41 | // 这里只用了 golang 自带的正则表达式, 功能比标准parser 弱 42 | // 自己手写了一个 parser 复杂度为 O(n) 43 | // 后来发现 golang 的正则表达式匹配复杂度也是 O(n),就把自己手写的抛弃了 44 | 45 | func parseAuth(line string, hashedMsg map[string]string){ 46 | // 严格讲应该是 parseResult := regexp.MustCompile(`([a-zA-Z]+)=(?:")(.+?)\1`).FindAllStringSubmatch(line,-1) 保证引号配对,可惜 google 为了效率没有支持backreference 47 | parseResult := regexp.MustCompile(`([a-zA-Z]+)="(.+?)"`).FindAllStringSubmatch(line,-1) 48 | for _,x := range parseResult { 49 | hashedMsg[x[1]] = x[2] 50 | } 51 | } 52 | 53 | func md5Sum(s string) string{ 54 | return fmt.Sprintf("%x", md5.Sum([]byte(s))) 55 | } 56 | 57 | func dumpMsg(){ 58 | 59 | } 60 | 61 | // 采用同步阻塞IO方式 62 | // 没有完整实现RFC3261,目前只实现 REGISTER 过程 63 | func main() { 64 | // input parameters 65 | user := "777" 66 | extension := 777 67 | pass := "abc123456" 68 | serverIPPort := "192.168.1.112:5060" 69 | 70 | conn, err := net.Dial("udp", serverIPPort) 71 | if err != nil { 72 | fmt.Println(err) 73 | } 74 | localAddr := conn.LocalAddr() 75 | localAddrStr := strings.Split(localAddr.String(),":") 76 | 77 | serverAddr := conn.RemoteAddr() 78 | serverAddrStr := strings.Split(serverAddr.String(),":") 79 | 80 | type Sess_t struct{ 81 | serverIP string 82 | serverPort int 83 | localIP string 84 | localPort int 85 | user string 86 | pass string 87 | extension int 88 | } 89 | 90 | var sess Sess_t 91 | sess.serverIP = serverAddrStr[0] 92 | sess.serverPort, err = strconv.Atoi(serverAddrStr[1]) 93 | sess.localIP = localAddrStr[0] 94 | sess.localPort, err = strconv.Atoi(localAddrStr[1]) 95 | sess.user = user 96 | sess.pass = pass 97 | sess.extension = extension 98 | 99 | // REGISTER Request 100 | lines := []string{ 101 | "REGISTER sip:%s:%d SIP/2.0\n", 102 | "Via: SIP/2.0/UDP %s:%d;branch=cbranch1\n", 103 | "Max-Forwards: 70\n", 104 | "To: %s \n", 105 | "From: %s ;tag=456789\n", 106 | "Call-ID: ChangeToRandom\n", 107 | "CSeq: %d REGISTER\n", 108 | "Contact: %s \n", 109 | "Expires: %d\n", 110 | "Content-Length: 0\n\n\n", 111 | } 112 | fmt1 := strings.Join(lines,"") 113 | registerRequest := fmt.Sprintf(fmt1, sess.localIP, sess.localPort, 114 | sess.serverIP, sess.serverPort, 115 | sess.user, sess.extension, sess.serverIP, sess.serverPort, 116 | sess.user, sess.extension, sess.serverIP, sess.serverPort, 117 | 1, 118 | sess.user, sess.extension, sess.localIP, sess.localPort, 119 | 3600) 120 | conn.Write([]byte(registerRequest)) 121 | fmt.Println("send completed, and start recving 1") 122 | 123 | ln, err := net.ListenUDP("udp4", &net.UDPAddr{ 124 | IP: net.IPv4(0, 0, 0, 0), 125 | Port: 5060, 126 | }) 127 | 128 | data := make([]byte, 4096) 129 | _, _, err = ln.ReadFromUDP(data) 130 | 131 | // parse received message 132 | msgList :=strings.Split(string(data),"\r\n") 133 | 134 | // turn string formatted message to map data structure 135 | msgHashed :=make(map[string]string) 136 | for _, x := range msgList[1:]{ 137 | colonpos:= strings.IndexByte(x,':') 138 | if colonpos > 0 { 139 | msgHashed[x[:colonpos]] = x[colonpos+2:] 140 | } 141 | } 142 | 143 | parseAuth((msgHashed["WWW-Authenticate"] + ",")[7:], msgHashed) // 默认用的是Digest,Basic明显有缺陷,密码容易被盗 144 | fmt.Println("analysis complete") 145 | 146 | // digest access authentication 147 | msgHashed["uri"] = "sip:" + sess.serverIP 148 | HA1:= user + ":" + msgHashed["realm"] + ":" + pass 149 | HA2:= "REGISTER" + ":" + msgHashed["uri"] 150 | msgHashed["responce"] = md5Sum(md5Sum(HA1) + ":" + msgHashed["nonce"] + ":" + md5Sum(HA2)) 151 | 152 | // send credentials this time 153 | lines = []string{ 154 | "REGISTER sip:%s:%d SIP/2.0\n", 155 | "Via: SIP/2.0/UDP %s:%d;branch=cbranch1\n", 156 | "Max-Forwards: 70\n", 157 | "To: %s \n", 158 | "From: %s ;tag=456789\n", 159 | "Call-ID: ChangeToRandom\n", 160 | "CSeq: %d REGISTER\n", 161 | "Contact: %s \n", 162 | "Expires: %d\n", 163 | "Allow: PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS\n", 164 | `Authorization: Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", algorithm=MD5 165 | `, 166 | "Content-Length: 0\n\n\n", 167 | } 168 | fmt1 = strings.Join(lines,"") 169 | registerRequest = fmt.Sprintf(fmt1, sess.localIP, sess.localPort, 170 | sess.serverIP, sess.serverPort, 171 | sess.user, sess.extension, sess.serverIP, sess.serverPort, 172 | sess.user, sess.extension, sess.serverIP, sess.serverPort, 173 | 2, 174 | sess.user, sess.extension, sess.localIP, sess.localPort, 175 | 3600, 176 | sess.user, msgHashed["realm"], msgHashed["nonce"], msgHashed["uri"], msgHashed["responce"]) 177 | fmt.Println(registerRequest) 178 | 179 | conn.Write([]byte(registerRequest)) 180 | fmt.Println("send completed, and start recving 2") 181 | 182 | _, _, err = ln.ReadFromUDP(data) 183 | fmt.Printf("%s\n\n", data) 184 | 185 | for { 186 | n, err:=conn.Read(data) 187 | if err != nil { 188 | fmt.Println(err) 189 | } 190 | if n>0 { 191 | fmt.Printf("%s",data) 192 | dumpMsg() 193 | break 194 | } 195 | } 196 | conn.Close() 197 | ln.Close() 198 | 199 | } 200 | --------------------------------------------------------------------------------