diff options
Diffstat (limited to 'src/gnunet/service/store.go')
-rw-r--r-- | src/gnunet/service/store.go | 379 |
1 files changed, 379 insertions, 0 deletions
diff --git a/src/gnunet/service/store.go b/src/gnunet/service/store.go new file mode 100644 index 0000000..1e5af8b --- /dev/null +++ b/src/gnunet/service/store.go | |||
@@ -0,0 +1,379 @@ | |||
1 | // This file is part of gnunet-go, a GNUnet-implementation in Golang. | ||
2 | // Copyright (C) 2019-2022 Bernd Fix >Y< | ||
3 | // | ||
4 | // gnunet-go is free software: you can redistribute it and/or modify it | ||
5 | // under the terms of the GNU Affero General Public License as published | ||
6 | // by the Free Software Foundation, either version 3 of the License, | ||
7 | // or (at your option) any later version. | ||
8 | // | ||
9 | // gnunet-go is distributed in the hope that it will be useful, but | ||
10 | // WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
12 | // Affero General Public License for more details. | ||
13 | // | ||
14 | // You should have received a copy of the GNU Affero General Public License | ||
15 | // along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
16 | // | ||
17 | // SPDX-License-Identifier: AGPL3.0-or-later | ||
18 | |||
19 | package service | ||
20 | |||
21 | import ( | ||
22 | "context" | ||
23 | "database/sql" | ||
24 | "encoding/binary" | ||
25 | "encoding/hex" | ||
26 | "errors" | ||
27 | "fmt" | ||
28 | "gnunet/crypto" | ||
29 | "gnunet/service/dht/blocks" | ||
30 | "gnunet/util" | ||
31 | "io/ioutil" | ||
32 | "os" | ||
33 | "strconv" | ||
34 | "strings" | ||
35 | |||
36 | redis "github.com/go-redis/redis/v8" | ||
37 | ) | ||
38 | |||
39 | // Error messages related to the key/value-store implementations | ||
40 | var ( | ||
41 | ErrStoreInvalidSpec = fmt.Errorf("Invalid Store specification") | ||
42 | ErrStoreUnknown = fmt.Errorf("Unknown Store type") | ||
43 | ErrStoreNotAvailable = fmt.Errorf("Store not available") | ||
44 | ) | ||
45 | |||
46 | //------------------------------------------------------------ | ||
47 | // Generic storage interface. Can be used for persistent or | ||
48 | // transient (caching) storage of key/value data. | ||
49 | // One set of methods (Get/Put) work on DHT queries and blocks, | ||
50 | // the other set (GetS, PutS) work on key/value strings. | ||
51 | // Each custom implementation can decide which sets to support. | ||
52 | //------------------------------------------------------------ | ||
53 | |||
54 | // Store is a key/value storage where the type of the key is either | ||
55 | // a SHA512 hash value or a string and the value is either a DHT | ||
56 | // block or a string. | ||
57 | type Store[K, V any] interface { | ||
58 | // Put block into storage under given key | ||
59 | Put(key K, val V) error | ||
60 | |||
61 | // Get block with given key from storage | ||
62 | Get(key K) (V, error) | ||
63 | |||
64 | // List all store queries | ||
65 | List() ([]K, error) | ||
66 | } | ||
67 | |||
68 | //------------------------------------------------------------ | ||
69 | // Types for custom store requirements | ||
70 | //------------------------------------------------------------ | ||
71 | |||
72 | // DHTStore for DHT queries and blocks | ||
73 | type DHTStore Store[blocks.Query, blocks.Block] | ||
74 | |||
75 | // KVStore for key/value string pairs | ||
76 | type KVStore Store[string, string] | ||
77 | |||
78 | //------------------------------------------------------------ | ||
79 | // NewDHTStore creates a new storage handler with given spec | ||
80 | // for use with DHT queries and blocks | ||
81 | func NewDHTStore(spec string) (DHTStore, error) { | ||
82 | specs := strings.SplitN(spec, ":", 2) | ||
83 | if len(specs) < 2 { | ||
84 | return nil, ErrStoreInvalidSpec | ||
85 | } | ||
86 | switch specs[0] { | ||
87 | //------------------------------------------------------------------ | ||
88 | // File-base storage | ||
89 | //------------------------------------------------------------------ | ||
90 | case "file_store": | ||
91 | return NewFileStore(specs[1]) | ||
92 | case "file_cache": | ||
93 | if len(specs) < 3 { | ||
94 | return nil, ErrStoreInvalidSpec | ||
95 | } | ||
96 | return NewFileCache(specs[1], specs[2]) | ||
97 | } | ||
98 | return nil, ErrStoreUnknown | ||
99 | } | ||
100 | |||
101 | //------------------------------------------------------------ | ||
102 | // NewKVStore creates a new storage handler with given spec | ||
103 | // for use with key/value string pairs. | ||
104 | func NewKVStore(spec string) (KVStore, error) { | ||
105 | specs := strings.SplitN(spec, ":", 2) | ||
106 | if len(specs) < 2 { | ||
107 | return nil, ErrStoreInvalidSpec | ||
108 | } | ||
109 | switch specs[0] { | ||
110 | //-------------------------------------------------------------- | ||
111 | // Redis service | ||
112 | //-------------------------------------------------------------- | ||
113 | case "redis": | ||
114 | if len(specs) < 4 { | ||
115 | return nil, ErrStoreInvalidSpec | ||
116 | } | ||
117 | return NewRedisStore(specs[1], specs[2], specs[3]) | ||
118 | |||
119 | //-------------------------------------------------------------- | ||
120 | // SQL database service | ||
121 | //-------------------------------------------------------------- | ||
122 | case "sql": | ||
123 | if len(specs) < 4 { | ||
124 | return nil, ErrStoreInvalidSpec | ||
125 | } | ||
126 | return NewSQLStore(specs[1]) | ||
127 | } | ||
128 | return nil, errors.New("unknown storage mechanism") | ||
129 | } | ||
130 | |||
131 | //------------------------------------------------------------ | ||
132 | // Filesystem-based storage | ||
133 | //------------------------------------------------------------ | ||
134 | |||
135 | // FileStore implements a filesystem-based storage mechanism for | ||
136 | // DHT queries and blocks. | ||
137 | type FileStore struct { | ||
138 | path string // storage path | ||
139 | cached []*crypto.HashCode // list of cached entries (key) | ||
140 | wrPos int // write position in cyclic list | ||
141 | } | ||
142 | |||
143 | // NewFileStore instantiates a new file storage. | ||
144 | func NewFileStore(path string) (DHTStore, error) { | ||
145 | // create file store | ||
146 | return &FileStore{ | ||
147 | path: path, | ||
148 | }, nil | ||
149 | } | ||
150 | |||
151 | // NewFileCache instantiates a new file-based cache. | ||
152 | func NewFileCache(path, param string) (DHTStore, error) { | ||
153 | // remove old cache content | ||
154 | os.RemoveAll(path) | ||
155 | |||
156 | // get number of cache entries | ||
157 | num, err := strconv.ParseUint(param, 10, 32) | ||
158 | if err != nil { | ||
159 | return nil, err | ||
160 | } | ||
161 | // create file store | ||
162 | return &FileStore{ | ||
163 | path: path, | ||
164 | cached: make([]*crypto.HashCode, num), | ||
165 | wrPos: 0, | ||
166 | }, nil | ||
167 | } | ||
168 | |||
169 | // Put block into storage under given key | ||
170 | func (s *FileStore) Put(query blocks.Query, block blocks.Block) (err error) { | ||
171 | // get query parameters for entry | ||
172 | var ( | ||
173 | btype uint16 // block type | ||
174 | expire util.AbsoluteTime // block expiration | ||
175 | ) | ||
176 | query.Get("blkType", &btype) | ||
177 | query.Get("expire", &expire) | ||
178 | |||
179 | // are we in caching mode? | ||
180 | if s.cached != nil { | ||
181 | // release previous block if defined | ||
182 | if key := s.cached[s.wrPos]; key != nil { | ||
183 | // get path and filename from key | ||
184 | path, fname := s.expandPath(key) | ||
185 | if err = os.Remove(path + "/" + fname); err != nil { | ||
186 | return | ||
187 | } | ||
188 | // free slot | ||
189 | s.cached[s.wrPos] = nil | ||
190 | } | ||
191 | } | ||
192 | // get path and filename from key | ||
193 | path, fname := s.expandPath(query.Key()) | ||
194 | // make sure the path exists | ||
195 | if err = os.MkdirAll(path, 0755); err != nil { | ||
196 | return | ||
197 | } | ||
198 | // write to file for storage | ||
199 | var fp *os.File | ||
200 | if fp, err = os.Create(path + "/" + fname); err == nil { | ||
201 | defer fp.Close() | ||
202 | // write block data | ||
203 | if err = binary.Write(fp, binary.BigEndian, btype); err == nil { | ||
204 | if err = binary.Write(fp, binary.BigEndian, expire); err == nil { | ||
205 | _, err = fp.Write(block.Data()) | ||
206 | } | ||
207 | } | ||
208 | } | ||
209 | // update cache list | ||
210 | if s.cached != nil { | ||
211 | s.cached[s.wrPos] = query.Key() | ||
212 | s.wrPos = (s.wrPos + 1) % len(s.cached) | ||
213 | } | ||
214 | return | ||
215 | } | ||
216 | |||
217 | // Get block with given key from storage | ||
218 | func (s *FileStore) Get(query blocks.Query) (block blocks.Block, err error) { | ||
219 | // get requested block type | ||
220 | var ( | ||
221 | btype uint16 = blocks.DHT_BLOCK_ANY | ||
222 | blkt uint16 // actual block type | ||
223 | expire util.AbsoluteTime // expiration date | ||
224 | data []byte // block data | ||
225 | ) | ||
226 | query.Get("blkType", &btype) | ||
227 | |||
228 | // get path and filename from key | ||
229 | path, fname := s.expandPath(query.Key()) | ||
230 | // read file content (block data) | ||
231 | var file *os.File | ||
232 | if file, err = os.Open(path + "/" + fname); err != nil { | ||
233 | return | ||
234 | } | ||
235 | // read block data | ||
236 | if err = binary.Read(file, binary.BigEndian, &blkt); err == nil { | ||
237 | if btype != blocks.DHT_BLOCK_ANY && btype != blkt { | ||
238 | // block types not matching | ||
239 | return | ||
240 | } | ||
241 | if err = binary.Read(file, binary.BigEndian, &expire); err == nil { | ||
242 | if data, err = ioutil.ReadAll(file); err == nil { | ||
243 | block = blocks.NewGenericBlock(data) | ||
244 | } | ||
245 | } | ||
246 | } | ||
247 | return | ||
248 | } | ||
249 | |||
250 | // Get a list of all stored block keys (generic query). | ||
251 | func (s *FileStore) List() ([]blocks.Query, error) { | ||
252 | return make([]blocks.Query, 0), nil | ||
253 | } | ||
254 | |||
255 | // expandPath returns the full path to the file for given key. | ||
256 | func (s *FileStore) expandPath(key *crypto.HashCode) (string, string) { | ||
257 | h := hex.EncodeToString(key.Bits) | ||
258 | return fmt.Sprintf("%s/%s/%s", s.path, h[:2], h[2:4]), h[4:] | ||
259 | } | ||
260 | |||
261 | //------------------------------------------------------------ | ||
262 | // Redis: only use for caching purposes on key/value strings | ||
263 | //------------------------------------------------------------ | ||
264 | |||
265 | // RedisStore uses a (local) Redis server for key/value storage | ||
266 | type RedisStore struct { | ||
267 | client *redis.Client // client connection | ||
268 | db int // index to database | ||
269 | } | ||
270 | |||
271 | // NewRedisStore creates a Redis service client instance. | ||
272 | func NewRedisStore(addr, passwd, db string) (s KVStore, err error) { | ||
273 | kvs := new(RedisStore) | ||
274 | if kvs.db, err = strconv.Atoi(db); err != nil { | ||
275 | err = ErrStoreInvalidSpec | ||
276 | return | ||
277 | } | ||
278 | kvs.client = redis.NewClient(&redis.Options{ | ||
279 | Addr: addr, | ||
280 | Password: passwd, | ||
281 | DB: kvs.db, | ||
282 | }) | ||
283 | if kvs.client == nil { | ||
284 | err = ErrStoreNotAvailable | ||
285 | } | ||
286 | s = kvs | ||
287 | return | ||
288 | } | ||
289 | |||
290 | // Put block into storage under given key | ||
291 | func (s *RedisStore) Put(key string, value string) (err error) { | ||
292 | return s.client.Set(context.TODO(), key, value, 0).Err() | ||
293 | } | ||
294 | |||
295 | // Get block with given key from storage | ||
296 | func (s *RedisStore) Get(key string) (value string, err error) { | ||
297 | return s.client.Get(context.TODO(), key).Result() | ||
298 | } | ||
299 | |||
300 | // List all keys in store | ||
301 | func (s *RedisStore) List() (keys []string, err error) { | ||
302 | var ( | ||
303 | crs uint64 | ||
304 | segm []string | ||
305 | ctx = context.TODO() | ||
306 | ) | ||
307 | keys = make([]string, 0) | ||
308 | for { | ||
309 | segm, crs, err = s.client.Scan(ctx, crs, "*", 10).Result() | ||
310 | if err != nil { | ||
311 | return | ||
312 | } | ||
313 | if crs == 0 { | ||
314 | break | ||
315 | } | ||
316 | keys = append(keys, segm...) | ||
317 | } | ||
318 | return | ||
319 | } | ||
320 | |||
321 | //------------------------------------------------------------ | ||
322 | // SQL-based key-value-store | ||
323 | //------------------------------------------------------------ | ||
324 | |||
325 | // SQLStore for generic SQL database handling | ||
326 | type SQLStore struct { | ||
327 | db *util.DbConn | ||
328 | } | ||
329 | |||
330 | // NewSQLStore creates a new SQL-based key/value store. | ||
331 | func NewSQLStore(spec string) (s KVStore, err error) { | ||
332 | kvs := new(SQLStore) | ||
333 | |||
334 | // connect to SQL database | ||
335 | kvs.db, err = util.DbPool.Connect(spec) | ||
336 | if err != nil { | ||
337 | return nil, err | ||
338 | } | ||
339 | // get number of key/value pairs (as a check for existing table) | ||
340 | row := kvs.db.QueryRow("select count(*) from store") | ||
341 | var num int | ||
342 | if row.Scan(&num) != nil { | ||
343 | return nil, ErrStoreNotAvailable | ||
344 | } | ||
345 | return kvs, nil | ||
346 | |||
347 | } | ||
348 | |||
349 | // Put a key/value pair into the store | ||
350 | func (s *SQLStore) Put(key string, value string) error { | ||
351 | _, err := s.db.Exec("insert into store(key,value) values(?,?)", key, value) | ||
352 | return err | ||
353 | } | ||
354 | |||
355 | // Get a value for a given key from store | ||
356 | func (s *SQLStore) Get(key string) (value string, err error) { | ||
357 | row := s.db.QueryRow("select value from store where key=?", key) | ||
358 | err = row.Scan(&value) | ||
359 | return | ||
360 | } | ||
361 | |||
362 | // List all keys in store | ||
363 | func (s *SQLStore) List() (keys []string, err error) { | ||
364 | var ( | ||
365 | rows *sql.Rows | ||
366 | key string | ||
367 | ) | ||
368 | keys = make([]string, 0) | ||
369 | rows, err = s.db.Query("select key from store") | ||
370 | if err == nil { | ||
371 | for rows.Next() { | ||
372 | if err = rows.Scan(&key); err != nil { | ||
373 | break | ||
374 | } | ||
375 | keys = append(keys, key) | ||
376 | } | ||
377 | } | ||
378 | return | ||
379 | } | ||