Browse Source

I have no idea what I'm doing.

pull/2/head
Justine Alexandra Roberts Tunney 11 years ago
parent
commit
eae40bdf3e
14 changed files with 515 additions and 397 deletions
  1. +1
    -0
      README.md
  2. +9
    -9
      example/echo/echo_test.go
  3. +28
    -39
      example/echo2/echo2_test.go
  4. +3
    -3
      example/options/options_test.go
  5. +25
    -5
      sip/addr.go
  6. +29
    -0
      sip/compact.go
  7. +45
    -32
      sip/messages.go
  8. +70
    -60
      sip/msg.go
  9. +0
    -96
      sip/phrases.go
  10. +25
    -28
      sip/prefs.go
  11. +182
    -0
      sip/status.go
  12. +87
    -121
      sip/transport.go
  13. +3
    -4
      sip/via.go
  14. +8
    -0
      util/util.go

+ 1
- 0
README.md View File

@ -66,3 +66,4 @@ This is what a sip stack looks like:
- [SDP (RFC 4566)](https://tools.ietf.org/html/rfc4566)
- [RTP (RFC 3550)](https://tools.ietf.org/html/rfc3550)
- [RTP DTMF (RFC 4733)](https://tools.ietf.org/html/rfc4733)
- [SIP 100rel (RFC 3262)](https://tools.ietf.org/html/rfc3262)

+ 9
- 9
example/echo/echo_test.go View File

@ -227,9 +227,9 @@ func TestCallToEchoApp(t *testing.T) {
t.Fatal("read 100 trying:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err := sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 100 trying", err)
msg := sip.ParseMsg(string(memory[0:amt]))
if msg.Error != nil {
t.Fatal("parse 100 trying", msg.Error)
}
if !msg.IsResponse || msg.Status != 100 || msg.Phrase != "Trying" {
t.Fatal("didn't get 100 trying :[")
@ -242,9 +242,9 @@ func TestCallToEchoApp(t *testing.T) {
t.Fatal("read 200 ok:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 200 ok:", err)
msg = sip.ParseMsg(string(memory[0:amt]))
if msg.Error != nil {
t.Fatal("parse 200 ok:", msg.Error)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted 200 ok but got:", msg.Status, msg.Phrase)
@ -346,9 +346,9 @@ func TestCallToEchoApp(t *testing.T) {
if err != nil {
t.Fatal(err)
}
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal(err)
msg = sip.ParseMsg(string(memory[0:amt]))
if msg.Error != nil {
t.Fatal(msg.Error)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted bye response 200 ok but got:", msg.Status, msg.Phrase)


+ 28
- 39
example/echo2/echo2_test.go View File

@ -3,7 +3,6 @@
package echo2_test
import (
"bytes"
"github.com/jart/gosip/rtp"
"github.com/jart/gosip/sdp"
"github.com/jart/gosip/sip"
@ -19,10 +18,11 @@ func TestCallToEchoApp(t *testing.T) {
from := &sip.Addr{Uri: &sip.URI{Host: "127.0.0.1"}}
// Create the SIP UDP transport layer.
tp, err := sip.NewUDPTransport(from)
tp, err := sip.NewTransport(from)
if err != nil {
t.Fatal(err)
}
defer tp.Sock.Close()
// Create an RTP socket.
rtpsock, err := net.ListenPacket("udp", "108.61.60.146:0")
@ -40,46 +40,35 @@ func TestCallToEchoApp(t *testing.T) {
t.Fatal(err)
}
// Receive provisional 100 Trying.
conn.SetDeadline(time.Now().Add(time.Second))
memory := make([]byte, 2048)
amt, err := conn.Read(memory)
if err != nil {
t.Fatal("read 100 trying:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err := sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 100 trying", err)
}
if !msg.IsResponse || msg.Status != 100 || msg.Phrase != "Trying" {
t.Fatal("didn't get 100 trying :[")
}
// Receive 200 OK.
conn.SetDeadline(time.Now().Add(5 * time.Second))
amt, err = conn.Read(memory)
if err != nil {
t.Fatal("read 200 ok:", err)
}
log.Printf("<<< %s\n%s\n", raddr, string(memory[0:amt]))
msg, err = sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal("parse 200 ok:", err)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {
t.Fatal("wanted 200 ok but got:", msg.Status, msg.Phrase)
}
if msg.Payload == "" || msg.Headers["Content-Type"] != "application/sdp" {
t.Fatal("200 ok didn't have sdp payload")
// Consume provisional messages until we receive answer.
var rrtpaddr *net.UDPAddr
for {
tp.Sock.SetDeadline(time.Now().Add(time.Second))
msg := tp.Recv()
if msg.Error != nil {
t.Fatal(msg.Error)
}
if msg.Status < sip.StatusOK {
log.Printf("Got provisional %d %s", msg.Status, msg.Phrase)
}
if msg.Headers["Content-Type"] == "application/sdp" {
log.Printf("Establishing media session")
rsdp, err := sdp.Parse(msg.Payload)
if err != nil {
t.Fatal("failed to parse sdp", err)
}
rrtpaddr = &net.UDPAddr{IP: net.ParseIP(rsdp.Addr), Port: int(rsdp.Audio.Port)}
}
if msg.Status == sip.StatusOK {
log.Printf("Answered!")
break
} else if msg.Status > sip.StatusOK {
t.Fatalf("Got %d %s", msg.Status, msg.Phrase)
NewAck(invite)
}
}
// Figure out where they want us to send RTP.
rsdp, err := sdp.Parse(msg.Payload)
if err != nil {
t.Fatal("failed to parse sdp", err)
}
rrtpaddr := &net.UDPAddr{IP: net.ParseIP(rsdp.Addr), Port: int(rsdp.Audio.Port)}
// Acknowledge the 200 OK to answer the call.
var ack sip.Msg


+ 3
- 3
example/options/options_test.go View File

@ -81,9 +81,9 @@ func TestOptions(t *testing.T) {
t.Fatal(err)
}
msg, err := sip.ParseMsg(string(memory[0:amt]))
if err != nil {
t.Fatal(err)
msg := sip.ParseMsg(string(memory[0:amt]))
if msg.Error != nil {
t.Fatal(msg.Error)
}
if !msg.IsResponse || msg.Status != 200 || msg.Phrase != "OK" {


+ 25
- 5
sip/addr.go View File

@ -22,7 +22,6 @@ import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"log"
"strings"
)
@ -45,7 +44,7 @@ func ParseAddr(s string) (addr *Addr, err error) {
// Extract display.
switch n := strings.IndexAny(s, "\"<"); {
case n < 0:
return nil, errors.New("invalid address")
return nil, errors.New("Invalid address")
case s[n] == '<': // Display is not quoted.
addr.Display, s = strings.Trim(s[0:n], " "), s[n+1:]
case s[n] == '"': // We found an opening quote.
@ -58,7 +57,7 @@ func ParseAddr(s string) (addr *Addr, err error) {
break LOL
case '\\': // Escape sequence.
if len(s) < 2 {
return nil, errors.New("evil quote escape")
return nil, errors.New("Evil quote escape")
}
switch s[1] {
case '"':
@ -73,7 +72,7 @@ func ParseAddr(s string) (addr *Addr, err error) {
}
}
if s == "" {
return nil, errors.New("no closing quote in display")
return nil, errors.New("No closing quote in display")
}
for s != "" {
c := s[0]
@ -111,7 +110,7 @@ func ParseAddr(s string) (addr *Addr, err error) {
if s != "" {
addr.Next, err = ParseAddr(s)
if err != nil {
log.Println("[NOTICE]", "dropping invalid bonus addr:", s, err)
return nil, err
}
}
}
@ -189,3 +188,24 @@ func (addr *Addr) Last() *Addr {
}
return addr
}
// Returns number of items in the linked list.
func (addr *Addr) Len() int {
count := 0
for ; addr != nil; addr = addr.Next {
count++
}
return count
}
// Returns self with linked list reversed.
func (addr *Addr) Reversed() *Addr {
var res *Addr
for ; addr != nil; addr = addr.Next {
a := new(Addr)
*a = *addr
a.Next = res
res = a
}
return addr
}

+ 29
- 0
sip/compact.go View File

@ -0,0 +1,29 @@
// SIP Compact Header Definitions
package sip
func uncompactHeader(k string) string {
if header, ok := compactHeaders[k]; ok {
return header
}
return k
}
// http://www.cs.columbia.edu/sip/compact.html
var compactHeaders = map[string]string{
"a": "Accept-Contact",
"b": "Referred-By",
"c": "Content-Type",
"e": "Content-Encoding",
"f": "From",
"i": "Call-ID",
"k": "Supported",
"l": "Content-Length",
"m": "Contact",
"o": "Event",
"r": "Refer-To",
"s": "Subject",
"t": "To",
"u": "Allow-Events",
"v": "Via",
}

+ 45
- 32
sip/messages.go View File

@ -6,49 +6,55 @@ import (
"log"
)
func NewRequest(tp Transport, method string, to, from *Addr) *Msg {
const (
GosipUserAgent = "gosip/1.o"
GosipAllow = "INVITE, ACK, CANCEL, BYE, OPTIONS"
)
func NewRequest(tp *Transport, method string, to, from *Addr) *Msg {
return &Msg{
Method: method,
Request: to.Uri.Copy(),
Via: tp.Via().Branch(),
From: from.Or(tp.Contact()).Tag(),
Via: tp.Via.Copy().Branch(),
From: from.Or(tp.Contact).Tag(),
To: to.Copy(),
Contact: tp.Contact(),
Contact: tp.Contact,
CallID: util.GenerateCallID(),
CSeq: util.GenerateCSeq(),
CSeqMethod: method,
Headers: Headers{"User-Agent": "gosip/1.o"},
Headers: DefaultHeaders(),
}
}
func NewResponse(msg *Msg, status int) *Msg {
return &Msg{
IsResponse: true,
Status: status,
Phrase: Phrases[status],
Via: msg.Via,
From: msg.From,
To: msg.To,
CallID: msg.CallID,
CSeq: msg.CSeq,
CSeqMethod: msg.CSeqMethod,
Headers: Headers{"User-Agent": "gosip/1.o"},
IsResponse: true,
Status: status,
Phrase: Phrase(status),
Via: msg.Via,
From: msg.From,
To: msg.To,
CallID: msg.CallID,
CSeq: msg.CSeq,
CSeqMethod: msg.CSeqMethod,
RecordRoute: msg.RecordRoute,
Headers: DefaultHeaders(),
}
}
// http://tools.ietf.org/html/rfc3261#section-17.1.1.3
func NewAck(invite *Msg) *Msg {
func NewAck(original, last *Msg) *Msg {
return &Msg{
Method: "ACK",
Request: invite.Request,
Via: invite.Via,
From: invite.From,
To: invite.To,
CallID: invite.CallID,
CSeq: invite.CSeq,
Request: original.Request,
Via: original.Via.Copy().SetNext(nil),
From: original.From,
To: last.To,
CallID: original.CallID,
CSeq: original.CSeq,
CSeqMethod: "ACK",
Route: invite.Route,
Headers: Headers{"User-Agent": "gosip/1.o"},
Route: last.RecordRoute.Reversed(),
Headers: DefaultHeaders(),
}
}
@ -65,23 +71,23 @@ func NewCancel(invite *Msg) *Msg {
CallID: invite.CallID,
CSeq: invite.CSeq,
CSeqMethod: "CANCEL",
Route: invite.Route,
Headers: Headers{"User-Agent": "gosip/1.o"},
Route: invite.RecordRoute.Reversed(),
Headers: DefaultHeaders(),
}
}
func NewBye(last, invite, ok200 *Msg) *Msg {
func NewBye(invite, last *Msg) *Msg {
return &Msg{
Request: ok200.Contact.Uri,
Via: invite.Via.Branch(),
Method: "BYE",
Request: last.Contact.Uri,
Via: invite.Via,
From: last.From,
To: last.To,
CallID: last.CallID,
Method: "BYE",
CSeq: last.CSeq + 1,
CSeqMethod: "BYE",
Route: ok200.RecordRoute,
Headers: make(map[string]string),
Route: last.RecordRoute.Reversed(),
Headers: DefaultHeaders(),
}
}
@ -109,3 +115,10 @@ func AttachSDP(msg *Msg, sdp *sdp.SDP) {
msg.Headers["Content-Type"] = "application/sdp"
msg.Payload = sdp.String()
}
func DefaultHeaders() Headers {
return Headers{
"User-Agent": GosipUserAgent,
"Allow": GosipAllow,
}
}

+ 70
- 60
sip/msg.go View File

@ -5,7 +5,9 @@ package sip
import (
"bytes"
"errors"
"fmt"
"log"
"net"
"strconv"
"strings"
)
@ -15,17 +17,19 @@ type Headers map[string]string
// Msg represents a SIP message. This can either be a request or a response.
// These fields are never nil unless otherwise specified.
type Msg struct {
magic int // Used for pseudo msgs or to mark direction
OutboundProxy string // Use Route instead if possible
// Special non-SIP fields.
Error error // Set to indicate an error with this message
SourceAddr *net.UDPAddr // Set by transport layer as received address
IsResponse bool // This is a response (like 404 GO DIE)
// Fields that aren't headers.
IsResponse bool // This is a response (like 404 Not Found)
Method string // Indicates type of request (if request)
Request *URI // dest URI (nil if response)
Status int // Indicates happiness of response (if response)
Phrase string // Explains happiness of response (if response)
Payload string // Stuff that comes after two line breaks
// Mandatory headers
// Mandatory headers.
Via *Via // Linked list of agents traversed (must have one)
Route *Addr // Used for goose routing and loose routing
RecordRoute *Addr // Used for loose routing
@ -60,21 +64,27 @@ func (msg *Msg) String() string {
}
// Parses a SIP message into a data structure. This takes ~70 µs on average.
func ParseMsg(packet string) (msg *Msg, err error) {
func ParseMsg(packet string) (msg *Msg) {
msg = new(Msg)
if packet == "" {
return nil, errors.New("empty msg")
msg.Error = errors.New("Empty msg")
return msg
}
if n := strings.Index(packet, "\r\n\r\n"); n > 0 {
packet, msg.Payload = packet[0:n], packet[n+4:]
}
lines := strings.Split(packet, "\r\n")
if lines == nil || len(lines) < 2 {
return nil, errors.New("too few lines")
msg.Error = errors.New("Too few lines")
return msg
}
var k, v string
var okVia, okTo, okFrom, okCallID, okComputer bool
err = msg.parseFirstLine(lines[0])
err := msg.parseFirstLine(lines[0])
if err != nil {
msg.Error = err
return msg
}
hdrs := lines[1:]
msg.Headers = make(map[string]string, len(hdrs))
msg.MaxForwards = 70
@ -94,6 +104,7 @@ func ParseMsg(packet string) (msg *Msg, err error) {
if k == "" || v == "" {
log.Println("[NOTICE]", "blank header found", hdr)
}
k = uncompactHeader(k)
} else {
log.Println("[NOTICE]", "header missing delimiter", hdr)
continue
@ -107,27 +118,29 @@ func ParseMsg(packet string) (msg *Msg, err error) {
okVia = true
*viap, err = ParseVia(v)
if err != nil {
return nil, errors.New("Via header - " + err.Error())
msg.Error = errors.New("Bad Via header: " + err.Error())
} else {
viap = &(*viap).Next
}
viap = &(*viap).Next
case "to":
okTo = true
msg.To, err = ParseAddr(v)
if err != nil {
return nil, errors.New("To header - " + err.Error())
msg.Error = errors.New("Bad To header: " + err.Error())
}
case "from":
okFrom = true
msg.From, err = ParseAddr(v)
if err != nil {
return nil, errors.New("From header - " + err.Error())
msg.Error = errors.New("Bad From header: " + err.Error())
}
case "contact":
*contactp, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Contact header - " + err.Error())
msg.Error = errors.New("Bad Contact header: " + err.Error())
} else {
contactp = &(*contactp).Last().Next
}
contactp = &(*contactp).Next
case "cseq":
okComputer = false
if n := strings.Index(v, " "); n > 0 {
@ -138,90 +151,68 @@ func ParseMsg(packet string) (msg *Msg, err error) {
}
}
if !okComputer {
return nil, errors.New("Bad CSeq Header")
msg.Error = errors.New("Bad CSeq Header")
}
case "content-length":
if cl, err := strconv.Atoi(v); err == nil {
if cl > len(msg.Payload) {
msg.Payload = msg.Payload[0:cl]
log.Println("[DEBUG]", "discarding extra sip payload bytes")
} else if cl > len(msg.Payload) {
return nil, errors.New("content-length > len(payload)")
if cl != len(msg.Payload) {
msg.Error = errors.New(fmt.Sprintf(
"Content-Length (%d) differs from payload length (%d)",
cl, len(msg.Payload)))
}
} else {
return nil, errors.New("Bad Content-Length header")
msg.Error = errors.New("Bad Content-Length header")
}
case "expires":
if cl, err := strconv.Atoi(v); err == nil && cl >= 0 {
msg.Expires = cl
} else {
return nil, errors.New("Bad Expires header")
msg.Error = errors.New("Bad Expires header")
}
case "min-expires":
if cl, err := strconv.Atoi(v); err == nil && cl > 0 {
msg.MinExpires = cl
} else {
return nil, errors.New("Bad Min-Expires header")
msg.Error = errors.New("Bad Min-Expires header")
}
case "max-forwards":
if cl, err := strconv.Atoi(v); err == nil && cl > 0 {
msg.MaxForwards = cl
} else {
return nil, errors.New("Bad Max-Forwards header")
msg.Error = errors.New("Bad Max-Forwards header")
}
case "route":
*routep, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad Route header: " + err.Error())
msg.Error = errors.New("Bad Route header: " + err.Error())
} else {
routep = &(*routep).Last().Next
}
routep = &(*routep).Next
case "record-route":
*rroutep, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad Record-Route header: " + err.Error())
msg.Error = errors.New("Bad Record-Route header: " + err.Error())
} else {
rroutep = &(*rroutep).Last().Next
}
rroutep = &(*rroutep).Next
case "p-asserted-identity":
msg.Paid, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad P-Asserted-Identity header: " + err.Error())
msg.Error = errors.New("Bad P-Asserted-Identity header: " + err.Error())
}
case "remote-party-id":
msg.Rpid, err = ParseAddr(v)
if err != nil {
return nil, errors.New("Bad Remote-Party-ID header: " + err.Error())
msg.Error = errors.New("Bad Remote-Party-ID header: " + err.Error())
}
default:
msg.Headers[k] = v
}
}
if !okVia || !okTo || !okFrom || !okCallID || !okComputer {
return nil, errors.New("Missing mandatory headers")
msg.Error = errors.New("Missing mandatory headers")
}
return msg, nil
}
func (msg *Msg) parseFirstLine(s string) (err error) {
toks := strings.Split(s, " ")
if toks != nil && len(toks) == 3 && toks[2] == "SIP/2.0" {
msg.Phrase = ""
msg.Status = 0
msg.Method = toks[0]
msg.Request = new(URI)
msg.Request, err = ParseURI(toks[1])
} else if toks != nil && len(toks) == 3 && toks[0] == "SIP/2.0" {
msg.IsResponse = true
msg.Method = ""
msg.Request = nil
msg.Phrase = toks[2]
msg.Status, err = strconv.Atoi(toks[1])
if err != nil {
return errors.New("Invalid status")
}
} else {
err = errors.New("Bad protocol or request line")
}
return err
return
}
func (msg *Msg) Copy() *Msg {
@ -267,11 +258,7 @@ func (msg *Msg) Append(b *bytes.Buffer) error {
return errors.New("Msg.Status >= 700")
}
if msg.Phrase == "" {
if v, ok := Phrases[msg.Status]; ok {
msg.Phrase = v
} else {
return errors.New("Msg.Phrase not set and Msg.Status is unknown")
}
msg.Phrase = Phrase(msg.Status)
}
b.WriteString("SIP/2.0 ")
b.WriteString(strconv.Itoa(msg.Status))
@ -376,3 +363,26 @@ func (msg *Msg) Append(b *bytes.Buffer) error {
return nil
}
func (msg *Msg) parseFirstLine(s string) (err error) {
toks := strings.Split(s, " ")
if toks != nil && len(toks) == 3 && toks[2] == "SIP/2.0" {
msg.Phrase = ""
msg.Status = 0
msg.Method = toks[0]
msg.Request = new(URI)
msg.Request, err = ParseURI(toks[1])
} else if toks != nil && len(toks) == 3 && toks[0] == "SIP/2.0" {
msg.IsResponse = true
msg.Method = ""
msg.Request = nil
msg.Phrase = toks[2]
msg.Status, err = strconv.Atoi(toks[1])
if err != nil {
return errors.New("Invalid status")
}
} else {
err = errors.New("Bad protocol or request line")
}
return err
}

+ 0
- 96
sip/phrases.go View File

@ -1,96 +0,0 @@
// Standard SIP Protocol Messages
//
// http://www.iana.org/assignments/sip-parameters
package sip
var Phrases = map[int]string{
// 1xx: Provisional -- request received, continuing to process the
// request;
100: "Trying", // indicates server is not totally pwnd
180: "Ringing", // i promise the remote phone is ringing
181: "Call Is Being Forwarded",
182: "Queued",
183: "Session Progress", // establish early media (pstn ringback)
// 2xx: Success -- the action was successfully received, understood,
// and accepted;
200: "OK", // call is answered
202: "Accepted", // [RFC3265]
204: "No Notification", // [RFC5839]
// 3xx: Redirection -- further action needs to be taken in order to
// complete the request;
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily", // send your call there instead kthx
305: "Use Proxy", // you fool! send your call there instead
380: "Alternative Service",
// 4xx: Client Error -- the request contains bad syntax or cannot be
// fulfilled at this server;
400: "Bad Request", // missing headers, bad format, etc.
401: "Unauthorized", // resend request with auth header
402: "Payment Required", // i am greedy
403: "Forbidden", // gtfo
404: "Not Found", // wat?
405: "Method Not Allowed", // i don't support that type of request
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Timeout",
409: "Conflict",
410: "Gone", // shaniqua don't live here no more
411: "Length Required",
412: "Conditional Request Failed", // [RFC3903]
413: "Request Entity Too Large",
414: "Request-URI Too Long",
415: "Unsupported Media Type",
416: "Unsupported URI Scheme",
417: "Unknown Resource-Priority",
420: "Bad Extension",
421: "Extension Required",
422: "Session Interval Too Small", // [RFC4028]
423: "Interval Too Brief",
428: "Use Identity Header", // [RFC4474]
429: "Provide Referrer Identity", // [RFC3892]
430: "Flow Failed", // [RFC5626]
433: "Anonymity Disallowed", // [RFC5079]
436: "Bad Identity-Info", // [RFC4474]
437: "Unsupported Certificate", // [RFC4474]
438: "Invalid Identity Header", // [RFC4474]
439: "First Hop Lacks Outbound Support", // [RFC5626]
440: "Max-Breadth Exceeded", // [RFC5393]
470: "Consent Needed", // [RFC5360]
480: "Temporarily Unavailable", // fast busy or soft fail
481: "Call/Transaction Does Not Exist", // bad news
482: "Loop Detected", // froot looping
483: "Too Many Hops", // froot looping
484: "Address Incomplete",
485: "Ambiguous",
486: "Busy Here",
487: "Request Terminated",
488: "Not Acceptable Here",
489: "Bad Event", // [RFC3265]
491: "Request Pending",
493: "Undecipherable",
494: "Security Agreement Required", // [RFC3329]
// 5xx: Server Error -- the server failed to fulfill an apparently
// valid request;
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "Version Not Supported",
513: "Message Too Large",
580: "Precondition Failure", // [RFC3312]
// 6xx: Global Failure -- the request cannot be fulfilled at any
// server.
600: "Busy Everywhere",
603: "Decline",
604: "Does Not Exist Anywhere",
606: "Not Acceptable",
687: "Dialog Terminated",
}

+ 25
- 28
sip/prefs.go View File

@ -1,7 +1,7 @@
// Global Settings. You can change these at startup to fine tune
// certain behaviors.
package gosip
package sip
import (
"os"
@ -9,54 +9,51 @@ import (
)
var (
// how often to check for shutdowns
// How often to check for shutdowns.
DeathClock = 200 * time.Millisecond
// sip egress msg length must not exceed me. if you are brave and
// use jumbo frames you can increase this
// SIP egress msg length must not exceed me. If you are brave and use jumbo
// frames you can increase this value.
MTU = 1450
// maximum number SRV/Redirects/Etc. to entertain. more
// accurately, this is the maximum number of time's we're allowed
// to enter the "calling" state.
// Maximum number SRV/Redirects/Etc. to entertain. More accurately, this is
// the maximum number of time's we're allowed to enter the "calling" state.
MaxAttempts = 10
// this is how long to wait (in nanoseconds) for a 100 trying
// response before retransmitting the invite. multiply this by
// RetransmitAttempts and that's how long it'll be before we try
// another server. that might seem like a very long time but
// happy networks facilitate fast failover by sending hard errors
// (ICMP, 480, 500-599)
// This is how long to wait (in nanoseconds) for a 100 trying response before
// retransmitting the invite. Multiply this by RetransmitAttempts and that's
// how long it'll be before we try another server. That might seem like a
// very long time but happy networks facilitate fast failover by sending hard
// errors (ICMP, 480, 500-599)
TryingTimeout = 500 * time.Millisecond
// how many times to attempt to retransmit before moving on
// How many times to attempt to retransmit before moving on.
RetransmitAttempts = 2
// number of nanoseconds (across all attempts) before we give up
// trying to connect a call. this doesn't mean the call has to
// have been answered but rather has moved beyond the "calling"
// state and doesn't go back.
// Number of nanoseconds (across all attempts) before we give up trying to
// connect a call. This doesn't mean the call has to have been answered but
// rather has moved beyond the "calling" state and doesn't go back.
GiveupTimeout = 3 * time.Second
// ToDo: how long to wait before refreshing a call with a
// re-INVITE message.
// TODO(jart): How long to wait before refreshing a call with a re-INVITE
// message.
RefreshTimeout = 15 * time.Minute
// how many times are proxies allowed to forward a message
// How many times are proxies allowed to forward a message.
MaxForwards int = 70
// aprox. how long the transaction engine remembers old Call-IDs
// to prevent a retransmit from accidentally opening a new dialog.
// Approx. how long the transaction engine remembers old Call-IDs to prevent
// a retransmit from accidentally opening a new dialog.
CallIDBanDuration = 35 * time.Second
// aprox. how often to sweep old transactions from ban list
// Approx. how often to sweep old transactions from ban list.
CallIDBanSweepDuration = 5 * time.Second
// should the transport layer add timestamps to Via headers
// (µsi/µse = utc microseconds since unix epoch ingress/egress)
// Should the transport layer add timestamps to Via headers (µsi/µse = UTC
// microseconds since unix epoch ingress/egress).
ViaTimestampTagging = true
// use this feature to print out raw sip messages the moment they
// are sent/received by the transport layer
// Use this feature to print out raw sip messages the moment they are
// sent/received by the transport layer.
TransportTrace = (os.Getenv("TPORT_LOG") == "true")
)

+ 182
- 0
sip/status.go View File

@ -0,0 +1,182 @@
// SIP Protocol Statuses
//
// http://www.iana.org/assignments/sip-parameters
package sip
const (
// 1xx: Provisional -- request received, continuing to process the request.
StatusTrying = 100 // Indicates server is not totally pwnd.
StatusRinging = 180 // Remote phone is definitely ringing.
StatusCallIsBeingForwarded = 181
StatusQueued = 182
StatusSessionProgress = 183 // Establish early media (PSTN ringback)
// 2xx: Success -- the action was successfully received, understood,
// and accepted;
StatusOK = 200 // Call is answered
StatusAccepted = 202 // [RFC3265]
StatusNoNotification = 204 // [RFC5839]
// 3xx: Redirection -- further action needs to be taken in order to
// complete the request;
StatusMultipleChoices = 300
StatusMovedPermanently = 301
StatusMovedTemporarily = 302 // Send your call there instead kthx.
StatusUseProxy = 305 // You fool! Send your call there instead.
StatusAlternativeService = 380
// 4xx: Client Error -- the request contains bad syntax or cannot be
// fulfilled at this server;
StatusBadRequest = 400 // Missing headers, bad format, etc.
StatusUnauthorized = 401 // Resend request with auth header.
StatusPaymentRequired = 402 // I am greedy.
StatusForbidden = 403 // gtfo
StatusNotFound = 404 // wat?
StatusMethodNotAllowed = 405 // I don't support that type of request.
StatusNotAcceptable = 406
StatusProxyAuthenticationRequired = 407
StatusRequestTimeout = 408
StatusConflict = 409
StatusGone = 410 // Shaniqua don't live here no more.
StatusLengthRequired = 411
StatusConditionalRequestFailed = 412 // [RFC3903]
StatusRequestEntityTooLarge = 413
StatusRequestURITooLong = 414
StatusUnsupportedMediaType = 415
StatusUnsupportedURIScheme = 416
StatusUnknownResourcePriority = 417
StatusBadExtension = 420
StatusExtensionRequired = 421
StatusSessionIntervalTooSmall = 422 // [RFC4028]
StatusIntervalTooBrief = 423
StatusUseIdentityHeader = 428 // [RFC4474]
StatusProvideReferrerIdentity = 429 // [RFC3892]
StatusFlowFailed = 430 // [RFC5626]
StatusAnonymityDisallowed = 433 // [RFC5079]
StatusBadIdentityInfo = 436 // [RFC4474]
StatusUnsupportedCertificate = 437 // [RFC4474]
StatusInvalidIdentityHeader = 438 // [RFC4474]
StatusFirstHopLacksOutboundSupport = 439 // [RFC5626]
StatusMaxBreadthExceeded = 440 // [RFC5393]
StatusConsentNeeded = 470 // [RFC5360]
StatusTemporarilyUnavailable = 480 // fast busy or soft fail
StatusCallTransactionDoesNotExist = 481 // Bad news
StatusLoopDetected = 482 // Froot looping
StatusTooManyHops = 483 // Froot looping
StatusAddressIncomplete = 484
StatusAmbiguous = 485
StatusBusyHere = 486
StatusRequestTerminated = 487
StatusNotAcceptableHere = 488
StatusBadEvent = 489 // [RFC3265]
StatusRequestPending = 491
StatusUndecipherable = 493
StatusSecurityAgreementRequired = 494 // [RFC3329]
// 5xx: Server Error -- the server failed to fulfill an apparently
// valid request;
StatusInternalServerError = 500
StatusNotImplemented = 501
StatusBadGateway = 502
StatusServiceUnavailable = 503
StatusGatewayTimeout = 504
StatusVersionNotSupported = 505
StatusMessageTooLarge = 513
StatusPreconditionFailure = 580 // [RFC3312]
// 6xx: Global Failure -- the request cannot be fulfilled at any
// server.
StatusBusyEverywhere = 600
StatusDecline = 603
StatusDoesNotExistAnywhere = 604
StatusNotAcceptable606 = 606
StatusDialogTerminated = 687
// 8xx: Special gosip errors
Status8xxProgrammerError = 800
Status8xxNetworkError = 801
)
func Phrase(status int) string {
if phrase, ok := phrases[status]; ok {
return phrase
}
return "Unknown Status Code"
}
var phrases = map[int]string{
StatusTrying: "Trying",
StatusRinging: "Ringing",
StatusCallIsBeingForwarded: "Call Is Being Forwarded",
StatusQueued: "Queued",
StatusSessionProgress: "Session Progress",
StatusOK: "OK",
StatusAccepted: "Accepted",
StatusNoNotification: "No Notification",
StatusMultipleChoices: "Multiple Choices",
StatusMovedPermanently: "Moved Permanently",
StatusMovedTemporarily: "Moved Temporarily",
StatusUseProxy: "Use Proxy",
StatusAlternativeService: "Alternative Service",
StatusBadRequest: "Bad Request",
StatusUnauthorized: "Unauthorized",
StatusPaymentRequired: "Payment Required",
StatusForbidden: "Forbidden",
StatusNotFound: "Not Found",
StatusMethodNotAllowed: "Method Not Allowed",
StatusNotAcceptable: "Not Acceptable",
StatusProxyAuthenticationRequired: "Proxy Authentication Required",
StatusRequestTimeout: "Request Timeout",
StatusConflict: "Conflict",
StatusGone: "Gone",
StatusLengthRequired: "Length Required",
StatusConditionalRequestFailed: "Conditional Request Failed",
StatusRequestEntityTooLarge: "Request Entity Too Large",
StatusRequestURITooLong: "Request-URI Too Long",
StatusUnsupportedMediaType: "Unsupported Media Type",
StatusUnsupportedURIScheme: "Unsupported URI Scheme",
StatusUnknownResourcePriority: "Unknown Resource-Priority",
StatusBadExtension: "Bad Extension",
StatusExtensionRequired: "Extension Required",
StatusSessionIntervalTooSmall: "Session Interval Too Small",
StatusIntervalTooBrief: "Interval Too Brief",
StatusUseIdentityHeader: "Use Identity Header",
StatusProvideReferrerIdentity: "Provide Referrer Identity",
StatusFlowFailed: "Flow Failed",
StatusAnonymityDisallowed: "Anonymity Disallowed",
StatusBadIdentityInfo: "Bad Identity-Info",
StatusUnsupportedCertificate: "Unsupported Certificate",
StatusInvalidIdentityHeader: "Invalid Identity Header",
StatusFirstHopLacksOutboundSupport: "First Hop Lacks Outbound Support",
StatusMaxBreadthExceeded: "Max-Breadth Exceeded",
StatusConsentNeeded: "Consent Needed",
StatusTemporarilyUnavailable: "Temporarily Unavailable",
StatusCallTransactionDoesNotExist: "Call/Transaction Does Not Exist",
StatusLoopDetected: "Loop Detected",
StatusTooManyHops: "Too Many Hops",
StatusAddressIncomplete: "Address StatusIncomplete",
StatusAmbiguous: "Ambiguous",
StatusBusyHere: "Busy Here",
StatusRequestTerminated: "Request Terminated",
StatusNotAcceptableHere: "Not Acceptable Here",
StatusBadEvent: "Bad Event",
StatusRequestPending: "Request Pending",
StatusUndecipherable: "Undecipherable",
StatusSecurityAgreementRequired: "Security Agreement Required",
StatusInternalServerError: "Internal Server Error",
StatusNotImplemented: "Not Implemented",
StatusBadGateway: "Bad Gateway",
StatusServiceUnavailable: "Service Unavailable",
StatusGatewayTimeout: "Gateway Time-out",
StatusVersionNotSupported: "Version Not Supported",
StatusMessageTooLarge: "Message Too Large",
StatusPreconditionFailure: "Precondition Failure",
StatusBusyEverywhere: "Busy Everywhere",
StatusDecline: "Decline",
StatusDoesNotExistAnywhere: "Does Not Exist Anywhere",
StatusNotAcceptable606: "Not Acceptable",
StatusDialogTerminated: "Dialog Terminated",
Status8xxProgrammerError: "Programmer Error",
Status8xxNetworkError: "Network Error",
}

+ 87
- 121
sip/transport.go View File

@ -7,7 +7,6 @@ import (
"bytes"
"errors"
"flag"
"fmt"
"github.com/jart/gosip/util"
"log"
"net"
@ -21,48 +20,34 @@ var (
timestampTagging = flag.Bool("timestampTagging", false, "Add microsecond timestamps to Via tags")
)
// Transport defines any object capable of sending and receiving SIP messages.
// Such objects are responsible for their own reliability. This means checking
// network errors, raising alarms, rebinding sockets, etc.
type Transport interface {
// Sends a SIP message. Will not modify msg.
Send(msg *Msg) error
// Transport sends and receives SIP messages over UDP with stateless routing.
type Transport struct {
// Receives a SIP message. Must only be called by one goroutine. The received
// address is injected into the first Via header as the "received" param.
Recv() (msg *Msg, err error)
// Closes underlying resources. Please make sure all calls using this
// transport complete first.
Close() error
// Thing returned by ListenPacket
Sock *net.UDPConn
// When you send an outbound request (not a response) you have to set the via
// tag: ``msg.Via = tport.Via().SetBranch().SetNext(msg.Via)``. The details
// of the branch parameter... are tricky.
Via() *Via
// tag: ``msg.Via = tp.Via.Copy().Branch().SetNext(msg.Via)``. The details of
// the branch parameter... are tricky.
Via *Via
// Returns a linked list of all canonical names and/or IP addresses that may
// be used to contact *this specific* transport.
Contact() *Addr
}
Contact *Addr
// Transport implementation that serializes messages to/from a UDP socket.
type udpTransport struct {
sock *net.UDPConn // thing returned by ListenUDP
addr *net.UDPAddr // handy for getting ip (contact might be host)
buf []byte // reusable memory for serialization
via *Via // who are we?
contact *Addr // uri that points to this specific transport
// Reusable memory for serialization
buf []byte
}
// Creates a new stateless network mechanism for transmitting and receiving SIP
// signalling messages.
//
// 'contact' is a SIP address, e.g. "<sip:1.2.3.4>", that tells how to bind
// sockets. This value is also used for contact headers which tell other
// user-agents where to send responses and hence should only contain an IP or
// canonical address.
func NewUDPTransport(contact *Addr) (tp Transport, err error) {
// contact is a SIP address, e.g. "<sip:1.2.3.4>", that tells how to bind
// sockets. If contact.Uri.Port is 0, then a port will be selected randomly.
// This value is also used for contact headers which tell other user-agents
// where to send responses and hence should only contain an IP or canonical
// address.
func NewTransport(contact *Addr) (tp *Transport, err error) {
saddr := util.HostPortToString(contact.Uri.Host, contact.Uri.Port)
sock, err := net.ListenPacket("udp", saddr)
if err != nil {
@ -72,31 +57,29 @@ func NewUDPTransport(contact *Addr) (tp Transport, err error) {
contact = contact.Copy()
contact.Uri.Port = uint16(addr.Port)
contact.Uri.Params["transport"] = addr.Network()
return &udpTransport{
sock: sock.(*net.UDPConn),
addr: addr,
buf: make([]byte, 2048),
contact: contact,
via: &Via{
Version: "2.0",
Proto: strings.ToUpper(addr.Network()),
Host: contact.Uri.Host,
Port: contact.Uri.Port,
return &Transport{
Sock: sock.(*net.UDPConn),
Contact: contact,
Via: &Via{
Host: contact.Uri.Host,
Port: contact.Uri.Port,
},
}, nil
}
func (tp *udpTransport) Send(msg *Msg) error {
// Sends a SIP message.
func (tp *Transport) Send(msg *Msg) error {
msg, saddr, err := tp.route(msg)
if err != nil {
return err
}
addr, err := net.ResolveUDPAddr("ip", saddr)
if err != nil {
return errors.New(fmt.Sprintf(
"udpTransport(%s) failed to resolve %s: %s", tp.addr, saddr, err))
return err
}
if msg.MaxForwards > 0 {
msg.MaxForwards--
}
msg.MaxForwards--
ts := time.Now()
addTimestamp(msg, ts)
var b bytes.Buffer
@ -104,58 +87,47 @@ func (tp *udpTransport) Send(msg *Msg) error {
if *tracing {
tp.trace("send", b.String(), addr, ts)
}
_, err = tp.sock.WriteTo(b.Bytes(), addr)
_, err = tp.Sock.WriteTo(b.Bytes(), addr)
if err != nil {
return errors.New(fmt.Sprintf(
"udpTransport(%s) write failed: %s", tp.addr, err))
return err
}
return nil
}
func (tp *udpTransport) Recv() (msg *Msg, err error) {
for {
amt, addr, err := tp.sock.ReadFromUDP(tp.buf)
if err != nil {
return nil, errors.New(fmt.Sprintf(
"udpTransport(%s) read failed: %s", tp.addr, err))
}
ts := time.Now()
packet := string(tp.buf[0:amt])
if *tracing {
tp.trace("recv", packet, addr, ts)
}
// Validation: http://tools.ietf.org/html/rfc3261#section-16.3
msg, err = ParseMsg(packet)
if err != nil {
log.Printf("udpTransport(%s) got bad message from %s: %s\n%s", tp.addr, addr, err, packet)
continue
}
if msg.Via.Host != addr.IP.String() || int(msg.Via.Port) != addr.Port {
msg.Via.Params["received"] = addr.String()
}
addTimestamp(msg, ts)
if !tp.sanityCheck(msg) {
continue
}
tp.preprocess(msg)
break
// Receives a SIP message. The received address is injected into the first Via
// header as the "received" param. The Error field of msg should be checked. If
// msg.Status is Status8xxNetworkError, it means the underlying socket died. If
// you set a deadline, you should check: util.IsTimeout(msg.Error).
//
// Warning: Must only be called by one goroutine.
func (tp *Transport) Recv() *Msg {
if tp.buf == nil {
tp.buf = make([]byte, 2048)
}
return msg, nil
}
func (tp *udpTransport) Via() *Via {
return tp.via
}
func (tp *udpTransport) Contact() *Addr {
return tp.contact
}
func (tp *udpTransport) Close() error {
return tp.sock.Close()
amt, addr, err := tp.Sock.ReadFromUDP(tp.buf)
if err != nil {
return &Msg{Status: Status8xxNetworkError, Error: err}
}
ts := time.Now()
packet := string(tp.buf[0:amt])
if *tracing {
tp.trace("recv", packet, addr, ts)
}
// Validation: http://tools.ietf.org/html/rfc3261#section-16.3
msg := ParseMsg(packet)
if msg.Error != nil {
return msg
}
addReceived(msg, addr)
addTimestamp(msg, ts)
if !tp.sanityCheck(msg) {
return msg
}
tp.preprocess(msg)
return msg
}
func (tp *udpTransport) trace(dir, pkt string, addr net.Addr, t time.Time) {
func (tp *Transport) trace(dir, pkt string, addr *net.UDPAddr, t time.Time) {
size := len(pkt)
bar := strings.Repeat("-", 72)
suffix := "\n "
@ -174,36 +146,33 @@ func (tp *udpTransport) trace(dir, pkt string, addr net.Addr, t time.Time) {
bar)
}
// Test if this message is acceptable.
func (tp *udpTransport) sanityCheck(msg *Msg) bool {
// Checks if message is acceptable, otherwise sets msg.Error and returns false.
func (tp *Transport) sanityCheck(msg *Msg) bool {
if msg.MaxForwards <= 0 {
log.Printf("udpTransport(%s) froot loop detected\n%s", tp.addr, msg)
go tp.Send(NewResponse(msg, 483))
return false
msg.Error = errors.New("Froot loop detected")
}
if msg.IsResponse {
if msg.Status >= 700 {
log.Printf("udpTransport(%s) msg has crazy status number\n%s", tp.addr, msg)
go tp.Send(NewResponse(msg, 400))
return false
msg.Error = errors.New("Crazy status number")
}
} else {
if msg.CSeqMethod == "" || msg.CSeqMethod != msg.Method {
log.Printf("udpTransport(%s) bad cseq number\n%s", tp.addr, msg)
go tp.Send(NewResponse(msg, 400))
return false
msg.Error = errors.New("Bad CSeq")
}
}
return true
return msg.Error == nil
}
// Perform some ingress message mangling.
func (tp *udpTransport) preprocess(msg *Msg) {
if tp.contact.Compare(msg.Route) {
log.Printf("udpTransport(%s) removing our route header: %s", tp.addr, msg.Route)
func (tp *Transport) preprocess(msg *Msg) {
if tp.Contact.Compare(msg.Route) {
log.Printf("Removing our route header: %s", msg.Route)
msg.Route = msg.Route.Next
}
if _, ok := msg.Request.Params["lr"]; ok && msg.Route != nil && tp.contact.Uri.Compare(msg.Request) {
if _, ok := msg.Request.Params["lr"]; ok && msg.Route != nil && tp.Contact.Uri.Compare(msg.Request) {
// RFC3261 16.4 Route Information Preprocessing
// RFC3261 16.12.1.2: Traversing a Strict-Routing Proxy
var oldReq, newReq *URI
@ -220,39 +189,34 @@ func (tp *udpTransport) preprocess(msg *Msg) {
seclast.Next = nil
msg.Route.Last()
}
log.Printf("udpTransport(%s) fixing request uri after strict router traversal: %s -> %s",
tp.addr, oldReq, newReq)
log.Printf("Fixing request URI after strict router traversal: %s -> %s", oldReq, newReq)
}
}
func (tp *udpTransport) route(old *Msg) (msg *Msg, saddr string, err error) {
func (tp *Transport) route(old *Msg) (msg *Msg, saddr string, err error) {
var host string
var port uint16
msg = new(Msg)
*msg = *old // Shallow copy is sufficient.
*msg = *old // Start off with a shallow copy.
if msg.IsResponse {
msg.Via = old.Via.Copy()
if msg.Via.CompareAddr(tp.via) {
if msg.Via.CompareAddr(tp.Via) {
// In proxy scenarios we have to remove our own Via.
msg.Via = msg.Via.Next
}
if msg.Via == nil {
return nil, "", errors.New("Ran out of Via headers when forwarding Response!")
return nil, "", errors.New("Message missing Via header")
}
if msg.Via != nil {
if received, ok := msg.Via.Params["received"]; ok {
return msg, received, nil
} else {
host, port = msg.Via.Host, msg.Via.Port
}
if received, ok := msg.Via.Params["received"]; ok {
return msg, received, nil
} else {
return nil, "", errors.New("Message missing Via header")
host, port = msg.Via.Host, msg.Via.Port
}
} else {
if msg.Request == nil {
return nil, "", errors.New("Missing request URI")
}
if !msg.Via.CompareAddr(tp.via) {
if !msg.Via.CompareAddr(tp.Via) {
return nil, "", errors.New("You forgot to say: msg.Via = tp.Via(msg.Via)")
}
if msg.Route != nil {
@ -276,14 +240,16 @@ func (tp *udpTransport) route(old *Msg) (msg *Msg, saddr string, err error) {
host, port = msg.Request.Host, msg.Request.Port
}
}
if msg.OutboundProxy != "" {
saddr = msg.OutboundProxy
} else {
saddr = util.HostPortToString(host, port)
}
saddr = util.HostPortToString(host, port)
return
}
func addReceived(msg *Msg, addr *net.UDPAddr) {
if msg.Via.Host != addr.IP.String() || int(msg.Via.Port) != addr.Port {
msg.Via.Params["received"] = addr.String()
}
}
func addTimestamp(msg *Msg, ts time.Time) {
if *timestampTagging {
msg.Via.Params["µsi"] = strconv.FormatInt(ts.UnixNano()/int64(time.Microsecond), 10)


+ 3
- 4
sip/via.go View File

@ -11,18 +11,18 @@ import (
)
var (
ViaBadHeader = errors.New("bad via header")
ViaProtoBlank = errors.New("via.Proto blank")
ViaBadHeader = errors.New("Bad Via header")
ViaProtoBlank = errors.New("Via Proto blank")
)
// Example: SIP/2.0/UDP 1.2.3.4:5060;branch=z9hG4bK556f77e6.
type Via struct {
Next *Via // pointer to next via header if any
Version string // protocol version e.g. "2.0"
Proto string // transport type "UDP"
Host string // name or ip of egress interface
Port uint16 // network port number
Params Params // params like branch, received, rport, etc.
Next *Via // pointer to next via header if any
}
// Parses a single SIP Via header, provided the part that comes after "Via: ".
@ -98,7 +98,6 @@ func (via *Via) Copy() *Via {
// Sets newly generated branch ID and returns self.
func (via *Via) Branch() *Via {
via = via.Copy()
via.Params["branch"] = util.GenerateBranch()
return via
}


+ 8
- 0
util/util.go View File

@ -8,6 +8,14 @@ import (
"strings"
)
// Returns true if error is an i/o timeout.
func IsTimeout(err error) bool {
if operr, ok := err.(*net.OpError); ok {
return operr.Timeout()
}
return false
}
// Returns true if IP contains a colon.
func IsIPv6(ip string) bool {
n := strings.Index(ip, ":")


Loading…
Cancel
Save