فهرست منبع

Refs #3 link stats

isundil 2 سال پیش
والد
کامیت
584b4278e8
7فایلهای تغییر یافته به همراه174 افزوده شده و 3 حذف شده
  1. 70 0
      models/access.js
  2. 2 0
      package.json
  3. 24 2
      router/input.js
  4. 2 1
      src/databaseHelper.js
  5. 25 0
      src/security.js
  6. 37 0
      static/public/js/stats.js
  7. 14 0
      templates/stats.js

+ 70 - 0
models/access.js

@@ -0,0 +1,70 @@
+
+const DatabaseModel = require("./DatabaseModel.js").DatabaseModel;
+const Security = require('../src/security.js');
+
+function AccessModel(privId, publicId, ipAddress) {
+    this.accessTime = new Date();
+    this.publicId = publicId;
+    this.privId = privId;
+    this.reverseIp = null;
+    this.ipAddress = ipAddress;
+    this.ipRegion = null;
+}
+
+Object.setPrototypeOf(AccessModel.prototype, DatabaseModel.prototype);
+
+AccessModel.prototype.resolveIp = async function() {
+    try {
+        this.ipRegion = Security.geoIp(this.ipAddress);
+    } catch (err) {
+        console.error("AccessModel::resolveIp: Cannot geolocalize ip " +this.ipAddress +": ", err);
+        this.ipRegion = "{}";
+    }
+    try {
+        this.reverseIp = await Security.reverseDns(this.ipAddress);
+    } catch (err) {
+        console.error("AccessModel::resolveIp: Cannot reverse ip " +this.ipAddress +": ", err);
+        this.reverseIp = "";
+    }
+}
+
+AccessModel.prototype.getTableName = function() {
+    return "access";
+}
+
+AccessModel.prototype.createOrUpdateBase = async function(dbHelper) {
+    await dbHelper.runSql(`CREATE TABLE IF NOT EXISTS 'access' (
+        privId string not null,
+        publicId string not null,
+        accessTime datetime not null,
+        ipAddress string not null,
+        reverseIp string,
+        ipRegion string,
+
+        PRIMARY KEY(publicId, accessTime, ipAddress),
+        FOREIGN KEY (privId) REFERENCES pasteContent(privId)
+        )`);
+}
+
+AccessModel.prototype.describe = function() {
+    return {
+        "privId": this.privId,
+        "publicId": this.publicId,
+        "accessTime": this.accessTime.getTime(),
+        "ipAddress": this.ipAddress,
+        "reverseIp": this.reverseIp,
+        "ipRegion": JSON.stringify(this.ipRegion)
+    };
+}
+
+AccessModel.prototype.fromDb = function(dbObj) {
+    this.privId = dbObj['privId'];
+    this.publicId = dbObj['publicId'];
+    this.accessTime = new Date(dbObj['accessTime']);
+    this.ipAddress = dbObj['ipAddress'];
+    this.reverseIp = dbObj['reverseIp'];
+    this.ipRegion = JSON.parse(dbObj['ipRegion']);
+}
+
+module.exports.AccessModel = AccessModel;
+

+ 2 - 0
package.json

