var opts = require('./opts'); if (require.main == module) opts.parse_args(); opts.load_defaults(); const khash = require("kana-hash"); var _ = require('../lib/underscore'), amusement = require('./amusement'), async = require('async'), auth = require('./auth'), caps = require('./caps'), check = require('./msgcheck').check, common = require('../common'), config = require('../config'), crypto = require('crypto'), db = require('../db'), fs = require('fs'), hooks = require('../hooks'), imager = require('../imager'), etc = require('../etc'), Muggle = etc.Muggle, okyaku = require('./okyaku'), render = require('./render'), request = require('request'), STATE = require('./state'), tripcode = require('../tripcode').make(),//{hash:function(a,b){return new khash.Kana(0, this.salt).once(a);}, setSalt:function(a){if(a) this.salt = new khash.Salt(a); else this.salt = khash.Salt.Default; return this.salt;}}, //require('./../tripcode/tripcode'), urlParse = require('url').parse, web = require('./web'), winston = require('winston'); require('../admin'); if (!imager.is_standalone()) require('../imager/daemon'); // preload and confirm it works if (config.CURFEW_BOARDS) require('../curfew/server'); try { var reportConfig = require('../report/config'); if (reportConfig.RECAPTCHA_SITE_KEY) require('../report/server'); } catch (e) {} var RES = STATE.resources; var dispatcher = okyaku.dispatcher; /* I always use encodeURI anyway */ var escape = common.escape_html; var safe = common.safe; dispatcher[common.PING] = function (msg, client) { if (msg.length) return false; client.send([0, common.PING]); return true; }; dispatcher[common.SYNCHRONIZE] = function (msg, client) { function checked(err, ident) { if (!err) _.extend(client.ident, ident); if (!synchronize(msg, client)) client.kotowaru(Muggle("Bad protocol.")); } var chunks = web.parse_cookie(msg.pop()); var cookie = auth.extract_login_cookie(chunks); if (cookie) { auth.check_cookie(cookie, checked); return true; } else return synchronize(msg, client); }; function synchronize(msg, client) { if (!check(['id', 'string', 'id=>nat', 'boolean'], msg)) return false; var id = msg[0], board = msg[1], syncs = msg[2], live = msg[3]; if (id in STATE.clients) { winston.error("Duplicate client id " + id); return false; } client.id = id; STATE.clients[id] = client; if (!caps.can_access_board(client.ident, board)) return false; var dead_threads = [], count = 0, op; for (var k in syncs) { k = parseInt(k, 10); if (db.OPs[k] != k || !db.OP_has_tag(board, k)) { delete syncs[k]; dead_threads.push(k); } op = k; if (++count > config.THREADS_PER_PAGE) { /* Sync logic isn't great yet; allow this for now */ // return false; } } client.watching = syncs; if (live) { /* XXX: This will break if a thread disappears during sync * (won't be reported) * Or if any of the threads they see on the first page * don't show up in the 'live' pub for whatever reason. * Really we should get them synced first and *then* switch * to the live pub. */ client.watching = {live: true}; count = 1; } client.board = board; if (client.db) client.db.disconnect(); client.db = new db.Yakusoku(board, client.ident); /* Race between subscribe and backlog fetch; client must de-dup */ client.db.kiku(client.watching, client.on_update.bind(client), client.on_thread_sink.bind(client), listening); function listening(errs) { if (errs && errs.length >= count) return client.kotowaru(Muggle( "Couldn't sync to board.")); else if (errs) { dead_threads.push.apply(dead_threads, errs); errs.forEach(function (thread) { delete client.watching[thread]; }); } client.db.fetch_backlogs(client.watching, got_backlogs); } function got_backlogs(errs, logs) { if (errs) { dead_threads.push.apply(dead_threads, errs); errs.forEach(function (thread) { delete client.watching[thread]; }); } if (client.ident.readOnly) { logs.push('0,' + common.MODEL_SET + ',["hot"],{"readOnly":true}'); } var sync = '0,' + common.SYNCHRONIZE; if (dead_threads.length) sync += ',' + JSON.stringify(dead_threads); logs.push(sync); client.socket.write('[[' + logs.join('],[') + ']]'); client.synced = true; var info = {client: client, live: live}; if (!live && count == 1) info.op = op; else info.board = board; hooks.trigger('clientSynced', info, function (err) { if (err) winston.error(err); }); } return true; } function setup_imager_relay(cb) { var onegai = new imager.Onegai; onegai.relay_client_messages(); onegai.once('relaying', function () { onegai.on('message', image_status); cb(null); }); } function image_status(client_id, status) { if (!check('id', client_id)) return; var client = STATE.clients[client_id]; if (client) { try { client.send([0, common.IMAGE_STATUS, status]); } catch (e) { // Swallow EINTR // anta baka? } } } function page_nav(thread_count, cur_page, ascending) { var page_count = Math.ceil(thread_count / config.THREADS_PER_PAGE); page_count = Math.max(page_count, 1); var info = {pages: page_count, threads: thread_count, cur_page: cur_page, ascending: ascending}; var step = ascending ? -1 : 1; var next = Math.max(cur_page, 0) + step; if (next >= 0 && next < page_count) info.next_page = 'page' + next; var prev = cur_page - step; if (prev >= 0 && prev < page_count) info.prev_page = 'page' + prev; return info; } function write_gzip_head(req, resp, headers) { var encoding = config.GZIP && req.headers['accept-encoding']; if (req.ident.slow || !encoding || encoding.indexOf('gzip') < 0) { resp.writeHead(200, headers); return resp; } resp.writeHead(200, _.extend({}, headers, { 'Content-Encoding': 'gzip', Vary: 'Accept-Encoding', })); var gz = require('zlib').createGzip(); gz.pipe(resp); return gz; } function redirect_thread(cb, num, op, tag) { if (!tag) cb(null, 'redirect', op + '#' + num); else /* Use a JS redirect to preserve the hash */ cb(null, 'redirect_js', '../' + tag + '/' + op + '#' + num); } // unless imager.config.DAEMON, we deal with image uploads in-process. if (!imager.is_standalone()) { web.route_post(/^\/upload\/$/, require('../imager/daemon').new_upload); } web.resource(/^\/$/, function (req, cb) { cb(null, 'redirect', config.DEFAULT_BOARD + '/'); }); if (config.DEBUG) { /* Shortcuts for convenience */ winston.warn("Running in (insecure) debug mode."); winston.warn("Do not use on the public internet."); web.route_get(/^\/login$/, function (req, resp) { auth.set_cookie(req, resp, {auth: 'Admin'}); }); web.route_get(/^\/mod$/, function (req, resp) { auth.set_cookie(req, resp, {auth: 'Moderator'}); }); } else { /* Production login endpoint */ web.route_get(/^\/login$/, auth.login); if (config.SERVE_STATIC_FILES) winston.warn("Recommended: nginx-like webserver instead of SERVE_STATIC_FILES."); if (config.SERVE_IMAGES) winston.warn("Recommended: nginx-like webserver instead of SERVE_IMAGES."); } web.route_get(/^\/logout$/, auth.logout); web.route_post(/^\/logout$/, auth.logout); function write_mod_js(resp, ident) { if (!RES.modJs) { resp.writeHead(500); resp.end('Mod js not built?!'); return; } var noCacheJs = _.clone(web.noCacheHeaders); noCacheJs['Content-Type'] = 'text/javascript; charset=UTF-8'; resp.writeHead(200, noCacheJs); resp.write('(function (IDENT) {'); resp.write(RES.modJs); resp.end('})(' + JSON.stringify(ident) + ');'); } web.resource_auth(/^\/admin\.js$/, function (req, cb) { if (!caps.can_administrate(req.ident)) cb(404); else cb(null, 'ok'); }, function (req, resp) { write_mod_js(resp, { auth: req.ident.auth, csrf: req.ident.csrf, user: req.ident.user, }); }); web.resource_auth(/^\/mod\.js$/, function (req, cb) { if (!caps.can_moderate(req.ident)) cb(404); else cb(null, 'ok'); }, function (req, resp) { write_mod_js(resp, { auth: req.ident.auth, csrf: req.ident.csrf, user: req.ident.user, }); }); web.resource(/^\/(\w+)$/, function (req, params, cb) { var board = params[1]; /* If arbitrary boards were allowed, need to escape this: */ var dest = board + '/'; if (req.ident.suspension) return cb(null, 'redirect', dest); /* TEMP */ if (!caps.can_ever_access_board(req.ident, board)) return cb(404); cb(null, 'redirect', dest); }); web.resource(/^\/(\w+)\/live$/, function (req, params, cb) { if (req.ident.suspension) return cb(null, 'redirect', '.'); /* TEMP */ if (!caps.can_ever_access_board(req.ident, params[1])) return cb(404); cb(null, 'redirect', '.'); }); web.resource(/^\/(\w+)\/$/, function (req, params, cb) { var board = params[1]; if (req.ident.suspension) return cb(null, 'ok'); /* TEMP */ if (!caps.can_ever_access_board(req.ident, board)) return cb(404); cb(null, 'ok', {board: board}); }, function (req, resp) { /* TEMP */ if (req.ident.suspension) return render_suspension(req, resp); var board = this.board; var info = {board: board, ident: req.ident, resp: resp}; hooks.trigger_sync('boardDiversion', info); if (info.diverted) return; var yaku = new db.Yakusoku(board, req.ident); yaku.get_tag(-1); var paginationHtml; yaku.once('begin', function (thread_count) { var nav = page_nav(thread_count, -1, board == 'archive'); var initScript = make_init_script(req.ident); render.write_board_head(resp, initScript, board, nav); paginationHtml = render.make_pagination_html(nav); resp.write(paginationHtml); resp.write('
\n'); }); resp = write_gzip_head(req, resp, web.noCacheHeaders); var opts = {fullLinks: true, board: board}; render.write_thread_html(yaku, req, resp, opts); yaku.once('end', function () { resp.write(paginationHtml); render.write_page_end(resp, req.ident, false); resp.end(); yaku.disconnect(); }); yaku.once('error', function (err) { winston.error('index:' + err); resp.end(); yaku.disconnect(); }); }); web.resource(/^\/(\w+)\/page(\d+)$/, function (req, params, cb) { var board = params[1]; if (!caps.temporal_access_check(req.ident, board)) return cb(null, 302, '..'); if (req.ident.suspension) return cb(null, 'ok'); /* TEMP */ if (!caps.can_access_board(req.ident, board)) return cb(404); var page = parseInt(params[2], 10); if (page > 0 && params[2][0] == '0') /* leading zeroes? */ return cb(null, 'redirect', 'page' + page); var yaku = new db.Yakusoku(board, req.ident); yaku.get_tag(page); yaku.once('nomatch', function () { cb(null, 302, '.'); yaku.disconnect(); }); yaku.once('begin', function (threadCount) { cb(null, 'ok', { board: board, page: page, yaku: yaku, threadCount: threadCount, }); }); }, function (req, resp) { /* TEMP */ if (req.ident.suspension) return render_suspension(req, resp); var board = this.board; var nav = page_nav(this.threadCount, this.page, board == 'archive'); resp = write_gzip_head(req, resp, web.noCacheHeaders); var initScript = make_init_script(req.ident); render.write_board_head(resp, initScript, board, nav); var paginationHtml = render.make_pagination_html(nav); resp.write(paginationHtml); resp.write('
\n'); var opts = {fullLinks: true, board: board}; render.write_thread_html(this.yaku, req, resp, opts); var self = this; this.yaku.once('end', function () { resp.write(paginationHtml); render.write_page_end(resp, req.ident, false); resp.end(); self.finished(); }); this.yaku.once('error', function (err) { winston.error('page' + self.page + ': ' + err); resp.end(); self.finished(); }); }, function () { this.yaku.disconnect(); }); web.resource(/^\/(\w+)\/page(\d+)\/$/, function (req, params, cb) { if (!caps.temporal_access_check(req.ident, params[1])) cb(null, 302, '..'); else cb(null, 'redirect', '../page' + params[2]); }); web.resource(/^\/(\w+)\/(\d+)$/, function (req, params, cb) { var board = params[1]; if (!caps.temporal_access_check(req.ident, board)) return cb(null, 302, '.'); if (req.ident.suspension) return cb(null, 'ok'); /* TEMP */ if (!caps.can_access_board(req.ident, board)) return cb(404); var num = parseInt(params[2], 10); if (!num) return cb(404); else if (params[2][0] == '0') return cb(null, 'redirect', '' + num); var op, json = web.prefers_json(req.headers.accept); if (board == 'graveyard') { op = num; } else { op = db.OPs[num]; if (!op) return cb(404); if (!json && !db.OP_has_tag(board, op)) { var tag = db.first_tag_of(op); if (tag) { if (!caps.can_access_board(req.ident, tag)) return cb(404); return redirect_thread(cb, num, op, tag); } else { winston.warn("Orphaned post " + num + "with tagless OP " + op); return cb(404); } } if (!json && op != num) return redirect_thread(cb, num, op); } if (!caps.can_access_thread(req.ident, op)) return cb(404); if (json) return cb(null, 'ok', {json: true, num: num}); var yaku = new db.Yakusoku(board, req.ident); var reader = new db.Reader(yaku); var opts = {redirect: true}; var lastN = detect_last_n(req.query); if (lastN) opts.abbrev = lastN + config.ABBREVIATED_REPLIES; if (caps.can_administrate(req.ident) && 'reported' in req.query) opts.showDead = true; reader.get_thread(board, num, opts); reader.once('nomatch', function () { cb(404); yaku.disconnect(); }); reader.once('redirect', function (op, tag) { redirect_thread(cb, num, op, tag); yaku.disconnect(); }); reader.once('begin', function (preThread) { var headers = web.noCacheHeaders; cb(null, 'ok', { headers: headers, board: board, op: op, subject: preThread.subject, yaku: yaku, reader: reader, abbrev: opts.abbrev, }); }); }, function (req, resp) { /* TEMP */ if (req.ident.suspension) return render_suspension(req, resp); if (this.json) return write_json_post(req, resp, this.num); var board = this.board, op = this.op; resp = write_gzip_head(req, resp, this.headers); var initScript = make_init_script(req.ident); render.write_thread_head(resp, initScript, board, op, { subject: this.subject, abbrev: this.abbrev, }); var opts = {fullPosts: true, board: board, loadAllPostsLink: true}; render.write_thread_html(this.reader, req, resp, opts); var self = this; this.reader.once('end', function () { render.write_page_end(resp, req.ident, true); resp.end(); self.finished(); }); function on_err(err) { winston.error('thread '+num+':', err); resp.end(); self.finished(); } this.reader.once('error', on_err); this.yaku.once('error', on_err); }, function () { this.yaku.disconnect(); }); function write_json_post(req, resp, num) { var json = {TODO: true}; var cache = json.editing ? 'no-cache' : 'private, max-age=86400'; resp = write_gzip_head(req, resp, { 'Content-Type': 'application/json', 'Cache-Control': cache, }); resp.end(JSON.stringify(json)); } function detect_last_n(query) { for (var k in query) { var m = /^last(\d+)$/.exec(k); if (m) { var n = parseInt(m[1], 10); if (common.reasonable_last_n(n)) return n; } } return 0; } web.resource(/^\/(\w+)\/(\d+)\/$/, function (req, params, cb) { if (!caps.temporal_access_check(req.ident, params[1])) cb(null, 302, '..'); else cb(null, 'redirect', '../' + params[2]); }); web.resource(/^\/outbound\/(g|iqdb)\/([\w+\/]{22}\.jpg)$/, function (req, params, cb) { var thumb = imager.config.MEDIA_URL + 'vint/' + params[2]; // attempt to make protocol more absolute var u = urlParse(thumb, false, true); if (!u.protocol) { u.protocol = 'http:'; thumb = u.format(); } var service = params[1] == 'iqdb' ? 'http://iqdb.org/?url=' : 'https://www.google.com/searchbyimage?image_url='; var dest = service + encodeURIComponent(thumb); cb(null, 303.1, dest); }); web.resource(/^\/outbound\/hash\/([\w+\/]{22})$/, function (req, params, cb) { var dest = 'http://archive.foolz.us/_/search/image/' + escape(params[1]); cb(null, 303.1, dest); }); web.resource(/^\/outbound\/a\/(\d{0,10})$/, function (req, params, cb) { var thread = parseInt(params[1], 10); var url = 'https://boards.4chan.org/a/'; if (thread) url += 'thread/' + thread; cb(null, 303.1, url); }); web.resource(/^\/outbound\/sysint\/(\d{0,10})$/, function (req, params, cb) { var thread = parseInt(params[1], 10); var url = '/sysint/'; if (thread) url += thread; cb(null, 303.1, url); }); web.resource(/^\/outbound\/nap\/(\d{0,10})$/, function (req, params, cb) { var thread = parseInt(params[1], 10); var url = 'https://nineball.party/nap/'; if (thread) url += thread; cb(null, 303.1, url); }); function make_init_script(ident) { var secretKey = STATE.hot.connTokenSecretKey; if (!ident || !secretKey) return ''; var country = ident.country || 'x'; var payload = JSON.stringify({ ip: ident.ip, cc: country, ts: Date.now(), }); // encrypt payload as 'ctoken' var iv = crypto.randomBytes(12); var cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv); var crypted = cipher.update(payload, 'utf8', 'hex'); crypted += cipher.final('hex'); var authTag = cipher.getAuthTag() if (authTag.length != 16) throw 'auth tag of unexpected length'; var combined = iv.toString('hex') + authTag.toString('hex') + crypted; return '\t\n'; } function decrypt_ctoken(ctoken) { var secretKey = STATE.hot.connTokenSecretKey; if (!secretKey) return null; if (ctoken.length < 56) { winston.warn('ctoken too short'); return null; } var iv = new Buffer(ctoken.slice(0, 24), 'hex'); if (iv.length != 12) { winston.warn('iv not hex'); return null; } var authTag = new Buffer(ctoken.slice(24, 56), 'hex'); if (authTag.length != 16) { winston.warn('authTag not hex'); return null; } try { var decipher = crypto.createDecipheriv('aes-256-gcm', secretKey, iv); decipher.setAuthTag(authTag); var plain = decipher.update(ctoken.slice(56), 'hex', 'utf8'); plain += decipher.final('utf8'); return JSON.parse(plain); } catch (e) { winston.warn(e); } return null; } var TWEET_CACHE = {}; var TWEET_CACHE_LEN = 0; function expire_tweet(key) { if (TWEET_CACHE[key]) { delete TWEET_CACHE[key]; TWEET_CACHE_LEN--; } } web.resource(/^\/outbound\/tweet\/(\w{1,15}\/status\/\d{4,20})$/, function (req, params, cb) { var url = 'https://twitter.com/' + params[1]; if (/^\d+$/.test(req.query.s)) url += '?s=' + req.query.s; var theme = req.query.theme == 'dark' ? 'dark' : 'light'; var params = { url: url, omit_script: true, theme: theme, }; var key = 'tw:'+url; if (TWEET_CACHE[key]) return cb(null, 'ok', TWEET_CACHE[key]); request.get({ uri: 'https://publish.twitter.com/oembed', qs: params, json: true, }, function (err, twResp, json) { if (err) return cb(err); var code = twResp.statusCode; if (code < 200 || code >= 300) { if (code == 404) cb(404); else cb('twitter returned ' + code); return; } if (!json.html) return cb('unexpected tweet form'); if (!TWEET_CACHE[key] && TWEET_CACHE_LEN < 50) { TWEET_CACHE[key] = {json: json}; TWEET_CACHE_LEN++; setTimeout(expire_tweet.bind(null, key), 600*1000); } cb(null, 'ok', {json: json}); }); }, function (req, resp) { resp = write_gzip_head(req, resp, { 'Content-Type': 'application/json', 'Cache-Control': config.DEBUG ? 'no-cache' : 'public, max-age=600', }); resp.end(JSON.stringify(this.json)); }); web.route_get_auth(/^\/dead\/(src|thumb|mid)\/(\w+\.\w{3})$/, function (req, resp, params) { if (!caps.can_administrate(req.ident)) return web.render_404(resp); imager.send_dead_image(params[1], params[2], resp); }); /* Must be prepared to receive callback instantly */ function valid_links(frag, state, ident, callback) { var links = {}; var onee = new common.OneeSama(function (num) { var op = db.OPs[num]; if (op && caps.can_access_thread(ident, op)) links[num] = db.OPs[num]; }); onee.callback = function (frag) {}; onee.state = state; onee.fragment(frag); callback(null, _.isEmpty(links) ? null : links); } var insertSpec = [{ frag: 'opt string', image: 'opt string', nonce: 'id', op: 'opt id', name: 'opt string', email: 'opt string', auth: 'opt string', subject: 'opt string', flavor: 'opt string', }]; dispatcher[common.INSERT_POST] = function (msg, client) { if (!check(insertSpec, msg)) return false; msg = msg[0]; if (client.post) return update_post(msg.frag, client); if (!caps.can_access_board(client.ident, client.board)) return false; var frag = msg.frag; if (frag && /^\s*$/g.test(frag)) return false; if (!frag && !msg.image) return false; if (config.DEBUG) debug_command(client, frag); allocate_post(msg, client, function (err) { if (err) client.kotowaru(Muggle("Allocation failure.", err)); }); return true; } function inactive_board_check(client) { if (caps.can_administrate(client.ident)) return true; return ['graveyard', 'archive'].indexOf(client.board) == -1; } function allocate_post(msg, client, callback) { if (client.post) return callback(Muggle("Already have a post.")); if (!inactive_board_check(client)) return callback(Muggle("Can't post here.")); var post = {time: Date.now(), nonce: msg.nonce}; var body = ''; var ip = client.ident.ip; var extra = {ip: ip, board: client.board}; var image_alloc; if (msg.image) { if (!/^\d+$/.test(msg.image)) return callback(Muggle('Expired image token.')); image_alloc = msg.image; } if (msg.frag) { if (/^\s*$/g.test(msg.frag)) return callback(Muggle('Bad post body.')); if (msg.frag.length > common.MAX_POST_CHARS) return callback(Muggle('Post is too long.')); body = msg.frag.replace(config.EXCLUDE_REGEXP, ''); } if (msg.op) { if (db.OPs[msg.op] != msg.op) return callback(Muggle('Thread does not exist.')); if (!db.OP_has_tag(extra.board, msg.op)) return callback(Muggle('Thread does not exist.')); post.op = msg.op; } else { if (!image_alloc) return callback(Muggle('Image missing.')); var subject = (msg.subject || '').trim(); subject = subject.replace(config.EXCLUDE_REGEXP, ''); subject = subject.replace(/[「」]/g, ''); subject = subject.slice(0, config.SUBJECT_MAX_LENGTH); if (subject) post.subject = subject; } /* TODO: Check against client.watching? */ if (msg.name) { var parsed = common.parse_name(msg.name); post.name = parsed[0]; var spec = STATE.hot.SPECIAL_TRIPCODES; if (spec && parsed[1] && parsed[1] in spec) { post.trip = spec[parsed[1]]; } else if (parsed[1] || parsed[2]) { var trip = tripcode.hash(parsed[1], parsed[2]); if (trip) post.trip = trip; } } if (msg.email) { post.email = msg.email.trim().substr(0, 320); if (common.is_noko(post.email)) delete post.email; } if (msg.flavor && /^\w+$/.test(msg.flavor)) { if (msg.flavor == 'floop') post.flavor = 'floop'; } post.state = common.initial_state(); if ('auth' in msg) { if (!msg.auth || !client.ident || msg.auth !== client.ident.auth) return callback(Muggle('Bad auth.')); post.auth = msg.auth; } if (post.op) client.db.check_thread_locked(post.op, checked); else client.db.check_throttle(ip, checked); function checked(err) { if (err) return callback(err); client.db.reserve_post(post.op, ip, got_reservation); } function got_reservation(err, num) { if (err) return callback(err); if (!client.synced) return callback(Muggle('Dropped; post aborted.')); if (client.post) return callback(Muggle('Already have a post.')); if (body.length && is_game_board(client.board)) amusement.roll_dice(body, post, extra); client.post = post; post.num = num; var supplements = { links: valid_links.bind(null, body, post.state, client.ident), }; if (image_alloc) supplements.image = imager.obtain_image_alloc.bind( null, image_alloc); async.parallel(supplements, got_supplements); } function got_supplements(err, rs) { if (err) { if (client.post === post) client.post = null; return callback(Muggle("Attachment error.", err)); } if (!client.synced) return callback(Muggle('Dropped; post aborted.')); post.links = rs.links; if (rs.image) extra.image_alloc = rs.image; client.db.insert_post(post, body, extra, inserted); } function inserted(err) { if (err) { if (client.post === post) client.post = null; return callback(Muggle("Couldn't allocate post.",err)); } post.body = body; callback(null); } return true; } function update_post(frag, client) { if (typeof frag != 'string') return false; if (config.DEBUG) debug_command(client, frag); frag = frag.replace(config.EXCLUDE_REGEXP, ''); var post = client.post; if (!post) return false; var limit = common.MAX_POST_CHARS; if (frag.length > limit || post.length >= limit) return false; var combined = post.length + frag.length; if (combined > limit) frag = frag.substr(0, combined - limit); var extra = {ip: client.ident.ip}; if (is_game_board(client.board)) amusement.roll_dice(frag, post, extra); post.body += frag; /* imporant: broadcast prior state */ var old_state = post.state.slice(); valid_links(frag, post.state, client.ident, function (err, links) { if (err) links = null; /* oh well */ if (links) { if (!post.links) post.links = {}; var new_links = {}; for (var k in links) { var link = links[k]; if (post.links[k] != link) { post.links[k] = link; new_links[k] = link; } } extra.links = links; extra.new_links = new_links; } client.db.append_post(post, frag, old_state, extra, function (err) { if (err) client.kotowaru(Muggle("Couldn't add text.", err)); }); }); return true; } dispatcher[common.UPDATE_POST] = update_post; function debug_command(client, frag) { if (!frag) return; if (/\bfail\b/.test(frag)) client.kotowaru(Muggle("Failure requested.")); else if (/\bclose\b/.test(frag)) client.socket.close(); } dispatcher[common.FINISH_POST] = function (msg, client) { if (!check([], msg)) return false; if (!client.post) return true; /* whatever */ client.finish_post(function (err) { if (err) client.kotowaru(Muggle("Couldn't finish post.", err)); }); return true; } dispatcher[common.DELETE_POSTS] = caps.mod_handler(function (nums, client) { if (!inactive_board_check(client)) return client.kotowaru(Muggle("Couldn't delete.")); /* Omit to-be-deleted posts that are inside to-be-deleted threads */ var ops = {}, OPs = db.OPs; nums.forEach(function (num) { if (num == OPs[num]) ops[num] = 1; }); nums = nums.filter(function (num) { var op = OPs[num]; return op == num || !(OPs[num] in ops); }); client.db.remove_posts(nums, function (err, dels) { if (err) client.kotowaru(Muggle("Couldn't delete.", err)); }); }); dispatcher[common.LOCK_THREAD] = caps.mod_handler(function (nums, client) { if (!inactive_board_check(client)) return client.kotowaru(Muggle("Couldn't (un)lock thread.")); nums = nums.filter(function (op) { return db.OPs[op] == op; }); async.forEach(nums, client.db.toggle_thread_lock.bind(client.db), function (err) { if (err) client.kotowaru(Muggle( "Couldn't (un)lock thread.", err)); }); }); dispatcher[common.DELETE_IMAGES] = caps.mod_handler(function (nums, client) { if (!inactive_board_check(client)) return client.kotowaru(Muggle("Couldn't delete images.")); client.db.remove_images(nums, function (err, dels) { if (err) client.kotowaru(Muggle("Couldn't delete images.",err)); }); }); dispatcher[common.INSERT_IMAGE] = function (msg, client) { if (!check(['string'], msg)) return false; var alloc = msg[0]; if (!client.post || client.post.image) return false; imager.obtain_image_alloc(alloc, function (err, alloc) { if (err) return client.kotowaru(Muggle("Image lost.", err)); if (!client.post || client.post.image) return; client.db.add_image(client.post, alloc, client.ident.ip, function (err) { if (err) client.kotowaru(Muggle( "Image insertion problem.", err)); }); }); return true; }; dispatcher[common.SPOILER_IMAGES] = caps.mod_handler(function (nums, client) { if (!inactive_board_check(client)) return client.kotowaru(Muggle("Couldn't spoiler images.")); client.db.force_image_spoilers(nums, function (err) { if (err) client.kotowaru(Muggle("Couldn't spoiler images.", err)); }); }); dispatcher[common.EXECUTE_JS] = function (msg, client) { if (!caps.can_administrate(client.ident)) return false; if (!check(['id'], msg)) return false; var op = msg[0]; client.db.set_fun_thread(op, function (err) { if (err) client.kotowaru(err); }); return true; }; function is_game_board(board) { return config.GAME_BOARDS.indexOf(board) >= 0; } function render_suspension(req, resp) { setTimeout(function () { var ban = req.ident.suspension, tmpl = RES.suspensionTmpl; resp.writeHead(200, web.noCacheHeaders); resp.write(tmpl[0]); resp.write(escape(ban.why || '')); resp.write(tmpl[1]); resp.write(escape(ban.until || '')); resp.write(tmpl[2]); resp.write(escape(STATE.hot.EMAIL || '')); resp.end(tmpl[3]); }, 2000); } function get_sockjs_script_sync() { var src = fs.readFileSync('tmpl/index.html', 'UTF-8'); return src.match(/sockjs-[\d.]+(?:\.min)?\.js/)[0]; } function sockjs_log(sev, message) { if (message.length > 80) message = message.slice(0, 60) + '[\u2026]' + message.slice(message.length - 14); if (sev == 'info') winston.verbose(message); else if (sev == 'error') winston.error(message); } if (config.DEBUG) { winston.remove(winston.transports.Console); winston.add(winston.transports.Console, {level: 'verbose'}); } else { winston.add(winston.transports.File, {level: 'warn', filename: 'error.log'}); } function start_server() { var is_unix_socket = (typeof config.LISTEN_PORT == 'string'); if (is_unix_socket) { try { fs.unlinkSync(config.LISTEN_PORT); } catch (e) {} } web.server.listen(config.LISTEN_PORT, config.LISTEN_HOST); if (is_unix_socket) { fs.chmodSync(config.LISTEN_PORT, '777'); // TEMP } var sockjsPath = 'js/' + get_sockjs_script_sync(); var sockOpts = { sockjs_url: imager.config.MEDIA_URL + sockjsPath, prefix: config.SOCKET_PATH, jsessionid: false, log: sockjs_log, websocket: config.USE_WEBSOCKETS, }; var sockJs = require('sockjs').createServer(sockOpts); web.server.on('upgrade', function (req, resp) { resp.end(); }); sockJs.installHandlers(web.server); sockJs.on('connection', function (socket) { var ip = socket.remoteAddress; var country; if (config.TRUST_X_FORWARDED_FOR) { var ff = web.parse_forwarded_for( socket.headers['x-forwarded-for']); if (ff) ip = ff; } if (!ip) { winston.warn('no ip from ' + socket); socket.close(); return; } // parse ctoken var url = urlParse(socket.url, true); if (url.query && url.query.ctoken) { var token = decrypt_ctoken(url.query.ctoken); if (token) { if (token.ts + 24*60*60*1000 < Date.now()) { // token expired, ask for a new one? winston.warn('ctoken: expired'); } if (ip != token.ip) winston.info('ctoken: ' + ip + ' != ' + token.ip); country = token.cc; } else { winston.log('ctoken: invalid from ' + ip); } } else { winston.warn('ctoken: MISSING from ' + ip); } var client = new okyaku.Okyaku(socket, ip, country); socket.on('data', client.on_message.bind(client)); socket.on('close', client.on_close.bind(client)); }); process.on('SIGHUP', hot_reloader); db.on_pub('reloadHot', hot_reloader); if (config.DAEMON) { var cfg = config.DAEMON; var daemon = require('daemon'); var pid = daemon.start(process.stdout.fd, process.stderr.fd); var lock = config.PID_FILE; daemon.lock(lock); winston.remove(winston.transports.Console); return; } process.nextTick(non_daemon_pid_setup); winston.info('Listening on ' + (config.LISTEN_HOST || '') + (is_unix_socket ? '' : ':') + (config.LISTEN_PORT + '.')); } function hot_reloader() { STATE.reload_hot_resources(function (err) { if (err) { winston.error("Error trying to reload:"); winston.error(err); return; } okyaku.scan_client_caps(); winston.info('Reloaded initial state.'); }); } function non_daemon_pid_setup() { var pidFile = config.PID_FILE; fs.writeFile(pidFile, process.pid+'\n', function (err) { if (err) return winston.warn("Couldn't write pid: " + err); process.once('SIGINT', delete_pid); process.once('SIGTERM', delete_pid); winston.info('PID ' + process.pid + ' written in ' + pidFile); }); function delete_pid() { try { fs.unlinkSync(pidFile); } catch (e) { } process.exit(1); } } if (require.main == module) { if (!process.getuid()) throw new Error("Refusing to run as root."); if (!tripcode.setSalt(config.SECURE_SALT)) throw "Bad SECURE_SALT"; async.series([ imager.make_media_dirs, setup_imager_relay, STATE.reload_hot_resources, db.track_OPs, ], function (err) { if (err) throw err; var yaku = new db.Yakusoku(null, db.UPKEEP_IDENT); var onegai; var writes = []; if (!config.READ_ONLY) { writes.push(yaku.finish_all.bind(yaku)); if (!imager.is_standalone()) { onegai = new imager.Onegai; writes.push(onegai.delete_temporaries.bind( onegai)); } } async.series(writes, function (err) { if (err) throw err; yaku.disconnect(); if (onegai) onegai.disconnect(); process.nextTick(start_server); }); }); }