diff --git a/package.json b/package.json index ec12dfb62..0a2677e83 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,10 @@ "png-chunks-encode": "^1.0.0", "png-chunks-extract": "^1.0.0", "rimraf": "^3.0.2", - "sharp": "^0.31.3" + "sharp": "^0.31.3", + "csrf-csrf": "^2.2.3", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5" }, "name": "TavernAI", "version": "1.2.7", diff --git a/public/index.html b/public/index.html index 3e237cdc3..28184acde 100644 --- a/public/index.html +++ b/public/index.html @@ -14,6 +14,7 @@ + @@ -223,15 +224,23 @@ - getSettings("def"); - getLastVersion(); - //var interval_getSettings = setInterval(getSettings, 1000); - getCharacters(); + var token; + $.ajaxPrefilter((options, originalOptions, xhr) => { + xhr.setRequestHeader("X-CSRF-Token", token); + }); + + $.get("/csrf-token") + .then(data => { + token = data.token; + getSettings("def"); + getLastVersion(); + getCharacters(); - printMessages(); - getBackgrounds(); - getUserAvatars(); - // + printMessages(); + getBackgrounds(); + getUserAvatars(); + }); + $('#characloud_url').click(function(){ window.open('https://boosty.to/tavernai', '_blank'); }); @@ -393,7 +402,10 @@ const response = await fetch("/getcharacters", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token, + }, body: JSON.stringify({                      "": ""                  }) @@ -427,7 +439,10 @@ const response = await fetch("/getbackgrounds", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token + }, body: JSON.stringify({                      "": ""                  }) @@ -451,7 +466,10 @@ is_checked_colab = true; const response = await fetch("/iscolab", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token + }, body: JSON.stringify({                      "": ""                  }) @@ -517,7 +535,10 @@ async function delBackground(bg) { const response = await fetch("/delbackground", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token + }, body: JSON.stringify({                      "bg": bg                  }) @@ -2222,7 +2243,10 @@ async function getUserAvatars(){ const response = await fetch("/getuseravatars", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + "X-CSRF-Token": token + }, body: JSON.stringify({                      "": ""                  }) diff --git a/public/scripts/jquery-cookie-1.4.1.min.js b/public/scripts/jquery-cookie-1.4.1.min.js new file mode 100644 index 000000000..c0f19d8a3 --- /dev/null +++ b/public/scripts/jquery-cookie-1.4.1.min.js @@ -0,0 +1,2 @@ +/*! jquery.cookie v1.4.1 | MIT */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?a(require("jquery")):a(jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}); \ No newline at end of file diff --git a/server.js b/server.js index fb543dccb..335220148 100644 --- a/server.js +++ b/server.js @@ -16,6 +16,10 @@ const sharp = require('sharp'); sharp.cache(false); const path = require('path'); +const cookieParser = require('cookie-parser'); +const crypto = require('crypto'); + + const config = require('./config.json'); const server_port = config.port; const whitelist = config.whitelist; @@ -45,12 +49,53 @@ var response_getlastversion; var api_key_novel; var is_colab = false; - +var charactersPath = 'public/characters/'; +var chatsPath = 'public/chats/'; +if (is_colab && process.env.googledrive == 2){ + charactersPath = '/content/drive/MyDrive/TavernAI/characters/'; + chatsPath = '/content/drive/MyDrive/TavernAI/chats/'; +} const jsonParser = express.json({limit: '100mb'}); const urlencodedParser = express.urlencoded({extended: true, limit: '100mb'}); const baseRequestArgs = { headers: { "Content-Type": "application/json" } }; const directories = { worlds: 'public/KoboldAI Worlds/' }; +// CSRF Protection // +const doubleCsrf = require('csrf-csrf').doubleCsrf; + +const CSRF_SECRET = crypto.randomBytes(8).toString('hex'); +const COOKIES_SECRET = crypto.randomBytes(8).toString('hex'); + +const { invalidCsrfTokenError, generateToken, doubleCsrfProtection } = doubleCsrf({ + getSecret: () => CSRF_SECRET, + cookieName: "X-CSRF-Token", + cookieOptions: { + httpOnly: true, + sameSite: "strict", + secure: false + }, + size: 64, + getTokenFromRequest: (req) => req.headers["x-csrf-token"] +}); + +app.get("/csrf-token", (req, res) => { + res.json({ + "token": generateToken(res) + }); +}); + +app.use(cookieParser(COOKIES_SECRET)); +app.use(doubleCsrfProtection); + +// CORS Settings // +const cors = require('cors'); +const CORS = cors({ + origin: 'null', + methods: ['OPTIONS'] +}) + +app.use(CORS); + app.use(function (req, res, next) { //Security const clientIp = req.connection.remoteAddress.split(':').pop(); if (whitelistMode === true && !whitelist.includes(clientIp)) { @@ -60,9 +105,27 @@ app.use(function (req, res, next) { //Security next(); }); - - +app.use((req, res, next) => { + if (req.url.startsWith('/characters/') && is_colab && process.env.googledrive == 2) { + + const filePath = path.join(charactersPath, req.url.substr('/characters'.length)); + fs.access(filePath, fs.constants.R_OK, (err) => { + if (!err) { + res.sendFile(filePath); + } else { + res.send('Character not found: '+filePath); + //next(); + } + }); + } else { + next(); + } +}); app.use(express.static(__dirname + "/public", { refresh: true })); + + + + app.use('/backgrounds', (req, res) => { const filePath = path.join(process.cwd(), 'public/backgrounds', req.url); fs.readFile(filePath, (err, data) => { @@ -75,7 +138,7 @@ app.use('/backgrounds', (req, res) => { }); }); app.use('/characters', (req, res) => { - const filePath = path.join(process.cwd(), 'public/characters', req.url); + const filePath = path.join(process.cwd(), charactersPath, req.url); fs.readFile(filePath, (err, data) => { if (err) { res.status(404).send('File not found'); @@ -197,7 +260,7 @@ app.post("/savechat", jsonParser, function(request, response){ var dir_name = String(request.body.avatar_url).replace('.png',''); let chat_data = request.body.chat; let jsonlData = chat_data.map(JSON.stringify).join('\n'); - fs.writeFile('public/chats/'+dir_name+"/"+request.body.file_name+'.jsonl', jsonlData, 'utf8', function(err) { + fs.writeFile(chatsPath+dir_name+"/"+request.body.file_name+'.jsonl', jsonlData, 'utf8', function(err) { if(err) { response.send(err); return console.log(err); @@ -218,23 +281,23 @@ app.post("/getchat", jsonParser, function(request, response){ //var bg = "body {background-image: linear-gradient(rgba(19,21,44,0.75), rgba(19,21,44,0.75)), url(../backgrounds/"+request.body.bg+");}"; var dir_name = String(request.body.avatar_url).replace('.png',''); - fs.stat('public/chats/'+dir_name, function(err, stat) { + fs.stat(chatsPath+dir_name, function(err, stat) { if(stat === undefined){ - fs.mkdirSync('public/chats/'+dir_name); + fs.mkdirSync(chatsPath+dir_name); response.send({}); return; }else{ if(err === null){ - fs.stat('public/chats/'+dir_name+"/"+request.body.file_name+".jsonl", function(err, stat) { + fs.stat(chatsPath+dir_name+"/"+request.body.file_name+".jsonl", function(err, stat) { if (err === null) { if(stat !== undefined){ - fs.readFile('public/chats/'+dir_name+"/"+request.body.file_name+".jsonl", 'utf8', (err, data) => { + fs.readFile(chatsPath+dir_name+"/"+request.body.file_name+".jsonl", 'utf8', (err, data) => { if (err) { console.error(err); response.send(err); @@ -323,12 +386,11 @@ function charaFormatData(data){ return char; } app.post("/createcharacter", urlencodedParser, function(request, response){ + if(!request.body) return response.sendStatus(400); - if (!fs.existsSync('public/characters/'+request.body.ch_name+'.png')){ - if(!fs.existsSync('public/chats/'+request.body.ch_name) )fs.mkdirSync('public/chats/'+request.body.ch_name); - //if(!fs.existsSync('public/characters/'+request.body.ch_name+'/chats')) fs.mkdirSync('public/characters/'+request.body.ch_name+'/chats'); - //if(!fs.existsSync('public/characters/'+request.body.ch_name+'/avatars')) fs.mkdirSync('public/characters/'+request.body.ch_name+'/avatars'); - + if (!fs.existsSync(charactersPath+request.body.ch_name+'.png')){ + if(!fs.existsSync(chatsPath+request.body.ch_name) )fs.mkdirSync(chatsPath+request.body.ch_name); + let filedata = request.file;     //console.log(filedata.mimetype); var fileType = ".png"; @@ -341,14 +403,6 @@ app.post("/createcharacter", urlencodedParser, function(request, response){ charaWrite('./public/img/fluffy.png', char, request.body.ch_name, response); - //fs.writeFile('public/characters/'+request.body.ch_name+"/"+request.body.ch_name+".json", char, 'utf8', function(err) { - //if(err) { - //response.send(err); - //return console.log(err); - //}else{ - - //} - //}); }else{ img_path = "./uploads/"; @@ -377,7 +431,7 @@ app.post("/editcharacter", urlencodedParser, function(request, response){     //console.log(filedata.mimetype); var fileType = ".png"; var img_file = "ai"; - var img_path = "./public/characters/"; + var img_path = charactersPath; var char = charaFormatData(request.body);//{"name": request.body.ch_name, "description": request.body.description, "personality": request.body.personality, "first_mes": request.body.first_mes, "avatar": request.body.avatar_url, "chat": request.body.chat, "last_mes": request.body.last_mes, "mes_example": ''}; char.chat = request.body.chat; @@ -398,14 +452,14 @@ app.post("/editcharacter", urlencodedParser, function(request, response){ }); app.post("/deletecharacter", urlencodedParser, function(request, response){ if(!request.body) return response.sendStatus(400); - rimraf('public/characters/'+request.body.avatar_url, (err) => { + rimraf(charactersPath+request.body.avatar_url, (err) => { if(err) { response.send(err); return console.log(err); }else{ //response.redirect("/"); let dir_name = request.body.avatar_url; - rimraf('public/chats/'+dir_name.replace('.png',''), (err) => { + rimraf(chatsPath+dir_name.replace('.png',''), (err) => { if(err) { response.send(err); return console.log(err); @@ -443,7 +497,7 @@ async function charaWrite(img_url, data, name, response = undefined, mes = 'ok') chunks.splice(-1, 0, PNGtext.encode('chara', base64EncodedData)); //chunks.splice(-1, 0, text.encode('lorem', 'ipsum')); - fs.writeFileSync('public/characters/'+name+'.png', new Buffer.from(encode(chunks))); + fs.writeFileSync(charactersPath+name+'.png', new Buffer.from(encode(chunks))); if(response !== undefined) response.send(mes); @@ -475,7 +529,7 @@ function charaRead(img_url){ } app.post("/getcharacters", jsonParser, function(request, response){ - fs.readdir("public/characters", (err, files) => { + fs.readdir(charactersPath, (err, files) => { if (err) { console.error(err); return; @@ -488,7 +542,7 @@ app.post("/getcharacters", jsonParser, function(request, response){ var i = 0; pngFiles.forEach(item => { //console.log(item); - var img_data = charaRead('./public/characters/'+item); + var img_data = charaRead(charactersPath+item); try { let jsonObject = JSON.parse(img_data); jsonObject.avatar = item; @@ -740,9 +794,9 @@ app.post('/deleteworldinfo', jsonParser, async (request, response) => { function getCharaterFile(directories,response,i){ //old need del if(directories.length > i){ - fs.stat('public/characters/'+directories[i]+'/'+directories[i]+".json", function(err, stat) { + fs.stat(charactersPath+directories[i]+'/'+directories[i]+".json", function(err, stat) { if (err == null) { - fs.readFile('public/characters/'+directories[i]+'/'+directories[i]+".json", 'utf8', (err, data) => { + fs.readFile(charactersPath+directories[i]+'/'+directories[i]+".json", 'utf8', (err, data) => { if (err) { console.error(err); return; @@ -880,7 +934,7 @@ app.post("/getallchatsofchatacter", jsonParser, function(request, response){ if(!request.body) return response.sendStatus(400); var char_dir = (request.body.avatar_url).replace('.png','') - fs.readdir('public/chats/'+char_dir, (err, files) => { + fs.readdir(chatsPath+char_dir, (err, files) => { if (err) { console.error(err); response.send({error: true}); @@ -899,7 +953,7 @@ app.post("/getallchatsofchatacter", jsonParser, function(request, response){ for(let i = jsonFiles.length-1; i >= 0; i--){ const file = jsonFiles[i]; - const fileStream = fs.createReadStream('public/chats/'+char_dir+'/'+file); + const fileStream = fs.createReadStream(chatsPath+char_dir+'/'+file); const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity @@ -933,7 +987,7 @@ app.post("/getallchatsofchatacter", jsonParser, function(request, response){ }); function getPngName(file){ - if (fs.existsSync('./public/characters/'+file+'.png')) { + if (fs.existsSync(charactersPath+file+'.png')) { file = file+'1'; } return file; @@ -955,14 +1009,14 @@ app.post("/importcharacter", urlencodedParser, function(request, response){ } const jsonData = JSON.parse(data); - if(jsonData.char_name !== undefined){//json Pygmalion notepad - png_name = getPngName(jsonData.char_name); - var char = {"name": jsonData.char_name, "description": jsonData.char_persona, "personality": '', "first_mes": jsonData.char_greeting, "avatar": 'none', "chat": Date.now(), "mes_example": jsonData.example_dialogue, "scenario": jsonData.world_scenario, "create_date": Date.now()}; + if(jsonData.name !== undefined){ + png_name = getPngName(jsonData.name); + var char = {"name": jsonData.name, "description": jsonData.description ?? '', "personality": jsonData.personality ?? '', "first_mes": jsonData.first_mes ?? '', "avatar": 'none', "chat": Date.now(), "mes_example": jsonData.mes_example ?? '', "scenario": jsonData.scenario ?? '', "create_date": Date.now()}; char = JSON.stringify(char); charaWrite('./public/img/fluffy.png', char, png_name, response, {file_name: png_name}); - }else if(jsonData.name !== undefined){ - png_name = getPngName(jsonData.name); - var char = {"name": jsonData.name, "description": jsonData.description, "personality": jsonData.personality, "first_mes": jsonData.first_mes, "avatar": 'none', "chat": Date.now(), "mes_example": '', "scenario": '', "create_date": Date.now()}; + }else if(jsonData.char_name !== undefined){//json Pygmalion notepad + png_name = getPngName(jsonData.char_name); + var char = {"name": jsonData.char_name, "description": jsonData.char_persona ?? '', "personality": '', "first_mes": jsonData.char_greeting ?? '', "avatar": 'none', "chat": Date.now(), "mes_example": jsonData.example_dialogue ?? '', "scenario": jsonData.world_scenario ?? '', "create_date": Date.now()}; char = JSON.stringify(char); charaWrite('./public/img/fluffy.png', char, png_name, response, {file_name: png_name}); }else{ @@ -977,7 +1031,7 @@ app.post("/importcharacter", urlencodedParser, function(request, response){ let jsonObject = JSON.parse(img_data); png_name = getPngName(jsonObject.name); if(jsonObject.name !== undefined){ - fs.copyFile('./uploads/'+filedata.filename, 'public/characters/'+png_name+'.png', (err) => { + fs.copyFile('./uploads/'+filedata.filename, charactersPath+png_name+'.png', (err) => { if(err) { response.send({error:true}); return console.log(err); @@ -1048,7 +1102,7 @@ app.post("/importchat", urlencodedParser, function(request, response){ i++; }); const chatJsonlData = new_chat.map(JSON.stringify).join('\n'); - fs.writeFile('public/chats/'+avatar_url+'/'+Date.now()+'.jsonl', chatJsonlData, 'utf8', function(err) { + fs.writeFile(chatsPath+avatar_url+'/'+Date.now()+'.jsonl', chatJsonlData, 'utf8', function(err) { if(err) { response.send(err); return console.log(err); @@ -1077,7 +1131,7 @@ app.post("/importchat", urlencodedParser, function(request, response){ let jsonData = JSON.parse(line); if(jsonData.user_name !== undefined){ - fs.copyFile('./uploads/'+filedata.filename, 'public/chats/'+avatar_url+'/'+Date.now()+'.jsonl', (err) => { + fs.copyFile('./uploads/'+filedata.filename, chatsPath+avatar_url+'/'+Date.now()+'.jsonl', (err) => { if(err) { response.send({error:true}); return console.log(err); @@ -1444,7 +1498,7 @@ app.listen(server_port, function() { console.log('Launching...'); open('http:127.0.0.1:'+server_port); console.log('TavernAI started: http://127.0.0.1:'+server_port); - if (fs.existsSync('public/characters/update.txt')) { //&& !is_colab <- this need to put again + if (fs.existsSync('public/characters/update.txt') && !is_colab) { convertStage1(); } @@ -1497,8 +1551,8 @@ function convertStage2(){ charaWrite(avatar, charactersB[key], directoriesB[key]); const files = fs.readdirSync('public/characters/'+directoriesB[key]+'/chats'); - if (!fs.existsSync('public/chats/'+char.name)) { - fs.mkdirSync('public/chats/'+char.name); + if (!fs.existsSync(chatsPath+char.name)) { + fs.mkdirSync(chatsPath+char.name); } files.forEach(function(file) { // Read the contents of the file @@ -1552,7 +1606,7 @@ function convertStage2(){ }); const jsonlData = new_chat_data.map(JSON.stringify).join('\n'); // Write the contents to the destination folder - fs.writeFileSync('public/chats/'+char.name+'/' + file+'l', jsonlData); + fs.writeFileSync(chatsPath+char.name+'/' + file+'l', jsonlData); }); //fs.rmSync('public/characters/'+directoriesB[key],{ recursive: true }); console.log(char.name+' update!'); @@ -1634,93 +1688,3 @@ function getCharaterFile2(directories,i){ convertStage2(); } } -/* -* async function aaa2(){ -try { - // Load the image in any format - - const image = await sharp('./original.jpg').resize(100, 100).toFormat('png'); - - image.metadata((err, metadata) => { - if (err) throw err; - if (!metadata.chunks) { - metadata.chunks = []; - } -const textData = text.encode('hello', 'world'); -const textBuffer = Buffer.from(`${textData.keyword}\0${textData.text}`); -metadata.chunks.push({ - type: 'tEXt', - data: textBuffer -}); - return metadata; - }) - .toFile('test-out.png') - .then(() => { - console.log('PNG image with tEXt chunks has been saved.'); - }) - .catch((err) => { - console.log(err); - }); -} catch (err) { - console.log(err); -} -} -* function writePNG(){ - -const buffer = fs.readFileSync('test-out.png'); -const chunks = extract(buffer); - const tEXtChunks = chunks.filter(chunk => chunk.name === 'tEXt'); - -// Remove all existing tEXt chunks -for (const tEXtChunk of tEXtChunks) { - chunks.splice(chunks.indexOf(tEXtChunk), 1); -} -// Add new chunks before the IEND chunk -chunks.splice(-1, 0, text.encode('hello', 'world')); -chunks.splice(-1, 0, text.encode('lorem', 'ipsum')); -  -fs.writeFileSync( -  'test-out.png', -  new Buffer.from(encode(chunks)) -); -} - * function readPNG2(){ - sharp('./test-out.png') - .metadata() - .then((metadata) => { - console.log(metadata); - if (!metadata.chunks) { - console.log("No tEXt chunks found in the image file"); - } - const textChunks = metadata.chunks.filter((chunk) => chunk.type === 'tEXt'); - textChunks.forEach((textChunk) => { - const textData = JSON.parse(textChunk.data.toString()); - console.log(textData); - }); - }) - .catch((err) => { - console.log(err); - }); - } -const requestListener = function (req, res) { - fs.readFile(__dirname + "/index.html") - .then(contents => { - res.setHeader("Content-Type", "text/html"); - res.writeHead(200); - res.end(contents); - }) - .catch(err => { - res.writeHead(500); - res.end(err); - return; - }); -}; - - - -const server = http.createServer(requestListener); -server.listen(port, host, () => { - console.log(`Server is running on http://${host}:${port}`); -}); -*/ -