Created
September 28, 2014 17:10
-
-
Save Barbayar/36a2098967a171255fd5 to your computer and use it in GitHub Desktop.
[ISUCON 4] [LAST VERSION]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var async = require('async'); | |
var bodyParser = require('body-parser'); | |
var crypto = require('crypto'); | |
var express = require('express'); | |
var session = require('express-session'); | |
var strftime = require('strftime'); | |
var mysql = require('mysql'); | |
var app = express(); | |
var fs = require('fs'); | |
var _login2User = {}; | |
var _ip2Failnum = {}; | |
var _bannedIps = new Array(); | |
var _lockedUsers = new Array(); | |
var globalConfig = { | |
userLockThreshold: process.env.ISU4_USER_LOCK_THRESHOLD || 3, | |
ipBanThreshold: process.env.ISU4_IP_BAN_THRESHOLD || 10 | |
}; | |
var mysqlPool = mysql.createPool({ | |
host: process.env.ISU4_DB_HOST || 'localhost', | |
user: process.env.ISU4_DB_USER || 'root', | |
password: process.env.ISU4_DB_PASSWORD || '', | |
database: process.env.ISU4_DB_NAME || 'isu4_qualifier' | |
}); | |
function saveCurrentState() { | |
console.log('saving current state...'); | |
fs.writeFileSync('data/_login2User', JSON.stringify(_login2User)); | |
fs.writeFileSync('data/_ip2Failnum', JSON.stringify(_ip2Failnum)); | |
fs.writeFileSync('data/_bannedIps', JSON.stringify(_bannedIps)); | |
fs.writeFileSync('data/_lockedUsers', JSON.stringify(_lockedUsers)); | |
console.log('saved'); | |
process.exit(); | |
} | |
function restoreLastState() { | |
var temp; | |
console.log('restoring last state...'); | |
try { | |
_login2User = JSON.parse(fs.readFileSync('data/_login2User')); | |
} catch (err) {} | |
try { | |
_ip2Failnum = JSON.parse(fs.readFileSync('data/_ip2Failnum')); | |
} catch (err) {} | |
try { | |
_bannedIps = JSON.parse(fs.readFileSync('data/_bannedIps')); | |
} catch (err) {} | |
try { | |
_lockedUsers = JSON.parse(fs.readFileSync('data/_lockedUsers')); | |
} catch (err) {} | |
console.log('restored'); | |
} | |
process.on('SIGINT', function() { | |
saveCurrentState(); | |
}); | |
process.on('SIGTERM', function() { | |
saveCurrentState(); | |
}); | |
function insertLog(log) { | |
if (log.succeeded) { | |
// Reset failnum | |
_login2User[log.login].failnum = 0; | |
_ip2Failnum[log.ip] = 0; | |
var curLogin = {}; | |
curLogin.login = log.login; | |
curLogin.created_at = log.created_at; | |
curLogin.ip = log.ip; | |
// save last login | |
if (_login2User[log.login].currentLogin != null) { | |
_login2User[log.login].lastLogin = _login2User[log.login].currentLogin; | |
} | |
else { | |
_login2User[log.login].lastLogin = curLogin; | |
} | |
// save current login | |
_login2User[log.login].currentLogin = curLogin; | |
} | |
else { | |
// user lock | |
if (_login2User[log.login]) { | |
_login2User[log.login].failnum ++; | |
if (_login2User[log.login].failnum == globalConfig.userLockThreshold) { | |
_lockedUsers.push(log.login); | |
} | |
} | |
// ip ban | |
if (log.ip in _ip2Failnum) { | |
_ip2Failnum[log.ip] ++; | |
if (_ip2Failnum[log.ip] == globalConfig.ipBanThreshold) { | |
_bannedIps.push(log.ip); | |
} | |
} | |
else { | |
_ip2Failnum[log.ip] = 1; | |
} | |
} | |
} | |
var helpers = { | |
calculatePasswordHash: function(password, salt) { | |
var c = crypto.createHash('sha256'); | |
c.update(password + ':' + salt); | |
return c.digest('hex'); | |
}, | |
isUserLocked: function(user, callback) { | |
if(!user) { | |
return callback(false); | |
}; | |
callback(globalConfig.userLockThreshold <= _login2User[user.login].failnum); | |
}, | |
isIPBanned: function(ip, callback) { | |
callback(globalConfig.ipBanThreshold <= _ip2Failnum[ip]); | |
}, | |
attemptLogin: function(req, callback) { | |
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; | |
var login = req.body.login; | |
var password = req.body.password; | |
async.waterfall([ | |
function(cb) { | |
cb(null, _login2User[login]); | |
}, | |
function(user, cb) { | |
helpers.isIPBanned(ip, function(banned) { | |
if(banned) { | |
cb('banned', user); | |
} else { | |
cb(null, user); | |
}; | |
}); | |
}, | |
function(user, cb) { | |
helpers.isUserLocked(user, function(locked) { | |
if(locked) { | |
cb('locked', user); | |
} else { | |
cb(null, user); | |
}; | |
}); | |
}, | |
function(user, cb) { | |
if(user && helpers.calculatePasswordHash(password, user.salt) == user.password_hash) { | |
cb(null, user); | |
} else if(user) { | |
cb('wrong_password', user); | |
} else { | |
cb('wrong_login', user); | |
}; | |
} | |
], function(err, user) { | |
var succeeded = !err; | |
var log = {}; | |
log.created_at = new Date(); | |
log.login = login; | |
log.ip = ip; | |
log.succeeded = succeeded; | |
insertLog(log); | |
callback(err, user); | |
}); | |
}, | |
getCurrentUser: function(login, callback) { | |
callback(_login2User[login]); | |
}, | |
getBannedIPs: function(callback) { | |
callback(_bannedIps); | |
}, | |
getLockedUsers: function(callback) { | |
callback(_lockedUsers); | |
} | |
}; | |
app.enable('trust proxy'); | |
app.use(bodyParser.urlencoded({ extended: false })); | |
app.use(session({ 'secret': 'isucon4-node-qualifier', resave: true, saveUninitialized: true })); | |
app.locals.strftime = function(format, date) { | |
return strftime(format, date); | |
}; | |
app.get('/', function(req, res) { | |
var notice = req.session.notice; | |
req.session.notice = null; | |
var html = '<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="/stylesheets/bootstrap.min.css"> <link rel="stylesheet" href="/stylesheets/bootflat.min.css"> <link rel="stylesheet" href="/stylesheets/isucon-bank.css"> <title>isucon4</title> </head> <body> <div class="container"> <h1 id="topbar"> <a href="/"><img src="/images/isucon-bank.png" alt="いすこん銀行 オンラインバンキングサービス"></a> </h1> <div id="be-careful-phising" class="panel panel-danger"> <div class="panel-heading"> <span class="hikaru-mozi">偽画面にご注意ください!</span> </div> <div class="panel-body"> <p>偽のログイン画面を表示しお客様の情報を盗み取ろうとする犯罪が多発しています。</p> <p>ログイン直後にダウンロード中や、見知らぬウィンドウが開いた場合、<br>すでにウィルスに感染している場合がございます。即座に取引を中止してください。</p> <p>また、残高照会のみなど、必要のない場面で乱数表の入力を求められても、<br>絶対に入力しないでください。</p> </div> </div> <div class="page-header"> <h1>ログイン</h1></div>'; | |
if (notice) { | |
html += '<div id="notice-message" class="alert alert-danger" role="alert">' + notice + '</div>'; | |
} | |
html += '<div class="container"> <form class="form-horizontal" role="form" action="/login" method="POST"> <div class="form-group"> <label for="input-username" class="col-sm-3 control-label">お客様ご契約ID</label> <div class="col-sm-9"> <input id="input-username" type="text" class="form-control" placeholder="半角英数字" name="login"> </div> </div> <div class="form-group"> <label for="input-password" class="col-sm-3 control-label">パスワード</label> <div class="col-sm-9"> <input type="password" class="form-control" id="input-password" name="password" placeholder="半角英数字・記号(2文字以上)"> </div> </div> <div class="form-group"> <div class="col-sm-offset-3 col-sm-9"> <button type="submit" class="btn btn-primary btn-lg btn-block">ログイン</button> </div> </div> </form> </div> </div> </body> </html>'; | |
res.send(html); | |
}); | |
app.post('/login', function(req, res) { | |
helpers.attemptLogin(req, function(err, user) { | |
if(err) { | |
switch(err) { | |
case 'locked': | |
req.session.notice = 'This account is locked.'; | |
break; | |
case 'banned': | |
req.session.notice = "You're banned."; | |
break; | |
default: | |
req.session.notice = 'Wrong username or password'; | |
break; | |
} | |
return res.redirect('/'); | |
} | |
req.session.login = user.login; | |
res.redirect('/mypage'); | |
}); | |
}); | |
app.get('/mypage', function(req, res) { | |
helpers.getCurrentUser(req.session.login, function(user) { | |
if(!user) { | |
req.session.notice = "You must be logged in"; | |
return res.redirect('/'); | |
} | |
var last_login = _login2User[user.login].lastLogin; | |
var html = '<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="/stylesheets/bootstrap.min.css"> <link rel="stylesheet" href="/stylesheets/bootflat.min.css"> <link rel="stylesheet" href="/stylesheets/isucon-bank.css"> <title>isucon4</title> </head> <body> <div class="container"> <h1 id="topbar"> <a href="/"><img src="/images/isucon-bank.png" alt="いすこん銀行 オンラインバンキングサービス"></a> </h1> <div class="alert alert-success" role="alert"> ログインに成功しました。<br> 未読のお知らせが0件、残っています。 </div> <dl class="dl-horizontal"> <dt>前回ログイン</dt> <dd id="last-logined-at">'; | |
html += strftime('%Y-%m-%d %H:%M:%S', last_login.created_at); | |
html += '</dd> <dt>最終ログインIPアドレス</dt> <dd id="last-logined-ip">'; | |
html += last_login.ip; | |
html += '</dd> </dl> <div class="panel panel-default"> <div class="panel-heading"> お客様ご契約ID:'; | |
html += last_login.login; | |
html += '様の代表口座 </div> <div class="panel-body"> <div class="row"> <div class="col-sm-4"> 普通預金<br> <small>東京支店 1111111111</small><br> </div> <div class="col-sm-4"> <p id="zandaka" class="text-right"> ―――円 </p> </div> <div class="col-sm-4"> <p> <a class="btn btn-success btn-block">入出金明細を表示</a> <a class="btn btn-default btn-block">振込・振替はこちらから</a> </p> </div> <div class="col-sm-12"> <a class="btn btn-link btn-block">定期預金・住宅ローンのお申込みはこちら</a> </div> </div> </div> </div> </div> </body> </html>'; | |
res.send(html); | |
}); | |
}); | |
app.get('/report', function(req, res) { | |
async.parallel({ | |
banned_ips: function(cb) { | |
helpers.getBannedIPs(function(ips) { | |
cb(null, ips); | |
}); | |
}, | |
locked_users: function(cb) { | |
helpers.getLockedUsers(function(users) { | |
cb(null, users); | |
}); | |
} | |
}, function(err, result) { | |
res.json(result); | |
}); | |
}); | |
app.get('/warmup', function(req, res) { | |
warmup(function () { | |
res.json('OK'); | |
}); | |
}); | |
function restore(cb) { | |
restoreLastState(); | |
if (Object.keys(_login2User).length == 0) { | |
console.log('local data is empty'); | |
warmup(cb); | |
return; | |
} | |
cb(); | |
} | |
function warmup(cb) { | |
console.log('fetching data from MySQL...'); | |
_login2User = {}; | |
_ip2Failnum = {}; | |
_bannedIps = new Array(); | |
_lockedUsers = new Array(); | |
async.parallel({ | |
users: function(cb) { | |
mysqlPool.query('SELECT * FROM users ORDER BY id', function(err, rows) { | |
cb(null, rows); | |
}); | |
}, | |
login_log: function(cb) { | |
mysqlPool.query('SELECT * FROM login_log ORDER BY id', function(err, rows) { | |
cb(null, rows); | |
}); | |
} | |
}, function(err, result) { | |
if (err) { | |
res.status(500).send('Error: ' + err.message); | |
return; | |
} | |
for (var i = 0; i < result.users.length; i++) { | |
user = result.users[i]; | |
user.failnum = 0; | |
_login2User[user.login] = user; | |
} | |
for (var i = 0; i < result.login_log.length; i++) { | |
log = result.login_log[i]; | |
insertLog(log); | |
} | |
console.log('fetched'); | |
cb(); | |
}); | |
} | |
app.use(function (err, req, res, next) { | |
res.status(500).send('Error: ' + err.message); | |
}); | |
restore(function() { | |
var server = app.listen(process.env.PORT || 8080, function() { | |
console.log('Listening on port %d', server.address().port); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment