Skip to content

Commit f57bbee

Browse files
libsql
1 parent 4052470 commit f57bbee

27 files changed

Lines changed: 794 additions & 194 deletions

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ SELECT JSON_EXTRACT(body, '$.result.properties.title') AS title,
2424
FROM http_response;
2525

2626
# 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
27+
SELECT url, cacheage(header, request_time, response_time) AS age,
28+
cachelifetime(header, response_time) AS lifetime,
29+
cachexpired(header, request_time, response_time, false) AS expired,
30+
cachexpiredttl(header, request_time, response_time, false, 3600) AS expiredTTLFallback
2831
FROM http_response;
2932
```
3033

@@ -36,13 +39,13 @@ You can configure the behaviour by passing parameters to a VIRTUAL TABLE.
3639
|-------|-------------|---------|
3740
| timeout | Timeout in milliseconds | 0 |
3841
| insecure | Insecure skip TLS validation | false |
39-
| status_code | Comma-separated list of HTTP status code to persist. Use empty to persist all status | 200,301,404 |
42+
| status_code | Comma-separated list of HTTP status code to persist. Use empty to persist all status | 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501 |
4043
| response_table | Database table used to store response data | http_response |
4144
| oauth2_client_id | Oauth2 Client ID | |
4245
| oauth2_client_secret | Oauth2 Client Secret | |
4346
| oauth2_token_url | Oauth2 Token URL (Client Credentials Flow) | |
4447
| cert_file | Mutual TLS: path to certificate file | |
45-
| crt_key_file | Mutual TLS: path to certificate key file | |
48+
| cert_key_file | Mutual TLS: path to certificate key file | |
4649
| ca_file | Path to CA certificate file | |
4750

4851
**Any other parameter will be included as an HTTP header in the request**
@@ -59,7 +62,7 @@ CREATE VIRTUAL TABLE temp.custom_request USING http_request(authorization=Bearer
5962

6063
```sh
6164
# Create a Virtual Table to customize options
62-
CREATE VIRTUAL TABLE temp.custom_request USING http_request(insecure=true, timeout=10000, accept=application/json, authorization=Bearer ${API_TOKEN}, response_table=films);
65+
CREATE VIRTUAL TABLE temp.custom_request USING http_request(insecure=true, timeout=10000, accept=application/json, authorization='Bearer ${API_TOKEN}', response_table=films);
6366

6467
# Insert URL into the Virtual Table to trigger the HTTP Request
6568
INSERT INTO temp.custom_request VALUES('https://swapi.tech/api/films/2');
@@ -104,6 +107,10 @@ sqlite-http-refresh file:example.db?_journal=WAL&_sync=NORMAL&_timeout=5000&_txl
104107

105108
- **Cron (Linux/macOS):** You can set up cron jobs to execute a script at specified intervals (e.g., every minute, hour, or day). This script would then connect to your SQLite database and perform the desired INSERT operations.
106109

110+
```sh
111+
sqlite3 -cmd ".load /usr/lib/httpcache.so" "INSERT INTO temp.http_request SELECT url FROM http_response WHERE unixepoch() - ((julianday(response_time) - 2440587.5) * 86400.0) > 3600;"
112+
```
113+
107114
- **Task Scheduler (Windows):** Similar to cron, Windows Task Scheduler allows you to schedule tasks, including running scripts or programs that interact with SQLite.
108115

109116
### Programming Language Libraries

