Browse Source

update websocket api to rest api

master
adb 7 months ago
parent
commit
7caef4ed1a
  1. 77
      app.js
  2. 32
      docker-compose.yml
  3. 102
      nginx.conf
  4. 133
      node_app/index.js
  5. 2
      package.json
  6. 55
      public/index.html
  7. 0
      public/manifest.json
  8. 0
      public/sym/dark.css
  9. 0
      public/sym/ic_close_white_24px.svg
  10. 0
      public/sym/script.js

77
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);

32
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

102
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;
}
}
}

133
node_app/index.js

@ -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
}

2
node_app/package.json → 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"
}

55
node_app/public/index.html → public/index.html

@ -8,42 +8,39 @@
<meta name="description" content="short your url"/>
<meta name='viewport' content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0'/>
<link rel="stylesheet" type="text/css" href="./sym/dark.css"/>
<script src="./sym/script.js"></script>
<link rel="stylesheet" type="text/css" href="sym/dark.css"/>
<script src="sym/script.js"></script>
<script>
const wsurl = 'ws://127.0.0.1:8081'
const socket = new WebSocket(wsurl)
socket.onopen = () => {
socket.send('new session')
}
socket.onerror = (error) => {
console.log(`WebSocket error: ${error}`)
}
socket.onclose = error => show_error('session timed out (refresh)')
socket.onmessage = (e) => {
console.log(e.data)
let msg = e.data.split(";", 2)
if (msg[0] === 'error') show_error(msg[1])
else if (msg[0] === 'surl') {
document.getElementById('surl-input').value = msg[1]
document.getElementById('surl-popup').style.display = 'block'
}
}
function copy_url() {
document.getElementById('surl-input').select()
document.execCommand("copy")
}
//document.surl_form.action = surl_submit()
function lurl_submit() {
socket.send('long_url;'+document.getElementById('longurl-input').value)
fetch('/api/v1/key', {
method: 'PUT',
body: JSON.stringify({ url: document.getElementById('longurl-input').value }),
headers: {
'content-type': 'application/json'
}
}).then(async res => {
let data = await res.json();
console.log(data)
if (data?.url) show_surl(`${location.host}/${data.key}`);
else if (data?.status && data?.message) show_error(data.message);
else show_error('unknown error');
}).catch(() => show_error('request failed'));
}
function show_surl(surl) {
document.getElementById('surl-input').value = surl;
document.getElementById('surl-popup').style.display = 'block';
}
function show_error(msg) {
let error_style = document.getElementById('error-box').style
document.getElementById('error-message').innerText = msg
error_style.display = "block"
error_style.animation = "slide-from-left alternate 0.2s"
setTimeout(to => {error_style.animation = ""}, 200)
let error_style = document.getElementById('error-box').style;
document.getElementById('error-message').innerText = msg;
error_style.display = "block";
error_style.animation = "slide-from-left alternate 0.2s";
setTimeout(() => {error_style.animation = ""}, 200);
}
</script>
</head>
@ -75,13 +72,13 @@
</div>
</div>
<div onclick="this.parentNode.style.display = 'none'" style="position: absolute; top:5px; right: 5px;" class="sym_btn-invisible">
<img class="icon" src="./sym/ic_close_white_24px.svg" />
<img class="icon" src="sym/ic_close_white_24px.svg" />
</div>
</div>
<div id="error-box" class="error">
<div onclick="this.parentNode.style.display = 'none'" style="position: absolute; top:5px; right: 5px;" class="sym_btn-invisible">
<img class="icon" src="./sym/ic_close_white_24px.svg">
<img class="icon" src="sym/ic_close_white_24px.svg">
</div>
<div id="error-message" class="btn-text">
</div>

0
node_app/public/manifest.json → public/manifest.json

0
node_app/public/sym/dark.css → public/sym/dark.css

0
node_app/public/sym/ic_close_white_24px.svg → public/sym/ic_close_white_24px.svg

Before

Width:  |  Height:  |  Size: 265 B

After

Width:  |  Height:  |  Size: 265 B

0
node_app/public/sym/script.js → public/sym/script.js

Loading…
Cancel
Save