Spaces:
Sleeping
Sleeping
| import Fastify from "fastify"; | |
| import Redis from "ioredis"; | |
| const BLOCKED_COMMANDS = new Set([ | |
| "flushall", "config", "shutdown", "save", "bgsave", "client", "monitor", | |
| "debug", "cluster", "replicaof", "slaveof", "replconf", "pfdebug", | |
| ]); | |
| const ALLOWED_COMMANDS = new Set([ | |
| "get", "set", "del", "exists", "expire", "ttl", "incr", "decr", | |
| "mget", "mset", "hget", "hset", "hdel", "hgetall", | |
| "lpush", "rpush", "lpop", "rpop", "lrange", | |
| "sadd", "srem", "smembers", | |
| "zadd", "zrem", "zrange", | |
| "ping", | |
| "scan", "keys", "flushdb", "setex", "psetex", "getdel", "getex", | |
| "zcard", "zscore", "zrevrange", | |
| "xadd", "xread", | |
| "setnx", "getset", "append", "strlen", "incrby", "decrby", | |
| "expireat", "pexpire", "pttl", "persist", | |
| "rename", "type", "lindex", "llen", "lrem", "ltrim", | |
| "hexists", "hincrby", "hincrbyfloat", "hkeys", "hvals", "hlen", "hmget", "hmset", | |
| "sismember", "scard", "sdiff", "sinter", "sunion", | |
| "zrank", "zcount", "zincrby", "zremrangebyrank", "zremrangebyscore", | |
| "srandmember", "spop", | |
| ]); | |
| const API_TOKEN = process.env.REDIS_REST_TOKEN || process.env.API_TOKEN; | |
| const REDIS_URL = process.env.INTERNAL_REDIS_URL || "redis://127.0.0.1:6379"; | |
| const PORT = parseInt(process.env.PORT || "7860", 10); | |
| const HOST = process.env.HOST || "0.0.0.0"; | |
| const BODY_LIMIT = parseInt(process.env.MAX_BODY_SIZE || "5242880", 10); | |
| const redis = new Redis(REDIS_URL, { | |
| lazyConnect: true, | |
| maxRetriesPerRequest: 3, | |
| retryStrategy: (times) => Math.min(times * 200, 3000), | |
| commandTimeout: 5000, | |
| }); | |
| redis.on("error", (err) => console.error("[redis]", err.message)); | |
| const app = Fastify({ | |
| bodyLimit: BODY_LIMIT, | |
| logger: { | |
| level: "info", | |
| serializers: { | |
| req: (req) => ({ method: req.method, url: req.url, remoteAddress: req.ip }), | |
| }, | |
| }, | |
| }); | |
| function formatResult(raw) { | |
| if (raw === null || raw === undefined) return null; | |
| if (typeof raw === "bigint") return Number(raw); | |
| if (Buffer.isBuffer(raw)) return raw.toString("utf-8"); | |
| if (Array.isArray(raw)) return raw.map(formatResult); | |
| // ioredis returns JS objects for HGETALL etc; flatten to [key,val,...] for upstash compat | |
| if (typeof raw === "object" && raw.constructor === Object) { | |
| const flat = []; | |
| for (const [k, v] of Object.entries(raw)) { | |
| flat.push(k, String(v)); | |
| } | |
| return flat; | |
| } | |
| return raw; | |
| } | |
| async function executeCommand(raw) { | |
| if (!Array.isArray(raw) || raw.length === 0) return { error: "Invalid command format" }; | |
| const cmd = String(raw[0]).toLowerCase(); | |
| const args = raw.slice(1).map(String); | |
| if (BLOCKED_COMMANDS.has(cmd)) return { error: `Command '${cmd.toUpperCase()}' is blocked` }; | |
| if (!ALLOWED_COMMANDS.has(cmd)) return { error: `Unsupported command: ${cmd.toUpperCase()}` }; | |
| try { | |
| const result = await redis.call(cmd, ...args); | |
| return { result: formatResult(result) }; | |
| } catch (err) { | |
| return { error: err.message }; | |
| } | |
| } | |
| function isAuthed(request) { | |
| if (!API_TOKEN) return true; | |
| const header = request.headers.authorization; | |
| if (!header) return false; | |
| const m = header.match(/^Bearer\s+(.+)$/i); | |
| return m ? m[1] === API_TOKEN : false; | |
| } | |
| function send401(reply) { | |
| reply.code(401); | |
| return { error: "Unauthorized" }; | |
| } | |
| // Health check — no auth | |
| app.get("/health", async () => { | |
| try { | |
| await redis.ping(); | |
| return { status: "ok", redis: "connected" }; | |
| } catch { | |
| return { status: "error", redis: "disconnected" }; | |
| } | |
| }); | |
| // Root GET — service info, no auth | |
| app.get("/", async () => ({ service: "hf-redis-rest", status: "running" })); | |
| // Path-style: GET /get/key, GET /set/key/value, etc. | |
| app.get("/:command/*", async (request, reply) => { | |
| if (!isAuthed(request)) return send401(reply); | |
| const parts = request.url.slice(1).split("/").filter(Boolean); | |
| return executeCommand(parts); | |
| }); | |
| // Pipeline: POST /pipeline | |
| app.post("/pipeline", async (request, reply) => { | |
| if (!isAuthed(request)) return send401(reply); | |
| const body = request.body; | |
| if (!Array.isArray(body)) { | |
| reply.code(400); | |
| return { error: "Request body must be an array of commands" }; | |
| } | |
| const results = []; | |
| for (const cmd of body) { | |
| results.push(await executeCommand(cmd)); | |
| } | |
| return results; | |
| }); | |
| // Main command endpoint: POST / | |
| app.post("/", async (request, reply) => { | |
| if (!isAuthed(request)) return send401(reply); | |
| const body = request.body; | |
| if (!body) return { service: "hf-redis-rest", status: "running" }; | |
| if (!Array.isArray(body)) { | |
| reply.code(400); | |
| return { error: "Request body must be a JSON array" }; | |
| } | |
| // Upstash pipeline: array of command arrays | |
| if (Array.isArray(body[0])) { | |
| const results = []; | |
| for (const cmd of body) results.push(await executeCommand(cmd)); | |
| return results; | |
| } | |
| return executeCommand(body); | |
| }); | |
| async function start() { | |
| try { | |
| await redis.connect(); | |
| console.log("[redis] connected to", REDIS_URL); | |
| await app.listen({ port: PORT, host: HOST }); | |
| console.log("[server] listening on", `${HOST}:${PORT}`); | |
| } catch (err) { | |
| console.error("[fatal]", err); | |
| process.exit(1); | |
| } | |
| } | |
| start(); | |