diff --git a/.circleci/config.yml b/.circleci/config.yml index e41af62..a76a359 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,6 +3,46 @@ # Check https://circleci.com/docs/2.0/language-go/ for more details version: 2 jobs: + build-tls: + machine: + enabled: true + image: ubuntu-1604:202004-01 + steps: + - checkout + - run: + name: Setting GOPATH + command: | + go version + go env -w GOPATH=$HOME/go + - run: + name: Generate a root CA and a server certificate using redis helpers + command: | + git clone git://github.com/antirez/redis.git --branch 6.0.1 + cd redis + ./utils/gen-test-certs.sh + cd .. + - run: + name: Copy RediSearch + command: | + docker run --rm --entrypoint cat redislabs/redisearch:edge /usr/lib/redis/modules/redisearch.so > redisearch.so + chmod 755 redisearch.so + - run: + name: Run RediSearch with tls support + command: | + docker run -d -v $(pwd)/redisearch.so:/data/redisearch.so \ + -v $(pwd)/redis/tests/tls/:/data \ + -p 6379:6379 redis redis-server --tls-port 6379 --port 0 \ + --tls-cert-file /data/redis.crt \ + --tls-key-file /data/redis.key \ + --tls-ca-cert-file /data/ca.crt \ + --tls-auth-clients no --loadmodule /data/redisearch.so + - run: + name: Run Examples + command: | + make examples TLS_CERT=redis/tests/tls/redis.crt \ + TLS_KEY=redis/tests/tls/redis.key \ + TLS_CACERT=redis/tests/tls/ca.crt + build: # test with redisearch:edge docker: - image: circleci/golang:1.9 @@ -31,6 +71,7 @@ workflows: commit: jobs: - build + - build-tls nightly: triggers: - schedule: @@ -40,4 +81,5 @@ workflows: only: - master jobs: - - build_nightly \ No newline at end of file + - build_nightly + - build-tls \ No newline at end of file diff --git a/.gitignore b/.gitignore index 499f048..b8bc9d2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ !.gitignore !.circleci/config.yml !/tests/*.bz2 +!Makefile diff --git a/Makefile b/Makefile index 186edbc..7359298 100644 --- a/Makefile +++ b/Makefile @@ -8,17 +8,28 @@ GOGET=$(GOCMD) get GOMOD=$(GOCMD) mod .PHONY: all test coverage -all: test coverage +all: test coverage examples get: - $(GOGET) -t -v ./... + $(GOGET) -t -v ./redisearch/... + +TLS_CERT ?= redis.crt +TLS_KEY ?= redis.key +TLS_CACERT ?= ca.crt +REDISEARCH_TEST_HOST ?= 127.0.0.1:6379 examples: get - $(GOBUILD) ./examples/quickstart/. - $(GOBUILD) ./examples/temporary/. - ./quickstart > /dev/null + @echo " " + @echo "Building the examples..." + $(GOBUILD) ./examples/redisearch_quickstart/. + $(GOBUILD) ./examples/redisearch_auth/. + $(GOBUILD) ./examples/redisearch_tls_client/. + ./redisearch_tls_client --tls-cert-file $(TLS_CERT) \ + --tls-key-file $(TLS_KEY) \ + --tls-ca-cert-file $(TLS_CACERT) \ + --host $(REDISEARCH_TEST_HOST) -test: get examples +test: get $(GOTEST) -race -covermode=atomic ./... coverage: get test diff --git a/examples/redisearch_auth/redisearch_auth.go b/examples/redisearch_auth/redisearch_auth.go new file mode 100644 index 0000000..67f4639 --- /dev/null +++ b/examples/redisearch_auth/redisearch_auth.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "github.com/RediSearch/redisearch-go/redisearch" + "github.com/gomodule/redigo/redis" + "log" + "time" +) + +// exemplifies the NewClientFromPool function +func main() { + host := "localhost:6379" + password := "" + pool := &redis.Pool{Dial: func() (redis.Conn, error) { + return redis.Dial("tcp", host, redis.DialPassword(password)) + }} + c := redisearch.NewClientFromPool(pool, "search-client-1") + + // Create a schema + sc := redisearch.NewSchema(redisearch.DefaultOptions). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.Index([]redisearch.Document{doc}...); err != nil { + log.Fatal(err) + } + + // Searching with limit and sorting + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + // Output: doc1 Hello world 1 +} diff --git a/examples/quickstart/quickstart.go b/examples/redisearch_quickstart/redisearch_quickstart.go similarity index 100% rename from examples/quickstart/quickstart.go rename to examples/redisearch_quickstart/redisearch_quickstart.go diff --git a/examples/redisearch_tls_client/redisearch_tls_client.go b/examples/redisearch_tls_client/redisearch_tls_client.go new file mode 100644 index 0000000..908cad0 --- /dev/null +++ b/examples/redisearch_tls_client/redisearch_tls_client.go @@ -0,0 +1,115 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "flag" + "fmt" + "github.com/RediSearch/redisearch-go/redisearch" + "github.com/gomodule/redigo/redis" + "io/ioutil" + "log" + "os" + "time" +) + +var ( + tlsCertFile = flag.String("tls-cert-file", "redis.crt", "A a X.509 certificate to use for authenticating the server to connected clients, masters or cluster peers. The file should be PEM formatted.") + tlsKeyFile = flag.String("tls-key-file", "redis.key", "A a X.509 privat ekey to use for authenticating the server to connected clients, masters or cluster peers. The file should be PEM formatted.") + tlsCaCertFile = flag.String("tls-ca-cert-file", "ca.crt", "A PEM encoded CA's certificate file.") + host = flag.String("host", "127.0.0.1:6379", "Redis host.") + password = flag.String("password", "", "Redis password.") +) + +func exists(filename string) (exists bool) { + exists = false + info, err := os.Stat(filename) + if os.IsNotExist(err) || info.IsDir() { + return + } + exists = true + return +} + +/* + * Example of how to establish an SSL connection from your app to the RedisAI Server + */ +func main() { + flag.Parse() + // Quickly check if the files exist + if !exists(*tlsCertFile) || !exists(*tlsKeyFile) || !exists(*tlsCaCertFile) { + fmt.Println("Some of the required files does not exist. Leaving example...") + return + } + + // Load client cert + cert, err := tls.LoadX509KeyPair(*tlsCertFile, *tlsKeyFile) + if err != nil { + log.Fatal(err) + } + + // Load CA cert + caCert, err := ioutil.ReadFile(*tlsCaCertFile) + if err != nil { + log.Fatal(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + clientTLSConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + + // InsecureSkipVerify controls whether a client verifies the + // server's certificate chain and host name. + // If InsecureSkipVerify is true, TLS accepts any certificate + // presented by the server and any host name in that certificate. + // In this mode, TLS is susceptible to man-in-the-middle attacks. + // This should be used only for testing. + clientTLSConfig.InsecureSkipVerify = true + + pool := &redis.Pool{Dial: func() (redis.Conn, error) { + return redis.Dial("tcp", *host, + redis.DialPassword(*password), + redis.DialTLSConfig(clientTLSConfig), + redis.DialUseTLS(true), + redis.DialTLSSkipVerify(true), + ) + }} + + c := redisearch.NewClientFromPool(pool, "search-client-1") + + // Create a schema + sc := redisearch.NewSchema(redisearch.DefaultOptions). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.Index([]redisearch.Document{doc}...); err != nil { + log.Fatal(err) + } + + // Searching with limit and sorting + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + // Output: doc1 Hello world 1 +} diff --git a/redisearch/client.go b/redisearch/client.go index 0b45381..29411e7 100644 --- a/redisearch/client.go +++ b/redisearch/client.go @@ -37,7 +37,7 @@ func NewClient(addr, name string) *Client { return ret } -// NewAutocompleter creates a new Autocompleter with the given pool and index name +// NewClientFromPool creates a new Client with the given pool and index name func NewClientFromPool(pool *redis.Pool, name string) *Client { ret := &Client{ pool: pool, diff --git a/redisearch/example_client_test.go b/redisearch/example_client_test.go new file mode 100644 index 0000000..5201282 --- /dev/null +++ b/redisearch/example_client_test.go @@ -0,0 +1,232 @@ +package redisearch_test + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "github.com/RediSearch/redisearch-go/redisearch" + "github.com/gomodule/redigo/redis" + "io/ioutil" + "log" + "os" + "time" +) + +// exemplifies the NewClient function +func ExampleNewClient() { + // Create a client. By default a client is schemaless + // unless a schema is provided when creating the index + c := redisearch.NewClient("localhost:6379", "myIndex") + + // Create a schema + sc := redisearch.NewSchema(redisearch.DefaultOptions). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.Index([]redisearch.Document{doc}...); err != nil { + log.Fatal(err) + } + + // Searching with limit and sorting + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + // Output: doc1 Hello world 1 +} + +// exemplifies the NewClientFromPool function +func ExampleNewClientFromPool() { + host := "localhost:6379" + password := "" + pool := &redis.Pool{Dial: func() (redis.Conn, error) { + return redis.Dial("tcp", host, redis.DialPassword(password)) + }} + c := redisearch.NewClientFromPool(pool, "search-client-1") + + // Create a schema + sc := redisearch.NewSchema(redisearch.DefaultOptions). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.Index([]redisearch.Document{doc}...); err != nil { + log.Fatal(err) + } + + // Searching with limit and sorting + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) + // Output: doc1 Hello world 1 +} + +//Example of how to establish an SSL connection from your app to the RedisAI Server +func ExampleNewClientFromPool_ssl() { + // Consider the following helper methods that provide us with the connection details (host and password) + // and the paths for: + // tls_cert - A a X.509 certificate to use for authenticating the server to connected clients, masters or cluster peers. The file should be PEM formatted + // tls_key - A a X.509 private key to use for authenticating the server to connected clients, masters or cluster peers. The file should be PEM formatted + // tls_cacert - A PEM encoded CA's certificate file + host, password := getConnectionDetails() + tlsready, tls_cert, tls_key, tls_cacert := getTLSdetails() + + // Skip if we dont have all files to properly connect + if tlsready == false { + return + } + + // Load client cert + cert, err := tls.LoadX509KeyPair(tls_cert, tls_key) + if err != nil { + log.Fatal(err) + } + + // Load CA cert + caCert, err := ioutil.ReadFile(tls_cacert) + if err != nil { + log.Fatal(err) + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + clientTLSConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + } + + // InsecureSkipVerify controls whether a client verifies the + // server's certificate chain and host name. + // If InsecureSkipVerify is true, TLS accepts any certificate + // presented by the server and any host name in that certificate. + // In this mode, TLS is susceptible to man-in-the-middle attacks. + // This should be used only for testing. + clientTLSConfig.InsecureSkipVerify = true + + pool := &redis.Pool{Dial: func() (redis.Conn, error) { + return redis.Dial("tcp", host, + redis.DialPassword(password), + redis.DialTLSConfig(clientTLSConfig), + redis.DialUseTLS(true), + redis.DialTLSSkipVerify(true), + ) + }} + + c := redisearch.NewClientFromPool(pool, "search-client-1") + + // Create a schema + sc := redisearch.NewSchema(redisearch.DefaultOptions). + AddField(redisearch.NewTextField("body")). + AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). + AddField(redisearch.NewNumericField("date")) + + // Drop an existing index. If the index does not exist an error is returned + c.Drop() + + // Create the index with the given schema + if err := c.CreateIndex(sc); err != nil { + log.Fatal(err) + } + + // Create a document with an id and given score + doc := redisearch.NewDocument("doc1", 1.0) + doc.Set("title", "Hello world"). + Set("body", "foo bar"). + Set("date", time.Now().Unix()) + + // Index the document. The API accepts multiple documents at a time + if err := c.Index([]redisearch.Document{doc}...); err != nil { + log.Fatal(err) + } + + // Searching with limit and sorting + docs, total, err := c.Search(redisearch.NewQuery("hello world"). + Limit(0, 2). + SetReturnFields("title")) + + fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) +} + +func getConnectionDetails() (host string, password string) { + value, exists := os.LookupEnv("REDISEARCH_TEST_HOST") + host = "localhost:6379" + password = "" + valuePassword, existsPassword := os.LookupEnv("REDISEARCH_TEST_PASSWORD") + if exists && value != "" { + host = value + } + if existsPassword && valuePassword != "" { + password = valuePassword + } + return +} + +func getTLSdetails() (tlsready bool, tls_cert string, tls_key string, tls_cacert string) { + tlsready = false + value, exists := os.LookupEnv("TLS_CERT") + if exists && value != "" { + info, err := os.Stat(value) + if os.IsNotExist(err) || info.IsDir() { + return + } + tls_cert = value + } else { + return + } + value, exists = os.LookupEnv("TLS_KEY") + if exists && value != "" { + info, err := os.Stat(value) + if os.IsNotExist(err) || info.IsDir() { + return + } + tls_key = value + } else { + return + } + value, exists = os.LookupEnv("TLS_CACERT") + if exists && value != "" { + info, err := os.Stat(value) + if os.IsNotExist(err) || info.IsDir() { + return + } + tls_cacert = value + } else { + return + } + tlsready = true + return +} diff --git a/redisearch/package.go b/redisearch/package.go deleted file mode 100644 index 96efcf9..0000000 --- a/redisearch/package.go +++ /dev/null @@ -1,53 +0,0 @@ -// Package redisearch provides a Go client for the RediSearch search engine. -// -// For the full documentation of RediSearch, see [http://redisearch.io](http://redisearch.io) -// -// Example Usage -// -//```go -// import ( -// "github.com/RediSearch/redisearch-go/redisearch" -// "log" -// "fmt" -// ) -// -// func ExampleClient() { -// // Create a client. By default a client is schemaless -// // unless a schema is provided when creating the index -// c := createClient("myIndex") -// -// // Create a schema -// sc := redisearch.NewSchema(redisearch.DefaultOptions). -// AddField(redisearch.NewTextField("body")). -// AddField(redisearch.NewTextFieldOptions("title", redisearch.TextFieldOptions{Weight: 5.0, Sortable: true})). -// AddField(redisearch.NewNumericField("date")) -// -// // Drop an existing index. If the index does not exist an error is returned -// c.Drop() -// -// // Create the index with the given schema -// if err := c.CreateIndex(sc); err != nil { -// log.Fatal(err) -// } -// -// // Create a document with an id and given score -// doc := redisearch.NewDocument("doc1", 1.0) -// doc.Set("title", "Hello world"). -// Set("body", "foo bar"). -// Set("date", time.Now().Unix()) -// -// // Index the document. The API accepts multiple documents at a time -// if err := c.IndexOptions(redisearch.DefaultIndexingOptions, doc); err != nil { -// log.Fatal(err) -// } -// -// // Searching with limit and sorting -// docs, total, err := c.Search(redisearch.NewQuery("hello world"). -// Limit(0, 2). -// SetReturnFields("title")) -// -// fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) -// // Output: doc1 Hello world 1 -// } -//``` -package redisearch diff --git a/redisearch/redisearch_test.go b/redisearch/redisearch_test.go index 23bd406..760d00b 100644 --- a/redisearch/redisearch_test.go +++ b/redisearch/redisearch_test.go @@ -468,45 +468,3 @@ func TestFilter(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 0, total) } - -func ExampleClient() { - - // Create a client. By default a client is schemaless - // unless a schema is provided when creating the index - c := createClient("myIndex") - - // Create a schema - sc := NewSchema(DefaultOptions). - AddField(NewTextField("body")). - AddField(NewTextFieldOptions("title", TextFieldOptions{Weight: 5.0, Sortable: true})). - AddField(NewNumericField("date")). - AddField(NewGeoFieldOptions("location", GeoFieldOptions{})) - - // Drop an existing index. If the index does not exist an error is returned - c.Drop() - - // Create the index with the given schema - if err := c.CreateIndex(sc); err != nil { - log.Fatal(err) - } - - // Create a document with an id and given score - doc := NewDocument("doc1", 1.0) - doc.Set("title", "Hello world"). - Set("body", "foo bar"). - Set("date", time.Now().Unix()). - Set("location", "13.361389,38.115556") - - // Index the document. The API accepts multiple documents at a time - if err := c.IndexOptions(DefaultIndexingOptions, doc); err != nil { - log.Fatal(err) - } - - // Searching with limit and sorting - docs, total, err := c.Search(NewQuery("hello world"). - Limit(0, 2). - SetReturnFields("title")) - - fmt.Println(docs[0].Id, docs[0].Properties["title"], total, err) - // Output: doc1 Hello world 1 -}