cmd/libsql-http-proxy/main.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"database/sql"
7+
"fmt"
8+
"log"
9+
"net"
10+
"net/http"
11+
"os"
12+
"path/filepath"
13+
"strconv"
14+
"strings"
15+
"time"
16+
17+
"github.com/elazarl/goproxy"
18+
"github.com/peterbourgon/ff/v4"
19+
"github.com/peterbourgon/ff/v4/ffhelp"
20+
"github.com/tursodatabase/go-libsql"
21+
22+
"github.com/walterwanderley/sqlite-http-cache/config"
23+
"github.com/walterwanderley/sqlite-http-cache/db"
24+
proxyhandler "github.com/walterwanderley/sqlite-http-cache/http/proxy"
25+
)
26+
27+
func main() {
28+
fs := ff.NewFlagSet("libsql-http-proxy")
29+
port := fs.Uint('p', "port", 8080, "Server port")
30+
dbPrimaryURL := fs.StringLong("db-primary-url", "", "Database primary URL")
31+
dbSyncInterval := fs.DurationLong("db-sync-interval", 30*time.Second, "Database sync interval")
32+
dbAuthToken := fs.StringLong("db-token", "", "Database authorization token")
33+
dbEncryptionKey := fs.StringLong("db-key", "", "Database encryption key")
34+
verbose := fs.Bool('v', "verbose", "Enable verbose mode")
35+
allowHTTP2 := fs.BoolLong("h2", "Allow HTTP2")
36+
statusCodes := fs.StringListLong("status-code", fmt.Sprintf("List of cacheable status code. Defaults to the heuristically cacheable codes: %v", config.DefaultStatusCodes()))
37+
ttl := fs.IntLong("ttl", 0, "Time to Live in seconds (0 is infinite time)")
38+
responseTables := fs.StringListLong("response-table", "List of database tables used to store response data")
39+
caCert := fs.StringLong("ca-cert", "", "Path to CA Certificate file (required to HTTPS proxy)")
40+
caCertKey := fs.StringLong("ca-cert-key", "", "Path to CA Certificate Key file (required to HTTPS proxy)")
41+
readOnly := fs.BoolLong("ro", "Read Only mode. Do not store new HTTP responses")
42+
rfc9111 := fs.BoolLong("rfc9111", "Use RFC9111 spec")
43+
shared := fs.BoolLong("shared", "Enable shared cache mode")
44+
_ = fs.String('c', "config", "", "config file (optional)")
45+
46+
if err := ff.Parse(fs, os.Args[1:],
47+
ff.WithEnvVarPrefix("LIBSQL_HTTP_PROXY"),
48+
ff.WithConfigFileFlag("config"),
49+
ff.WithConfigFileParser(ff.PlainParser),
50+
); err != nil {
51+
fmt.Printf("%s\n", ffhelp.Flags(fs))
52+
fmt.Printf("err=%v\n", err)
53+
return
54+
}
55+
56+
if len(fs.GetArgs()) == 0 {
57+
log.Fatalf("Usage: %s <FLAGS> [DatabasePath1] [DatabasePathN\n\nExample:\n\t%s example.db example2.db example3.db\n", os.Args[0], os.Args[0])
58+
}
59+
60+
if *verbose {
61+
fmt.Printf("Using options: port=%d db-primary-url=%s, h2=%v, ttl=%d, response-tables=%v, ca-cert=%s, ca-cert-key=%s, read-only=%v, rfc9111=%v shared-cache=%v\n",
62+
*port, *dbPrimaryURL, *allowHTTP2, *ttl, *responseTables, *caCert, *caCertKey, *readOnly, *rfc9111, *shared)
63+
}
64+
65+
dbs := make([]*sql.DB, 0)
66+
var (
67+
repository db.Repository
68+
tableList []string
69+
err error
70+
)
71+
72+
dbOpts := make([]libsql.Option, 0)
73+
if *dbAuthToken != "" {
74+
dbOpts = append(dbOpts, libsql.WithAuthToken(*dbAuthToken))
75+
}
76+
if *dbEncryptionKey != "" {
77+
dbOpts = append(dbOpts, libsql.WithEncryption(*dbEncryptionKey))
78+
}
79+
if *dbSyncInterval > 0 {
80+
dbOpts = append(dbOpts, libsql.WithSyncInterval(*dbSyncInterval))
81+
}
82+
83+
fnRegisterResonseTables := func(sqlDB *sql.DB, dbPath string) {
84+
if responseTables == nil || len(*responseTables) == 0 {
85+
tableList, err = db.ResponseTables(sqlDB)
86+
if err != nil {
87+
log.Fatalf("discovery response tables: %v", err)
88+
}
89+
} else {
90+
tableList = *responseTables
91+
err := db.CreateResponseTables(sqlDB, tableList...)
92+
if err != nil {
93+
log.Fatalf("create response tables on DB %q: %v", dbPath, err)
94+
}
95+
}
96+
}
97+
98+
for _, dbPath := range fs.GetArgs() {
99+
if strings.HasPrefix(dbPath, "file:") {
100+
sqlDB, err := sql.Open("libsql", dbPath)
101+
if err != nil {
102+
log.Fatalf("connecting to database %q: %v", dbPath, err)
103+
}
104+
fnRegisterResonseTables(sqlDB, dbPath)
105+
dbs = append(dbs, sqlDB)
106+
continue
107+
}
108+
dir, err := os.MkdirTemp("", "libsql-*")
109+
if err != nil {
110+
log.Fatalf("creating database directory: %v", err)
111+
}
112+
defer os.RemoveAll(dir)
113+
114+
connector, err := libsql.NewEmbeddedReplicaConnector(filepath.Join(dir, dbPath), *dbPrimaryURL, dbOpts...)
115+
if err != nil {
116+
log.Fatalf("creating database connector: %v", err)
117+
}
118+
defer connector.Close()
119+
120+
sqlDB := sql.OpenDB(connector)
121+
defer func() {
122+
if closeError := sqlDB.Close(); closeError != nil {
123+
fmt.Println("Error closing database", closeError)
124+
if err == nil {
125+
err = closeError
126+
}
127+
}
128+
}()
129+
130+
fnRegisterResonseTables(sqlDB, dbPath)
131+
dbs = append(dbs, sqlDB)
132+
133+
}
134+
if len(dbs) == 1 {
135+
repository, err = db.NewRepository(dbs[0], tableList...)
136+
if err != nil {
137+
log.Fatalf("new repository: %v", err)
138+
}
139+
} else {
140+
repository, err = db.NewMultiDatabaseRepository(dbs)
141+
if err != nil {
142+
log.Fatalf("new multi database repository: %v", err)
143+
}
144+
}
145+
defer repository.Close()
146+
147+
proxy := goproxy.NewProxyHttpServer()
148+
proxy.Verbose = *verbose
149+
proxy.AllowHTTP2 = *allowHTTP2
150+
151+
if *caCert != "" && *caCertKey != "" {
152+
proxy.Logger.Printf("INFO: Starting HTTP/HTTPS Proxy...")
153+
cert, err := parseCA([]byte(*caCert), []byte(*caCertKey))
154+
if err != nil {
155+
log.Fatal(err)
156+
}
157+
158+
customCaMitm := &goproxy.ConnectAction{Action: goproxy.ConnectMitm, TLSConfig: goproxy.TLSConfigFromCA(cert)}
159+
var customAlwaysMitm goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
160+
return customCaMitm, host
161+
}
162+
proxy.OnRequest().HandleConnect(customAlwaysMitm)
163+
} else {
164+
proxy.Logger.Printf("INFO: Starting HTTP Proxy...")
165+
}
166+
167+
cacheableStatus := make([]int, 0)
168+
for _, status := range *statusCodes {
169+
statusStr := strings.TrimSpace(status)
170+
code, err := strconv.Atoi(statusStr)
171+
if err != nil {
172+
log.Fatalf("Invalid status-code %q. Must be integer: %v", status, err)
173+
}
174+
cacheableStatus = append(cacheableStatus, code)
175+
}
176+
if len(cacheableStatus) == 0 {
177+
cacheableStatus = config.DefaultStatusCodes()
178+
}
179+
180+
proxy.OnRequest().Do(proxyhandler.NewRequestHandler(
181+
proxyhandler.RequestConfig{
182+
Querier: repository,
183+
CacheableStatus: cacheableStatus,
184+
TTL: *ttl,
185+
RFC9111: *rfc9111,
186+
SharedCache: *shared,
187+
ReadOnly: *readOnly,
188+
Verbose: *verbose,
189+
},
190+
))
191+
192+
if !*readOnly {
193+
proxy.OnResponse().Do(proxyhandler.NewResponseHandler(
194+
proxyhandler.ResponseConfig{
195+
Writer: repository,
196+
RFC9111: *rfc9111,
197+
TTL: *ttl,
198+
SharedCache: *shared,
199+
Verbose: *verbose,
200+
},
201+
))
202+
}
203+
204+
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
205+
if err != nil {
206+
log.Fatalf("cannot open port %d: %v", port, err)
207+
}
208+
209+
proxy.Logger.Printf("LibSQL-HTTP-Proxy listening port=%d", *port)
210+
log.Fatal(http.Serve(lis, proxy))
211+
}
212+
213+
func parseCA(caCert, caKey []byte) (*tls.Certificate, error) {
214+
parsedCert, err := tls.X509KeyPair(caCert, caKey)
215+
if err != nil {
216+
return nil, err
217+
}
218+
if parsedCert.Leaf, err = x509.ParseCertificate(parsedCert.Certificate[0]); err != nil {
219+
return nil, err
220+
}
221+
return &parsedCert, nil
222+
}

