Spaces:
Sleeping
Sleeping
File size: 5,099 Bytes
7459c16 c8afbcd 7459c16 c8afbcd 7459c16 cf11bfd 7459c16 c8afbcd 7459c16 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | 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();
|