kopia lustrzana https://github.com/OpenBuilds/OpenBuilds-CONTROL
SSL Implementation
rodzic
32b1220be4
commit
2d85c9878f
|
@ -3,6 +3,7 @@
|
||||||
#electron dist
|
#electron dist
|
||||||
dist
|
dist
|
||||||
upload
|
upload
|
||||||
|
ssl
|
||||||
|
|
||||||
# Compiled source #
|
# Compiled source #
|
||||||
###################
|
###################
|
||||||
|
|
146
index.js
146
index.js
|
@ -1,12 +1,48 @@
|
||||||
console.log("Starting OpenBuilds Machine Driver v" + require('./package').version)
|
console.log("Starting OpenBuilds Machine Driver v" + require('./package').version)
|
||||||
|
|
||||||
|
var config = {};
|
||||||
|
config.webPort = process.env.WEB_PORT || 3000;
|
||||||
|
config.posDecimals = process.env.DRO_DECIMALS || 2;
|
||||||
|
config.grblWaitTime = 1;
|
||||||
|
config.firmwareWaitTime = 4;
|
||||||
|
|
||||||
|
var express = require("express");
|
||||||
|
var app = express();
|
||||||
|
var http = require("http").Server(app);
|
||||||
|
var https = require('https');
|
||||||
|
|
||||||
|
// var io = require("socket.io")(https);
|
||||||
|
|
||||||
|
var ioServer = require('socket.io');
|
||||||
|
var io = new ioServer();
|
||||||
|
// var oneIo = io.listen(https);
|
||||||
|
// var anotherIo = io.listen(https);
|
||||||
|
|
||||||
|
var fs = require('fs');
|
||||||
|
var httpsOptions = {
|
||||||
|
key: fs.readFileSync('ssl/localhost.key'),
|
||||||
|
cert: fs.readFileSync('ssl/localhost.cer')
|
||||||
|
};
|
||||||
|
|
||||||
|
const httpsserver = https.createServer(httpsOptions, app).listen(3001, function() {
|
||||||
|
console.log('https: listening on:' + ip.address() + ":3001");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const httpserver = http.listen(config.webPort, '0.0.0.0', function() {
|
||||||
|
console.log('http: listening on:' + ip.address() + ":" + config.webPort);
|
||||||
|
// Now refresh library
|
||||||
|
refreshGcodeLibrary();
|
||||||
|
});
|
||||||
|
|
||||||
|
io.attach(httpserver);
|
||||||
|
io.attach(httpsserver);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const grblStrings = require("./grblStrings.js");
|
const grblStrings = require("./grblStrings.js");
|
||||||
var path = require("path");
|
var path = require("path");
|
||||||
const join = require('path').join;
|
const join = require('path').join;
|
||||||
var express = require("express");
|
|
||||||
var app = express();
|
|
||||||
var http = require("http").Server(app);
|
|
||||||
var io = require("socket.io")(http);
|
|
||||||
const serialport = require('serialport');
|
const serialport = require('serialport');
|
||||||
var SerialPort = serialport;
|
var SerialPort = serialport;
|
||||||
var md5 = require('md5');
|
var md5 = require('md5');
|
||||||
|
@ -26,7 +62,32 @@ var colors = {
|
||||||
var width = 250;
|
var width = 250;
|
||||||
var height = 200;
|
var height = 200;
|
||||||
|
|
||||||
var uploadsDir = __dirname + '/upload';
|
// Electron app
|
||||||
|
const electron = require('electron');
|
||||||
|
// Module to control application life.
|
||||||
|
const electronApp = electron.app;
|
||||||
|
|
||||||
|
console.log("Local User Data: " + electronApp.getPath('userData'))
|
||||||
|
|
||||||
|
const BrowserWindow = electron.BrowserWindow;
|
||||||
|
const Tray = electron.Tray;
|
||||||
|
const nativeImage = require('electron').nativeImage
|
||||||
|
const Menu = require('electron').Menu
|
||||||
|
// Keep a global reference of the window object, if you don't, the window will
|
||||||
|
// be closed automatically when the JavaScript object is garbage collected.
|
||||||
|
var appIcon = null,
|
||||||
|
jogWindow = null,
|
||||||
|
mainWindow = null
|
||||||
|
|
||||||
|
var uploadsDir = electronApp.getPath('userData') + '/upload/';
|
||||||
|
|
||||||
|
fs.existsSync(uploadsDir) || fs.mkdirSync(uploadsDir)
|
||||||
|
// fs.mkdir(uploadsDir, err => {
|
||||||
|
// if (err && err.code != 'EEXIST') throw 'up'
|
||||||
|
// // already exists
|
||||||
|
// })
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var oldportslist;
|
var oldportslist;
|
||||||
const iconPath = path.join(__dirname, 'app/icon.png');
|
const iconPath = path.join(__dirname, 'app/icon.png');
|
||||||
|
@ -38,15 +99,6 @@ const iconAlarm = path.join(__dirname, 'app/icon-bell.png');
|
||||||
|
|
||||||
|
|
||||||
var iosocket;
|
var iosocket;
|
||||||
|
|
||||||
var config = {};
|
|
||||||
config.webPort = process.env.WEB_PORT || 3000;
|
|
||||||
config.serverVersion = "0.0.1";
|
|
||||||
config.apiVersion = "0.0.1";
|
|
||||||
config.posDecimals = process.env.DRO_DECIMALS || 2;
|
|
||||||
config.grblWaitTime = 1;
|
|
||||||
config.firmwareWaitTime = 4;
|
|
||||||
|
|
||||||
var isAlarmed = false;
|
var isAlarmed = false;
|
||||||
var lastmd5sum = '00000000000000000000000000000000'
|
var lastmd5sum = '00000000000000000000000000000000'
|
||||||
var lastGcode = []
|
var lastGcode = []
|
||||||
|
@ -269,14 +321,11 @@ var status = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// if (!fs.existsSync(uploadsDir)) {
|
|
||||||
// fs.mkdirSync(uploadsDir);
|
|
||||||
// }
|
|
||||||
|
|
||||||
function refreshGcodeLibrary() {
|
function refreshGcodeLibrary() {
|
||||||
|
if (fs.existsSync(uploadsDir)) {
|
||||||
const dirTree = require('directory-tree');
|
const dirTree = require('directory-tree');
|
||||||
|
|
||||||
var tree = dirTree('./upload', {
|
var tree = dirTree(uploadsDir, {
|
||||||
extensions: /\.gcode|\.nc|\.tap|\.cnc|\.gc|\.g-code$/
|
extensions: /\.gcode|\.nc|\.tap|\.cnc|\.gc|\.g-code$/
|
||||||
}, (item, PATH) => {
|
}, (item, PATH) => {
|
||||||
// if a gcode is found, then
|
// if a gcode is found, then
|
||||||
|
@ -284,12 +333,13 @@ function refreshGcodeLibrary() {
|
||||||
ConvertGCODEtoPNG(item.path, item.path + ".png")
|
ConvertGCODEtoPNG(item.path, item.path + ".png")
|
||||||
});
|
});
|
||||||
// console.log("---------------")
|
// console.log("---------------")
|
||||||
var tree = dirTree('./upload', {
|
var tree = dirTree(uploadsDir, {
|
||||||
extensions: /\.gcode|\.png/
|
extensions: /\.gcode|\.png/
|
||||||
});
|
});
|
||||||
var treeData = JSON.stringify(tree, null, 2)
|
var treeData = JSON.stringify(tree, null, 2)
|
||||||
// console.log(treeData);
|
// console.log(treeData);
|
||||||
fs.writeFileSync(join(__dirname, 'upload/data.json'), treeData, 'utf-8')
|
fs.writeFileSync(join(uploadsDir + '/data.json'), treeData, 'utf-8')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ConvertGCODEtoPNG(file, out) {
|
function ConvertGCODEtoPNG(file, out) {
|
||||||
|
@ -401,12 +451,12 @@ app.post('/upload', function(req, res) {
|
||||||
// }));
|
// }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// form.on('fileBegin', function(name, file) {
|
form.on('fileBegin', function(name, file) {
|
||||||
// // Emitted whenever a new file is detected in the upload stream. Use this event if you want to stream the file to somewhere else while buffering the upload on the file system.
|
// Emitted whenever a new file is detected in the upload stream. Use this event if you want to stream the file to somewhere else while buffering the upload on the file system.
|
||||||
// console.log('Uploading ' + file.name);
|
console.log('Uploading ' + file.name);
|
||||||
// file.path = __dirname + '/upload/' + file.name;
|
file.path = uploadsDir + file.name;
|
||||||
// // io.sockets.in('sessionId').emit('startupload', 'STARTING');
|
// io.sockets.in('sessionId').emit('startupload', 'STARTING');
|
||||||
// });
|
});
|
||||||
|
|
||||||
form.on('progress', function(bytesReceived, bytesExpected) {
|
form.on('progress', function(bytesReceived, bytesExpected) {
|
||||||
uploadprogress = parseInt(((bytesReceived * 100) / bytesExpected).toFixed(0));
|
uploadprogress = parseInt(((bytesReceived * 100) / bytesExpected).toFixed(0));
|
||||||
|
@ -421,7 +471,7 @@ app.post('/upload', function(req, res) {
|
||||||
console.log('Uploaded ' + file.path);
|
console.log('Uploaded ' + file.path);
|
||||||
// io.sockets.in('sessionId').emit('doneupload', 'COMPLETE');
|
// io.sockets.in('sessionId').emit('doneupload', 'COMPLETE');
|
||||||
|
|
||||||
// refreshGcodeLibrary();
|
refreshGcodeLibrary();
|
||||||
|
|
||||||
if (jogWindow === null) {
|
if (jogWindow === null) {
|
||||||
createJogWindow();
|
createJogWindow();
|
||||||
|
@ -437,20 +487,31 @@ app.post('/upload', function(req, res) {
|
||||||
jogWindow.setAlwaysOnTop(false);
|
jogWindow.setAlwaysOnTop(false);
|
||||||
}
|
}
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
|
||||||
fs.readFile(file.path, 'utf8',
|
fs.readFile(file.path, 'utf8',
|
||||||
function(err, data) {
|
function(err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
process.exit(1);
|
io.sockets.emit('data', "ERROR: File Upload Failed");
|
||||||
|
appIcon.displayBalloon({
|
||||||
|
icon: nativeImage.createFromPath(iconPath),
|
||||||
|
title: "ERROR: File Upload Failed",
|
||||||
|
content: "OpenBuilds Machine Driver ERROR: File Upload Failed"
|
||||||
|
})
|
||||||
|
// process.exit(1);
|
||||||
}
|
}
|
||||||
// console.log(data)
|
// console.log(data)
|
||||||
|
if (data) {
|
||||||
io.sockets.emit('gcodeupload', data);
|
io.sockets.emit('gcodeupload', data);
|
||||||
});
|
|
||||||
appIcon.displayBalloon({
|
appIcon.displayBalloon({
|
||||||
icon: nativeImage.createFromPath(iconPath),
|
icon: nativeImage.createFromPath(iconPath),
|
||||||
title: "GCODE Received",
|
title: "GCODE Received",
|
||||||
content: "OpenBuilds Machine Driver received new GCODE"
|
content: "OpenBuilds Machine Driver received new GCODE"
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
}, 1500);
|
}, 1500);
|
||||||
// console.log("Done, now lets work with " + file.path)
|
// console.log("Done, now lets work with " + file.path)
|
||||||
});
|
});
|
||||||
|
@ -1531,14 +1592,6 @@ io.on("connection", function(socket) {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
http.listen(config.webPort, '0.0.0.0', function() {
|
|
||||||
console.log('listening on:' + ip.address() + ":" + config.webPort);
|
|
||||||
// Now refresh library
|
|
||||||
// refreshGcodeLibrary();
|
|
||||||
});
|
|
||||||
|
|
||||||
function machineSend(gcode) {
|
function machineSend(gcode) {
|
||||||
// console.log("SENDING: " + gcode)
|
// console.log("SENDING: " + gcode)
|
||||||
if (port.isOpen) {
|
if (port.isOpen) {
|
||||||
|
@ -1994,21 +2047,6 @@ function isElectron() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Electron app
|
|
||||||
const electron = require('electron');
|
|
||||||
// Module to control application life.
|
|
||||||
const electronApp = electron.app;
|
|
||||||
const BrowserWindow = electron.BrowserWindow;
|
|
||||||
const Tray = electron.Tray;
|
|
||||||
const nativeImage = require('electron').nativeImage
|
|
||||||
const Menu = require('electron').Menu
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
|
||||||
var appIcon = null,
|
|
||||||
jogWindow = null,
|
|
||||||
mainWindow = null
|
|
||||||
|
|
||||||
const shouldQuit = electronApp.makeSingleInstance((commandLine, workingDirectory) => {
|
const shouldQuit = electronApp.makeSingleInstance((commandLine, workingDirectory) => {
|
||||||
// Someone tried to run a second instance, we should focus our window.
|
// Someone tried to run a second instance, we should focus our window.
|
||||||
if (jogWindow === null) {
|
if (jogWindow === null) {
|
||||||
|
@ -2228,5 +2266,5 @@ if (electronApp) {
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('uncaughtException', function(error) {
|
process.on('uncaughtException', function(error) {
|
||||||
console.log("Uncaught Error " + error)
|
// console.log("Uncaught Error " + error)
|
||||||
});
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIGUjCCBTqgAwIBAgIQClWtvwS1NxkRScvn5oVB9jANBgkqhkiG9w0BAQsFADBe
|
||||||
|
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||||
|
d3cuZGlnaWNlcnQuY29tMR0wGwYDVQQDExRSYXBpZFNTTCBSU0EgQ0EgMjAxODAe
|
||||||
|
Fw0xODAzMDUwMDAwMDBaFw0yMDAxMDMxMjAwMDBaMCMxITAfBgNVBAMTGGludmVu
|
||||||
|
dGFibGVzbG9jYWxob3N0LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC
|
||||||
|
ggEBAJ6fHhN1etEIA/S7rbE1n46KjdL+Ne3ofe0q0pK4IYCXA44v44yDqsqmAtkh
|
||||||
|
hn6XND9DjJGkLEEXI/LbgSGHIHSBD9yxFMfVqPh0/Zv60xxPbn0nFrV69//WFbnF
|
||||||
|
KhVFhkkyggglRl6EvpcUVP8pkwc6N3xfmhTfJ3ktsnVJ7JAYLYtAbik89dmVKyRP
|
||||||
|
3/uZHRF5A1ftzSuCXreqkWcc3lyiv7QJ1loYs8ryKAmJ50kO4J8c0QZaVrmmchZt
|
||||||
|
APRKjXgPR1UpKD2tSWYki5M9mBuXW/IDpI+2cMIyJ8BR6rPYEfLjVsskhrhsp0WD
|
||||||
|
isyrVSsD4K/PiuAMYDIraoqpFYMCAwEAAaOCA0UwggNBMB8GA1UdIwQYMBaAFFPK
|
||||||
|
F1n8a8ADIS8aruSqqByCVtp1MB0GA1UdDgQWBBSJEd9qPEk/942YZNLmle4lfaTC
|
||||||
|
mzBBBgNVHREEOjA4ghhpbnZlbnRhYmxlc2xvY2FsaG9zdC5jb22CHHd3dy5pbnZl
|
||||||
|
bnRhYmxlc2xvY2FsaG9zdC5jb20wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQG
|
||||||
|
CCsGAQUFBwMBBggrBgEFBQcDAjA+BgNVHR8ENzA1MDOgMaAvhi1odHRwOi8vY2Rw
|
||||||
|
LnJhcGlkc3NsLmNvbS9SYXBpZFNTTFJTQUNBMjAxOC5jcmwwTAYDVR0gBEUwQzA3
|
||||||
|
BglghkgBhv1sAQIwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
|
||||||
|
Y29tL0NQUzAIBgZngQwBAgEwdQYIKwYBBQUHAQEEaTBnMCYGCCsGAQUFBzABhhpo
|
||||||
|
dHRwOi8vc3RhdHVzLnJhcGlkc3NsLmNvbTA9BggrBgEFBQcwAoYxaHR0cDovL2Nh
|
||||||
|
Y2VydHMucmFwaWRzc2wuY29tL1JhcGlkU1NMUlNBQ0EyMDE4LmNydDAJBgNVHRME
|
||||||
|
AjAAMIIBewYKKwYBBAHWeQIEAgSCAWsEggFnAWUAdQCkuQmQtBhYFIe7E6LMZ3AK
|
||||||
|
PDWYBPkb37jjd80OyA3cEAAAAWH22uXJAAAEAwBGMEQCIGOH6O/3iLvzcvQ3d62L
|
||||||
|
+8kLUiFyVBZ/c2E8+RpIO0g7AiA7lJjGysLIsuuzDKQEocTnxw4NSIr+PwVxNYXa
|
||||||
|
vE8puwB1AId1v+dZfPiMQ5lfvfNu/1aNR1Y2/0q1YMG06v9eoIMPAAABYfba5qQA
|
||||||
|
AAQDAEYwRAIgdunaZF21wBG6N05WSs45UWSzH3Rxz+GqJ4/6ALgGfM0CIBhGqfah
|
||||||
|
4cimOZdrNRvn22MoOk4AZcUA2mVMrO2o6ZP6AHUAu9nfvB+KcbWTlCOXqpJ7RzhX
|
||||||
|
lQqrUugakJZkNo4e0YUAAAFh9trmjwAABAMARjBEAiA7XN5My2jvCYsT1/TQH8lI
|
||||||
|
5r6bPjiJiQvuun5RVtW8lgIgNmo5I4GWcU5WWmGAfYjJx7hPFQqBiwJVqprAh2zl
|
||||||
|
ktwwDQYJKoZIhvcNAQELBQADggEBAFaGMqhgUJYFxA6MgJUWJkt34bx+RMBuOF0W
|
||||||
|
WPgCckJ82JmMjZihRAXOV5wWVqrhnEU0XLk8gdZvO56wK3p/DBOYtz1sDdhmm9Hg
|
||||||
|
rX2Po6EqaclZtybXO5g8B3L4XK7yjnwlVOUb+zDDLppkTZrtFK616oSJOmQ5mZkL
|
||||||
|
pGf0H6w+XRw40mAHPCHoIbOlyUztvQesI3Q973YbsnSWAmdE9TXvyoDr2z+r4v4q
|
||||||
|
dQHPXxRFtTsV78a0FMqFAIZHzpT+e0/EoXUtMkdsKnrrpCu/yd0M50onKKwmqOUp
|
||||||
|
xT2KXS/PxJtiG5x3eN7dJ3OlRb3MeZ+c8d+gMadr3101ve98fGI=
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,27 @@
|
||||||
|
-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEAnp8eE3V60QgD9LutsTWfjoqN0v417eh97SrSkrghgJcDji/j
|
||||||
|
jIOqyqYC2SGGfpc0P0OMkaQsQRcj8tuBIYcgdIEP3LEUx9Wo+HT9m/rTHE9ufScW
|
||||||
|
tXr3/9YVucUqFUWGSTKCCCVGXoS+lxRU/ymTBzo3fF+aFN8neS2ydUnskBgti0Bu
|
||||||
|
KTz12ZUrJE/f+5kdEXkDV+3NK4Jet6qRZxzeXKK/tAnWWhizyvIoCYnnSQ7gnxzR
|
||||||
|
BlpWuaZyFm0A9EqNeA9HVSkoPa1JZiSLkz2YG5db8gOkj7ZwwjInwFHqs9gR8uNW
|
||||||
|
yySGuGynRYOKzKtVKwPgr8+K4AxgMitqiqkVgwIDAQABAoIBAB24wvHyeWjhj5wz
|
||||||
|
7n/eBF+5Jon6iDBj9/SQqZREXEK0CT6DSqfxo/cE1FPLLGBcBLY1+gjwMjqgE2RW
|
||||||
|
LQQTRcmOxWIc7D/lkRu9EChB/3y2hYV95Ytr1zxg6QE+KHyD4n2ksSjFk0HyraOx
|
||||||
|
c5u8NoiMKAluAHkYt1TFc0L27xSwp5DH+xJ9aUkFnZbJMJmSqRt75gVv5581yi3c
|
||||||
|
LUHrSK5hPJ1xZZ+K0lKSX3GOr/P3I9XUXvesBv5Hw0SdwwObPHk2IYM4/vfQwTOS
|
||||||
|
TlEoUUKrZAdEbwiZKXbH07nIetqgJerCDkbLqGDp12T0ylEcrSCbTfPHqYqyeWr4
|
||||||
|
PCw6QgECgYEAzOYub1IGBogH+TbCIdfLylC1WkQgQEZsJMF32qmtuNf/7XdHGXng
|
||||||
|
OZa+Xy6YHzAcdpwbl/DObE9BA+zZpqDLSLtho9NQAQ2/vTAOnhukEFbVIGlhTphh
|
||||||
|
KlVaKLBcNz7gtHGtWbQl/Vnx3Sbt6qU4B2TRH8c+eNZdmt8adOACk+MCgYEAxi5V
|
||||||
|
9Z3VdZr0n+houz9AglKWmYoW/idZvc4KH1kGMNL6+6AIjX37ELCEy5vGjivPuqCa
|
||||||
|
Y7HApP0vMjLkvzJYaIBAWBi6QQU9PrzEVcvZhYnKQ9Er9fcpiXhTBjM6e4W/e/bJ
|
||||||
|
uty/2Kil+lezflerYIJ6fzfVq8FiorBCV83faeECgYEAmezdu1ECJ8vvOX+ybRwh
|
||||||
|
AxaIdowxMjf1K9OPR1wqnm7d2zW82t2c3YZp8zUcoGlTKKNzczw6xlDvhZRbmXq3
|
||||||
|
3CawXhLzyibbALPmd05KfN/Oce/YYuPEMro15dU/IV2CDuxLDtVvqJj2Qm3pU1nU
|
||||||
|
8nEBTP8v5jUb0qmBxYU3SoMCgYB7A7gHxfkCDzVZLQIVeKWqP2mL1NOA3xwtXP+b
|
||||||
|
hb75/3wbRLMKYPC+41MKr58IENnYlmg/Cc7ymtX46u5iX/XQqAtIo9C5G29wyet0
|
||||||
|
9hwHcAhFIEmRW+JEmpOufY4HrnW1lPKTMwNCkSy1wEqCWhjexx8SaK4Q9vEq2w3T
|
||||||
|
Qs8zAQKBgGXKx4Y1KoM+smq3akYMu9mZqnlgN9HMTBTJyX9Oyj4PWpWq8VJS4J/L
|
||||||
|
aDKCIhVO7QAyVlh6u094wfTeWkrvG2/f4k+39TVFlMGy89UQPzRhFSuwe8qzwdN5
|
||||||
|
NEUYYopETCDoIU11aqu4iFIS3bashPjjAc8O37t1hoy+4YElE2Gf
|
||||||
|
-----END RSA PRIVATE KEY-----
|
Ładowanie…
Reference in New Issue