cmd/sqlite-http-proxy/main.go

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ import (
1010
"net/http"
1111
"os"
1212
"path/filepath"
13+
"strconv"
1314
"strings"
1415

1516
"github.com/elazarl/goproxy"
1617
_ "github.com/mattn/go-sqlite3"
1718
"github.com/peterbourgon/ff/v4"
1819
"github.com/peterbourgon/ff/v4/ffhelp"
1920

21+
"github.com/walterwanderley/sqlite-http-cache/config"
2022
"github.com/walterwanderley/sqlite-http-cache/db"
23+
proxyhandler "github.com/walterwanderley/sqlite-http-cache/http/proxy"
2124
)
2225

2326
func main() {
@@ -26,7 +29,8 @@ func main() {
2629
dbParams := fs.StringLong("db-params", "_journal=WAL&_sync=NORMAL&_timeout=5000&_txlock=immediate", "Database connection params")
2730
verbose := fs.Bool('v', "verbose", "Enable verbose mode")
2831
allowHTTP2 := fs.BoolLong("h2", "Allow HTTP2")
29-
ttl := fs.UintLong("ttl", 0, "Time to Live in seconds (0 is infinite time)")
32+
statusCodes := fs.StringListLong("status-code", fmt.Sprintf("List of cacheable status code. Defaults to the heuristically cacheable codes: %v", config.DefaultStatusCodes()))
33+
ttl := fs.IntLong("ttl", 0, "Time to Live in seconds (0 is infinite time)")
3034
responseTables := fs.StringListLong("response-table", "List of database tables used to store response data")
3135
caCert := fs.StringLong("ca-cert", "", "Path to CA Certificate file (required to HTTPS proxy)")
3236
caCertKey := fs.StringLong("ca-cert-key", "", "Path to CA Certificate Key file (required to HTTPS proxy)")
@@ -50,8 +54,8 @@ func main() {
5054
}
5155

5256
if *verbose {
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)
57+
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, rfc9111=%v shared-cache=%v\n",
58+
*port, *dbParams, *allowHTTP2, *ttl, *responseTables, *caCert, *caCertKey, *readOnly, *rfc9111, *shared)
5559
}
5660

