package lighthouse import ( "context" "math" "strconv" "strings" ) // presencePrefix marks a presence channel. const presencePrefix = "presence:" // ParseChannel returns the authorizer namespace of channel, the WinterCMS // parseChannel rule: // // - a "presence:presence:" prefix yields "" (deny); // - one "presence:" prefix is stripped, and presence is reported true; // - more than three ":"-separated segments yield "" (deny); // - otherwise the namespace is the first segment. // // The lookup is byte-exact: no case folding or Unicode normalization. func ParseChannel(channel string) (namespace string, presence bool) { presence = strings.HasPrefix(channel, presencePrefix) if strings.HasPrefix(channel, presencePrefix+presencePrefix) { return "", presence } name := strings.TrimPrefix(channel, presencePrefix) parts := strings.Split(name, ":") if len(parts) > 3 { return "", presence } return parts[0], presence } // ChannelID returns segment 1 of channel (0 when it is missing) converted // with PHP's (int) cast, as the WinterCMS authorizers read // explode(':', $channel)[1]. "5abc" is 5, "abc" is 0, "1e3" is 1000, and // out-of-range values saturate. For "presence:collection:5" segment 1 is // "collection", so the id is 0. func ChannelID(channel string) int64 { parts := strings.SplitN(channel, ":", 3) if len(parts) < 2 { return 0 } return PHPInt(parts[1]) } // PHPInt converts s the way PHP 8's (int) cast converts a string: leading // whitespace and a sign are allowed, the longest numeric prefix (digits, // an optional fraction and exponent) is used, integers saturate at the // int64 bounds, and non-finite floats become 0. func PHPInt(s string) int64 { i := 0 for i < len(s) && isPHPSpace(s[i]) { i++ } start := i if i < len(s) && (s[i] == '+' || s[i] == '-') { i++ } digitsStart := i for i < len(s) && isDigit(s[i]) { i++ } intDigits := i - digitsStart isFloat := false if i < len(s) && s[i] == '.' { j := i + 1 for j < len(s) && isDigit(s[j]) { j++ } if intDigits > 0 || j > i+1 { isFloat = true i = j } } if intDigits == 0 && !isFloat { return 0 } if i < len(s) && (s[i] == 'e' || s[i] == 'E') { j := i + 1 if j < len(s) && (s[j] == '+' || s[j] == '-') { j++ } k := j for k < len(s) && isDigit(s[k]) { k++ } if k > j { isFloat = true i = k } } num := s[start:i] if !isFloat { n, err := strconv.ParseInt(num, 10, 64) if err != nil { if strings.HasPrefix(num, "-") { return math.MinInt64 } return math.MaxInt64 } return n } f, _ := strconv.ParseFloat(num, 64) switch { case math.IsInf(f, 0) || math.IsNaN(f): return 0 case f >= math.MaxInt64: return math.MaxInt64 case f <= math.MinInt64: return math.MinInt64 default: return int64(f) } } func isPHPSpace(c byte) bool { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f' } func isDigit(c byte) bool { return c >= '0' && c <= '9' } // FormatChannels lowercases each channel (ASCII, as PHP strtolower) and // prefixes lowercase(namespace)+":" unless the namespace is empty or the // channel already starts with that prefix. func FormatChannels(namespace string, channels []string) []string { out := make([]string, len(channels)) prefix := "" if namespace != "" && namespace != "0" { prefix = asciiLower(namespace) + ":" } for i, ch := range channels { name := asciiLower(ch) if prefix != "" && !strings.HasPrefix(name, prefix) { name = prefix + name } out[i] = name } return out } func asciiLower(s string) string { b := []byte(s) for i, c := range b { if c >= 'A' && c <= 'Z' { b[i] = c + ('a' - 'A') } } return string(b) } type clientIDKey struct{} // WithClientID returns ctx carrying the realtime client id of a subscribe // request. Drivers call it before invoking an Authorizer. func WithClientID(ctx context.Context, id string) context.Context { if ctx == nil { ctx = context.Background() } return context.WithValue(ctx, clientIDKey{}, id) } // ClientID returns the realtime client id placed in ctx by the driver, or // "" outside a subscribe request. func ClientID(ctx context.Context) string { if ctx == nil { return "" } id, _ := ctx.Value(clientIDKey{}).(string) return id }