@@ -1,6 +1,8 @@
 {
   "dependencies": {
     "@mdi/font": "^7.1.96",
+    "dns": "^0.2.2",
+    "geoip-lite": "^1.4.7",
     "mime-types": "^2.1.35",
     "node-simple-router": "^0.10.2",
     "request": "^2.88.2",

+ 24 - 2
router/input.js

@@ -4,11 +4,22 @@ const fs = require('fs');
 
 const ApiKeyModel = require('../models/apiKey.js').ApiKeyModel;
 const PasteContent = require('../models/pasteContent.js').PasteContent;
+const AccessModel = require('../models/access.js').AccessModel;
 const mCrypto = require('../src/crypto.js');
 const Security = require('../src/security.js');
 const CONFIG = require('../src/config.js');
 
+async function onAccessContent(app, req, entity) {
+    if (entity.privId !== req.params.id) {
+        //FIXME ?rel
+        let accessEntry = new AccessModel(entity.privId, req.params.id, Security.getRequestIp(req));
+        await accessEntry.resolveIp();
+        await app.databaseHelper.insertOne(accessEntry);
+    }
+}
+
 async function renderRawPage(app, req, res, entity) {
+    await onAccessContent(app, req, entity);
     if (entity.type === 'paste')
         return await app.routerUtils.staticServe(res, app.getData(entity.privId));
     else if (entity.type === 'file') {
@@ -21,6 +32,7 @@ async function renderRawPage(app, req, res, entity) {
 }
 
 async function renderPublicPage(app, req, res, entity) {
+    await onAccessContent(app, req, entity);
     if (entity.type === 'paste')
         return await app.routerUtils.staticServe(res, app.getData(entity.privId));
     else if (entity.type === 'file') {
@@ -33,10 +45,20 @@ async function renderPublicPage(app, req, res, entity) {
     app.routerUtils.onInternalError(res, "Unknown type: " +entity.type);
 }
 
-function renderPrivatePage(app, res, entity) {
+async function _readAccess(app, entityId) {
+    return (await app.databaseHelper.fetch(AccessModel, { privId: entityId }))
+            .map(x => x.describe())
+            .map(x => { delete x.ipAddress; delete x.privId; x.ipRegion = JSON.parse(x.ipRegion); return x; });
+}
+
+async function renderPrivatePage(app, res, entity) {
     let stat;
     try { stat = fs.statSync(app.dataDir+entity.privId); } catch (e) { stat = { error: e }; }
-    app.routerUtils.jsonResponse(res, { ...entity.describe(), ...stat, ...{ path: app.getData(entity.privId) } });
+    const access = await _readAccess(app, entity.privId);
+
+    let context = app.routerUtils.commonRenderInfos();
+    context.page_title += " - Pastit";
+    res.end(whiskers.render(require('../templates/stats.js'), { ...context, ...{size: stat.size}, ...{ access: JSON.stringify(access) }}));
 }
 
 module.exports = { register: app => {

+ 2 - 1
src/databaseHelper.js

@@ -4,6 +4,7 @@ const CONFIG = require('./config.js');
 
 const PasteContentModel = require('../models/pasteContent.js').PasteContent;
 const ApiKeyModel = require('../models/apiKey.js').ApiKeyModel;
+const AccessModel = require('../models/access.js').AccessModel;
 
 function DatabaseHelper() {
     this.db = null;
@@ -16,7 +17,7 @@ DatabaseHelper.prototype.init = function() {
                 ko(err);
                 return;
             }
-            let types = [ PasteContentModel, ApiKeyModel ];
+            let types = [ PasteContentModel, ApiKeyModel, AccessModel ];
             for (let i =0; i < types.length; ++i) {
                 let instance = new types[i]();
                 await instance.createOrUpdateBase(this);

+ 25 - 0
src/security.js

@@ -1,5 +1,7 @@
 
 const request = require('request');
+const dns = require('dns');
+const geoip = require('geoip-lite');
 
 const CONFIG = require('./config.js');
 
@@ -33,3 +35,26 @@ function captchaCheck(captcha, remoteIp) {
 };
 
 module.exports.captchaCheck = captchaCheck;
+module.exports.reverseDns = function(ip) {
+    return new Promise((ok, ko) => {
+        dns.reverse(ip, (err, addr) => {
+            if (err || !addr || !addr[0]) {
+                ko(err);
+                return;
+            }
+            ok(addr[0]);
+        });
+    });
+}
+
+module.exports.geoip = function(ip) {
+    const geoipResponse = geoip.lookup(ip);
+    return {
+        ll: geoipResponse.ll,
+        regionFull: `${geoipResponse.country}/${geoipResponse.region}`,
+        country: geoipResponse.country,
+        region: geoipResponse.region,
+        city: geoipResponse.city
+    };
+}
+

+ 37 - 0
static/public/js/stats.js

@@ -0,0 +1,37 @@
+
+(()=>{
+    const NOW = Date.now();
+    let tmp = new Date();
+    tmp.setHours(0);
+    tmp.setMinutes(0);
+    tmp.setSeconds(0);
+    let today = tmp.getTime();
+    if (tmp.getDay() === 0)
+        tmp.setDate(tmp.getDate() -7);
+    if (tmp.getDay() !== 1)
+        tmp.setDate(tmp.getDate()-tmp.getDay() +1);
+    const ONE_WEEK = tmp.getTime();
+    console.log(tmp);
+    tmp.setTime(today);
+    tmp.setDate(1);
+    const ONE_MONTH = tmp.getTime();
+    console.log(tmp);
+
+    for (let i of ["All"].concat(Object.keys(ACCESS.reduce((acc, i) => { acc[i.publicId] = true; return acc; }, {})).sort())) {
+        let node = document.createElement("option");
+        node.textContent = i;
+        document.getElementById("relSource").appendChild(node);
+    }
+    function selectSource(source) {
+        let _access = source === "All" ? ACCESS : ACCESS.filter(i => i.publicId === source);
+        let lastAccess = _access.reduce((prev, i) => (prev && prev.accessTime > i.accessTime) ? prev : i, null)
+        document.getElementById("size").textContent = FILE_SIZE +'o';
+        document.getElementById('totalAccessCount').textContent = _access.length;
+        document.getElementById('weekAccess').textContent = _access.reduce((acc, i) => acc + (i.accessTime >= NOW -ONE_WEEK ? 1 : 0), 0);
+        document.getElementById('monthAccess').textContent = _access.reduce((acc, i) => acc + (i.accessTime >= NOW -ONE_MONTH ? 1 : 0), 0);
+        document.getElementById('lastAccess').textContent = new Date(lastAccess?.accessTime);
+    }
+    selectSource(document.getElementById("relSource").selectedOptions?.[0]?.textContent || "All");
+    document.getElementById("relSource").addEventListener("change", () => selectSource(document.getElementById("relSource").selectedOptions?.[0]?.textContent || "All"));
+})();
+

+ 14 - 0
templates/stats.js

@@ -0,0 +1,14 @@
+module.exports = require('./header.js') +`
+<select id="relSource"></select>
+<table>
+<tr><td>Size</td><td><span id="size"></span></td>
+<tr><td>Last Access: </td><td><span id="lastAccess"></span></td></tr>
+<tr><td>Access this week: </td><td><span id="weekAccess"></span></td></tr>
+<tr><td>Access this month: </td><td><span id="monthAccess"></span></td></tr>
+<tr><td>Total Access Count: </td><td><span id="totalAccessCount"></span></td></tr>
+</table>
+<script>
+const ACCESS=JSON.parse('{access}');
+const FILE_SIZE={size};
+</script><script src="/public/js/stats.js"></script>
+` + require('./footer.js');