Skip to content

Commit dfdd74a

Browse files
authored
Multiple SSL files, dynamic SSL, works with Træfik provider (#2)
* Fix blank data returned and setup for multi ssl with providers * Enhance multiple certificates with providers (Træfik) * RC for 1.0 - Manage multi SSL done * Fix CI * Fix CI * Add Header response initialization * Add dynamic certs * Update doc * Fix travis * Fix linter * Makefile * Handle acme.json doesn't exist or an error occurred
1 parent 65b4a78 commit dfdd74a

14 files changed

Lines changed: 272 additions & 33 deletions

Dockerfile-dev

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ RUN apk update && apk upgrade && \
66
RUN mkdir -p /app/src/github.com/darkweak/souin
77
ADD ./*.go /app/src/github.com/darkweak/souin/
88
ADD ./cache /app/src/github.com/darkweak/souin/cache
9+
ADD ./providers /app/src/github.com/darkweak/souin/providers
910
ADD ./default/server.* /app/src/github.com/darkweak/souin/
1011

1112
WORKDIR /app/src/github.com/darkweak/souin

Dockerfile-prod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ RUN mkdir -p /app/src/github.com/darkweak/cmd
77
RUN mkdir -p /app/src/github.com/darkweak/souin
88
ADD ./*.go /app/src/github.com/darkweak/souin/
99
ADD ./cache /app/src/github.com/darkweak/souin/cache
10+
ADD ./providers /app/src/github.com/darkweak/souin/providers
1011
ADD ./default/server.* /app/src/github.com/darkweak/souin/
1112
ADD ./entrypoint.sh /app/src/github.com/darkweak/souin/entrypoint.sh
1213

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ help:
3232
@grep -E '(^[0-9a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-25s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
3333

3434
lint: ## Run lint
35-
$(DC_EXEC) souin /app/bin/golint ./cache
35+
$(DC_EXEC) souin /app/bin/golint ./...
3636

3737
tests: ## Run tests
3838
$(DC_EXEC) souin go test -v ./...

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ services:
101101
GOPATH: /app
102102
volumes:
103103
- ./cmd:/app/cmd
104+
- ./acme.json:/app/src/github.com/darkweak/souin/acme.json
104105
<<: *networks
105106

106107
redis:
@@ -111,3 +112,15 @@ networks:
111112
your_network:
112113
external: true
113114
```
115+
116+
## SSL
117+
118+
### Træfik
119+
As Souin is compatible with Træfik, it can use (and it should use) acme.json provided on træfik. Souin will get new/updated certs from Træfik, then your SSL certs will be up to date as far as Træfik will be too
120+
To provide, acme, use just have to map volume as above
121+
```yaml
122+
volumes:
123+
- /anywhere/acme.json:/app/src/github.com/darkweak/souin/acme.json
124+
```
125+
At the moment you can't choose the path for the acme.json in the container, in the future you'll be able to do that just setting one env var
126+
If none acme.json is provided to container, a default cert will be served.

cache/redisConnection.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ func pathnameNotInRegex(pathname string) bool {
2121
return !b
2222
}
2323

24-
func getRequestInCache(pathname string) ReverseResponse {
25-
client := redisClientConnectionFactory()
24+
func getRequestInCache(pathname string, client *redis.Client) ReverseResponse {
2625
val2, err := client.Get(pathname).Result()
2726

2827
if err != nil {
@@ -32,20 +31,17 @@ func getRequestInCache(pathname string) ReverseResponse {
3231
return ReverseResponse{val2, nil, nil}
3332
}
3433

35-
func deleteKey(key string) {
36-
client := redisClientConnectionFactory();
34+
func deleteKey(key string, client *redis.Client) {
3735
client.Do("del", key)
3836
}
3937

40-
func deleteKeys(regex string) {
41-
client := redisClientConnectionFactory();
38+
func deleteKeys(regex string, client *redis.Client) {
4239
for _, i := range client.Keys(regex).Val() {
4340
client.Do("del", i)
4441
}
4542
}
4643

47-
func setRequestInCache(pathname string, data []byte) {
48-
client := redisClientConnectionFactory()
44+
func setRequestInCache(pathname string, data []byte, client *redis.Client) {
4945
value, _ := strconv.Atoi(os.Getenv("TTL"))
5046

5147
err := client.Set(pathname, string(data), time.Duration(value) * time.Second).Err()

cache/requestService.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"bytes"
99
"encoding/json"
1010
"strings"
11+
"github.com/go-redis/redis"
12+
"strconv"
1113
)
1214

1315
//RequestResponse object contains the request belongs to reverse-proxy
@@ -38,20 +40,23 @@ func getKeyFromResponse(resp *http.Response) string {
3840
return resp.Request.Host + resp.Request.URL.Path
3941
}
4042

41-
func rewriteBody(resp *http.Response) (err error) {
43+
func rewriteBody(resp *http.Response, redisClient *redis.Client) (err error) {
4244
b := bytes.Replace(commonLoadingRequest(resp), []byte("server"), []byte("schmerver"), -1)
4345
body := ioutil.NopCloser(bytes.NewReader(b))
46+
resp.Header = make(http.Header)
47+
resp.Body = body
48+
resp.ContentLength = int64(len(b))
49+
resp.Header.Set("Content-Length", strconv.Itoa(len(b)))
4450

4551
if pathnameNotInRegex(resp.Request.Host+resp.Request.URL.Path) && !hasNotAllowedHeaders(resp) && nil == resp.Request.Context().Err() {
46-
if http.MethodGet == resp.Request.Method {
52+
key := getKeyFromResponse(resp)
53+
if http.MethodGet == resp.Request.Method && len(b) > 0 {
4754
r, _ := json.Marshal(RequestResponse{b, resp.Header})
48-
4955
go func() {
50-
setRequestInCache(getKeyFromResponse(resp), r)
56+
setRequestInCache(key, r, redisClient)
5157
}()
5258
} else {
53-
key := getKeyFromResponse(resp)
54-
deleteKey(key)
59+
deleteKey(key, redisClient)
5560

5661
if http.MethodDelete == resp.Request.Method || http.MethodPut == resp.Request.Method || http.MethodPatch == resp.Request.Method {
5762
newKeySplitted := strings.Split(key, "/")
@@ -63,25 +68,28 @@ func rewriteBody(resp *http.Response) (err error) {
6368
newKey += "/"
6469
}
6570
}
66-
deleteKey(newKey)
71+
go func() {
72+
deleteKey(newKey, redisClient)
73+
}()
6774
}
6875
}
6976
}
7077

71-
resp.Body = body
7278
return nil
7379
}
7480

75-
func requestReverseProxy(req *http.Request, url *url.URL) ReverseResponse {
81+
func requestReverseProxy(req *http.Request, url *url.URL, redisClient *redis.Client) ReverseResponse {
7682
req.URL.Host = req.Host
7783
req.URL.Scheme = url.Scheme
7884
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
7985

8086
proxy := httputil.NewSingleHostReverseProxy(url)
81-
proxy.ModifyResponse = rewriteBody
87+
proxy.ModifyResponse = func(response *http.Response) error {
88+
return rewriteBody(response, redisClient)
89+
}
8290

8391
return ReverseResponse{
84-
"",
92+
"bad",
8593
proxy,
8694
req,
8795
}

cache/requestService_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import (
1515
const DOMAIN = "domain.com"
1616
const PATH = "/testing"
1717

18+
func mockRedis() *redis.Client {
19+
return redisClientConnectionFactory()
20+
}
21+
1822
func mockResponse(path string, method string, body string, code int) *http.Response {
1923
return &http.Response{
2024
Status: "",
@@ -82,7 +86,7 @@ func shouldNotHaveKey(pathname string) bool {
8286

8387
func TestKeyShouldBeDeletedOnPost(t *testing.T) {
8488
populateRedisWithFakeData()
85-
rewriteBody(mockResponse(PATH, http.MethodPost, "My second response", 201))
89+
rewriteBody(mockResponse(PATH, http.MethodPost, "My second response", 201), mockRedis())
8690
time.Sleep(10 * time.Second)
8791
if !shouldNotHaveKey(PATH) {
8892
generateError(t, "The key "+DOMAIN+PATH+" shouldn't exist.")
@@ -101,12 +105,12 @@ func verifyKeysExists(t *testing.T, path string, keys []string) {
101105

102106
func TestKeyShouldBeDeletedOnPut(t *testing.T) {
103107
populateRedisWithFakeData()
104-
rewriteBody(mockResponse(PATH+"/1", http.MethodPut, "My second response", 200))
108+
rewriteBody(mockResponse(PATH+"/1", http.MethodPut, "My second response", 200), mockRedis())
105109
verifyKeysExists(t, PATH, []string{"", "/1"})
106110
}
107111

108112
func TestKeyShouldBeDeletedOnDelete(t *testing.T) {
109113
populateRedisWithFakeData()
110-
rewriteBody(mockResponse(PATH+"/1", http.MethodDelete, "", 200))
114+
rewriteBody(mockResponse(PATH+"/1", http.MethodDelete, "", 200), mockRedis())
111115
verifyKeysExists(t, PATH, []string{"", "/1"})
112116
}

cache/souin.go

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,29 @@ import (
66
"os"
77
"net/http/httputil"
88
"encoding/json"
9+
"github.com/go-redis/redis"
10+
"crypto/tls"
11+
"github.com/darkweak/souin/providers"
12+
"fmt"
13+
"context"
14+
"net"
915
)
1016

1117
// ReverseResponse object contains the response from reverse-proxy
1218
type ReverseResponse struct {
1319
response string
14-
proxy *httputil.ReverseProxy
15-
request *http.Request
20+
proxy *httputil.ReverseProxy
21+
request *http.Request
1622
}
1723

18-
func serveReverseProxy(res http.ResponseWriter, req *http.Request) {
24+
func serveReverseProxy(res http.ResponseWriter, req *http.Request, redisClient *redis.Client) {
1925
url, _ := url.Parse(os.Getenv("REVERSE_PROXY"))
2026
ctx := req.Context()
2127

2228
responses := make(chan ReverseResponse)
2329
go func() {
24-
responses<- getRequestInCache(req.Host + req.URL.Path)
25-
responses<- requestReverseProxy(req, url)
30+
responses <- getRequestInCache(req.Host+req.URL.Path, redisClient)
31+
responses <- requestReverseProxy(req, url, redisClient)
2632
}()
2733

2834
response := <-responses
@@ -33,6 +39,7 @@ func serveReverseProxy(res http.ResponseWriter, req *http.Request) {
3339
if err != nil {
3440
panic(err)
3541
}
42+
3643
for k, v := range responseJSON.Headers {
3744
res.Header().Set(k, v[0])
3845
}
@@ -44,15 +51,67 @@ func serveReverseProxy(res http.ResponseWriter, req *http.Request) {
4451
}
4552
}
4653

54+
func startServer(tlsconfig *tls.Config) (net.Listener, *http.Server) {
55+
tlsconfig.BuildNameToCertificate()
56+
server := http.Server{
57+
Addr: ":443",
58+
Handler: nil,
59+
TLSConfig: tlsconfig,
60+
}
61+
listener, err := tls.Listen("tcp", ":443", tlsconfig)
62+
if err != nil {
63+
fmt.Println(err)
64+
}
65+
go func() {
66+
error := server.Serve(listener)
67+
fmt.Println("YO")
68+
fmt.Println(error)
69+
fmt.Println("LO")
70+
if nil != error {
71+
fmt.Println(error)
72+
}
73+
}()
74+
75+
return listener, &server
76+
}
77+
4778
// Start cache system
4879
func Start() {
49-
http.HandleFunc("/", serveReverseProxy)
80+
redisClient := redisClientConnectionFactory()
81+
configChannel := make(chan int)
82+
tlsconfig := &tls.Config{
83+
Certificates: make([]tls.Certificate, 0),
84+
NameToCertificate: make(map[string]*tls.Certificate),
85+
InsecureSkipVerify: true,
86+
}
87+
v, _ := tls.LoadX509KeyPair("server.crt", "server.key")
88+
tlsconfig.Certificates = append(tlsconfig.Certificates, v)
89+
certificates := providers.CommonProvider{
90+
Certificates: make(map[string]providers.Certificate),
91+
}
92+
5093
go func() {
51-
if err := http.ListenAndServeTLS(":" + os.Getenv("CACHE_TLS_PORT"), "server.crt", "server.key", nil); err != nil {
52-
panic(err)
94+
providers.InitProviders(&certificates, tlsconfig, &configChannel)
95+
}()
96+
97+
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
98+
serveReverseProxy(writer, request, redisClient)
99+
})
100+
go func() {
101+
listener, server := startServer(tlsconfig)
102+
for {
103+
select {
104+
case <- configChannel:
105+
listener.Close()
106+
if err := server.Shutdown(context.Background()); err != nil {
107+
fmt.Errorf("Shutdown failed: %s", err)
108+
}
109+
listener, server = startServer(tlsconfig)
110+
}
53111
}
112+
54113
}()
55-
if err := http.ListenAndServe(":" + os.Getenv("CACHE_PORT"), nil); err != nil {
114+
if err := http.ListenAndServe(":"+os.Getenv("CACHE_PORT"), nil); err != nil {
56115
panic(err)
57116
}
58117

docker-compose.yml.prod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ services:
2323
GOPATH: /app
2424
volumes:
2525
- ./cmd:/app/cmd
26+
- ./acme.json:/app/src/github.com/darkweak/souin/acme.json
2627
<<: *networks
2728

2829
redis:

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@ module github.com/darkweak/souin
22

33
go 1.13
44

5-
require github.com/go-redis/redis v6.15.6+incompatible
5+
require (
6+
github.com/fsnotify/fsnotify v1.4.7
7+
github.com/go-redis/redis v6.15.6+incompatible
8+
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e // indirect
9+
)

0 commit comments

Comments
 (0)