diff --git a/client/session.go b/client/session.go index 76275dc..787bd93 100644 --- a/client/session.go +++ b/client/session.go @@ -26,9 +26,9 @@ import ( "errors" "fmt" "log" + "net" "reflect" "sort" - "strings" "time" "github.com/apache/thrift/lib/go/thrift" @@ -1334,14 +1334,32 @@ func NewClusterSession(clusterConfig *ClusterConfig) (Session, error) { return newClusterSessionWithSqlDialect(clusterConfig) } +// parseNodeURL splits a "host:port" node URL into a host and port, accepting +// bracketed IPv6 forms such as "[::1]:6667" and "[2001:db8::1]:6667" in +// addition to IPv4 and hostname URLs, consistent with the [ipv6]:port endpoint +// format standardized in apache/iotdb#18162. Malformed URLs (missing port, +// empty host, unbalanced brackets, or a bare IPv6 address without brackets) are +// rejected with an error rather than silently mis-parsed. +func parseNodeURL(nodeURL string) (endPoint, error) { + host, port, err := net.SplitHostPort(nodeURL) + if err != nil { + return endPoint{}, fmt.Errorf("invalid node url %q: %w", nodeURL, err) + } + if host == "" || port == "" { + return endPoint{}, fmt.Errorf("invalid node url %q: host and port must be non-empty", nodeURL) + } + return endPoint{Host: host, Port: port}, nil +} + func newClusterSessionWithSqlDialect(clusterConfig *ClusterConfig) (Session, error) { session := Session{} session.endPointList = make([]endPoint, len(clusterConfig.NodeUrls)) for i := 0; i < len(clusterConfig.NodeUrls); i++ { - node := endPoint{} - node.Host = strings.Split(clusterConfig.NodeUrls[i], ":")[0] - node.Port = strings.Split(clusterConfig.NodeUrls[i], ":")[1] - session.endPointList[i] = node + ep, err := parseNodeURL(clusterConfig.NodeUrls[i]) + if err != nil { + return session, err + } + session.endPointList[i] = ep } var err error var lastErr error diff --git a/client/session_test.go b/client/session_test.go new file mode 100644 index 0000000..67f83e9 --- /dev/null +++ b/client/session_test.go @@ -0,0 +1,62 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package client + +import "testing" + +func TestParseNodeURL(t *testing.T) { + tests := []struct { + name string + nodeURL string + wantHost string + wantPort string + wantErr bool + }{ + {"ipv4", "127.0.0.1:6667", "127.0.0.1", "6667", false}, + {"hostname", "localhost:6667", "localhost", "6667", false}, + {"ipv6 loopback", "[::1]:6667", "::1", "6667", false}, + {"ipv6 full", "[2001:db8::1]:6667", "2001:db8::1", "6667", false}, + // Bare (unbracketed) IPv6 with a port is ambiguous and rejected; the + // bracketed [ipv6]:port form must be used (see apache/iotdb#18162). + {"bare ipv6 rejected", "::1:6667", "", "", true}, + {"ipv6 missing port", "[::1]6667", "", "", true}, + {"ipv6 unbalanced bracket", "[::1:6667", "", "", true}, + {"no colon", "nocolon", "", "", true}, + {"empty port bracketed", "[::1]:", "", "", true}, + {"empty port", "host:", "", "", true}, + {"empty host", ":6667", "", "", true}, + {"empty", "", "", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ep, err := parseNodeURL(tt.nodeURL) + if (err != nil) != tt.wantErr { + t.Fatalf("parseNodeURL(%q) error = %v, wantErr %v", tt.nodeURL, err, tt.wantErr) + } + if tt.wantErr { + return + } + if ep.Host != tt.wantHost || ep.Port != tt.wantPort { + t.Errorf("parseNodeURL(%q) = {host %q, port %q}, want {host %q, port %q}", + tt.nodeURL, ep.Host, ep.Port, tt.wantHost, tt.wantPort) + } + }) + } +}