diff --git a/auth.go b/auth.go index ccd9981c..8cfc6f6d 100644 --- a/auth.go +++ b/auth.go @@ -64,19 +64,15 @@ func (as *AuthServer) GenerateHeader() HeaderValue { switch m { case Basic: ret = append(ret, (&HeaderAuth{ - Prefix: "Basic", - Values: map[string]string{ - "realm": as.realm, - }, + Method: Basic, + Realm: &as.realm, }).Write()...) case Digest: ret = append(ret, (&HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "realm": as.realm, - "nonce": as.nonce, - }, + Method: Digest, + Realm: &as.realm, + Nonce: &as.nonce, }).Write()...) } } @@ -110,46 +106,41 @@ func (as *AuthServer) ValidateHeader(v HeaderValue, method Method, ur *url.URL) return err } - inRealm, ok := auth.Values["realm"] - if !ok { + if auth.Realm == nil { return fmt.Errorf("realm not provided") } - inNonce, ok := auth.Values["nonce"] - if !ok { + if auth.Nonce == nil { return fmt.Errorf("nonce not provided") } - inUsername, ok := auth.Values["username"] - if !ok { + if auth.Username == nil { return fmt.Errorf("username not provided") } - inUri, ok := auth.Values["uri"] - if !ok { + if auth.URI == nil { return fmt.Errorf("uri not provided") } - inResponse, ok := auth.Values["response"] - if !ok { + if auth.Response == nil { return fmt.Errorf("response not provided") } - if inNonce != as.nonce { + if *auth.Nonce != as.nonce { return fmt.Errorf("wrong nonce") } - if inRealm != as.realm { + if *auth.Realm != as.realm { return fmt.Errorf("wrong realm") } - if inUsername != as.user { + if *auth.Username != as.user { return fmt.Errorf("wrong username") } uri := ur.String() - if inUri != uri { + if *auth.URI != uri { // VLC strips the subpath newUrl := *ur newUrl.Path = func() string { @@ -163,7 +154,7 @@ func (as *AuthServer) ValidateHeader(v HeaderValue, method Method, ur *url.URL) }() uri = newUrl.String() - if inUri != uri { + if *auth.URI != uri { return fmt.Errorf("wrong url") } } @@ -171,7 +162,7 @@ func (as *AuthServer) ValidateHeader(v HeaderValue, method Method, ur *url.URL) response := md5Hex(md5Hex(as.user+":"+as.realm+":"+as.pass) + ":" + as.nonce + ":" + md5Hex(string(method)+":"+uri)) - if inResponse != response { + if *auth.Response != response { return fmt.Errorf("wrong response") } @@ -209,13 +200,11 @@ func newAuthClient(v HeaderValue, user string, pass string) (*authClient, error) return nil, err } - realm, ok := auth.Values["realm"] - if !ok { + if auth.Realm == nil { return nil, fmt.Errorf("realm not provided") } - nonce, ok := auth.Values["nonce"] - if !ok { + if auth.Nonce == nil { return nil, fmt.Errorf("nonce not provided") } @@ -223,8 +212,8 @@ func newAuthClient(v HeaderValue, user string, pass string) (*authClient, error) user: user, pass: pass, method: Digest, - realm: realm, - nonce: nonce, + realm: *auth.Realm, + nonce: *auth.Nonce, }, nil } @@ -241,8 +230,7 @@ func newAuthClient(v HeaderValue, user string, pass string) (*authClient, error) return nil, err } - realm, ok := auth.Values["realm"] - if !ok { + if auth.Realm == nil { return nil, fmt.Errorf("realm not provided") } @@ -250,7 +238,7 @@ func newAuthClient(v HeaderValue, user string, pass string) (*authClient, error) user: user, pass: pass, method: Basic, - realm: realm, + realm: *auth.Realm, }, nil } @@ -271,14 +259,15 @@ func (ac *authClient) GenerateHeader(method Method, ur *url.URL) HeaderValue { ac.nonce + ":" + md5Hex(string(method)+":"+ur.String())) return (&HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "username": ac.user, - "realm": ac.realm, - "nonce": ac.nonce, - "uri": ur.String(), - "response": response, - }, + Method: Digest, + Username: &ac.user, + Realm: &ac.realm, + Nonce: &ac.nonce, + URI: func() *string { + v := ur.String() + return &v + }(), + Response: &response, }).Write() } diff --git a/header-auth.go b/header-auth.go index 05ee4dbb..4e9895e6 100644 --- a/header-auth.go +++ b/header-auth.go @@ -2,18 +2,68 @@ package gortsplib import ( "fmt" - "regexp" - "sort" "strings" ) // HeaderAuth is an Authenticate or a WWWW-Authenticate header. type HeaderAuth struct { - Prefix string - Values map[string]string + // authentication method + Method AuthMethod + + // (optional) username + Username *string + + // (optional) realm + Realm *string + + // (optional) nonce + Nonce *string + + // (optional) uri + URI *string + + // (optional) response + Response *string + + // (optional) opaque + Opaque *string + + // (optional) stale + Stale *string + + // (optional) algorithm + Algorithm *string } -var regHeaderAuthKeyValue = regexp.MustCompile("^([a-z]+)=(\"(.*?)\"|([a-zA-Z0-9]+))(, *|$)") +func findValue(v0 string) (string, string, error) { + if v0 == "" { + return "", "", nil + } + + if v0[0] == '"' { + i := 1 + for { + if i >= len(v0) { + return "", "", fmt.Errorf("apices not closed (%v)", v0) + } + + if v0[i] == '"' { + return v0[1:i], v0[i+1:], nil + } + + i++ + } + } + + i := 0 + for { + if i >= len(v0) || v0[i] == ',' { + return v0[:i], v0[i:], nil + } + + i++ + } +} // ReadHeaderAuth parses an Authenticate or a WWW-Authenticate header. func ReadHeaderAuth(v HeaderValue) (*HeaderAuth, error) { @@ -25,28 +75,79 @@ func ReadHeaderAuth(v HeaderValue) (*HeaderAuth, error) { return nil, fmt.Errorf("value provided multiple times (%v)", v) } - ha := &HeaderAuth{ - Values: make(map[string]string), - } + ha := &HeaderAuth{} v0 := v[0] - i := strings.IndexByte(v[0], ' ') + i := strings.IndexByte(v0, ' ') if i < 0 { - return nil, fmt.Errorf("unable to find prefix (%s)", v0) + return nil, fmt.Errorf("unable to find method (%s)", v0) } - ha.Prefix, v0 = v0[:i], v0[i+1:] + + switch v0[:i] { + case "Basic": + ha.Method = Basic + + case "Digest": + ha.Method = Digest + + default: + return nil, fmt.Errorf("invalid method (%s)", v0[:i]) + } + v0 = v0[i+1:] for len(v0) > 0 { - m := regHeaderAuthKeyValue.FindStringSubmatch(v0) - if m == nil { - return nil, fmt.Errorf("unable to parse key-value (%s)", v0) + i := strings.IndexByte(v0, '=') + if i < 0 { + return nil, fmt.Errorf("unable to find key (%s)", v0) } - v0 = v0[len(m[0]):] + var key string + key, v0 = v0[:i], v0[i+1:] - m[2] = strings.TrimPrefix(m[2], "\"") - m[2] = strings.TrimSuffix(m[2], "\"") - ha.Values[m[1]] = m[2] + var val string + var err error + val, v0, err = findValue(v0) + if err != nil { + return nil, err + } + + switch key { + case "username": + ha.Username = &val + + case "realm": + ha.Realm = &val + + case "nonce": + ha.Nonce = &val + + case "uri": + ha.URI = &val + + case "response": + ha.Response = &val + + case "opaque": + ha.Opaque = &val + + case "stale": + ha.Stale = &val + + case "algorithm": + ha.Algorithm = &val + + // ignore non-standard keys + } + + // skip comma + if len(v0) > 0 && v0[0] == ',' { + v0 = v0[1:] + } + + // skip spaces + for len(v0) > 0 && v0[0] == ' ' { + v0 = v0[1:] + } } return ha, nil @@ -54,48 +155,53 @@ func ReadHeaderAuth(v HeaderValue) (*HeaderAuth, error) { // Write encodes an Authenticate or a WWW-Authenticate header. func (ha *HeaderAuth) Write() HeaderValue { - ret := ha.Prefix + " " + ret := "" - // follow a specific order, otherwise some clients/servers do not work correctly - var sortedKeys []string - for key := range ha.Values { - sortedKeys = append(sortedKeys, key) - } - score := func(v string) int { - switch v { - case "username": - return 0 - case "realm": - return 1 - case "nonce": - return 2 - case "uri": - return 3 - case "response": - return 4 - case "opaque": - return 5 - case "stale": - return 6 - case "algorithm": - return 7 - } - return 8 - } - sort.Slice(sortedKeys, func(a, b int) bool { - sa := score(sortedKeys[a]) - sb := score(sortedKeys[b]) - if sa != sb { - return sa < sb - } - return a < b - }) + switch ha.Method { + case Basic: + ret += "Basic" - var tmp []string - for _, key := range sortedKeys { - tmp = append(tmp, key+"=\""+ha.Values[key]+"\"") + case Digest: + ret += "Digest" } - ret += strings.Join(tmp, ", ") + + ret += " " + + var vals []string + + if ha.Username != nil { + vals = append(vals, "username=\""+*ha.Username+"\"") + } + + if ha.Realm != nil { + vals = append(vals, "realm=\""+*ha.Realm+"\"") + } + + if ha.Nonce != nil { + vals = append(vals, "nonce=\""+*ha.Nonce+"\"") + } + + if ha.URI != nil { + vals = append(vals, "uri=\""+*ha.URI+"\"") + } + + if ha.Response != nil { + vals = append(vals, "response=\""+*ha.Response+"\"") + } + + if ha.Opaque != nil { + vals = append(vals, "opaque=\""+*ha.Opaque+"\"") + } + + if ha.Stale != nil { + vals = append(vals, "stale=\""+*ha.Stale+"\"") + } + + if ha.Algorithm != nil { + vals = append(vals, "algorithm=\""+*ha.Algorithm+"\"") + } + + ret += strings.Join(vals, ", ") return HeaderValue{ret} } diff --git a/header-auth_test.go b/header-auth_test.go index bf91d969..c3e26b3c 100644 --- a/header-auth_test.go +++ b/header-auth_test.go @@ -17,10 +17,11 @@ var casesHeaderAuth = []struct { HeaderValue{`Basic realm="4419b63f5e51"`}, HeaderValue{`Basic realm="4419b63f5e51"`}, &HeaderAuth{ - Prefix: "Basic", - Values: map[string]string{ - "realm": "4419b63f5e51", - }, + Method: Basic, + Realm: func() *string { + v := "4419b63f5e51" + return &v + }(), }, }, { @@ -28,12 +29,19 @@ var casesHeaderAuth = []struct { HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, &HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "realm": "4419b63f5e51", - "nonce": "8b84a3b789283a8bea8da7fa7d41f08b", - "stale": "FALSE", - }, + Method: Digest, + Realm: func() *string { + v := "4419b63f5e51" + return &v + }(), + Nonce: func() *string { + v := "8b84a3b789283a8bea8da7fa7d41f08b" + return &v + }(), + Stale: func() *string { + v := "FALSE" + return &v + }(), }, }, { @@ -41,12 +49,19 @@ var casesHeaderAuth = []struct { HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`}, HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`}, &HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "realm": "4419b63f5e51", - "nonce": "8b84a3b789283a8bea8da7fa7d41f08b", - "stale": "FALSE", - }, + Method: Digest, + Realm: func() *string { + v := "4419b63f5e51" + return &v + }(), + Nonce: func() *string { + v := "8b84a3b789283a8bea8da7fa7d41f08b" + return &v + }(), + Stale: func() *string { + v := "FALSE" + return &v + }(), }, }, { @@ -54,12 +69,19 @@ var casesHeaderAuth = []struct { HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`}, HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`}, &HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "realm": "4419b63f5e51", - "nonce": "133767111917411116111311118211673010032", - "stale": "FALSE", - }, + Method: Digest, + Realm: func() *string { + v := "4419b63f5e51" + return &v + }(), + Nonce: func() *string { + v := "133767111917411116111311118211673010032" + return &v + }(), + Stale: func() *string { + v := "FALSE" + return &v + }(), }, }, { @@ -67,14 +89,27 @@ var casesHeaderAuth = []struct { HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`}, HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`}, &HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "username": "aa", - "realm": "bb", - "nonce": "cc", - "uri": "dd", - "response": "ee", - }, + Method: Digest, + Username: func() *string { + v := "aa" + return &v + }(), + Realm: func() *string { + v := "bb" + return &v + }(), + Nonce: func() *string { + v := "cc" + return &v + }(), + URI: func() *string { + v := "dd" + return &v + }(), + Response: func() *string { + v := "ee" + return &v + }(), }, }, { @@ -82,14 +117,27 @@ var casesHeaderAuth = []struct { HeaderValue{`Digest username="", realm="IPCAM", nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, HeaderValue{`Digest username="", realm="IPCAM", nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", response="c072ae90eb4a27f4cdcb90d62266b2a1"`}, &HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "username": "", - "realm": "IPCAM", - "nonce": "5d17cd12b9fa8a85ac5ceef0926ea5a6", - "uri": "rtsp://localhost:8554/mystream", - "response": "c072ae90eb4a27f4cdcb90d62266b2a1", - }, + Method: Digest, + Username: func() *string { + v := "" + return &v + }(), + Realm: func() *string { + v := "IPCAM" + return &v + }(), + Nonce: func() *string { + v := "5d17cd12b9fa8a85ac5ceef0926ea5a6" + return &v + }(), + URI: func() *string { + v := "rtsp://localhost:8554/mystream" + return &v + }(), + Response: func() *string { + v := "c072ae90eb4a27f4cdcb90d62266b2a1" + return &v + }(), }, }, { @@ -97,14 +145,27 @@ var casesHeaderAuth = []struct { HeaderValue{`Digest realm="Please log in with a valid username",nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE,algorithm=MD5`}, HeaderValue{`Digest realm="Please log in with a valid username", nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`}, &HeaderAuth{ - Prefix: "Digest", - Values: map[string]string{ - "realm": "Please log in with a valid username", - "nonce": "752a62306daf32b401a41004555c7663", - "opaque": "", - "stale": "FALSE", - "algorithm": "MD5", - }, + Method: Digest, + Realm: func() *string { + v := "Please log in with a valid username" + return &v + }(), + Nonce: func() *string { + v := "752a62306daf32b401a41004555c7663" + return &v + }(), + Opaque: func() *string { + v := "" + return &v + }(), + Stale: func() *string { + v := "FALSE" + return &v + }(), + Algorithm: func() *string { + v := "MD5" + return &v + }(), }, }, } diff --git a/header-transport.go b/header-transport.go index 2eb7fa64..b885b9d2 100644 --- a/header-transport.go +++ b/header-transport.go @@ -75,15 +75,14 @@ func ReadHeaderTransport(v HeaderValue) (*HeaderTransport, error) { switch parts[0] { case "RTP/AVP", "RTP/AVP/UDP": ht.Protocol = StreamProtocolUDP - parts = parts[1:] case "RTP/AVP/TCP": ht.Protocol = StreamProtocolTCP - parts = parts[1:] default: return nil, fmt.Errorf("invalid protocol (%v)", v) } + parts = parts[1:] switch parts[0] { case "unicast": @@ -144,6 +143,8 @@ func ReadHeaderTransport(v HeaderValue) (*HeaderTransport, error) { v = strings.TrimSuffix(v, "\"") ht.Mode = &v } + + // ignore non-standard keys } return ht, nil