diff --git a/app.js b/app.js new file mode 100644 index 0000000..18645df --- /dev/null +++ b/app.js @@ -0,0 +1,77 @@ +const redis = require('redis'); +const validUrl = require('valid-url'); +const express = require('express'); + +const app = express(); +app.use(express.json()); +const port = 3000; + +//redis client +const redisCli = redis.createClient({ + host: 'redis', + port: 6379 +}) +redisCli.on("error", console.error); + +//validation +const outpath = ['sym', '', 'manifest.json', 'admin', 'stats', 'api', 'swagger']; +const validateKey = key => !outpath.find(path=>path===key); + +//random key +const getKey = length => { + let forbidden = false; + let key = ''; + let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + do{ + for (let i = 0; i < length; i++ ) + key += characters.charAt(Math.floor(Math.random() * characters.length)); + forbidden = !validateKey(key); + redisCli.hget('surl;'+key, 'url', (err, obj) => { + if(err) console.error(err); + if(obj) forbidden = true; + }); + } while (forbidden); + return key; +} + +const keyLength = 4; + +//validate url +const validateUrl = url => validUrl.isUri(url); + +//express server +app.put('/api/v1/key', async (req, res) => { + if (req.body.url === undefined){ + res.status(400); + res.send({status: 400, message: 'url is undefined'}); + return + } + if (!validateUrl(req.body.url)){ + res.status(400); + res.send({status: 400, message: 'url is invalid'}); + return + } + try{ + let key = getKey(keyLength); + await redisCli.HMSET('surl;'+key, 'url', req.body.url, 'time', Date.now()); + res.status(201); + res.send({status: 201, url: req.body.url, key}); + }catch (e){ + console.error(e); + res.status(500); + res.send({status: 500, message: 'server error'}); + } +}); + +app.get('/:key', (req, res) => { + redisCli.HGET('surl;'+req.params.key, 'url', (err, obj) => { + if(obj){ + res.redirect(302, obj); + }else{ + res.status(404); + res.send({status: 404, message: 'this short url does not exist'}); + } + }); +}); + +app.listen(port); diff --git a/docker-compose.yml b/docker-compose.yml index 72ff25b..4f65015 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,26 +1,25 @@ version: '3' services: - node: - image: node - container_name: surl_node + app: + image: node:alpine restart: always ports: - - "8080:8080" - - "8081:8081" + - "3000:3000" volumes: - - ./node_app/:/home/node/app/ - # - ./node_modules/:/home/node/app/node_modules/ - - ./node_logs/:/var/log/ + - ./:/home/node/app/ + - ./logs/:/var/log/ working_dir: /home/node/app/ environment: - NODE_ENV=production - command: sh -c 'npm i && nodejs index.js' + command: sh -c 'npm i && nodejs app.js' links: - redis + depends_on: + - redis + redis: image: redis - container_name: surl_redis expose: - 6379 restart: always @@ -29,3 +28,16 @@ services: - ./redis_conf/:/usr/local/etc/redis/ environment: - REDIS_REPLICATION_MODE=master + + nginx: + image: nginx:alpine + restart: always + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + - ./public:/var/www/html + ports: + - "8080:8080" + links: + - app + depends_on: + - app diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..585ee99 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,102 @@ +worker_processes auto; +worker_cpu_affinity auto; + +error_log /var/log/nginx/error.log; +pid /var/run/nginx.pid; + +#daemon off; + +events { + worker_connections 1024; +} + +http { +# rewrite_log on; + + include mime.types; + default_type application/json; + access_log /var/log/nginx/access.log; + sendfile on; +# tcp_nopush on; + keepalive_timeout 3; +# tcp_nodelay on; +# gzip on; + + client_max_body_size 1m; + + upstream app { + server app:3000; + } + + server { + listen 8080 default_server; + server_name _; + + error_page 400 = @400; + location @400 { return 400 '{"status":400,"message":"Bad request"}\n'; } + + error_page 401 = @401; + location @401 { return 401 '{"status":401,"message":"Unauthorized"}\n'; } + + error_page 403 = @403; + location @403 { return 403 '{"status":403,"message":"Forbidden"}\n'; } + + error_page 404 = @404; + location @404 { return 404 '{"status":404,"message":"Resource not found"}\n'; } + + error_page 405 = @405; + location @405 { return 405 '{"status":405,"message":"Method not allowed"}\n'; } + + error_page 408 = @408; + location @408 { return 408 '{"status":408,"message":"Request timeout"}\n'; } + + error_page 413 = @413; + location @413 { return 413 '{"status":413,"message":"Payload too large"}\n'; } + + error_page 414 = @414; + location @414 { return 414 '{"status":414,"message":"Request URI too large"}\n'; } + + error_page 415 = @415; + location @415 { return 415 '{"status":415,"message":"Unsupported media type"}\n'; } + + error_page 426 = @426; + location @426 { return 426 '{"status":426,"message":"HTTP request was sent to HTTPS port"}\n'; } + + error_page 429 = @429; + location @429 { return 429 '{"status":429,"message":"API rate limit exceeded"}\n'; } + + error_page 495 = @495; + location @495 { return 495 '{"status":495,"message":"Client certificate authentication error"}\n'; } + + error_page 496 = @496; + location @496 { return 496 '{"status":496,"message":"Client certificate not presented"}\n'; } + + error_page 497 = @497; + location @497 { return 497 '{"status":497,"message":"HTTP request was sent to mutual TLS port"}\n'; } + + error_page 500 = @500; + location @500 { return 500 '{"status":500,"message":"Server error"}\n'; } + + error_page 501 = @501; + location @501 { return 501 '{"status":501,"message":"Not implemented"}\n'; } + + error_page 502 = @502; + location @502 { return 502 '{"status":502,"message":"Bad gateway"}\n'; } + + location @api { + default_type application/json; + proxy_pass http://app; + } + + location /api { + default_type application/json; + proxy_pass http://app; + } + + location / { + index index.html; + root /var/www/html; + try_files $uri $uri/ @api; + } + } +} \ No newline at end of file diff --git a/node_app/index.js b/node_app/index.js deleted file mode 100644 index 384f212..0000000 --- a/node_app/index.js +++ /dev/null @@ -1,133 +0,0 @@ -const http = require('http') -const url = require('url') -const fs = require('fs') -const ws = require('ws') -const redis = require('redis') -const mime = require('mime') -const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest - -const host = 'http://127.0.0.1:8080/' -const outpath = ['sym', '', 'manifest.json', 'admin', 'stats'] - -//redis client -const redis_cli = redis.createClient({ - host: 'redis', - port: 6379 -}) -redis_cli.on("error", function (error) { - console.error(error) -}) - -//HTTP server -http.createServer(function (req, res) { - const q = url.parse(req.url, true); - let filename = "./public" + q.pathname; - let path_split = q.pathname.split("/", 3); - if (path_split[path_split.length - 1] === "") filename += "/index.html"; - let file_type = mime.getType(filename) - if (function valid_path(){ - //outpath.forEach( path => {if (request_path === path) return true}) - for (let i = 0; i < outpath.length; i++) if (path_split[1] === outpath[i]) return true - return false - }() === true){ - fs.readFile(filename, function(err, data) { - if (err) { - res.writeHead(404, {'Content-Type': "text/html"}); - return res.end("404 Not Found"); - } - res.writeHead(200, {'Content-Type': file_type}); - res.write(data); - return res.end(); - }) - }else{ - redis_cli.hget("surl;"+q.pathname.split("/", 2)[1], "url", function(err, obj) { - if(err) console.log(err); - if(obj){ - res.writeHead(302, {'Location': obj}); - return res.end(); - }else{ - res.writeHead(404, {'Content-Type': "text/html"}); - return res.end("404 this short-url does not exist :/"); - } - }); - } -}).listen(8080); - -//WS server -const wss = new ws.Server({ - port: 8081, - perMessageDeflate: { - zlibDeflateOptions: { - // See zlib defaults. - chunkSize: 1024, - memLevel: 7, - level: 3 - }, - zlibInflateOptions: { - chunkSize: 10 * 1024 - }, - // Other options settable: - clientNoContextTakeover: true, // Defaults to negotiated value. - serverNoContextTakeover: true, // Defaults to negotiated value. - serverMaxWindowBits: 10, // Defaults to negotiated value. - // Below options specified as default values. - concurrencyLimit: 10, // Limits zlib concurrency for perf. - threshold: 1024 // Size (in bytes) below which messages - // should not be compressed. - } -}); - -//WS handler -wss.on('connection', ws => { - ws.on('message', message => { - console.log(`Received message => ${message}`) - let msg = `${message}`.split(";", 2) - if (msg[0] === 'long_url') { - if (msg[1] === '') ws.send('error;url is empty') - else if (msg[1].length > 2000) ws.send('error;your url is too long') - else{ - let xhr = new XMLHttpRequest(); - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - switch(xhr.status) { - case 200: - let ran_key = get_key(4) - //write to redis - redis_cli.hmset("surl;"+ran_key, "url", msg[1], "time", Date.now()) - ws.send('surl;'+host+ran_key) - console.log(ran_key+' --> '+msg[1]) - break; - case 404: ws.send('error;site does not exist (404)');break; - case 502: ws.send('error;remote server error (502)');break; - case 500: ws.send('error;remote server error (500)');break; - case 503: ws.send('error;remote server error (503)');break; - default: ws.send('error;no valid url');break; - } - } - } - xhr.open('GET', msg[1], true); - xhr.timeout = 1000; - xhr.ontimeout = function(e) {ws.send('error;url is to slow')} - xhr.send(); - setTimeout(to => {if (xhr.readyState !== 4) ws.send('error;url timed out');xhr.abort()}, 1500) - } - } - }) - ws.send('websocket connected') -}) - -//random key -function get_key(length) { - let forbidden = false; let output = '' - let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' - do{ - for (let i = 0; i < length; i++ ) - output += characters.charAt(Math.floor(Math.random() * characters.length)) - for (let i = 0; i < output.length; i++) if (output === outpath[i]) forbidden = true - redis_cli.hget("surl;"+output, "url", function(err, obj) { - if(err) console.log(err) - if(obj) forbidden = true - }) - } while (forbidden) - return output -} diff --git a/node_app/package.json b/package.json similarity index 75% rename from node_app/package.json rename to package.json index 9032c0f..3f51a64 100644 --- a/node_app/package.json +++ b/package.json @@ -2,8 +2,10 @@ "name": "surl", "version": "1.1.0", "dependencies": { + "express": "^4.17.1", "mime": "^2.4.6", "redis": "^3.0.2", + "valid-url": "^1.0.9", "ws": "^7.3.1", "xmlhttprequest": "^1.8.0" } diff --git a/node_app/public/index.html b/public/index.html similarity index 67% rename from node_app/public/index.html rename to public/index.html index 692ce5f..0e02373 100644 --- a/node_app/public/index.html +++ b/public/index.html @@ -8,42 +8,39 @@ - - + + @@ -75,13 +72,13 @@
- +
- +
diff --git a/node_app/public/manifest.json b/public/manifest.json similarity index 100% rename from node_app/public/manifest.json rename to public/manifest.json diff --git a/node_app/public/sym/dark.css b/public/sym/dark.css similarity index 100% rename from node_app/public/sym/dark.css rename to public/sym/dark.css diff --git a/node_app/public/sym/ic_close_white_24px.svg b/public/sym/ic_close_white_24px.svg similarity index 100% rename from node_app/public/sym/ic_close_white_24px.svg rename to public/sym/ic_close_white_24px.svg diff --git a/node_app/public/sym/script.js b/public/sym/script.js similarity index 100% rename from node_app/public/sym/script.js rename to public/sym/script.js