diff --git a/examples/data/ourdb_server.vsh b/examples/data/ourdb_server.vsh new file mode 100755 index 00000000..fa0417c4 --- /dev/null +++ b/examples/data/ourdb_server.vsh @@ -0,0 +1,17 @@ +#!/usr/bin/env -S v -n -w -gc none -cc tcc -d use_openssl -enable-globals run + +import freeflowuniverse.herolib.data.ourdb +import os + +mut server := ourdb.new_server( + port: 9000 + allowed_hosts: ['localhost'] + allowed_operations: ['set', 'get', 'delete'] + secret_key: 'secret' + config: ourdb.OurDBConfig{ + path: '/tmp/ourdb' + incremental_mode: true + } +)! + +server.run() diff --git a/lib/data/ourdb/SERVER.md b/lib/data/ourdb/SERVER.md new file mode 100644 index 00000000..816be693 --- /dev/null +++ b/lib/data/ourdb/SERVER.md @@ -0,0 +1,98 @@ +# OurDB Server + +OurDBServer is a lightweight key-value store server built in V, designed for simplicity and performance. It provides basic CRUD operations with security features such as host and operation restrictions. + +## Features +- Supports `set`, `get`, and `delete` operations. +- Allows configurable host restrictions. +- Middleware for logging and security. +- Incremental mode for automatic ID assignment. +- Configurable storage options. + +## Installation +- Ensure you have V installed on your system: + +## Usage +### Starting the Server +You can start the server using the following command: +```v +mut server := new_server(OurDBServerArgs{ + port: 3000 + allowed_hosts: ['localhost'] // Add more hosts as needed + allowed_operations: ['set', 'get', 'delete'] // Add more operations as needed, these are the current supported operations + secret_key: 'your-secret-key' + config: OurDBConfig{ + path: '/tmp/ourdb' + incremental_mode: true + reset: true + } +}) or { panic(err) } +server.run(background: false ) +``` + +### API Endpoints + +#### 1. Set a Record +**Endpoint:** +```http +POST /set +``` +**Request Body:** +```json +{ + "id": 0, // ID is optional in incremental mode + "value": "Some data" +} +``` + + +#### 2. Get a Record +**Endpoint:** +```http +GET /get/:id +``` + +#### 3. Delete a Record +**Endpoint:** +```http +DELETE /delete/:id +``` + +## Configuration +You can customize the server settings by modifying `OurDBServerArgs` when initializing the server. + +| Parameter | Description | Default Value | +|---------------------|-----------------------------------|--------------| +| `port` | Server port | `3000` | +| `allowed_hosts` | List of allowed hosts | `['localhost']` | +| `allowed_operations` | List of permitted operations | `['set', 'get', 'delete']` | +| `secret_key` | Secret key for authentication | Auto-generated | +| `record_nr_max` | Max number of records | `100` | +| `record_size_max` | Max size per record (bytes) | `1024` | +| `file_size` | Max file storage size (bytes) | `10_000` | +| `incremental_mode` | Auto-generate IDs if enabled | `true` | +| `reset` | Clears storage on restart | `true` | + +## Middleware +OurDB includes middleware for logging, host verification, and operation control: +- **Logger Middleware:** Logs incoming requests with details. +- **Allowed Hosts Middleware:** Blocks requests from unauthorized hosts. +- **Allowed Operations Middleware:** Blocks requests for unsupported operations. + +## Running Tests +You can test the server using: +```sh +v -enable-globals -stats test lib/data/ourdb/server_test.v +``` + +## Running Server +You can run the server using: +```sh +v -enable-globals -stats run lib/data/ourdb/server.v +``` + +or use the created example + +```sh +examples/data/ourdb_server.vsh +``` diff --git a/lib/data/ourdb/server.v b/lib/data/ourdb/server.v new file mode 100644 index 00000000..bc2b757a --- /dev/null +++ b/lib/data/ourdb/server.v @@ -0,0 +1,226 @@ +module ourdb + +import freeflowuniverse.herolib.ui.console +import veb +import rand +import time +import json + +// Represents the server context, extending the veb.Context +pub struct ServerContext { + veb.Context +} + +// Represents the OurDB server instance +@[heap] +pub struct OurDBServer { + veb.Middleware[ServerContext] +pub mut: + db &OurDB // Reference to the database instance + port int // Port on which the server runs + allowed_hosts []string // List of allowed hostnames + allowed_operations []string // List of allowed operations (e.g., set, get, delete) + secret_key string // Secret key for authentication +} + +// Represents the arguments required to initialize the OurDB server +@[params] +pub struct OurDBServerArgs { +pub mut: + port int = 3000 // Server port, default is 3000 + allowed_hosts []string = ['localhost'] // Allowed hosts + allowed_operations []string = ['set', 'get', 'delete'] // Allowed operations + secret_key string = rand.string_from_set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + 32) // Generated secret key + config OurDBConfig // Database configuration parameters +} + +// Creates a new instance of the OurDB server +pub fn new_server(args OurDBServerArgs) !OurDBServer { + mut db := new( + record_nr_max: args.config.record_nr_max + record_size_max: args.config.record_size_max + file_size: args.config.file_size + path: args.config.path + incremental_mode: args.config.incremental_mode + reset: args.config.reset + ) or { return error('Failed to create ourdb: ${err}') } + + mut server := OurDBServer{ + port: args.port + allowed_hosts: args.allowed_hosts + allowed_operations: args.allowed_operations + secret_key: args.secret_key + db: &db + } + + server.use(handler: server.logger_handler) + server.use(handler: server.allowed_hosts_handler) + server.use(handler: server.allowed_operations_handler) + return server +} + +// Middleware for logging incoming requests and responses +fn (self &OurDBServer) logger_handler(mut ctx ServerContext) bool { + start_time := time.now() + request := ctx.req + method := request.method.str().to_upper() + client_ip := ctx.req.header.get(.x_forwarded_for) or { ctx.req.host.str().split(':')[0] } + user_agent := ctx.req.header.get(.user_agent) or { 'Unknown' } + + console.print_header('${start_time.format()} | [Request] IP: ${client_ip} | Method: ${method} | Path: ${request.url} | User-Agent: ${user_agent}') + return true +} + +// Middleware to check if the client host is allowed +fn (self &OurDBServer) allowed_hosts_handler(mut ctx ServerContext) bool { + client_host := ctx.req.host.str().split(':')[0].to_lower() + if !self.allowed_hosts.contains(client_host) { + ctx.request_error('403 Forbidden: Host not allowed') + console.print_stderr('Unauthorized host: ${client_host}') + return false + } + return true +} + +// Middleware to check if the requested operation is allowed +fn (self &OurDBServer) allowed_operations_handler(mut ctx ServerContext) bool { + url_parts := ctx.req.url.split('/') + operation := url_parts[1] + if operation !in self.allowed_operations { + ctx.request_error('403 Forbidden: Operation not allowed') + console.print_stderr('Unauthorized operation: ${operation}') + return false + } + return true +} + +// Parameters for running the server +@[params] +pub struct RunParams { +pub mut: + background bool // If true, the server runs in the background +} + +// Starts the OurDB server +pub fn (mut self OurDBServer) run(params RunParams) { + if params.background { + spawn veb.run[OurDBServer, ServerContext](mut self, self.port) + } else { + veb.run[OurDBServer, ServerContext](mut self, self.port) + } +} + +// Represents a generic success response +@[params] +struct SuccessResponse[T] { + message string // Success message + data T // Response data +} + +// Represents an error response +@[params] +struct ErrorResponse { + error string @[required] // Error type + message string @[required] // Error message +} + +// Returns an error response +fn (server OurDBServer) error(args ErrorResponse) ErrorResponse { + return args +} + +// Returns a success response +fn (server OurDBServer) success[T](args SuccessResponse[T]) SuccessResponse[T] { + return args +} + +// Request body structure for the `/set` endpoint +struct SetRequestBody { +mut: + id u32 // Record ID + value string // Value to store +} + +// API endpoint to set a key-value pair in the database +@['/set'; post] +pub fn (mut server OurDBServer) set(mut ctx ServerContext) veb.Result { + request_body := ctx.req.data.str() + mut decoded_body := json.decode(SetRequestBody, request_body) or { + ctx.res.set_status(.bad_request) + return ctx.json[ErrorResponse](server.error( + error: 'bad_request' + message: 'Invalid request body' + )) + } + + if server.db.incremental_mode && decoded_body.id > 0 { + ctx.res.set_status(.bad_request) + return ctx.json[ErrorResponse](server.error( + error: 'bad_request' + message: 'Cannot set id when incremental mode is enabled' + )) + } + + mut record := if server.db.incremental_mode { + server.db.set(data: decoded_body.value.bytes()) or { + ctx.res.set_status(.bad_request) + return ctx.json[ErrorResponse](server.error( + error: 'bad_request' + message: 'Failed to set key: ${err}' + )) + } + } else { + server.db.set(id: decoded_body.id, data: decoded_body.value.bytes()) or { + ctx.res.set_status(.bad_request) + return ctx.json[ErrorResponse](server.error( + error: 'bad_request' + message: 'Failed to set key: ${err}' + )) + } + } + + decoded_body.id = record + ctx.res.set_status(.created) + return ctx.json(server.success(message: 'Successfully set the key', data: decoded_body)) +} + +// API endpoint to retrieve a record by ID +@['/get/:id'; get] +pub fn (mut server OurDBServer) get(mut ctx ServerContext, id string) veb.Result { + id_ := id.u32() + record := server.db.get(id_) or { + ctx.res.set_status(.not_found) + return ctx.json[ErrorResponse](server.error( + error: 'not_found' + message: 'Record does not exist: ${err}' + )) + } + + data := SetRequestBody{ + id: id_ + value: record.bytestr() + } + + ctx.res.set_status(.ok) + return ctx.json(server.success(message: 'Successfully get record', data: data)) +} + +// API endpoint to delete a record by ID +@['/delete/:id'; delete] +pub fn (mut server OurDBServer) delete(mut ctx ServerContext, id string) veb.Result { + id_ := id.u32() + + server.db.delete(id_) or { + ctx.res.set_status(.not_found) + return ctx.json[ErrorResponse](server.error( + error: 'not_found' + message: 'Failed to delete key: ${err}' + )) + } + + ctx.res.set_status(.no_content) + return ctx.json({ + 'message': 'Successfully deleted record' + }) +} diff --git a/lib/data/ourdb/server_test.v b/lib/data/ourdb/server_test.v new file mode 100644 index 00000000..9f9aeabd --- /dev/null +++ b/lib/data/ourdb/server_test.v @@ -0,0 +1,59 @@ +module ourdb + +import time +import json +import rand +import net.http + +fn test_ourdb_server() { + mut server := new_server(OurDBServerArgs{ + port: 3000 + allowed_hosts: ['localhost'] + allowed_operations: ['set', 'get', 'delete'] + secret_key: rand.string_from_set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + 32) + config: OurDBConfig{ + record_nr_max: 100 + record_size_max: 1024 + file_size: 10_000 + path: '/tmp/ourdb' + incremental_mode: true + reset: true + } + }) or { panic(err) } + + server.run(RunParams{ background: true }) + time.sleep(1 * time.second) + + // Test set record + mut request_body := json.encode({ + 'value': 'Test Value' + }) + + mut req := http.new_request(.post, 'http://localhost:3000/set', request_body) + mut response := req.do()! + + assert response.status_code == 201 + + mut decoded_response := json.decode(map[string]string, response.body)! + assert decoded_response['message'].str() == 'Successfully set the key' + + // Test get record + time.sleep(500 * time.millisecond) + req = http.new_request(.get, 'http://localhost:3000/get/0', '') + response = req.do()! + + assert response.status_code == 200 + decoded_response = json.decode(map[string]string, response.body)! + assert decoded_response['message'].str() == 'Successfully get record' + + // Test delete record + req = http.new_request(.delete, 'http://localhost:3000/delete/0', '') + response = req.do()! + assert response.status_code == 204 + + // Test invalid operation + req = http.new_request(.post, 'http://localhost:3000/invalid', '') + response = req.do()! + assert response.status_code == 400 +}