Skip to content

Commit 8da4a24

Browse files
proxy TTL
1 parent 1a34b65 commit 8da4a24

14 files changed

Lines changed: 544 additions & 217 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
*.so
22
*.db
3+
*.db-shm
4+
*.db-wal
5+

cmd/sqlite-http-proxy/main.go

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,45 @@ import (
44
"crypto/tls"
55
"crypto/x509"
66
"database/sql"
7-
"errors"
87
"flag"
98
"fmt"
109
"log"
1110
"net"
1211
"net/http"
1312
"os"
1413
"strings"
15-
"time"
1614

1715
"github.com/elazarl/goproxy"
1816
_ "github.com/mattn/go-sqlite3"
1917

2018
"github.com/walterwanderley/sqlite-http-cache/db"
2119
)
2220

23-
func main() {
24-
var (
25-
port uint
26-
allowHTTP2 bool
27-
verbose bool
21+
var (
22+
port uint
23+
allowHTTP2 bool
24+
verbose bool
25+
26+
ttl uint
2827

29-
caCert string
30-
caCertKey string
28+
caCert string
29+
caCertKey string
30+
31+
responseTables string
32+
forceCreateTables bool
33+
readOnly bool
34+
)
3135

32-
responseTables string
33-
)
36+
func main() {
3437
flag.UintVar(&port, "p", 8080, "Server port")
3538
flag.BoolVar(&verbose, "v", false, "Enable verbose mode")
3639
flag.BoolVar(&allowHTTP2, "h2", false, "Allow HTTP2")
40+
flag.UintVar(&ttl, "ttl", 0, "Time to Live in seconds (0 is infinite time)")
3741
flag.StringVar(&responseTables, "response-tables", "", "Comma separated list of database tables used to store response data")
42+
flag.BoolVar(&forceCreateTables, "force-create-tables", false, "Force create response tables if not exists")
3843
flag.StringVar(&caCert, "ca-cert", "", "Path to CA Certificate file (required to HTTPS proxy)")
3944
flag.StringVar(&caCertKey, "ca-cert-key", "", "Path to CA Certificate Key file (required to HTTPS proxy)")
45+
flag.BoolVar(&readOnly, "ro", false, "Read Only mode. Do not store new HTTP responses")
4046
flag.Parse()
4147

4248
if len(flag.Args()) != 1 {
@@ -58,6 +64,12 @@ func main() {
5864
}
5965
} else {
6066
tableList = strings.Split(responseTables, ",")
67+
if forceCreateTables {
68+
err := db.CreateResponseTables(sqlDB, tableList...)
69+
if err != nil {
70+
log.Fatalf("force create tables: %v", err)
71+
}
72+
}
6173
}
6274

6375
repository, err := db.NewRepository(sqlDB, tableList...)
@@ -86,30 +98,14 @@ func main() {
8698
proxy.Logger.Printf("INFO: Starting HTTP Proxy...")
8799
}
88100

89-
proxy.OnRequest().DoFunc(
90-
func(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
91-
if r.Method != http.MethodGet || ctx.Req == nil || ctx.Req.URL == nil {
92-
return r, nil
93-
}
94-
95-
url := ctx.Req.URL.String()
96-
resp, err := repository.FindByURL(r.Context(), url)
97-
if err != nil {
98-
if !errors.Is(err, sql.ErrNoRows) {
99-
proxy.Logger.Printf("ERROR: query error: %s", err.Error())
100-
}
101-
return r, nil
102-
}
103-
if verbose {
104-
proxy.Logger.Printf("INFO: serving from database url=%s status=%d timestamp=%s", url, resp.Status, resp.Timestamp.Format(time.RFC3339))
105-
}
106-
107-
return r, &http.Response{
108-
StatusCode: resp.Status,
109-
Body: resp.Body,
110-
Header: http.Header(resp.Headers),
111-
}
101+
proxy.OnRequest().Do(&requestHandler{
102+
querier: repository,
103+
})
104+
if !readOnly {
105+
proxy.OnResponse().Do(&responseHandler{
106+
writer: repository,
112107
})
108+
}
113109

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

cmd/sqlite-http-proxy/request.go

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+
"database/sql"
6+
"errors"
7+
"log/slog"
8+
"net/http"
9+
"time"
10+
11+
"github.com/elazarl/goproxy"
12+
13+
"github.com/walterwanderley/sqlite-http-cache/db"
14+
)
15+
16+
type requestQuerier interface {
17+
FindByURL(ctx context.Context, url string) (*db.Response, error)
18+
}
19+
20+
type requestHandler struct {
21+
querier requestQuerier
22+
}
23+
24+
func (h *requestHandler) Handle(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
25+
if r.Method != http.MethodGet {
26+
return r, nil
27+
}
28+
29+
url := ctx.Req.URL.String()
30+
resp, err := h.querier.FindByURL(r.Context(), url)
31+
if err != nil {
32+
if !errors.Is(err, sql.ErrNoRows) {
33+
slog.Error("database query", "error", err.Error())
34+
}
35+
// tell the responseHandler to save the new response data
36+
ctx.UserData = ""
37+
return r, nil
38+
}
39+
40+
if !readOnly && uint(time.Since(resp.Timestamp).Seconds()) > ttl {
41+
// data is too old, tell the responseHandler to save the new data
42+
ctx.UserData = resp.TableName
43+
return r, nil
44+
}
45+
if verbose {
46+
slog.Info("serving from database", "url", url, "status", resp.Status, "timestamp", resp.Timestamp.Format(time.RFC3339))
47+
}
48+
49+
return r, &http.Response{
50+
StatusCode: resp.Status,
51+
Body: resp.Body,
52+
Header: http.Header(resp.Headers),
53+
}
54+
}

cmd/sqlite-http-proxy/response.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"net/http"
7+
8+
"github.com/elazarl/goproxy"
9+
"github.com/walterwanderley/sqlite-http-cache/db"
10+
)
11+
12+
type responseWriter interface {
13+
Write(ctx context.Context, url string, resp *db.Response) error
14+
}
15+
16+
type responseHandler struct {
17+
writer responseWriter
18+
}
19+
20+
func (h *responseHandler) Handle(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
21+
if ctx.UserData != nil {
22+
if verbose {
23+
slog.Info("recording response", "url", ctx.Req.URL.String(), "status", resp.StatusCode)
24+
}
25+
responseDB, err := db.HttpToResponse(resp)
26+
if err != nil {
27+
slog.Error("adapter response body", "error", err)
28+
} else {
29+
responseDB.TableName = ctx.UserData.(string)
30+
err := h.writer.Write(context.Background(), ctx.Req.URL.String(), responseDB)
31+
if err != nil {
32+
slog.Error("recording response", "error", err, "url", ctx.Req.URL.String(), "status", resp.StatusCode)
33+
}
34+
}
35+
}
36+
return resp
37+
}

cmd/sqlite-http-refresh/main.go

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
_ "github.com/mattn/go-sqlite3"
1616

1717
"github.com/walterwanderley/sqlite-http-cache/config"
18+
"github.com/walterwanderley/sqlite-http-cache/db"
1819
_ "github.com/walterwanderley/sqlite-http-cache/extension"
1920
)
2021

@@ -26,7 +27,7 @@ var (
2627
timeout uint
2728
insecure bool
2829
ignoreStatusError bool
29-
responseTableName string
30+
responseTables string
3031

3132
oauth2ClientID string
3233
oauth2ClientSecret string
@@ -41,12 +42,12 @@ func main() {
4142
// Scheduler strategy
4243
flag.DurationVar(&interval, "check-interval", 30*time.Second, "Interval to wait for check expired data")
4344
flag.UintVar(&ttl, "ttl", 30*60, "Time to Live in seconds")
44-
flag.StringVar(&matchURL, "match-url", "", "Filter URLs")
45+
flag.StringVar(&matchURL, "match-url", "%", "Filter URLs (SQL syntax)")
4546
// Request/Store config
4647
flag.UintVar(&timeout, "timeout", 30*1000, "Timeout in milliseconds")
4748
flag.BoolVar(&insecure, "insecure", false, "Disable TLS verification")
4849
flag.BoolVar(&ignoreStatusError, "ignore-status-error", false, "ignore responses with status code != 2xx")
49-
flag.StringVar(&responseTableName, "response-table", config.DefaultResponseTableName, "Database table used to store response data")
50+
flag.StringVar(&responseTables, "response-tables", "", "Comma separated list of database tables used to store response data")
5051
// Oauth2 Client Credentials
5152
flag.StringVar(&oauth2ClientID, "oauth2-client-id", "", "Oauth2 Client ID")
5253
flag.StringVar(&oauth2ClientSecret, "oauth2-client-secret", "", "Oauth2 Client Secret")
@@ -57,39 +58,50 @@ func main() {
5758
flag.StringVar(&caFile, "ca-file", "", "Path to the CA file")
5859
flag.Parse()
5960

60-
if len(strings.TrimSpace(responseTableName)) == 0 {
61-
log.Fatalf("response-table cannot be empty")
62-
}
63-
6461
args := flag.Args()
6562
if len(args) != 1 {
6663
log.Fatalf("Usage: %s <flags> [DSN]\n\nExample:\n\t%s file:example.db?_journal=WAL&_sync=NORMAL&_timeout=5000&_txlock=immediate\n", os.Args[0], os.Args[0])
6764
}
6865
dsn := args[0]
6966

70-
db, err := sql.Open("sqlite3", dsn)
67+
sqlDB, err := sql.Open("sqlite3", dsn)
7168
if err != nil {
7269
log.Fatalf("cannot connect to the database: %v", err)
7370
}
74-
defer db.Close()
71+
defer sqlDB.Close()
72+
73+
var tableList []string
74+
if responseTables == "" {
75+
tableList, err = db.ResponseTables(sqlDB)
76+
if err != nil {
77+
log.Fatalf("discovery response tables: %v", err)
78+
}
79+
} else {
80+
tableList = strings.Split(responseTables, ",")
7581

76-
_, err = db.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE temp.http_refresh USING http_request(%s)", opts()))
77-
if err != nil {
78-
log.Fatalf("error creating virtual table: %v", err)
7982
}
8083

81-
stmt, err := db.Prepare(fmt.Sprintf(`INSERT INTO temp.http_refresh(url)
84+
stmts := make(map[string]*sql.Stmt)
85+
for _, responseTableName := range tableList {
86+
_, err = sqlDB.Exec(fmt.Sprintf("CREATE VIRTUAL TABLE temp.%s_refresh USING http_request(%s)", responseTableName, opts(responseTableName)))
87+
if err != nil {
88+
log.Fatalf("error creating virtual table: %v", err)
89+
}
90+
91+
stmt, err := sqlDB.Prepare(fmt.Sprintf(`INSERT INTO temp.%s_refresh(url)
8292
SELECT url FROM %s
83-
WHERE url LIKE ? AND unixepoch() - unixepoch(timestamp) > ?`, responseTableName))
84-
if err != nil {
85-
log.Fatalf("error preparing statement: %v", err)
93+
WHERE url LIKE ? AND unixepoch() - unixepoch(timestamp) > ?`, responseTableName, responseTableName))
94+
if err != nil {
95+
log.Fatalf("error preparing statement: %v", err)
96+
}
97+
defer stmt.Close()
98+
stmts[responseTableName] = stmt
8699
}
87-
defer stmt.Close()
88100

89101
done := make(chan os.Signal, 1)
90102
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
91103

92-
refreshData(stmt)
104+
refreshData(stmts)
93105
if interval == 0 {
94106
return
95107
}
@@ -102,27 +114,29 @@ func main() {
102114
for {
103115
select {
104116
case <-ticker.C:
105-
refreshData(stmt)
117+
refreshData(stmts)
106118
case <-done:
107119
return
108120
}
109121
}
110122
}
111123

112-
func refreshData(stmt *sql.Stmt) {
124+
func refreshData(stmts map[string]*sql.Stmt) {
113125
slog.Info("starting data verification")
114126

115-
res, err := stmt.Exec(matchURL, ttl)
116-
if err != nil {
117-
slog.Error("error refreshing data", "error", err)
118-
return
119-
}
127+
for tableName, stmt := range stmts {
128+
res, err := stmt.Exec(matchURL, ttl)
129+
if err != nil {
130+
slog.Error("error refreshing data", "error", err, "table", tableName)
131+
continue
132+
}
120133

121-
rowsAffected, _ := res.RowsAffected()
122-
slog.Info("verification finished", "rows_affected", rowsAffected)
134+
rowsAffected, _ := res.RowsAffected()
135+
slog.Info("verification finished", "rows_affected", rowsAffected, "table", tableName)
136+
}
123137
}
124138

125-
func opts() string {
139+
func opts(responseTableName string) string {
126140
opts := make([]string, 0)
127141
opts = append(opts, fmt.Sprintf("%s='%d'", config.Timeout, timeout))
128142
opts = append(opts, fmt.Sprintf("%s='%v'", config.Insecure, insecure))

0 commit comments

Comments
 (0)