Browse Source

Use Ragel for parsing URIs.

pull/2/head
Justine Alexandra Roberts Tunney 11 years ago
parent
commit
7884e6ac1d
6 changed files with 3740 additions and 159 deletions
  1. +58
    -72
      sip/uri.go
  2. +3396
    -0
      sip/uri_parse.go
  3. +140
    -0
      sip/uri_parse.rl
  4. +133
    -86
      sip/uri_test.go
  5. +12
    -0
      sip/util.go
  6. +1
    -1
      util/escape.go

+ 58
- 72
sip/uri.go View File

@ -24,7 +24,7 @@ import (
"bytes"
"errors"
"github.com/jart/gosip/util"
"strings"
"sort"
)
const (
@ -32,86 +32,25 @@ const (
)
var (
URIEmpty = errors.New("empty uri")
URISchemeNotFound = errors.New("scheme not found")
URIMissingHost = errors.New("host missing")
URIBadPort = errors.New("invalid port number")
)
type Params map[string]string
type URIHeaders map[string]string
type URI struct {
Scheme string // sip, tel, etc.
User string // sip:USER@host
Pass string // sip:user:PASS@host
Host string // example.com, 1.2.3.4, etc.
Port uint16 // 5060, 80, etc.
Params Params // semicolon delimited params after uris and addrs
Scheme string // e.g. sip, sips, tel, etc.
User string // e.g. sip:USER@host
Pass string // e.g. sip:user:PASS@host
Host string // e.g. example.com, 1.2.3.4, etc.
Port uint16 // e.g. 5060, 80, etc.
Params Params // e.g. ;isup-oli=00;day=tuesday
Headers URIHeaders // e.g. ?subject=project%20x&lol=cat
}
// Parses a SIP URI.
func ParseURI(s string) (uri *URI, err error) {
uri = new(URI)
if s == "" {
return nil, URIEmpty
}
// Extract scheme.
n := strings.IndexAny(s, delims)
if n < 0 || s[n] != ':' {
return nil, URISchemeNotFound
}
uri.Scheme, s = s[0:n], s[n+1:]
// Extract user/pass.
n = strings.IndexAny(s, delims)
if n > 0 && s[n] == ':' { // sip:user:pass@host
// if next token isn't '@' then assume 'sip:host:port'
s2 := s[n+1:]
n2 := strings.IndexAny(s2, delims)
if n2 > 0 && s2[n2] == '@' {
uri.User = s[0:n]
s, n = s2, n2
if n < 0 || s[n] != '@' {
return nil, URIMissingHost
}
uri.Pass, s = s[0:n], s[n+1:]
}
} else if n > 0 && s[n] == '@' { // user@host
uri.User, s = s[0:n], s[n+1:]
}
// Extract host/port.
s, uri.Host, uri.Port, err = extractHostPort(s)
if err != nil {
return nil, err
}
// Extract semicolon delimited params.
if s != "" && s[0] == ';' {
uri.Params = parseParams(s[1:])
s = ""
}
// if s != "" {
// fmt.Fprintf(os.Stderr, "leftover data: %v\n", s)
// }
uri.User, err = util.URLUnescape(uri.User, false)
if err != nil {
return nil, err
}
uri.Pass, err = util.URLUnescape(uri.Pass, false)
if err != nil {
return nil, err
}
uri.Host, err = util.URLUnescape(uri.Host, false)
if err != nil {
return nil, err
}
return uri, nil
}
//go:generate ragel -Z -G2 -o uri_parse.go uri_parse.rl
// Deep copies a URI object.
func (uri *URI) Copy() *URI {
@ -121,9 +60,12 @@ func (uri *URI) Copy() *URI {
res := new(URI)
*res = *uri
res.Params = uri.Params.Copy()
res.Headers = uri.Headers.Copy()
return res
}
// TODO(jart): URI Comparison https://tools.ietf.org/html/rfc3261#section-19.1.4
func (uri *URI) String() string {
if uri == nil {
return "<nil>"
@ -158,6 +100,7 @@ func (uri *URI) Append(b *bytes.Buffer) {
b.WriteString(":" + portstr((uri.Port)))
}
uri.Params.Append(b)
uri.Headers.Append(b)
}
func (uri *URI) CompareHostPort(other *URI) bool {
@ -191,9 +134,17 @@ func (params Params) Copy() Params {
func (params Params) Append(b *bytes.Buffer) {
if params != nil && len(params) > 0 {
for k, v := range params {
keys := make([]string, len(params))
i := 0
for k, _ := range params {
keys[i] = k
i++
}
sort.Strings(keys)
for _, k := range keys {
b.WriteString(";")
b.WriteString(util.URLEscape(k, false))
v := params[k]
if v != "" {
b.WriteString("=")
b.WriteString(util.URLEscape(v, false))
@ -206,3 +157,38 @@ func (params Params) Has(k string) bool {
_, ok := params["lr"]
return ok
}
func (headers URIHeaders) Copy() URIHeaders {
res := make(URIHeaders, len(headers))
for k, v := range headers {
res[k] = v
}
return res
}
func (headers URIHeaders) Append(b *bytes.Buffer) {
if headers != nil && len(headers) > 0 {
keys := make([]string, len(headers))
i := 0
for k, _ := range headers {
keys[i] = k
i++
}
sort.Strings(keys)
first := true
for _, k := range keys {
if first {
b.WriteString("?")
first = false
} else {
b.WriteString("&")
}
b.WriteString(util.URLEscape(k, false))
v := headers[k]
if v != "" {
b.WriteString("=")
b.WriteString(util.URLEscape(v, false))
}
}
}
}

+ 3396
- 0
sip/uri_parse.go
File diff suppressed because it is too large
View File


+ 140
- 0
sip/uri_parse.rl View File

@ -0,0 +1,140 @@
package sip
import (
"bytes"
"errors"
"fmt"
"strconv"
)
%% machine uri;
%% write data;
// ParseURI turns a a SIP URI into a data structure.
func ParseURI(s string) (uri *URI, err error) {
if s == "" {
return nil, errors.New("Empty URI")
}
return ParseURIBytes([]byte(s))
}
// ParseURI turns a a SIP URI byte slice into a data structure.
func ParseURIBytes(data []byte) (uri *URI, err error) {
if data == nil {
return nil, nil
}
uri = new(URI)
cs := 0
p := 0
pe := len(data)
eof := len(data)
buf := make([]byte, len(data))
amt := 0
var b1, b2 string
var hex byte
%%{
action strStart { amt = 0 }
action strChar { buf[amt] = fc; amt++ }
action strLower { buf[amt] = fc + 0x20; amt++ }
action hexHi { hex = unhex(fc) * 16 }
action hexLo { hex += unhex(fc)
buf[amt] = hex; amt++ }
action user { uri.User = string(buf[0:amt]) }
action pass { uri.Pass = string(buf[0:amt]) }
action host { uri.Host = string(buf[0:amt]) }
action scheme { uri.Scheme = string(buf[0:amt]) }
action b1 { b1 = string(buf[0:amt]); amt = 0 }
action b2 { b2 = string(buf[0:amt]); amt = 0 }
action port {
if amt > 0 {
port, err := strconv.ParseUint(string(buf[0:amt]), 10, 16)
if err != nil { goto fail }
uri.Port = uint16(port)
}
}
action param {
if uri.Params == nil {
uri.Params = Params{}
}
uri.Params[b1] = b2
}
action header {
if uri.Headers == nil {
uri.Headers = URIHeaders{}
}
uri.Headers[b1] = b2
}
# TODO(jart): Use BNF from SIP RFC: https://tools.ietf.org/html/rfc3261#section-25.1
# Define what a single character is allowed to be.
toxic = ( cntrl | 127 ) ;
scary = ( toxic | space | "\"" | "#" | "%" | "<" | ">" | "=" ) ;
schmchars = ( lower | digit | "+" | "-" | "." ) ;
authdelims = ( "/" | "?" | "#" | ":" | "@" | ";" | "[" | "]" | "&" ) ;
userchars = any -- ( authdelims | scary ) ;
passchars = userchars ;
hostchars = passchars -- upper;
hostcharsEsc = ( hostchars | ":" ) -- upper;
portchars = digit ;
paramchars = userchars -- upper ;
headerchars = userchars ;
# Define how characters trigger actions.
escape = "%" xdigit xdigit ;
unescape = "%" ( xdigit @hexHi ) ( xdigit @hexLo ) ;
schmfirst = ( upper @strLower ) | ( lower @strChar ) ;
schmchar = ( upper @strLower ) | ( schmchars @strChar ) ;
userchar = unescape | ( userchars @strChar ) ;
passchar = unescape | ( passchars @strChar ) ;
hostchar = unescape | ( upper @strLower ) | ( hostchars @strChar ) ;
hostcharEsc = unescape | ( upper @strLower ) | ( hostcharsEsc @strChar ) ;
portchar = unescape | ( portchars @strChar ) ;
paramchar = unescape | ( upper @strLower ) | ( paramchars @strChar ) ;
headerchar = unescape | ( headerchars @strChar ) ;
# Define multi-character patterns.
scheme = ( schmfirst schmchar* ) >strStart %scheme ;
user = userchar+ >strStart %user ;
pass = passchar+ >strStart %pass ;
hostPlain = hostchar+ >strStart %host ;
hostQuoted = "[" ( hostcharEsc+ >strStart %host ) "]" ;
host = hostQuoted | hostPlain ;
port = portchar* >strStart %port ;
paramkey = paramchar+ >strStart >b2 %b1 ;
paramval = paramchar+ >strStart %b2 ;
param = space* ";" paramkey ( "=" paramval )? %param ;
headerkey = headerchar+ >strStart >b2 %b1 ;
headerval = headerchar+ >strStart %b2 ;
header = headerkey ( "=" headerval )? %header ;
headers = "?" header ( "&" header )* ;
userpass = user ( ":" pass )? ;
hostport = host ( ":" port )? ;
uriSansUser := space* scheme ":" hostport param* space* headers? space* ;
uriWithUser := space* scheme ":" userpass "@" hostport param* space* headers? space* ;
}%%
%% write init;
if bytes.IndexByte(data, '@') == -1 {
cs = uri_en_uriSansUser;
} else {
cs = uri_en_uriWithUser;
}
%% write exec;
if cs < uri_first_final {
if p == pe {
return nil, errors.New(fmt.Sprintf("Unexpected EOF: %s", data))
} else {
return nil, errors.New(fmt.Sprintf("Error in URI at pos %d: %s", p, data))
}
}
return uri, nil
fail:
return nil, errors.New(fmt.Sprintf("Bad URI: %s", data))
}

+ 133
- 86
sip/uri_test.go View File

@ -1,185 +1,232 @@
package sip_test
import (
"errors"
"github.com/jart/gosip/sip"
"reflect"
"testing"
)
type uriTest struct {
s string // user input we want to convert
uri sip.URI // what 's' should become after parsing
err error // if we expect parsing to fail
s string
e error
uri *sip.URI
skipFormat bool
}
var uriTests = []uriTest{
uriTest{
s: "sip:google.com",
uri: sip.URI{
s: "",
e: errors.New("Empty URI"),
},
uriTest{
s: "sip:",
e: errors.New("Unexpected EOF: sip:"),
},
uriTest{
s: "sip:example.com:LOL",
e: errors.New("Error in URI at pos 16: sip:example.com:LOL"),
},
uriTest{
s: "sip:example.com",
uri: &sip.URI{
Scheme: "sip",
Host: "google.com",
Host: "example.com",
},
},
uriTest{
s: "sip:jart@google.com",
uri: sip.URI{
s: "sip:example.com:",
uri: &sip.URI{
Scheme: "sip",
User: "jart",
Host: "google.com",
Host: "example.com",
},
skipFormat: true,
},
uriTest{
s: "sip:jart@google.com:5060",
uri: sip.URI{
s: "sip:example.com:5060",
uri: &sip.URI{
Scheme: "sip",
User: "jart",
Host: "google.com",
Host: "example.com",
Port: 5060,
},
},
uriTest{
s: "sip:google.com:666",
uri: sip.URI{
Scheme: "sip",
s: "sips:jart@google.com",
uri: &sip.URI{
Scheme: "sips",
User: "jart",
Host: "google.com",
Port: 666,
},
},
uriTest{
s: "sip:+12125650666@cat.lol",
uri: sip.URI{
Scheme: "sip",
User: "+12125650666",
Host: "cat.lol",
s: "sips:jart@google.com:5060",
uri: &sip.URI{
Scheme: "sips",
User: "jart",
Host: "google.com",
Port: 5060,
},
},
uriTest{
s: "sip:jart:lawl@google.com",
uri: sip.URI{
Scheme: "sip",
s: "sips:jart:letmein@google.com",
uri: &sip.URI{
Scheme: "sips",
User: "jart",
Pass: "lawl",
Pass: "letmein",
Host: "google.com",
},
},
uriTest{
s: "sip:jart:lawl@google.com;isup-oli=00;omg;lol=cat",
uri: sip.URI{
Scheme: "sip",
s: "sips:jart:LetMeIn@google.com:5060",
uri: &sip.URI{
Scheme: "sips",
User: "jart",
Pass: "lawl",
Pass: "LetMeIn",
Host: "google.com",
Params: sip.Params{
"isup-oli": "00",
"omg": "",
"lol": "cat",
},
Port: 5060,
},
},
uriTest{
s: "sip:jart@google.com;isup-oli=00;omg;lol=cat",
uri: sip.URI{
Scheme: "sip",
User: "jart",
s: "sips:GOOGLE.com",
uri: &sip.URI{
Scheme: "sips",
Host: "google.com",
Params: sip.Params{
"isup-oli": "00",
"omg": "",
"lol": "cat",
},
},
skipFormat: true,
},
uriTest{
s: "sip:[dead:beef::666]",
uri: sip.URI{
s: "sip:[dead:beef::666]:5060",
uri: &sip.URI{
Scheme: "sip",
Host: "dead:beef::666",
Port: 5060,
},
},
uriTest{
s: "sips:[dead:beef::666]:5060",
uri: sip.URI{
Scheme: "sips",
s: "sip:dead:beef::666:5060",
e: errors.New("Error in URI at pos 9: sip:dead:beef::666:5060"),
},
uriTest{
s: "tel:+12126660420",
uri: &sip.URI{
Scheme: "tel",
Host: "+12126660420",
},
},
uriTest{
s: "sip:bob%20barker:priceisright@[dead:beef::666]:5060;isup-oli=00",
uri: &sip.URI{
Scheme: "sip",
User: "bob barker",
Pass: "priceisright",
Host: "dead:beef::666",
Port: 5060,
Params: sip.Params{
"isup-oli": "00",
},
},
},
uriTest{
s: "sip:lol:cat@[dead:beef::666]:65535",
uri: sip.URI{
s: "sips:google.com ;lol ;h=omg",
uri: &sip.URI{
Scheme: "sips",
Host: "google.com",
Params: sip.Params{
"lol": "",
"h": "omg",
},
},
skipFormat: true,
},
uriTest{
s: "SIP:example.com",
uri: &sip.URI{
Scheme: "sip",
User: "lol",
Pass: "cat",
Host: "dead:beef::666",
Port: 65535,
Host: "example.com",
},
skipFormat: true,
},
uriTest{
s: "sip:lol:cat@[dead:beef::666]:65535;oh;my;goth",
uri: sip.URI{
s: "sips:alice@atlanta.com?priority=urgent&subject=project%20x",
uri: &sip.URI{
Scheme: "sips",
User: "alice",
Host: "atlanta.com",
Headers: sip.URIHeaders{
"subject": "project x",
"priority": "urgent",
},
},
},
uriTest{
s: "sip:+1-212-555-1212:1234@gateway.com;user=phone",
uri: &sip.URI{
Scheme: "sip",
User: "lol",
Pass: "cat",
Host: "dead:beef::666",
Port: 65535,
User: "+1-212-555-1212",
Pass: "1234",
Host: "gateway.com",
Params: sip.Params{
"oh": "",
"my": "",
"goth": "",
"user": "phone",
},
},
},
uriTest{
s: "sip:jart%3e:la%3ewl@google%3e.net:65535" +
";isup%3e-oli=00%3e;%3eomg;omg;lol=cat",
uri: sip.URI{
s: "sip:atlanta.com;method=register?to=alice%40atlanta.com",
uri: &sip.URI{
Scheme: "sip",
User: "jart>",
Pass: "la>wl",
Host: "google>.net",
Port: 65535,
Host: "atlanta.com",
Params: sip.Params{
"isup>-oli": "00>",
">omg": "",
"omg": "",
"lol": "cat",
"method": "register",
},
Headers: sip.URIHeaders{
"to": "alice@atlanta.com",
},
},
},
// TODO(jart): sip:alice;day=tuesday@atlanta.com
}
func TestParse(t *testing.T) {
func TestParseURI(t *testing.T) {
for _, test := range uriTests {
uri, err := sip.ParseURI(test.s)
if err != nil {
if test.err == nil {
t.Errorf("%v", err)
continue
} else { // test was supposed to fail
panic("TODO(jart): Implement failing support.")
if !reflect.DeepEqual(test.e, err) {
t.Errorf("%s\nWant: %#v\nGot: %#v", test.s, test.e, err)
}
} else {
if !reflect.DeepEqual(test.uri, uri) {
t.Errorf("%s\nWant: %#v\nGot: %#v", test.s, test.uri, uri)
}
}
if !reflect.DeepEqual(&test.uri, uri) {
t.Errorf("%#v != %#v", &test.uri, uri)
}
}
}
func TestFormat(t *testing.T) {
func TestFormatURI(t *testing.T) {
for _, test := range uriTests {
if test.skipFormat || test.e != nil {
continue
}
uri := test.uri.String()
if test.s != uri {
t.Error(test.s, "!=", uri)


+ 12
- 0
sip/util.go View File

@ -82,3 +82,15 @@ func parsePort(s string) (port uint16, err error) {
port = uint16(i)
return
}
func unhex(b byte) byte {
switch {
case '0' <= b && b <= '9':
return b - '0'
case 'a' <= b && b <= 'f':
return b - 'a' + 10
case 'A' <= b && b <= 'F':
return b - 'A' + 10
}
return 0
}

+ 1
- 1
util/escape.go View File

@ -41,7 +41,7 @@ func shouldEscape(c byte) bool {
switch c {
case '<', '>', '#', '%', '"', // RFC 2396 delims
'{', '}', '|', '\\', '^', '[', ']', '`', // RFC2396 unwise
'?', '&', '=': // RFC 2396 reserved in path
'?', '&', '=', '@': // RFC 2396 reserved in path
return true
}
return false


Loading…
Cancel
Save