Part 1. Untitled
A year ago, I wrote a note “ Almost OCR to get a VPNBook password. PHP + Mikrotik ” about how to set up automatic password retrieval in the Mikrotik router for free VPN access via VPNBook. The beginning of the story is there.
Since then, a lot of water has flowed, in Russia they blocked the VPNBook site, but not the public VPN servers that are published on it. That PHP script for decoding a PNG image of a password into a text string should now also work when launched on a server whose traffic is not passed through the blocking system. But some time ago, experimenting with the Google Apps Script (GAS) service script.google.com , I decided to abandon the PHP script on an external web server, replacing it partially or fully with a GAS script running as a Web App (web application). I did not understand the execution policy and GAS restrictions, but everything I did works in a free Google account and does not ask for money yet. I don’t have a goal to describe Google Apps Script in detail. GAS is based on the JavaScript language, you can use third-party JS libraries, you can publish the script as a web application, which can be made available to everyone without authorization. The capabilities of the current GAS implementation were not enough for me, so I had to get out and look for workarounds.
At first I decided to write a proxy for PNG images. The web script had to request a password image from the VPNBook website (I recall that the password was published there in PNG) and give it to the client who called this script for decoding. Such a way to get around the lock. Here the first restriction of GAS met. It turns out that the script cannot render MIME image / png, but only text formats, JSON, TEXT, XML, etc. But there was a way to get around this. You can encode PNG to Base64 and return a text string to the client. There are similar scripts on the Internet, for example techslides.com/image-proxy-with-google-app-scripts . I just simplified one of them. I needed only one image and outputting only Base64 strings. The result is a script consisting of only one doGet function - a GET request handler that returns a string in response.
function doGet() { var response = UrlFetchApp.fetch('https://www.vpnbook.com/password.php'); var b64 = Utilities.base64Encode(response.getContent()); //var data = 'data:'+type+';base64,'+b64; return ContentService.createTextOutput(b64); }
Example browser output:
iVBORw0KGgoAAAANSUhEUgAAAGQAAAANAQMAAABl11mFAAAABlBMVEX29vZMTExY89ZbAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAVUlEQVQImWNgIBrwSzCw/2ZgOADhSc5gYJCG8wxQedLdCcYFNXcgPHOZsxuSZxx7BuFZzsjdcJi34TBU5Y3cjc3IvM3McJ7kjNxtzDwwffwSIB7UTACt/h52C5DFqQAAAABJRU5ErkJggg==
Next, the PHP script comes into operation, which can be placed on the server inside the zone with resource locking. It is very similar to the script from the previous article, with the exception of a small change in the parameters of the cURL call. You need to allow cURL to go over HTTP / 1.1 302 Moved Temporarily, because GAS, when called, redirects from the web script address to a dynamic temporary address:
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
And Base64 decoding:
$imgOCR = imagecreatefromstring(base64_decode($output));
Script itself
<?php // $wchar = 9; $hchar = 13; $strDict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '; $imgDict = imagecreatetruecolor(2 + strlen($strDict)* $wchar, $hchar); $bg = imagecolorallocate($imgDict, 0xF6, 0xF6, 0xF6); $textcolor = imagecolorallocate($imgDict, 0x4C, 0x4C, 0x4C); imagefill($imgDict, 0, 0, $bg); imagestring($imgDict, 5, 2, 0, $strDict, $textcolor); // cURL $ch = curl_init(); // url, //curl_setopt($ch, CURLOPT_URL, 'https://www.vpnbook.com/password.php'); curl_setopt($ch, CURLOPT_URL, 'https://script.google.com/macros/s/AKfycbwYPfaZobtjbFv0mSYI8U4NIXPh1Sft_DkGH8QKgg/exec'); // , string curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_BINARYTRANSFER, 1); // also, this seems wise considering output is image. // $output = curl_exec($ch); // cURL curl_close($ch); // echo $output; $imgOCR = imagecreatefromstring(base64_decode($output)); //$imgOCR = imageCreateFromPng('password.png'); // 10 . 2 + 10*9 = 92 < 100 $maxchar = floor((imagesx($imgOCR) - 2) / 9); $imgBox = imagecreatetruecolor($wchar, $hchar); $hashDict = Array(); // for ($k = 0; $k < strlen($strDict) ; $k++) { imagecopy($imgBox, $imgDict, 0, 0, 2 + $k * $wchar, 0, $wchar, $hchar); $hashStr = ""; for($y = 0; $y < $hchar ; $y++) for($x = 0; $x < $wchar; $x++) $hashStr .= (imagecolorat($imgBox, $x, $y) != 0xF6F6F6)? '1': '0'; $hashDict[$hashStr] = $strDict[$k]; } // for ($k = 0; $k < $maxchar ; $k++) { imagecopy($imgBox, $imgOCR, 0, 0, 2 + $k * $wchar, 0, $wchar, $hchar); $hashStr = ""; for($y = 0; $y < $hchar ; $y++) for($x = 0; $x < $wchar; $x++) $hashStr .= (imagecolorat($imgBox, $x, $y) != 0xF6F6F6)? '1': '0'; $tempchar = $hashDict[$hashStr]; if ($tempchar=='u' || $tempchar=='y') // $tempchar = (mt_rand(0, 1))? 'u': 'y'; //$tempchar = (time() / 60 % 60 % 2)? 'u': 'y'; elseif ($tempchar==' ') break; print($tempchar); } /* header('Content-type: image/png'); imagepng($imgDict); */ //var_dump($hashDict); imagedestroy($imgDict); imagedestroy($imgOCR); imagedestroy($imgBox); ?>
This PHP script decodes the PNG password and returns it as a text string. Further, as in the first article in the part about Mikrotik. The router picks up the password using fetch.
The result is such a working scheme of 2 intermediate services in front of Mikrotik.
Part 2. Push on GAS. Get rid of PHP decoder script
During experiments with GAS, the idea arose of abandoning the password decoder in PHP by rewriting it in GAS. And here a big problem was discovered: Google script does not have PNG processing functions, the only thing that can be done is to convert PNG to a byte array. There was no question of any manipulations with parts of the image and pixels. I climbed on Github in search of a JS library for working with PNG, I found a lot of them: PNG.js, UPNG.js, pngjs. Some do not support the 1-bit color depth of a PNG pixel (image with password). They pulled along various zlib compression libraries. In general, it all seemed a little cumbersome to me, and I decided to write on my own a primitive converter only for my PNG image to a bitmap with the function of accessing pixels by XY coordinates. Then came a complete immersion in the PNG format: hex editor, reading standards, heaps of descriptions on the network. And finally, I ran into the PNG section of the IDAT file, packed with zlib, which contained an array of pixels.
It required a function for unpacking zlib, which of course was not in GAS. Surprisingly, they have gzip / ungzip and zip / unzip, but no zlib. After reading about gzip (the second level of immersion after PNG format), I came to the conclusion that it would not be possible to assemble a “bicycle” as a gzip quasi-archive from the IDAT section, although zlib compression was used there and there. Because To build a valid gzip archive, you need to know the length of the unpacked data, which I could not get without unpacking them :) And with the wrong length, GAS considered the archive corrupted. In the end, I turned to Github and found a great solution: the zlib.js for Google Apps Script library (https://github.com/hinimub/zlib.js/blob/develop/README.en.md). Which was specially prepared for integration into GAS projects through project key libraries. Then the puzzle began to converge. After writing the decompression of the pixel array and the function for accessing the coordinates of the XY pixel, it was possible to transfer the decoder script from PHP to GAS.
Separately calculated a hash table of a dictionary of possible password characters. This is a one-time action that I did in a third-party program (in LabVIEW, hello, colleagues). Each character in the image can be allocated as 8 bits (without indentation) x 10 lines. 1 byte is enough to encode 8 pixels of a line of one character. You can store a string of pixels in a whole number (byte), and the entire character as a sequence of 10 bytes. It turns out 10 hex numbers per character. Next, the GAS decoder repeats its PHP progenitor.
The result is a script that works fully in GAS.
function doGet() { //var file = DriveApp.getFilesByName("password2.png").next(); //var image = file.getBlob(); var image = UrlFetchApp.fetch('https://www.vpnbook.com/password.php').getBlob(); var imageString = image.getDataAsString(); var imageArray = image.getBytes().map(function(e) { return e & 0xff; }); // imageArray = blobToUint8(imageArray); var chunkIDATStart = imageString.indexOf("IDAT") + 4; // IDAT var chunkIDATLen = bytesToUint32(imageArray, imageString.indexOf("IDAT") - 4); // IDAT var IDATArray = imageArray.slice(chunkIDATStart, chunkIDATStart + chunkIDATLen) var inflate = new zlibjs.Inflate(IDATArray); // IDAT zlib var plain = inflate.decompress(); const Width = 100; // const Height = 13; // const wchar = 9; // const hchar = 13; // //Logger.log(typeof(plain)); //Logger.log(plain); rowlen = (Width / 8) >> 0; // , if ((Width - rowlen * 8) > 0) { // rowlen+=2; // +1 filter byte at the beginning of each row. PNG } else { rowlen++; } function getXY(x, y) { // : 0/1 var xbyte = (x / 8 >> 0); // , //Logger.log("xbyte: " + xbyte); var xbit = x - xbyte * 8; // , //Logger.log("xbit: " + xbit); return (plain[xbyte + 1 + y * rowlen] << xbit & 0x80) >> 7; // +1 filter byte at the beginning of each row } // Logger.log("getXY: " + getXY(4, 3)); // - . 8 ( ) x 10 , (byte), 10 . // 10 hex . var hashDict = {'183C66C3C3C3FFC3C3C3':'A','FCC6C3C6FCC6C3C3C6FC':'B','3E63C1C0C0C0C0C1633E':'C','FCC6C3C3C3C3C3C3C6FC':'D', 'FEC0C0C0FCC0C0C0C0FE':'E','FFC0C0C0FCC0C0C0C0C0':'F','3E63C0C0C0C7C3C3633E':'G','C3C3C3C3FFC3C3C3C3C3':'H', '7E18181818181818187E':'I','1E666666466C38':'J','C3C6CCD8F0F0D8CCC6C3':'K','C0C0C0C0C0C0C0C0C0FE':'L', 'C3E7FFDBDBDBC3C3C3C3':'M','C3E3F3F3DBDBCFC7C7C3':'N','3C66C3C3C3C3C3C3663C':'O','FEC3C3C3FEC0C0C0C0C0':'P', '3C66C3C3C3C3DBCF663D':'Q','FEC3C3C3FEF8CCC6C3C3':'R','7EC3C0C07E333C37E':'S','FF181818181818181818':'T', 'C3C3C3C3C3C3C3C3663C':'U','C3C3C36666663C3C1818':'V','C3C3C3C3DBDBDBFFE7C3':'W','C3C3663C18183C66C3C3':'X', 'C3C3663C181818181818':'Y','FE66C183060C0C0FE':'Z','0003E6337FC3C77B':'a','C0C0C0DCE6C3C3C3E6DC':'b', '0003E63C0C0C0633E':'c','3333B67C3C3C3673B':'d','0003C66C3FFC0633E':'e','1E33333030FC30303030':'f', '0007DC7C6C67CC07E':'g','C0C0C0DCE6C3C3C3C3C3':'h','181803818181818187E':'i','660E66666C6':'j', '606060666C78786C6663':'k','3818181818181818183C':'l','000B6DBDBDBDBDBDB':'m','000DCE6C3C3C3C3C3':'n', '0003C66C3C3C3663C':'o','000DCE6C3C3C3E6DC':'p','0003B67C3C3C3673B':'q','000DE736060606060':'r', '0007EC3C07E3C37E':'s','03030FC30303030331E':'t','000C3C3C3C3C3673B':'u','000C3C366663C3C18':'v', '000C3C3DBDBDBFF66':'w','000C3663C183C66C3':'x','000C3C3C3C3C3673B':'y','0007E6C1830607E':'z', '183C66C3C3C3C3663C18':'0','1838781818181818187E':'1','3C66C336C183060FF':'2','7CC6361C633C67C':'3', '6E1E3666C6FF666':'4','FEC0C0DCE633C3663C':'5','3C66C2C0DCE6C3C3663C':'6','FF336C183060C0C0':'7', '3C66C3663C66C3C3663C':'8','3C66C3C3673B343663C':'9','0000000000':' '}; // 10 . 2 + 10*9 = 92 < 100 const maxchar = (Width - 2) / wchar >> 0; var password = ''; for (var charX = 2; charX < maxchar * wchar + 2; charX+=wchar) { // var hash = ''; // for (var charY = 3; charY < hchar; charY++) { // Y- var charrow = 0; // - 8 for (var charXbit = 0; charXbit < 8; charXbit++) { // X- charrow <<= 1; charrow |= getXY(charX + charXbit, charY); } hash += charrow.toString(16).toUpperCase(); //Logger.log("charrow: " + charrow.toString(2)); //Logger.log("charrow: " + charrow.toString(16).toUpperCase()); } var tempChar = hashDict[hash]; if (tempChar === 'u' || tempChar === 'y') { // tempChar = (Date.now() % 2) ? 'u': 'y'; } if (tempChar !== ' ') { password += tempChar; // Logger.log("hash: " + hash); // Logger.log("Char: " + tempChar); } } Logger.log("password: " + password); return ContentService.createTextOutput(password); } function blobToUint8(blob) { return blob.map(function(e){ return e & 0xff; }); } function bytesToUint32(byteArray, start) { var value = 0; for (var i = start; i < start + 4; i++) { value = (value * 256) + (byteArray[i] & 0xff); } return value; } function my2() { var file = DriveApp.getFilesByName("password2.png").next(); // var file = DriveApp.getFilesByName("test.bin").next(); var image = file.getBlob(); //var imageArray = image.getBytes(); //var img = UrlFetchApp.fetch('http://example.com/image.png'); var reader = new pngjs.PNGReader(image.getBytes()); var png = reader.parse(function(err, png){ if (err) throw err; return png; }); Logger.log(png); }
'A', 'FCC6C3C6FCC6C3C3C6FC': 'B', '3E63C1C0C0C0C0C1633E': 'C', 'FCC6C3C3C3C3C3C3C6FC': 'D', 'FEC0C0C0FCC0C0C0C0FE': 'E', 'FFC0C0C0FCC0C0C0C0C0': 'F function doGet() { //var file = DriveApp.getFilesByName("password2.png").next(); //var image = file.getBlob(); var image = UrlFetchApp.fetch('https://www.vpnbook.com/password.php').getBlob(); var imageString = image.getDataAsString(); var imageArray = image.getBytes().map(function(e) { return e & 0xff; }); // imageArray = blobToUint8(imageArray); var chunkIDATStart = imageString.indexOf("IDAT") + 4; // IDAT var chunkIDATLen = bytesToUint32(imageArray, imageString.indexOf("IDAT") - 4); // IDAT var IDATArray = imageArray.slice(chunkIDATStart, chunkIDATStart + chunkIDATLen) var inflate = new zlibjs.Inflate(IDATArray); // IDAT zlib var plain = inflate.decompress(); const Width = 100; // const Height = 13; // const wchar = 9; // const hchar = 13; // //Logger.log(typeof(plain)); //Logger.log(plain); rowlen = (Width / 8) >> 0; // , if ((Width - rowlen * 8) > 0) { // rowlen+=2; // +1 filter byte at the beginning of each row. PNG } else { rowlen++; } function getXY(x, y) { // : 0/1 var xbyte = (x / 8 >> 0); // , //Logger.log("xbyte: " + xbyte); var xbit = x - xbyte * 8; // , //Logger.log("xbit: " + xbit); return (plain[xbyte + 1 + y * rowlen] << xbit & 0x80) >> 7; // +1 filter byte at the beginning of each row } // Logger.log("getXY: " + getXY(4, 3)); // - . 8 ( ) x 10 , (byte), 10 . // 10 hex . var hashDict = {'183C66C3C3C3FFC3C3C3':'A','FCC6C3C6FCC6C3C3C6FC':'B','3E63C1C0C0C0C0C1633E':'C','FCC6C3C3C3C3C3C3C6FC':'D', 'FEC0C0C0FCC0C0C0C0FE':'E','FFC0C0C0FCC0C0C0C0C0':'F','3E63C0C0C0C7C3C3633E':'G','C3C3C3C3FFC3C3C3C3C3':'H', '7E18181818181818187E':'I','1E666666466C38':'J','C3C6CCD8F0F0D8CCC6C3':'K','C0C0C0C0C0C0C0C0C0FE':'L', 'C3E7FFDBDBDBC3C3C3C3':'M','C3E3F3F3DBDBCFC7C7C3':'N','3C66C3C3C3C3C3C3663C':'O','FEC3C3C3FEC0C0C0C0C0':'P', '3C66C3C3C3C3DBCF663D':'Q','FEC3C3C3FEF8CCC6C3C3':'R','7EC3C0C07E333C37E':'S','FF181818181818181818':'T', 'C3C3C3C3C3C3C3C3663C':'U','C3C3C36666663C3C1818':'V','C3C3C3C3DBDBDBFFE7C3':'W','C3C3663C18183C66C3C3':'X', 'C3C3663C181818181818':'Y','FE66C183060C0C0FE':'Z','0003E6337FC3C77B':'a','C0C0C0DCE6C3C3C3E6DC':'b', '0003E63C0C0C0633E':'c','3333B67C3C3C3673B':'d','0003C66C3FFC0633E':'e','1E33333030FC30303030':'f', '0007DC7C6C67CC07E':'g','C0C0C0DCE6C3C3C3C3C3':'h','181803818181818187E':'i','660E66666C6':'j', '606060666C78786C6663':'k','3818181818181818183C':'l','000B6DBDBDBDBDBDB':'m','000DCE6C3C3C3C3C3':'n', '0003C66C3C3C3663C':'o','000DCE6C3C3C3E6DC':'p','0003B67C3C3C3673B':'q','000DE736060606060':'r', '0007EC3C07E3C37E':'s','03030FC30303030331E':'t','000C3C3C3C3C3673B':'u','000C3C366663C3C18':'v', '000C3C3DBDBDBFF66':'w','000C3663C183C66C3':'x','000C3C3C3C3C3673B':'y','0007E6C1830607E':'z', '183C66C3C3C3C3663C18':'0','1838781818181818187E':'1','3C66C336C183060FF':'2','7CC6361C633C67C':'3', '6E1E3666C6FF666':'4','FEC0C0DCE633C3663C':'5','3C66C2C0DCE6C3C3663C':'6','FF336C183060C0C0':'7', '3C66C3663C66C3C3663C':'8','3C66C3C3673B343663C':'9','0000000000':' '}; // 10 . 2 + 10*9 = 92 < 100 const maxchar = (Width - 2) / wchar >> 0; var password = ''; for (var charX = 2; charX < maxchar * wchar + 2; charX+=wchar) { // var hash = ''; // for (var charY = 3; charY < hchar; charY++) { // Y- var charrow = 0; // - 8 for (var charXbit = 0; charXbit < 8; charXbit++) { // X- charrow <<= 1; charrow |= getXY(charX + charXbit, charY); } hash += charrow.toString(16).toUpperCase(); //Logger.log("charrow: " + charrow.toString(2)); //Logger.log("charrow: " + charrow.toString(16).toUpperCase()); } var tempChar = hashDict[hash]; if (tempChar === 'u' || tempChar === 'y') { // tempChar = (Date.now() % 2) ? 'u': 'y'; } if (tempChar !== ' ') { password += tempChar; // Logger.log("hash: " + hash); // Logger.log("Char: " + tempChar); } } Logger.log("password: " + password); return ContentService.createTextOutput(password); } function blobToUint8(blob) { return blob.map(function(e){ return e & 0xff; }); } function bytesToUint32(byteArray, start) { var value = 0; for (var i = start; i < start + 4; i++) { value = (value * 256) + (byteArray[i] & 0xff); } return value; } function my2() { var file = DriveApp.getFilesByName("password2.png").next(); // var file = DriveApp.getFilesByName("test.bin").next(); var image = file.getBlob(); //var imageArray = image.getBytes(); //var img = UrlFetchApp.fetch('http://example.com/image.png'); var reader = new pngjs.PNGReader(image.getBytes()); var png = reader.parse(function(err, png){ if (err) throw err; return png; }); Logger.log(png); }
'C3E3F3F3DBDBCFC7C7C3': 'N', '3C66C3C3C3C3C3C3663C': 'O', 'FEC3C3C3FEC0C0C0C0C0': 'P', '3C66C3C3C3C3DBCF663D': 'Q', 'FEC3C3C3FEF8CCC6C3C3': 'R', '7EC3C0C07E333C37E function doGet() { //var file = DriveApp.getFilesByName("password2.png").next(); //var image = file.getBlob(); var image = UrlFetchApp.fetch('https://www.vpnbook.com/password.php').getBlob(); var imageString = image.getDataAsString(); var imageArray = image.getBytes().map(function(e) { return e & 0xff; }); // imageArray = blobToUint8(imageArray); var chunkIDATStart = imageString.indexOf("IDAT") + 4; // IDAT var chunkIDATLen = bytesToUint32(imageArray, imageString.indexOf("IDAT") - 4); // IDAT var IDATArray = imageArray.slice(chunkIDATStart, chunkIDATStart + chunkIDATLen) var inflate = new zlibjs.Inflate(IDATArray); // IDAT zlib var plain = inflate.decompress(); const Width = 100; // const Height = 13; // const wchar = 9; // const hchar = 13; // //Logger.log(typeof(plain)); //Logger.log(plain); rowlen = (Width / 8) >> 0; // , if ((Width - rowlen * 8) > 0) { // rowlen+=2; // +1 filter byte at the beginning of each row. PNG } else { rowlen++; } function getXY(x, y) { // : 0/1 var xbyte = (x / 8 >> 0); // , //Logger.log("xbyte: " + xbyte); var xbit = x - xbyte * 8; // , //Logger.log("xbit: " + xbit); return (plain[xbyte + 1 + y * rowlen] << xbit & 0x80) >> 7; // +1 filter byte at the beginning of each row } // Logger.log("getXY: " + getXY(4, 3)); // - . 8 ( ) x 10 , (byte), 10 . // 10 hex . var hashDict = {'183C66C3C3C3FFC3C3C3':'A','FCC6C3C6FCC6C3C3C6FC':'B','3E63C1C0C0C0C0C1633E':'C','FCC6C3C3C3C3C3C3C6FC':'D', 'FEC0C0C0FCC0C0C0C0FE':'E','FFC0C0C0FCC0C0C0C0C0':'F','3E63C0C0C0C7C3C3633E':'G','C3C3C3C3FFC3C3C3C3C3':'H', '7E18181818181818187E':'I','1E666666466C38':'J','C3C6CCD8F0F0D8CCC6C3':'K','C0C0C0C0C0C0C0C0C0FE':'L', 'C3E7FFDBDBDBC3C3C3C3':'M','C3E3F3F3DBDBCFC7C7C3':'N','3C66C3C3C3C3C3C3663C':'O','FEC3C3C3FEC0C0C0C0C0':'P', '3C66C3C3C3C3DBCF663D':'Q','FEC3C3C3FEF8CCC6C3C3':'R','7EC3C0C07E333C37E':'S','FF181818181818181818':'T', 'C3C3C3C3C3C3C3C3663C':'U','C3C3C36666663C3C1818':'V','C3C3C3C3DBDBDBFFE7C3':'W','C3C3663C18183C66C3C3':'X', 'C3C3663C181818181818':'Y','FE66C183060C0C0FE':'Z','0003E6337FC3C77B':'a','C0C0C0DCE6C3C3C3E6DC':'b', '0003E63C0C0C0633E':'c','3333B67C3C3C3673B':'d','0003C66C3FFC0633E':'e','1E33333030FC30303030':'f', '0007DC7C6C67CC07E':'g','C0C0C0DCE6C3C3C3C3C3':'h','181803818181818187E':'i','660E66666C6':'j', '606060666C78786C6663':'k','3818181818181818183C':'l','000B6DBDBDBDBDBDB':'m','000DCE6C3C3C3C3C3':'n', '0003C66C3C3C3663C':'o','000DCE6C3C3C3E6DC':'p','0003B67C3C3C3673B':'q','000DE736060606060':'r', '0007EC3C07E3C37E':'s','03030FC30303030331E':'t','000C3C3C3C3C3673B':'u','000C3C366663C3C18':'v', '000C3C3DBDBDBFF66':'w','000C3663C183C66C3':'x','000C3C3C3C3C3673B':'y','0007E6C1830607E':'z', '183C66C3C3C3C3663C18':'0','1838781818181818187E':'1','3C66C336C183060FF':'2','7CC6361C633C67C':'3', '6E1E3666C6FF666':'4','FEC0C0DCE633C3663C':'5','3C66C2C0DCE6C3C3663C':'6','FF336C183060C0C0':'7', '3C66C3663C66C3C3663C':'8','3C66C3C3673B343663C':'9','0000000000':' '}; // 10 . 2 + 10*9 = 92 < 100 const maxchar = (Width - 2) / wchar >> 0; var password = ''; for (var charX = 2; charX < maxchar * wchar + 2; charX+=wchar) { // var hash = ''; // for (var charY = 3; charY < hchar; charY++) { // Y- var charrow = 0; // - 8 for (var charXbit = 0; charXbit < 8; charXbit++) { // X- charrow <<= 1; charrow |= getXY(charX + charXbit, charY); } hash += charrow.toString(16).toUpperCase(); //Logger.log("charrow: " + charrow.toString(2)); //Logger.log("charrow: " + charrow.toString(16).toUpperCase()); } var tempChar = hashDict[hash]; if (tempChar === 'u' || tempChar === 'y') { // tempChar = (Date.now() % 2) ? 'u': 'y'; } if (tempChar !== ' ') { password += tempChar; // Logger.log("hash: " + hash); // Logger.log("Char: " + tempChar); } } Logger.log("password: " + password); return ContentService.createTextOutput(password); } function blobToUint8(blob) { return blob.map(function(e){ return e & 0xff; }); } function bytesToUint32(byteArray, start) { var value = 0; for (var i = start; i < start + 4; i++) { value = (value * 256) + (byteArray[i] & 0xff); } return value; } function my2() { var file = DriveApp.getFilesByName("password2.png").next(); // var file = DriveApp.getFilesByName("test.bin").next(); var image = file.getBlob(); //var imageArray = image.getBytes(); //var img = UrlFetchApp.fetch('http://example.com/image.png'); var reader = new pngjs.PNGReader(image.getBytes()); var png = reader.parse(function(err, png){ if (err) throw err; return png; }); Logger.log(png); }
The script implements only the GET method. When executing a GET request to this script published as Web App, the response will immediately contain the decoded password in the form of a string.
Part 3. Mikrotik and Moved Temporarily 302
So, we have a script that runs on external Web App servers, which is independent of locks and returns a plain text password. And it seems there is nothing easier than requesting it with the fetch command in RouterOS Mikrotik. But then another surprise awaited me. In response to the request (real addresses changed) fetch returns “302 Moved Temporarily”.
[admin@MikroTik] /environment> :put ([/tool fetch url="https://script.google.com/macros/s/A.....A/exec" http-method=get output=user as-value]->"data") failure: closing connection: <302 Moved Temporarily "https://script.googleusercontent.com/macros/echo?user_content_key=....."> 173.194.222.138:443 (4) [admin@MikroTik] /environment>
At the beginning of the article, I already wrote about this. When accessing a persistent known URL of the Web App script, Google redirects to a temporary URL, which in turn returns a response to the request. But unlike the PHP cURL, fetch RouterOS does not know how to go through redirects, instead it returns failure. But forum.mikrotik.com did not immediately, but there was a workaround. You can redirect standard fetch output from the console to a file by calling asynchronous execution in a separate task by wrapping: execute. You can then retrieve the redirect URL and re-fetch with the new address. Which is done below.
# . Moved Temporarily 302. fetch gasfetchout.txt :local jobid [:execute script={/tool fetch url="https://script.google.com/macros/s/A.....A/exec" output=user as-value} file=gasfetchout.txt] # , :while ([:len [/system script job find .id=$jobid ]] > 0) do={ delay 1s } # gasfetchout.txt, URL :local fetchOut [/file get gasfetchout.txt contents] :local startURL [:find $fetchOut "http" -1] :local endURL [:find $fetchOut "\"> " startURL] :local moveURL [:pick $fetchOut $startURL $endURL] :global VPNBookPass2 ([/tool fetch url=$moveURL output=user as-value]->"data")
Here is the full text of the Mikrotik script for working with the GAS Web App
# VPNBookScript v4 :local VPNBookpIfName "pptp-out1" :local VPNBookServerAddresses {"PL226.vpnbook.com";"de4.vpnbook.com";"us1.vpnbook.com";"us2.vpnbook.com";"fr1.vpnbook.com ";"fr8.vpnbook.com ";"ca222.vpnbook.com ";"ca198.vpnbook.com"} :local VPNBookErr false :global VPNBookPass :global VPNBookRun :global VPNBookServerIndex :if ([:typeof $VPNBookServerIndex] != "num") do={:set VPNBookServerIndex 0} :if ([/interface pptp-client get $VPNBookpIfName running]) do={ :set VPNBookRun true } else { :if (!$VPNBookRun) do={ :set VPNBookServerIndex ($VPNBookServerIndex + 1) :if ($VPNBookServerIndex>=[:len $VPNBookServerAddresses]) do={:set VPNBookServerIndex 0} } else { :set VPNBookRun false } :if (![/interface pptp-client get $VPNBookpIfName disabled]) do={/interface pptp-client set $VPNBookpIfName disabled=yes} # :do {:set VPNBookPass ([/tool fetch url="http://serv/vpnbookpass_googlescript.php" output=user as-value]->"data")} on-error={:set VPNBookErr true} :do { # First request with Moved Temporarily. Fetch out to gasfetchout.txt :local jobid [:execute script={/tool fetch url="https://script.google.com/macros/s/A.....g/exec" output=user as-value} file=gasfetchout.txt] # Wait end job :while ([:len [/system script job find .id=$jobid ]] > 0) do={ delay 1s } # parse new URL for second fetch :local fetchOut [/file get gasfetchout.txt contents] :local startURL [:find $fetchOut "http" -1] :local endURL [:find $fetchOut "\"> " startURL] :local moveURL [:pick $fetchOut $startURL $endURL] :set VPNBookPass ([/tool fetch url=$moveURL output=user as-value]->"data") } on-error={:set VPNBookErr true} :if (!$VPNBookErr) do={ :if ([/interface pptp-client get $VPNBookpIfName password] != $VPNBookPass) do={/interface pptp-client set $VPNBookpIfName password=$VPNBookPass} :if ([/interface pptp-client get $VPNBookpIfName connect-to] != $VPNBookServerAddresses->$VPNBookServerIndex) do={/interface pptp-client set $VPNBookpIfName connect-to=($VPNBookServerAddresses->$VPNBookServerIndex)} :log info "VPNBook: Attempt to connect to: $($VPNBookServerAddresses->$VPNBookServerIndex). Password: $VPNBookPass" /interface pptp-client set $VPNBookpIfName disabled=no } }
Part 4. Telegram GAS proxy
I decided to devote this part to the next iteration of integrating the Telegram service into Mikrotik. Using GAS here would be of purely academic interest, if not for the reality of blocking the Telegram service, including api.telegram.org, through which bots work with the service. The idea repeats the idea at the beginning of the article about proxying the request for PNG images.
In this case, the GAS Web App is written to proxy requests from Mikrtotik to api.telegram.org. As a basis, I took a ready-made script from manzoorwanijk, WPTelegram Google Script gist.github.com/manzoorwanijk/ee9ed032caedf2bb0c83dea73bc9a28e . This script can proxy many Telegram API methods (but not all). In args, you can pass a JSON object containing the request parameters, for example
{"chat_id":"123","text":"HelloWorld"}
. But for my task of sending text messages from RouterOS Mikrtotik, the implementation seemed complicated and I simplified it. Ultimately, you can generally write several Web App scripts to proxy various Telegram API methods. Here is my implementation for the sendMessage method. It can be further simplified by embedding the name of the sendMessage method being called, and even bot_token and chat_id in the body of the requestHandler function.
function doGet(e) { if(typeof e !== 'undefined'){ return ContentService.createTextOutput(requestHandler(e)); } } function doPost(e) { if(typeof e !== 'undefined'){ return ContentService.createTextOutput(requestHandler(e)); } } function requestHandler(e){ if (typeof e.parameter.bot_token === 'undefined'){ return 'Error! Bot token not provided'; } else if (typeof e.parameter.method === 'undefined') { return 'Error! Method name not provided'; } else if (typeof e.parameter.chat_id === 'undefined') { return 'Error! Chat id not provide'; } else if (typeof e.parameter.text === 'undefined') { return 'Error! Text not provide'; } /* if(typeof e.parameter.args !== 'undefined'){ var args = e.parameter.args; data.payload = JSON.parse(args); } */ if (e.parameter.method === 'sendMessage') { var data = { "method": "post", "muteHttpExceptions": true, payload : 'chat_id=' + e.parameter.chat_id + '&text=' + e.parameter.text } return UrlFetchApp.fetch('https://api.telegram.org/bot' + e.parameter.bot_token + '/' + e.parameter.method, data).getContentText(); } }
After publishing the script in the Web App, you can execute a request in the GET browser to check:
https://script.google.com/macros/s/A.....A/exec?bot_token=3.....3&method=sendMessage&chat_id=2.....3&text=testtext123
Or in RouterOS POST request:
:do { /tool fetch url=("https://script.google.com/macros/s/A.....A/exec") keep-result=no http-method=post http-data=("bot_token=3.....3&method=sendMessage&chat_id=2.....3&text=testtext123") } on-error={ }
The request is wrapped in do-on-error, because, as shown above, the first call to fetch will throw an exception “Moved Temporarily 302” and the script without the on-error handler will stop at this point. One call to fetch without forwarding is enough for the message to be sent, so a second call to fetch is not necessary if you do not need the JSON object returned by the Telegram API.
Part 5. Final
I brought my real applications at the junction of Google Apps Script with other services. You can come up with a lot more. For example, write a Telegram bot in GAS, which will respond with a VPNBook password with caching requests to reduce the load on the VPNBook (Cache Service), and all this will be in one GAS script. You can write on GAS a logging system or backup configurations for Mikrtotik, which will be placed in Google Docs and Google Sheets files and much more.