add memcached provider (#296)

* add memcached provider

* add testing
This commit is contained in:
Clint Armstrong 2016-10-24 05:03:18 -04:00 committed by xenolf
parent e953bbc8b9
commit 4bb8bea031
6 changed files with 206 additions and 0 deletions

View file

@ -3,6 +3,10 @@ go:
- 1.6.3 - 1.6.3
- 1.7 - 1.7
- tip - tip
services:
- memcached
env:
- MEMCACHED_HOSTS=localhost:11211
install: install:
- go get -t ./... - go get -t ./...
script: script:

4
cli.go
View file

@ -138,6 +138,10 @@ func main() {
Name: "webroot", Name: "webroot",
Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge", Usage: "Set the webroot folder to use for HTTP based challenges to write directly in a file in .well-known/acme-challenge",
}, },
cli.StringSliceFlag{
Name: "memcached-host",
Usage: "Set the memcached host(s) to use for HTTP based challenges. Challenges will be written to all specified hosts.",
},
cli.StringFlag{ cli.StringFlag{
Name: "http", Name: "http",
Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port", Usage: "Set the port and interface to use for HTTP based challenges to listen on. Supported: interface:port or :port",

View file

@ -31,6 +31,7 @@ import (
"github.com/xenolf/lego/providers/dns/rfc2136" "github.com/xenolf/lego/providers/dns/rfc2136"
"github.com/xenolf/lego/providers/dns/route53" "github.com/xenolf/lego/providers/dns/route53"
"github.com/xenolf/lego/providers/dns/vultr" "github.com/xenolf/lego/providers/dns/vultr"
"github.com/xenolf/lego/providers/http/memcached"
"github.com/xenolf/lego/providers/http/webroot" "github.com/xenolf/lego/providers/http/webroot"
) )
@ -101,6 +102,18 @@ func setup(c *cli.Context) (*Configuration, *Account, *acme.Client) {
// infer that the user also wants to exclude all other challenges // infer that the user also wants to exclude all other challenges
client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01}) client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01})
} }
if c.GlobalIsSet("memcached-host") {
provider, err := memcached.NewMemcachedProvider(c.GlobalStringSlice("memcached-host"))
if err != nil {
logger().Fatal(err)
}
client.SetChallengeProvider(acme.HTTP01, provider)
// --memcached-host=foo:11211 indicates that the user specifically want to do a HTTP challenge
// infer that the user also wants to exclude all other challenges
client.ExcludeChallenges([]acme.Challenge{acme.DNS01, acme.TLSSNI01})
}
if c.GlobalIsSet("http") { if c.GlobalIsSet("http") {
if strings.Index(c.GlobalString("http"), ":") == -1 { if strings.Index(c.GlobalString("http"), ":") == -1 {
logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.") logger().Fatalf("The --http switch only accepts interface:port or :port for its argument.")

View file

@ -0,0 +1,15 @@
# Memcached http provider
Publishes challenges into memcached where they can be retrieved by nginx. Allows
specifying multiple memcached servers and the responses will be published to all
of them, making it easier to verify when your domain is hosted on a cluster of
servers.
Example nginx config:
```
location /.well-known/acme-challenge/ {
set $memcached_key "$uri";
memcached_pass 127.0.0.1:11211;
}
```

View file

@ -0,0 +1,59 @@
// Package webroot implements a HTTP provider for solving the HTTP-01 challenge using web server's root path.
package memcached
import (
"fmt"
"path"
"github.com/rainycape/memcache"
"github.com/xenolf/lego/acme"
)
// HTTPProvider implements ChallengeProvider for `http-01` challenge
type MemcachedProvider struct {
hosts []string
}
// NewHTTPProvider returns a HTTPProvider instance with a configured webroot path
func NewMemcachedProvider(hosts []string) (*MemcachedProvider, error) {
if len(hosts) == 0 {
return nil, fmt.Errorf("No memcached hosts provided")
}
c := &MemcachedProvider{
hosts: hosts,
}
return c, nil
}
// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given webroot path
func (w *MemcachedProvider) Present(domain, token, keyAuth string) error {
var errs []error
challengePath := path.Join("/", acme.HTTP01ChallengePath(token))
for _, host := range w.hosts {
mc, err := memcache.New(host)
if err != nil {
errs = append(errs, err)
continue
}
mc.Add(&memcache.Item{
Key: challengePath,
Value: []byte(keyAuth),
Expiration: 60,
})
}
if len(errs) == len(w.hosts) {
return fmt.Errorf("Unable to store key in any of the memcache hosts -> %v", errs)
}
return nil
}
// CleanUp removes the file created for the challenge
func (w *MemcachedProvider) CleanUp(domain, token, keyAuth string) error {
// Memcached will clean up itself, that's what expiration is for.
return nil
}

View file

@ -0,0 +1,111 @@
package memcached
import (
"os"
"path"
"strings"
"testing"
"github.com/rainycape/memcache"
"github.com/stretchr/testify/assert"
"github.com/xenolf/lego/acme"
)
var (
memcachedHosts []string
)
const (
domain = "lego.test"
token = "foo"
keyAuth = "bar"
)
func init() {
memcachedHostsStr := os.Getenv("MEMCACHED_HOSTS")
if len(memcachedHostsStr) > 0 {
memcachedHosts = strings.Split(memcachedHostsStr, ",")
}
}
func TestNewMemcachedProviderEmpty(t *testing.T) {
emptyHosts := make([]string, 0)
_, err := NewMemcachedProvider(emptyHosts)
assert.EqualError(t, err, "No memcached hosts provided")
}
func TestNewMemcachedProviderValid(t *testing.T) {
if len(memcachedHosts) == 0 {
t.Skip("Skipping memcached tests")
}
_, err := NewMemcachedProvider(memcachedHosts)
assert.NoError(t, err)
}
func TestMemcachedPresentSingleHost(t *testing.T) {
if len(memcachedHosts) == 0 {
t.Skip("Skipping memcached tests")
}
p, err := NewMemcachedProvider(memcachedHosts[0:1])
assert.NoError(t, err)
challengePath := path.Join("/", acme.HTTP01ChallengePath(token))
err = p.Present(domain, token, keyAuth)
assert.NoError(t, err)
mc, err := memcache.New(memcachedHosts[0])
assert.NoError(t, err)
i, err := mc.Get(challengePath)
assert.NoError(t, err)
assert.Equal(t, i.Value, []byte(keyAuth))
}
func TestMemcachedPresentMultiHost(t *testing.T) {
if len(memcachedHosts) <= 1 {
t.Skip("Skipping memcached multi-host tests")
}
p, err := NewMemcachedProvider(memcachedHosts)
assert.NoError(t, err)
challengePath := path.Join("/", acme.HTTP01ChallengePath(token))
err = p.Present(domain, token, keyAuth)
assert.NoError(t, err)
for _, host := range memcachedHosts {
mc, err := memcache.New(host)
assert.NoError(t, err)
i, err := mc.Get(challengePath)
assert.NoError(t, err)
assert.Equal(t, i.Value, []byte(keyAuth))
}
}
func TestMemcachedPresentPartialFailureMultiHost(t *testing.T) {
if len(memcachedHosts) == 0 {
t.Skip("Skipping memcached tests")
}
hosts := append(memcachedHosts, "5.5.5.5:11211")
p, err := NewMemcachedProvider(hosts)
assert.NoError(t, err)
challengePath := path.Join("/", acme.HTTP01ChallengePath(token))
err = p.Present(domain, token, keyAuth)
assert.NoError(t, err)
for _, host := range memcachedHosts {
mc, err := memcache.New(host)
assert.NoError(t, err)
i, err := mc.Get(challengePath)
assert.NoError(t, err)
assert.Equal(t, i.Value, []byte(keyAuth))
}
}
func TestMemcachedCleanup(t *testing.T) {
if len(memcachedHosts) == 0 {
t.Skip("Skipping memcached tests")
}
p, err := NewMemcachedProvider(memcachedHosts)
assert.NoError(t, err)
assert.NoError(t, p.CleanUp(domain, token, keyAuth))
}