5761
dbs := make([]*sql.DB, 0)
@@ -102,7 +106,7 @@ func main() {
102106
tableList = *responseTables
103107
err := db.CreateResponseTables(sqlDB, tableList...)
104108
if err != nil {
105-
log.Fatalf("force create tables on DB %q: %v", dsn, err)
109+
log.Fatalf("create response tables on DB %q: %v", dsn, err)
106110
}
107111

108112
}
@@ -140,35 +144,41 @@ func main() {
140144
proxy.Logger.Printf("INFO: Starting HTTP Proxy...")
141145
}
142146

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,
156-
})
157-
}
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-
})
147+
cacheableStatus := make([]int, 0)
148+
for _, status := range *statusCodes {
149+
statusStr := strings.TrimSpace(status)
150+
code, err := strconv.Atoi(statusStr)
151+
if err != nil {
152+
log.Fatalf("Invalid status-code %q. Must be integer: %v", status, err)
170153
}
154+
cacheableStatus = append(cacheableStatus, code)
155+
}
156+
if len(cacheableStatus) == 0 {
157+
cacheableStatus = config.DefaultStatusCodes()
158+
}
171159

160+
proxy.OnRequest().Do(proxyhandler.NewRequestHandler(
161+
proxyhandler.RequestConfig{
162+
Querier: repository,
163+
CacheableStatus: cacheableStatus,
164+
TTL: *ttl,
165+
RFC9111: *rfc9111,
166+
SharedCache: *shared,
167+
ReadOnly: *readOnly,
168+
Verbose: *verbose,
169+
},
170+
))
171+
172+
if !*readOnly {
173+
proxy.OnResponse().Do(proxyhandler.NewResponseHandler(
174+
proxyhandler.ResponseConfig{
175+
Writer: repository,
176+
RFC9111: *rfc9111,
177+
TTL: *ttl,
178+
SharedCache: *shared,
179+
Verbose: *verbose,
180+
},
181+
))
172182
}
173183

174184
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))

0 commit comments

Comments
 (0)