diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..23d3932
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,12 @@
+{
+ "compact": false,
+ "comments": false,
+ "presets": ["es2015"],
+ "plugins": [
+ "syntax-async-functions",
+ "syntax-object-rest-spread",
+ "transform-object-rest-spread",
+ "transform-regenerator",
+ "transform-runtime"
+ ]
+}
diff --git a/.gitignore b/.gitignore
index 123ae94..e933182 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,7 @@ build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
+
+lib
+cache
+log
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..ba49d74
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,2 @@
+src
+node_modules
diff --git a/README.md b/README.md
index cbdd6bd..8955d1b 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,5 @@
# kcsp
+
+[](https://nodei.co/npm/kcsp/)
+
A simple proxy server for KanColle to avoid network problems.
diff --git a/bin/kcsp b/bin/kcsp
new file mode 100644
index 0000000..1a1e39a
--- /dev/null
+++ b/bin/kcsp
@@ -0,0 +1,39 @@
+#!/usr/bin/node
+
+var getopt = require('node-getopt');
+var env = require('../lib/env');
+
+var cmd = getopt.create([
+ ['c', 'cache-path=ARG', 'set cache database path'],
+ ['l', 'log-path=ARG', 'set log file path'],
+ ['p', 'path=ARG', 'set base path'],
+ ['v', 'version', 'show version']
+]).bindHelp().parseSystem();
+
+if (cmd.options['version']) {
+ console.log(env.APP_VERSION);
+ process.exit();
+}
+
+require('daemon')();
+
+var args = [];
+if (cmd.options['path']) {
+ args.push('--path=' + cmd.options['path']);
+}
+if (cmd.options['cache-path']) {
+ args.push('--cache-path=' + cmd.options['cache-path']);
+}
+if (cmd.options['log-path']) {
+ args.push('--log-path=' + cmd.options['log-path']);
+}
+
+process.title = 'kcsp monitor';
+
+var forever = require('forever-monitor');
+
+var child = new (forever.Monitor)(__dirname + '/../lib/cli.js', {
+ args: args
+});
+
+child.start();
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..bf92faa
--- /dev/null
+++ b/package.json
@@ -0,0 +1,46 @@
+{
+ "name": "kcsp",
+ "version": "0.4.0",
+ "description": "A simple proxy server for KanColle to avoid network problems.",
+ "bin": {
+ "kcsp": "./bin/kcsp"
+ },
+ "scripts": {
+ "compile": "babel -d lib/ src/",
+ "prepublish": "npm run compile"
+ },
+ "author": "Gizeta",
+ "license": "GPL-2.0",
+ "files": [
+ "bin",
+ "lib"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/Gizeta/kcsp.git"
+ },
+ "keywords": [
+ "kancolle",
+ "proxy"
+ ],
+ "bugs": {
+ "url": "https://github.com/Gizeta/kcsp/issues"
+ },
+ "homepage": "https://github.com/Gizeta/kcsp",
+ "dependencies": {
+ "babel-polyfill": "^6.3.14",
+ "daemon": "^1.1.0",
+ "forever-monitor": "^1.7.0",
+ "level": "^1.4.0",
+ "level-ttl": "^3.1.0",
+ "node-getopt": "^0.2.3",
+ "request": "^2.67.0",
+ "tracer": "^0.8.2"
+ },
+ "devDependencies": {
+ "babel-cli": "^6.4.0",
+ "babel-plugin-transform-object-rest-spread": "^6.3.13",
+ "babel-plugin-transform-regenerator": "^6.3.26",
+ "babel-preset-es2015": "^6.3.13"
+ }
+}
diff --git a/src/cli.js b/src/cli.js
new file mode 100644
index 0000000..86b1ad2
--- /dev/null
+++ b/src/cli.js
@@ -0,0 +1,7 @@
+import server from './server';
+import config from './config';
+import logger from './logger';
+import 'babel-polyfill';
+
+server.listen(config.port);
+logger.info('kcsp server started.');
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..134dbd3
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,20 @@
+import getopt from 'node-getopt';
+
+let cmd = getopt.create([
+ ['b', 'path=ARG', 'set base path'],
+ ['c', 'cache-path=ARG', 'set cache database path'],
+ ['l', 'log-path=ARG', 'set log file path'],
+ ['p', 'port=ARG', 'set web port']
+]).bindHelp().parseSystem();
+
+let basePath = cmd.options['path'] || __dirname + '/..';
+let logPath = cmd.options['log-path'] || (basePath + '/log');
+let cachePath = cmd.options['cache-path'] || (basePath + '/cache');
+let port = cmd.options['cache-path'] ? parseInt(cmd.options['cache-path']) : 8099;
+
+module.exports = {
+ basePath: basePath,
+ cachePath: cachePath,
+ logPath: logPath,
+ port: port
+};
diff --git a/src/db.js b/src/db.js
new file mode 100644
index 0000000..fdb5cb4
--- /dev/null
+++ b/src/db.js
@@ -0,0 +1,43 @@
+import levelup from 'level';
+import ttl from 'level-ttl';
+import config from './config';
+import logger from './logger';
+
+let db = levelup(config.cachePath);
+db = ttl(db, {
+ checkFrequency: 2 * 60 * 60 * 1000,
+ defaultTTL: 30 * 60 * 1000
+});
+
+export async function get(key) {
+ return new Promise((resolve, reject) => {
+ db.get(key, function(err, value) {
+ if (err) {
+ if (err.notFound) {
+ resolve();
+ return;
+ }
+ logger.error('database error, %s', err);
+ reject(err);
+ return;
+ }
+ resolve(value);
+ });
+ });
+}
+
+export function put(key, value) {
+ db.put(key, value, function(err) {
+ if (err) {
+ logger.error('database error, %s', err);
+ }
+ });
+}
+
+export function del(key) {
+ db.del(key, function(err) {
+ if (err) {
+ logger.error('database error, %s', err);
+ }
+ });
+}
diff --git a/src/env.js b/src/env.js
new file mode 100644
index 0000000..2c608b5
--- /dev/null
+++ b/src/env.js
@@ -0,0 +1,3 @@
+module.exports = {
+ APP_VERSION: '0.4.0'
+}
diff --git a/src/logger.js b/src/logger.js
new file mode 100644
index 0000000..2dbb5fe
--- /dev/null
+++ b/src/logger.js
@@ -0,0 +1,11 @@
+import tracer from 'tracer';
+import fs from 'fs';
+import config from './config';
+
+(function(){
+ if (!fs.existsSync(config.logPath)) {
+ fs.mkdirSync(config.logPath);
+ }
+})();
+
+module.exports = tracer.dailyfile({root: config.logPath});
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 0000000..45883f2
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,300 @@
+import http from 'http';
+import url from 'url';
+import request from 'request';
+import zlib from 'zlib';
+import logger from './logger';
+import * as db from './db';
+import env from './env';
+
+const responseError = {
+ 403: '
HTTP 403 - Forbidden
参数错误或无访问权限。',
+ 410: 'HTTP 410 - Gone
获取数据失败。',
+ 500: 'HTTP 500 - Internal Server Error
服务器内部执行过程中遇到错误。请向webmaster提交错误报告以解决问题。',
+ 503: 'HTTP 503 - Service Unavailable
暂未获取到数据。请稍后再试。'
+}
+
+const kcIpList = [
+ '203.104.209.7',
+ '203.104.209.71',
+ '125.6.184.15',
+ '125.6.184.16',
+ '125.6.187.205',
+ '125.6.187.229',
+ '125.6.187.253',
+ '125.6.188.25',
+ '203.104.248.135',
+ '125.6.189.7',
+ '125.6.189.39',
+ '125.6.189.71',
+ '125.6.189.103',
+ '125.6.189.135',
+ '125.6.189.167',
+ '125.6.189.215',
+ '125.6.189.247',
+ '203.104.209.23',
+ '203.104.209.39',
+ '203.104.209.55',
+ '203.104.209.102'
+];
+const kcCacheableApiList = [
+ '/kcsapi/api_start2'
+];
+
+let server = http.createServer((req, resp) => {
+ let chunks = [];
+ let chunkSize = 0;
+ req.params = {
+ ip: getIp(req),
+ requestPath: '',
+ postData: '',
+ requestTime: new Date().getTime()
+ };
+
+ req.on('data', chunk => {
+ chunks.push(chunk);
+ chunkSize += chunk.length;
+ });
+
+ req.on('end', async () => {
+ let data = null;
+ switch (chunks.length) {
+ case 0:
+ data = new Buffer(0);
+ break;
+ case 1:
+ data = chunks[0];
+ break;
+ default:
+ data = new Buffer(chunkSize);
+ for (var i = 0, pos = 0, l = chunks.length; i < l; i++) {
+ var chunk = chunks[i];
+ chunk.copy(data, pos);
+ pos += chunk.length;
+ }
+ break;
+ }
+ req.params.postData = data.toString();
+
+ logger.info(`${req.params.ip} requests ${req.url}`);
+
+ let locked = await db.get('lock');
+ if (locked === 'true') {
+ renderErrorPage(resp, 503);
+ logger.info(`send error 503 to ${req.params.ip}, handled in ${(new Date().getTime() - req.params.requestTime) / 1000}s`);
+ return;
+ }
+
+ if (!validateRequest(req)) {
+ renderErrorPage(resp, 403);
+ logger.info(`send error 403 to ${req.params.ip}, handled in ${(new Date().getTime() - req.params.requestTime) / 1000}s`);
+ return;
+ }
+
+ try {
+ let content = await processRequest(req);
+ renderContent(resp, {
+ ...content,
+ acceptEncoding: req.headers['accept-encoding'] || ''
+ });
+ logger.info(`response to ${req.params.ip}, handled in ${(new Date().getTime() - req.params.requestTime) / 1000}s`);
+ }
+ catch(err) {
+ let errCode = 500;
+ switch(err) {
+ case "unavailable":
+ errCode = 503;
+ break;
+ case "gone":
+ errCode = 410;
+ break;
+ case "forbidden":
+ errCode = 403;
+ break;
+ }
+ renderErrorPage(resp, errCode);
+ logger.info(`send error ${errCode} to ${req.params.ip}, handled in ${(new Date().getTime() - req.params.requestTime) / 1000}s`);
+ }
+ });
+});
+
+function validateRequest(req) {
+ if (req.method !== 'POST' ||
+ req.headers['request-uri'] == null ||
+ req.headers['cache-token'] == null)
+ return false;
+
+ let objUrl = url.parse(req.headers['request-uri']);
+ if (objUrl.pathname.startsWith('/kcsapi/') && kcIpList.indexOf(objUrl.hostname) >= 0) {
+ req.params.requestPath = objUrl.pathname;
+ return true;
+ }
+
+ return false;
+}
+
+async function processRequest(req) {
+ let cacheable = kcCacheableApiList.indexOf(req.params.requestPath) >= 0;
+ let cacheToken = cacheable ? req.params.requestPath : req.headers['cache-token'];
+
+ logger.info(`process request, user: ${req.params.ip}, token: ${cacheToken}`);
+
+ let data = await db.get(cacheToken);
+ if (data === '__REQUEST__') {
+ throw new Error('unavailable');
+ }
+ else if (data === '__BLOCK__') {
+ throw new Error('gone');
+ }
+ else if (data != null) {
+ /* maybe still need to post to KADOKAWA although api data can be cached like api_start2 */
+ /* not implement to avoid sending repeat request */
+
+ return {
+ statusCode: 200,
+ content: data
+ };
+ }
+ else {
+ return await postToRemote({
+ url: req.headers['request-uri'],
+ headers: filterHeaders(req.headers),
+ postData: req.params.postData,
+ cacheToken: cacheToken,
+ cacheable: cacheable
+ });
+ }
+}
+
+async function postToRemote(conn) {
+ logger.info(`requesting ${conn.url}`);
+
+ db.put(conn.cacheToken, '__REQUEST__');
+ return new Promise((resolve, reject) => {
+ request.post({
+ url: conn.url,
+ form: conn.postData,
+ headers: conn.headers,
+ timeout: 180000,
+ gzip: true
+ }, function(error, response, body) {
+ if (error) {
+ logger.error([
+ 'meet error during requesting.',
+ `error: ${error}`,
+ `url: ${conn.url}`,
+ `headers: ${JSON.stringify(conn.headers)}`,
+ `post: ${conn.postData}`
+ ].join('\n\t'));
+
+ if (conn.cacheable) {
+ reject(new Error('unavailable'));
+ }
+ else {
+ db.put(conn.cacheToken, '__BLOCK__');
+ reject(new Error('gone'));
+ }
+ return;
+ }
+
+ if (response.statusCode >= 400) {
+ logger.error([
+ 'remote server response error.',
+ `code: ${response.statusCode}`,
+ `url: ${conn.url}`,
+ `headers: ${JSON.stringify(conn.headers)}`,
+ `post: ${conn.postData}`,
+ `response: ${body}`
+ ].join('\n\t'));
+
+ if (conn.cacheable) {
+ reject(new Error('unavailable'));
+ }
+ else {
+ db.put(conn.cacheToken, body);
+ resolve({
+ statusCode: response.statusCode,
+ content: body
+ });
+ }
+ return;
+ }
+
+ logger.info(`remote server responsed, code: ${response.statusCode}`);
+ db.put(conn.cacheToken, body);
+ resolve({
+ statusCode: response.statusCode,
+ content: body
+ });
+ });
+ });
+}
+
+function filterHeaders(data) {
+ var headers = {};
+ for (var key in data) {
+ if (key !== 'host' &&
+ key !== 'expect' &&
+ key !== 'connection' &&
+ key !== 'proxy-connection' &&
+ key !== 'content-length' &&
+ key !== 'cache-token' &&
+ key !== 'request-uri') {
+ headers[key] = data[key];
+ }
+ }
+
+ return headers;
+}
+
+function getIp(req) {
+ return req.headers['x-forwarded-for'] ||
+ req.connection.remoteAddress ||
+ req.socket.remoteAddress ||
+ req.connection.socket.remoteAddress;
+}
+
+function renderContent(resp, data) {
+ if (data.acceptEncoding.indexOf('gzip') >= 0) {
+ zlib.gzip(data.content, function(err, result) {
+ if (err) {
+ logger.error([
+ 'meet error during compressing.',
+ `error: ${err}`,
+ `content: ${data.content}`
+ ].join('\n\t'));
+ throw new Error(err);
+ }
+
+ resp.writeHead(data.statusCode, {'content-type': 'text/plain', 'content-encoding': 'gzip'});
+ resp.end(result);
+ });
+ }
+ else if (data.acceptEncoding.indexOf('deflate') >= 0) {
+ zlib.deflate(data.content, function(err, result) {
+ if (err) {
+ logger.error([
+ 'meet error during compressing.',
+ `error: ${err}`,
+ `content: ${data.content}`
+ ].join('\n\t'));
+ throw new Error(err);
+ }
+
+ resp.writeHead(data.statusCode, {'content-type': 'text/plain', 'content-encoding': 'deflate'});
+ resp.end(result);
+ });
+ }
+ else {
+ resp.writeHead(data.statusCode, {'content-type': 'text/plain'});
+ resp.end(result);
+ }
+}
+
+function renderErrorPage(resp, code) {
+ resp.writeHead(code, {'content-type': 'text/html'});
+ resp.write('');
+ resp.write(responseError[code]);
+ resp.end('
Powered by KCSP Server/' + env.APP_VERSION + '');
+}
+
+export default server;