Skip to content

Commit 4052470

Browse files
initial implementation rfc9111
1 parent 5340a84 commit 4052470

22 files changed

Lines changed: 751 additions & 121 deletions

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ sqlite3
1919
INSERT INTO temp.http_request VALUES('https://swapi.tech/api/films/1');
2020

2121
# Fetch data from http_response table (created by the extension)
22-
SELECT * FROM http_response;
23-
2422
SELECT JSON_EXTRACT(body, '$.result.properties.title') AS title,
2523
JSON_EXTRACT(body, '$.result.properties.release_date') AS release_date
2624
FROM http_response;
25+
26+
# Use cacheage, cachelifetime or cachexpired function to check cache validity based on RFC9111
27+
SELECT url, cacheage(header) AS age, cachelifetime(header, request_time, true) AS lifetime, cachexpired(header, request_time, true) AS expired
28+
FROM http_response;
2729
```
2830

2931
## Configuring
@@ -34,7 +36,7 @@ You can configure the behaviour by passing parameters to a VIRTUAL TABLE.
3436
|-------|-------------|---------|
3537
| timeout | Timeout in milliseconds | 0 |
3638
| insecure | Insecure skip TLS validation | false |
37-
| ignore_status_error | Don't persist responses if status code != 2xx | false |
39+
| status_code | Comma-separated list of HTTP status code to persist. Use empty to persist all status | 200,301,404 |
3840
| response_table | Database table used to store response data | http_response |
3941
| oauth2_client_id | Oauth2 Client ID | |
4042
| oauth2_client_secret | Oauth2 Client Secret | |

cmd/sqlite-http-proxy/main.go

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ func main() {
2828
allowHTTP2 := fs.BoolLong("h2", "Allow HTTP2")
2929
ttl := fs.UintLong("ttl", 0, "Time to Live in seconds (0 is infinite time)")
3030
responseTables := fs.StringListLong("response-table", "List of database tables used to store response data")
31-
forceCreateTables := fs.BoolLong("force-create-tables", "Force create response tables if not exists")
3231
caCert := fs.StringLong("ca-cert", "", "Path to CA Certificate file (required to HTTPS proxy)")
3332
caCertKey := fs.StringLong("ca-cert-key", "", "Path to CA Certificate Key file (required to HTTPS proxy)")
3433
readOnly := fs.BoolLong("ro", "Read Only mode. Do not store new HTTP responses")
34+
rfc9111 := fs.BoolLong("rfc9111", "Use RFC9111 spec")
35+
shared := fs.BoolLong("shared", "Enable shared cache mode")
3536
_ = fs.String('c', "config", "", "config file (optional)")
3637

3738
if err := ff.Parse(fs, os.Args[1:],
@@ -49,8 +50,8 @@ func main() {
4950
}
5051

5152
if *verbose {
52-
fmt.Printf("Using options: port=%d db-params=%s, h2=%v, ttl=%d, response-tables=%v, force-create-tables=%v, ca-cert=%s, ca-cert-key=%s, read-only=%v\n",
53-
*port, *dbParams, *allowHTTP2, *ttl, *responseTables, *forceCreateTables, *caCert, *caCertKey, *readOnly)
53+
fmt.Printf("Using options: port=%d db-params=%s, h2=%v, ttl=%d, response-tables=%v, ca-cert=%s, ca-cert-key=%s, read-only=%v\n",
54+
*port, *dbParams, *allowHTTP2, *ttl, *responseTables, *caCert, *caCertKey, *readOnly)
5455
}
5556

5657
dbs := make([]*sql.DB, 0)
@@ -99,12 +100,11 @@ func main() {
99100
}
100101
} else {
101102
tableList = *responseTables
102-
if *forceCreateTables {
103-
err := db.CreateResponseTables(sqlDB, tableList...)
104-
if err != nil {
105-
log.Fatalf("force create tables on DB %q: %v", dsn, err)
106-
}
103+
err := db.CreateResponseTables(sqlDB, tableList...)
104+
if err != nil {
105+
log.Fatalf("force create tables on DB %q: %v", dsn, err)
107106
}
107+
108108
}
109109
}
110110
if len(dbs) == 1 {
@@ -140,18 +140,36 @@ func main() {
140140
proxy.Logger.Printf("INFO: Starting HTTP Proxy...")
141141
}
142142

143-
proxy.OnRequest().Do(&requestHandler{
144-
querier: repository,
145-
verbose: *verbose,
146-
ttl: *ttl,
147-
readOnly: *readOnly,
148-
})
149-
if !*readOnly {
150-
proxy.OnResponse().Do(&responseHandler{
151-
writer: repository,
152-
verbose: *verbose,
143+
if *rfc9111 {
144+
proxy.OnRequest().Do(&requestRFC9111Handler{
145+
shared: *shared,
146+
querier: repository,
147+
verbose: *verbose,
148+
readOnly: *readOnly,
149+
})
150+
} else {
151+
proxy.OnRequest().Do(&requestHandler{
152+
querier: repository,
153+
verbose: *verbose,
154+
ttl: *ttl,
155+
readOnly: *readOnly,
153156
})
154157
}
158+
if !*readOnly {
159+
if *rfc9111 {
160+
proxy.OnResponse().Do(&responseRFC9111Handler{
161+
shared: *shared,
162+
writer: repository,
163+
verbose: *verbose,
164+
})
165+
} else {
166+
proxy.OnResponse().Do(&responseHandler{
167+
writer: repository,
168+
verbose: *verbose,
169+
})
170+
}
171+
172+
}
155173

156174
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
157175
if err != nil {

cmd/sqlite-http-proxy/request.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/elazarl/goproxy"
1313

1414
"github.com/walterwanderley/sqlite-http-cache/db"
15+
cachehttp "github.com/walterwanderley/sqlite-http-cache/http"
1516
)
1617

1718
type requestQuerier interface {
@@ -25,6 +26,12 @@ type requestHandler struct {
2526
querier requestQuerier
2627
}
2728

29+
type userData struct {
30+
requestTime time.Time
31+
databaseID int
32+
tableName string
33+
}
34+
2835
func (h *requestHandler) Handle(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
2936
if r.Method != http.MethodGet {
3037
return r, nil
@@ -37,25 +44,33 @@ func (h *requestHandler) Handle(r *http.Request, ctx *goproxy.ProxyCtx) (*http.R
3744
slog.Error("database query", "error", err.Error())
3845
}
3946
// tell the responseHandler to save the new response data
40-
ctx.UserData = ":-1"
47+
ctx.UserData = userData{
48+
requestTime: time.Now(),
49+
databaseID: -1,
50+
}
4151
return r, nil
4252
}
4353

44-
if !h.readOnly && h.ttl > 0 && uint(time.Since(resp.Timestamp).Seconds()) > h.ttl {
54+
if !h.readOnly && h.ttl > 0 && uint(time.Since(resp.ResponseTime).Seconds()) > h.ttl {
4555
// data is too old, tell the responseHandler to save the new data
46-
ctx.UserData = fmt.Sprintf("%s:%d", resp.TableName, resp.DatabaseID)
56+
ctx.UserData = userData{
57+
requestTime: time.Now(),
58+
databaseID: resp.DatabaseID,
59+
tableName: resp.TableName,
60+
}
4761
return r, nil
4862
}
4963
if h.verbose {
50-
slog.Info("serving from database", "url", url, "status", resp.Status, "timestamp", resp.Timestamp.Format(time.RFC3339))
64+
slog.Info("serving from database", "url", url, "status", resp.Status, "request_time", resp.RequestTime.Format(time.RFC3339), "response_time", resp.ResponseTime.Format(time.RFC3339))
5165
}
5266

5367
header := http.Header(resp.Header)
54-
if date := header.Get("Date"); date != "" {
55-
d, err := time.Parse(time.RFC1123, date)
56-
if err == nil {
57-
header.Set("Age", fmt.Sprint(int(time.Since(d).Seconds())))
58-
}
68+
if header.Get("Date") == "" {
69+
header.Set("Date", time.Now().Format(time.RFC1123))
70+
}
71+
// TODO fix age calc based on RFC 9111
72+
if age := cachehttp.Age(header); age != nil {
73+
header.Set("Age", fmt.Sprint(*age))
5974
}
6075

6176
return r, &http.Response{
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"errors"
6+
"fmt"
7+
"log/slog"
8+
"net/http"
9+
"time"
10+
11+
"github.com/elazarl/goproxy"
12+
13+
cachehttp "github.com/walterwanderley/sqlite-http-cache/http"
14+
)
15+
16+
type requestRFC9111Handler struct {
17+
shared bool
18+
verbose bool
19+
readOnly bool
20+
querier requestQuerier
21+
}
22+
23+
func (h *requestRFC9111Handler) Handle(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
24+
now := time.Now()
25+
if r.Method != http.MethodGet {
26+
return r, nil
27+
}
28+
29+
cc := cachehttp.ParseCacheControl(r.Header, nil, h.shared)
30+
if !cc.Cacheable() {
31+
return r, nil
32+
}
33+
if h.shared && r.Header.Get("Authorization") != "" {
34+
return r, nil
35+
}
36+
37+
url := ctx.Req.URL.String()
38+
resp, err := h.querier.FindByURL(r.Context(), url)
39+
if err != nil {
40+
if !errors.Is(err, sql.ErrNoRows) {
41+
slog.Error("database query", "error", err.Error())
42+
}
43+
44+
if !h.readOnly {
45+
// tell the responseHandler to save the new response data
46+
ctx.UserData = userData{
47+
requestTime: now,
48+
databaseID: -1,
49+
}
50+
}
51+
return r, nil
52+
}
53+
54+
respCC := cachehttp.ParseCacheControl(http.Header(resp.Header), &now, h.shared)
55+
56+
if respCC.Expired() {
57+
fmt.Println("AQUIIIII")
58+
if !h.readOnly {
59+
// data is too old, tell the responseHandler to save the new data
60+
ctx.UserData = userData{
61+
requestTime: now,
62+
databaseID: resp.DatabaseID,
63+
tableName: resp.TableName,
64+
}
65+
}
66+
return r, nil
67+
}
68+
if h.verbose {
69+
slog.Info("serving from database", "url", url, "status", resp.Status, "request_time", resp.RequestTime.Format(time.RFC3339), "response_time", resp.ResponseTime.Format(time.RFC3339))
70+
}
71+
72+
header := http.Header(resp.Header)
73+
if header.Get("Date") == "" {
74+
header.Set("Date", time.Now().Format(time.RFC1123))
75+
}
76+
// TODO fix age calc based on RFC 9111
77+
if age := cachehttp.Age(header); age != nil {
78+
header.Set("Age", fmt.Sprint(*age))
79+
}
80+
81+
return r, &http.Response{
82+
StatusCode: resp.Status,
83+
Body: resp.Body,
84+
Header: header,
85+
}
86+
}

cmd/sqlite-http-proxy/response.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import (
44
"context"
55
"log/slog"
66
"net/http"
7-
"strconv"
8-
"strings"
7+
"time"
98

109
"github.com/elazarl/goproxy"
1110
"github.com/walterwanderley/sqlite-http-cache/db"
@@ -21,7 +20,8 @@ type responseHandler struct {
2120
}
2221

2322
func (h *responseHandler) Handle(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
24-
if ctx.UserData != nil {
23+
ud, ok := ctx.UserData.(userData)
24+
if ok {
2525
if h.verbose {
2626
slog.Info("recording response", "url", ctx.Req.URL.String(), "status", resp.StatusCode)
2727
}
@@ -30,12 +30,10 @@ func (h *responseHandler) Handle(resp *http.Response, ctx *goproxy.ProxyCtx) *ht
3030
slog.Error("adapter response body", "error", err)
3131
} else {
3232
go func() {
33-
userData := ctx.UserData.(string)
34-
tableName, databaseID, ok := strings.Cut(userData, ":")
35-
if ok {
36-
responseDB.DatabaseID, _ = strconv.Atoi(databaseID)
37-
}
38-
responseDB.TableName = tableName
33+
responseDB.RequestTime = ud.requestTime
34+
responseDB.ResponseTime = time.Now()
35+
responseDB.DatabaseID = ud.databaseID
36+
responseDB.TableName = ud.tableName
3937
err := h.writer.Write(context.Background(), ctx.Req.URL.String(), responseDB)
4038
if err != nil {
4139
slog.Error("recording response", "error", err, "url", ctx.Req.URL.String(), "status", resp.StatusCode)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"net/http"
7+
"time"
8+
9+
"github.com/elazarl/goproxy"
10+
"github.com/walterwanderley/sqlite-http-cache/db"
11+
cachehttp "github.com/walterwanderley/sqlite-http-cache/http"
12+
)
13+
14+
type responseRFC9111Handler struct {
15+
shared bool
16+
writer responseWriter
17+
verbose bool
18+
}
19+
20+
func (h *responseRFC9111Handler) Handle(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
21+
now := time.Now()
22+
cc := cachehttp.ParseCacheControl(resp.Header, &now, h.shared)
23+
if !cc.Cacheable() {
24+
return resp
25+
}
26+
27+
ud, ok := ctx.UserData.(userData)
28+
if !ok {
29+
return resp
30+
}
31+
responseDB, err := db.HttpToResponse(resp)
32+
if err != nil {
33+
slog.Error("adapter response body", "error", err)
34+
} else {
35+
if !cc.Expired() {
36+
return resp
37+
}
38+
if h.verbose {
39+
slog.Info("recording response", "url", ctx.Req.URL.String(), "status", resp.StatusCode)
40+
}
41+
go func() {
42+
responseDB.RequestTime = ud.requestTime
43+
responseDB.ResponseTime = time.Now()
44+
responseDB.DatabaseID = ud.databaseID
45+
responseDB.TableName = ud.tableName
46+
err := h.writer.Write(context.Background(), ctx.Req.URL.String(), responseDB)
47+
if err != nil {
48+
slog.Error("recording response", "error", err, "url", ctx.Req.URL.String(), "status", resp.StatusCode)
49+
}
50+
}()
51+
}
52+
53+
return resp
54+
}

0 commit comments

Comments
 (0)