diff --git a/memcache/fakecerts_test.go b/memcache/fakecerts_test.go new file mode 100644 index 00000000..a59fe492 --- /dev/null +++ b/memcache/fakecerts_test.go @@ -0,0 +1,62 @@ +package memcache + +import "strings" + +// Copied from Go's net/http/internal/testcert package. + +// LocalhostCert is a PEM-encoded TLS cert with SAN IPs +// "127.0.0.1" and "[::1]", expiring at Jan 29 16:00:00 2084 GMT. +// generated from src/crypto/tls: +// go run generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h +var LocalhostCert = []byte(`-----BEGIN CERTIFICATE----- +MIIDOTCCAiGgAwIBAgIQSRJrEpBGFc7tNb1fb5pKFzANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw +MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEA6Gba5tHV1dAKouAaXO3/ebDUU4rvwCUg/CNaJ2PT5xLD4N1Vcb8r +bFSW2HXKq+MPfVdwIKR/1DczEoAGf/JWQTW7EgzlXrCd3rlajEX2D73faWJekD0U +aUgz5vtrTXZ90BQL7WvRICd7FlEZ6FPOcPlumiyNmzUqtwGhO+9ad1W5BqJaRI6P +YfouNkwR6Na4TzSj5BrqUfP0FwDizKSJ0XXmh8g8G9mtwxOSN3Ru1QFc61Xyeluk +POGKBV/q6RBNklTNe0gI8usUMlYyoC7ytppNMW7X2vodAelSu25jgx2anj9fDVZu +h7AXF5+4nJS4AAt0n1lNY7nGSsdZas8PbQIDAQABo4GIMIGFMA4GA1UdDwEB/wQE +AwICpDATBgNVHSUEDDAKBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBStsdjh3/JCXXYlQryOrL4Sh7BW5TAuBgNVHREEJzAlggtleGFtcGxlLmNv +bYcEfwAAAYcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAxWGI +5NhpF3nwwy/4yB4i/CwwSpLrWUa70NyhvprUBC50PxiXav1TeDzwzLx/o5HyNwsv +cxv3HdkLW59i/0SlJSrNnWdfZ19oTcS+6PtLoVyISgtyN6DpkKpdG1cOkW3Cy2P2 ++tK/tKHRP1Y/Ra0RiDpOAmqn0gCOFGz8+lqDIor/T7MTpibL3IxqWfPrvfVRHL3B +grw/ZQTTIVjjh4JBSW3WyWgNo/ikC1lrVxzl4iPUGptxT36Cr7Zk2Bsg0XqwbOvK +5d+NTDREkSnUbie4GeutujmX3Dsx88UiV6UY/4lHJa6I5leHUNOHahRbpbWeOfs/ +WkBKOclmOV2xlTVuPw== +-----END CERTIFICATE-----`) + +// LocalhostKey is the private key for LocalhostCert. +var LocalhostKey = []byte(testingKey(`-----BEGIN RSA TESTING KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDoZtrm0dXV0Aqi +4Bpc7f95sNRTiu/AJSD8I1onY9PnEsPg3VVxvytsVJbYdcqr4w99V3AgpH/UNzMS +gAZ/8lZBNbsSDOVesJ3euVqMRfYPvd9pYl6QPRRpSDPm+2tNdn3QFAvta9EgJ3sW +URnoU85w+W6aLI2bNSq3AaE771p3VbkGolpEjo9h+i42TBHo1rhPNKPkGupR8/QX +AOLMpInRdeaHyDwb2a3DE5I3dG7VAVzrVfJ6W6Q84YoFX+rpEE2SVM17SAjy6xQy +VjKgLvK2mk0xbtfa+h0B6VK7bmODHZqeP18NVm6HsBcXn7iclLgAC3SfWU1jucZK +x1lqzw9tAgMBAAECggEABWzxS1Y2wckblnXY57Z+sl6YdmLV+gxj2r8Qib7g4ZIk +lIlWR1OJNfw7kU4eryib4fc6nOh6O4AWZyYqAK6tqNQSS/eVG0LQTLTTEldHyVJL +dvBe+MsUQOj4nTndZW+QvFzbcm2D8lY5n2nBSxU5ypVoKZ1EqQzytFcLZpTN7d89 +EPj0qDyrV4NZlWAwL1AygCwnlwhMQjXEalVF1ylXwU3QzyZ/6MgvF6d3SSUlh+sq +XefuyigXw484cQQgbzopv6niMOmGP3of+yV4JQqUSb3IDmmT68XjGd2Dkxl4iPki +6ZwXf3CCi+c+i/zVEcufgZ3SLf8D99kUGE7v7fZ6AQKBgQD1ZX3RAla9hIhxCf+O +3D+I1j2LMrdjAh0ZKKqwMR4JnHX3mjQI6LwqIctPWTU8wYFECSh9klEclSdCa64s +uI/GNpcqPXejd0cAAdqHEEeG5sHMDt0oFSurL4lyud0GtZvwlzLuwEweuDtvT9cJ +Wfvl86uyO36IW8JdvUprYDctrQKBgQDycZ697qutBieZlGkHpnYWUAeImVA878sJ +w44NuXHvMxBPz+lbJGAg8Cn8fcxNAPqHIraK+kx3po8cZGQywKHUWsxi23ozHoxo ++bGqeQb9U661TnfdDspIXia+xilZt3mm5BPzOUuRqlh4Y9SOBpSWRmEhyw76w4ZP +OPxjWYAgwQKBgA/FehSYxeJgRjSdo+MWnK66tjHgDJE8bYpUZsP0JC4R9DL5oiaA +brd2fI6Y+SbyeNBallObt8LSgzdtnEAbjIH8uDJqyOmknNePRvAvR6mP4xyuR+Bv +m+Lgp0DMWTw5J9CKpydZDItc49T/mJ5tPhdFVd+am0NAQnmr1MCZ6nHxAoGABS3Y +LkaC9FdFUUqSU8+Chkd/YbOkuyiENdkvl6t2e52jo5DVc1T7mLiIrRQi4SI8N9bN +/3oJWCT+uaSLX2ouCtNFunblzWHBrhxnZzTeqVq4SLc8aESAnbslKL4i8/+vYZlN +s8xtiNcSvL+lMsOBORSXzpj/4Ot8WwTkn1qyGgECgYBKNTypzAHeLE6yVadFp3nQ +Ckq9yzvP/ib05rvgbvrne00YeOxqJ9gtTrzgh7koqJyX1L4NwdkEza4ilDWpucn0 +xiUZS4SoaJq6ZvcBYS62Yr1t8n09iG47YL8ibgtmH3L+svaotvpVxVK+d7BLevA/ +ZboOWVe3icTy64BT3OQhmg== +-----END RSA TESTING KEY-----`)) + +func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") } diff --git a/memcache/fakeserver_test.go b/memcache/fakeserver_test.go index 6e644bc2..4508c9e4 100644 --- a/memcache/fakeserver_test.go +++ b/memcache/fakeserver_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2023 The gomemcache AUTHORS + +Licensed 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 memcache import ( diff --git a/memcache/memcache.go b/memcache/memcache.go index 9a63b9c0..b2ebacd2 100644 --- a/memcache/memcache.go +++ b/memcache/memcache.go @@ -132,8 +132,12 @@ func NewFromSelector(ss ServerSelector) *Client { // Client is a memcache client. // It is safe for unlocked use by multiple concurrent goroutines. type Client struct { - // DialContext connects to the address on the named network - // using the provided context + // DialContext connects to the address on the named network using the + // provided context. + // + // To connect to servers using TLS (memcached running with "--enable-ssl"), + // use a DialContext func that uses tls.Dialer.DialContext. See this + // package's tests as an example. DialContext func(ctx context.Context, network, address string) (net.Conn, error) // Timeout specifies the socket read/write timeout. diff --git a/memcache/memcache_test.go b/memcache/memcache_test.go index e9e8ec32..4b60a15d 100644 --- a/memcache/memcache_test.go +++ b/memcache/memcache_test.go @@ -19,17 +19,25 @@ package memcache import ( "bufio" + "bytes" + "context" + "crypto/tls" + "flag" "fmt" "io" "io/ioutil" "net" "os" "os/exec" + "path/filepath" + "strconv" "strings" "testing" "time" ) +var debug = flag.Bool("debug", false, "be more verbose") + const localhostTCPAddr = "localhost:11211" func TestLocalhost(t *testing.T) { @@ -81,6 +89,77 @@ func TestFakeServer(t *testing.T) { testWithClient(t, New(ln.Addr().String())) } +func TestTLS(t *testing.T) { + t.Parallel() + td := t.TempDir() + + // Test whether our memcached binary has TLS support. We --enable-ssl first, + // before --version, as memcached evaluates the flags in the order provided + // and we want it to fail if it's built without TLS support (as it is in + // Debian, but not Ubuntu or Homebrew). + out, err := exec.Command("memcached", "--enable-ssl", "--version").CombinedOutput() + if err != nil { + t.Skipf("skipping test; couldn't find memcached or no TLS support in binary: %v, %s", err, out) + } + t.Logf("version: %s", bytes.TrimSpace(out)) + + if err := os.WriteFile(filepath.Join(td, "/cert.pem"), LocalhostCert, 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(td, "/key.pem"), LocalhostKey, 0644); err != nil { + t.Fatal(err) + } + + // Find some unused port. This is racy but we hope for the best and hope the kernel + // doesn't reassign our ephemeral port to somebody in the tiny race window. + ln, err := net.Listen("tcp", "localhost:0") + if err != nil { + t.Fatalf("failed to listen: %v", err) + } + port := ln.Addr().(*net.TCPAddr).Port + ln.Close() + + cmd := exec.Command("memcached", + "--port="+strconv.Itoa(port), + "--listen=127.0.0.1", + "--enable-ssl", + "-o", "ssl_chain_cert=cert.pem", + "-o", "ssl_key=key.pem") + cmd.Dir = td + if *debug { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + if err := cmd.Start(); err != nil { + t.Fatalf("failed to start memcached: %v", err) + } + defer cmd.Wait() + defer cmd.Process.Kill() + + // Wait a bit for the server to be running. + for i := 0; i < 10; i++ { + nc, err := net.Dial("tcp", "localhost:"+strconv.Itoa(port)) + if err == nil { + t.Logf("localhost:%d is up.", port) + nc.Close() + break + } + t.Logf("waiting for localhost:%d to be up...", port) + time.Sleep(time.Duration(25*i) * time.Millisecond) + } + + c := New(net.JoinHostPort("127.0.0.1", strconv.Itoa(port))) + c.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + var td tls.Dialer + td.Config = &tls.Config{ + InsecureSkipVerify: true, + } + return td.DialContext(ctx, network, addr) + + } + testWithClient(t, c) +} + func mustSetF(t *testing.T, c *Client) func(*Item) { return func(it *Item) { if err := c.Set(it); err != nil {