commit adaec81731ce28758f74c2fb0a99d594c173baef Author: Fabian Stamm Date: Fri Nov 20 18:43:59 2020 +0100 First Commit diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0ed4985 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root=true +[*] +charset = utf-8 +end_of_line = lf +indent_size = 3 +indent_style = space +insert_final_newline = true +[*.yml] +indent_size = 2 +[*.md] +indent_size = 2 diff --git a/gen.js b/gen.js new file mode 100644 index 0000000..db84ea9 --- /dev/null +++ b/gen.js @@ -0,0 +1,190 @@ +#!node +const fs = require("fs"); +const p = require("path"); +const qrimage = require("./qr/qr"); +const crypto = require("crypto"); + + +const deleteRec = (path) => { + path = p.resolve(path); + if (fs.existsSync(path)) { + if (fs.statSync(path).isDirectory()) { + fs.readdirSync(path).forEach(entry => deleteRec(p.join(path, entry))); + fs.rmdirSync(path); + } else { + fs.unlinkSync(path); + } + } +} + +const config = fs.existsSync("config.json") ? JSON.parse(fs.readFileSync("config.json").toString()) : {}; + +const child = require("child_process"); +const getPrivate = () => { + return child.execSync("wg genkey").toString().replace("\n", "").replace("\r", "").trim(); +} + +const getPublic = (private) => { + return child.execSync(`echo ${private} | wg pubkey`).toString().replace("\n", "").replace("\r", "").trim(); +} + +const keyPair = () => { + const private = getPrivate(); + const public = getPublic(private); + return { private, public }; +} + +const clientArray = (clients) => Object.keys(clients).map(key => ({ ...clients[key], name: key })) + +const ipTablesPostUP = [ + "iptables -A FORWARD -i %i -j ACCEPT", + "iptables -A FORWARD -o %i -j ACCEPT", + "iptables -t nat -A POSTROUTING -o %i -j MASQUERADE", + "iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", + "iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE" +].join("; "); + +const ipTablesPostDOWN = [ + "iptables -D FORWARD -i %i -j ACCEPT", + "iptables -D FORWARD -o %i -j ACCEPT", + "iptables -t nat -D POSTROUTING -o %i -j MASQUERADE", + "iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE", + "iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE" +].join("; "); + +function makeServer({ port, subnet, subnet6, server, clients }) { + const base = `[Interface] #Server +ListenPort = ${port} +Address = ${subnet}1/32,${subnet6}1/48 +PrivateKey = ${server.private} +PostUp = ${ipTablesPostUP} +PostDown = ${ipTablesPostDOWN} +# --------------------------- + +` + + let peers = clientArray(clients).map(client => { + return `[Peer] #${client.name} +PublicKey = ${client.public} +AllowedIPs = ${subnet}${client.ip}/32,${subnet6}${client.ip}/128 +` + (client.endpoint ? "\nEndpoint = " + client.endpoint + ":" + port : ""); + }).join("\n\n"); + + fs.writeFileSync("out/server/server.conf", base + peers); + fs.writeFileSync("out/server/server.public", server.public); +} + +function makeClients({ port, subnet, subnet6, server, clients }) { + clientArray(clients).map(client => { + const config = `[Interface] +ListenPort = ${port} +Address = ${subnet}${client.ip}/32, ${subnet6}${client.ip}/128 +PrivateKey = ${client.private} +DNS = 1.1.1.1 + +[Peer] #Server +PublicKey = ${server.public} +AllowedIPs = 0.0.0.0/0, ::0/0 +Endpoint = ${server.endpoint}:${port} +` + + fs.writeFileSync(`out/clients/${client.name}.conf`, config); + fs.writeFileSync(`out/clients/${client.name}.public`, client.public); + var qr_svg = qrimage.image(config, { type: 'png' }); + qr_svg.pipe(require('fs').createWriteStream(`out/clients/${client.name}.png`)); + }) +} + +try { + if (!config.subnet6) { + let sn = crypto.randomBytes(5).toString("hex"); + let s1 = sn.substr(0, 2); + let s23 = sn.substr(2, 4); + let s45 = sn.substr(6, 4); + config.subnet6 = `fd${s1}:${s23}:${s45}::`; + } + + switch (process.argv[2]) { + case "init": { + const hostname = process.argv[3]; + let subnet = process.argv[4]; + const port = Number(process.argv[5]) | 51823; + + if (!hostname) + throw new Error("Hostname required"); + + if (!subnet) { + let randomIP1 = Math.floor(Math.random() * 256); + let randomIP2 = Math.floor(Math.random() * 256); + subnet = `10.${randomIP1}.${randomIP2}.`; + } else { + if (!subnet.endsWith(".")) + subnet += "."; + } + + + + config.subnet = subnet; + config.port = port; + config.server = { + endpoint: hostname, + ...keyPair() + } + config.clients = {}; + console.log("Created new configuration"); + console.log("Server Hostname:", hostname); + console.log(" Port:", port); + console.log(" Subnet:", subnet + "0/24"); + + break; + } + case "generate": { + deleteRec("./out"); + fs.mkdirSync("./out") + fs.mkdirSync("./out/server") + fs.mkdirSync("./out/clients") + makeServer(config) + makeClients(config) + break; + } + case "add": { + const name = process.argv[3]; + const endpoint = process.argv[4]; + if (!name) + throw new Error("No name!"); + if (config.clients[name]) { + throw new Error("A device with this name exists already!\n Remove with 'remove '"); + } + + let freeIP = 2; + clientArray(config.clients).forEach(c => c.ip >= freeIP ? freeIP = c.ip + 1 : undefined); + config.clients[name] = { + ip: freeIP, + endpoint, + ...keyPair() + } + console.log("Peer added:") + console.log(`IP: ${config.subnet}${freeIP}`) + break; + } + + case "remove": { + const name = process.argv[3]; + if (!name) + throw new Error("No name!"); + delete config.clients[name]; + break; + } + + default: { + console.log("Wireguard VPN Config generator v0.1"); + console.log("Usage:") + console.log("generate, add, remove") + } + } +} catch (err) { + console.error(err); + console.error(err.message); +} + +fs.writeFileSync("config.json", JSON.stringify(config, undefined, " ")); \ No newline at end of file diff --git a/qr/LICENSE b/qr/LICENSE new file mode 100644 index 0000000..3884cac --- /dev/null +++ b/qr/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Yandex LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/qr/crc32buffer.js b/qr/crc32buffer.js new file mode 100644 index 0000000..e4bd213 --- /dev/null +++ b/qr/crc32buffer.js @@ -0,0 +1,50 @@ +'use strict'; + +var crc_table = []; + +for (var n = 0; n < 256; n++) { + var c = crc_table[n] = Buffer.alloc(4); + c.writeUInt32BE(n, 0); + + for (var k = 0; k < 8; k++) { + var b0 = c[0] & 1; + var b1 = c[1] & 1; + var b2 = c[2] & 1; + var b3 = c[3] & 1; + + c[0] = (c[0] >> 1) ^ (b3 ? 0xed : 0); + c[1] = (c[1] >> 1) ^ (b3 ? 0xb8 : 0) ^ (b0 ? 0x80 : 0); + c[2] = (c[2] >> 1) ^ (b3 ? 0x83 : 0) ^ (b1 ? 0x80 : 0); + c[3] = (c[3] >> 1) ^ (b3 ? 0x20 : 0) ^ (b2 ? 0x80 : 0); + } +} + +function update(c, buf) { + var l = buf.length; + for (var n = 0; n < l; n++) { + var e = crc_table[c[3] ^ buf[n]]; + c[3] = e[3] ^ c[2]; + c[2] = e[2] ^ c[1]; + c[1] = e[1] ^ c[0]; + c[0] = e[0]; + } +} + +function crc32(/* arguments */) { + var l = arguments.length; + var c = Buffer.alloc(4); + c.fill(0xff); + + for (var i = 0; i < l; i++) { + update(c, Buffer.from(arguments[i])); + } + + c[0] = c[0] ^ 0xff; + c[1] = c[1] ^ 0xff; + c[2] = c[2] ^ 0xff; + c[3] = c[3] ^ 0xff; + + return c.readUInt32BE(0); +} + +module.exports = crc32; diff --git a/qr/encode.js b/qr/encode.js new file mode 100644 index 0000000..fc56dbd --- /dev/null +++ b/qr/encode.js @@ -0,0 +1,175 @@ +"use strict"; + +function pushBits(arr, n, value) { + for (var bit = 1 << (n - 1); bit; bit = bit >>> 1) { + arr.push(bit & value ? 1 : 0); + } +} + +// {{{1 8bit encode +function encode_8bit(data) { + var len = data.length; + var bits = []; + + for (var i = 0; i < len; i++) { + pushBits(bits, 8, data[i]); + } + + var res = {}; + + var d = [0, 1, 0, 0]; + pushBits(d, 16, len); + res.data10 = res.data27 = d.concat(bits); + + if (len < 256) { + var d = [0, 1, 0, 0]; + pushBits(d, 8, len); + res.data1 = d.concat(bits); + } + + return res; +} + +// {{{1 alphanumeric encode +var ALPHANUM = (function (s) { + var res = {}; + for (var i = 0; i < s.length; i++) { + res[s[i]] = i; + } + return res; +})('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'); + +function encode_alphanum(str) { + var len = str.length; + var bits = []; + + for (var i = 0; i < len; i += 2) { + var b = 6; + var n = ALPHANUM[str[i]]; + if (str[i + 1]) { + b = 11; + n = n * 45 + ALPHANUM[str[i + 1]]; + } + pushBits(bits, b, n); + } + + var res = {}; + + var d = [0, 0, 1, 0]; + pushBits(d, 13, len); + res.data27 = d.concat(bits); + + if (len < 2048) { + var d = [0, 0, 1, 0]; + pushBits(d, 11, len); + res.data10 = d.concat(bits); + } + + if (len < 512) { + var d = [0, 0, 1, 0]; + pushBits(d, 9, len); + res.data1 = d.concat(bits); + } + + return res; +} + +// {{{1 numeric encode +function encode_numeric(str) { + var len = str.length; + var bits = []; + + for (var i = 0; i < len; i += 3) { + var s = str.substr(i, 3); + var b = Math.ceil(s.length * 10 / 3); + pushBits(bits, b, parseInt(s, 10)); + } + + var res = {}; + + var d = [0, 0, 0, 1]; + pushBits(d, 14, len); + res.data27 = d.concat(bits); + + if (len < 4096) { + var d = [0, 0, 0, 1]; + pushBits(d, 12, len); + res.data10 = d.concat(bits); + } + + if (len < 1024) { + var d = [0, 0, 0, 1]; + pushBits(d, 10, len); + res.data1 = d.concat(bits); + } + + return res; +} + +// {{{1 url encode +function encode_url(str) { + var slash = str.indexOf('/', 8) + 1 || str.length; + var res = encode(str.slice(0, slash).toUpperCase(), false); + + if (slash >= str.length) { + return res; + } + + var path_res = encode(str.slice(slash), false); + + res.data27 = res.data27.concat(path_res.data27); + + if (res.data10 && path_res.data10) { + res.data10 = res.data10.concat(path_res.data10); + } + + if (res.data1 && path_res.data1) { + res.data1 = res.data1.concat(path_res.data1); + } + + return res; +} + +// {{{1 Choose encode mode and generates struct with data for different version +function encode(data, parse_url) { + var str; + var t = typeof data; + + if (t == 'string' || t == 'number') { + str = '' + data; + data = Buffer.from(str); + } else if (Buffer.isBuffer(data)) { + str = data.toString(); + } else if (Array.isArray(data)) { + data = Buffer.from(data); + str = data.toString(); + } else { + throw new Error("Bad data"); + } + + if (/^[0-9]+$/.test(str)) { + if (data.length > 7089) { + throw new Error("Too much data"); + } + return encode_numeric(str); + } + + if (/^[0-9A-Z \$%\*\+\.\/\:\-]+$/.test(str)) { + if (data.length > 4296) { + throw new Error("Too much data"); + } + return encode_alphanum(str); + } + + if (parse_url && /^https?:/i.test(str)) { + return encode_url(str); + } + + if (data.length > 2953) { + throw new Error("Too much data"); + } + return encode_8bit(data); +} + +// {{{1 export functions +module.exports = encode; diff --git a/qr/errorcode.js b/qr/errorcode.js new file mode 100644 index 0000000..afb99c4 --- /dev/null +++ b/qr/errorcode.js @@ -0,0 +1,77 @@ +"use strict"; + +// {{{1 Galois Field Math +var GF256_BASE = 285; + +var EXP_TABLE = [1]; +var LOG_TABLE = []; + +for (var i = 1; i < 256; i++) { + var n = EXP_TABLE[i - 1] << 1; + if (n > 255) n = n ^ GF256_BASE; + EXP_TABLE[i] = n; +} + +for (var i = 0; i < 255; i++) { + LOG_TABLE[EXP_TABLE[i]] = i; +} + +function exp(k) { + while (k < 0) k += 255; + while (k > 255) k -= 255; + return EXP_TABLE[k]; +} + +function log(k) { + if (k < 1 || k > 255) { + throw Error('Bad log(' + k + ')'); + } + return LOG_TABLE[k]; +} + +// {{{1 Generator Polynomials +var POLYNOMIALS = [ + [0], // a^0 x^0 + [0, 0], // a^0 x^1 + a^0 x^0 + [0, 25, 1], // a^0 x^2 + a^25 x^1 + a^1 x^0 + // and so on... +]; + +function generatorPolynomial(num) { + if (POLYNOMIALS[num]) { + return POLYNOMIALS[num]; + } + var prev = generatorPolynomial(num - 1); + var res = []; + + res[0] = prev[0]; + for (var i = 1; i <= num; i++) { + res[i] = log(exp(prev[i]) ^ exp(prev[i - 1] + num - 1)); + } + POLYNOMIALS[num] = res; + return res; +} + +// {{{1 export functions +module.exports = function calculate_ec(msg, ec_len) { + // `msg` could be array or buffer + // convert `msg` to array + msg = [].slice.call(msg); + + // Generator Polynomial + var poly = generatorPolynomial(ec_len); + + for (var i = 0; i < ec_len; i++) msg.push(0); + while (msg.length > ec_len) { + if (!msg[0]) { + msg.shift(); + continue; + } + var log_k = log(msg[0]); + for (var i = 0; i <= ec_len; i++) { + msg[i] = msg[i] ^ exp(poly[i] + log_k); + } + msg.shift(); + } + return Buffer.from(msg); +} diff --git a/qr/matrix.js b/qr/matrix.js new file mode 100644 index 0000000..aea0fd7 --- /dev/null +++ b/qr/matrix.js @@ -0,0 +1,352 @@ +"use strict"; + +// {{{1 Initialize matrix with zeros +function init(version) { + var N = version * 4 + 17; + var matrix = []; + var zeros = new Buffer(N); + zeros.fill(0); + zeros = [].slice.call(zeros); + for (var i = 0; i < N; i++) { + matrix[i] = zeros.slice(); + } + return matrix; +} + +// {{{1 Put finders into matrix +function fillFinders(matrix) { + var N = matrix.length; + for (var i = -3; i <= 3; i++) { + for (var j = -3; j <= 3; j++) { + var max = Math.max(i, j); + var min = Math.min(i, j); + var pixel = (max == 2 && min >= -2) || (min == -2 && max <= 2) ? 0x80 : 0x81; + matrix[3 + i][3 + j] = pixel; + matrix[3 + i][N - 4 + j] = pixel; + matrix[N - 4 + i][3 + j] = pixel; + } + } + for (var i = 0; i < 8; i++) { + matrix[7][i] = matrix[i][7] = + matrix[7][N - i - 1] = matrix[i][N - 8] = + matrix[N - 8][i] = matrix[N - 1 - i][7] = 0x80; + } +} + +// {{{1 Put align and timinig +function fillAlignAndTiming(matrix) { + var N = matrix.length; + if (N > 21) { + var len = N - 13; + var delta = Math.round(len / Math.ceil(len / 28)); + if (delta % 2) delta++; + var res = []; + for (var p = len + 6; p > 10; p -= delta) { + res.unshift(p); + } + res.unshift(6); + for (var i = 0; i < res.length; i++) { + for (var j = 0; j < res.length; j++) { + var x = res[i], y = res[j]; + if (matrix[x][y]) continue; + for (var r = -2; r <=2 ; r++) { + for (var c = -2; c <=2 ; c++) { + var max = Math.max(r, c); + var min = Math.min(r, c); + var pixel = (max == 1 && min >= -1) || (min == -1 && max <= 1) ? 0x80 : 0x81; + matrix[x + r][y + c] = pixel; + } + } + } + } + } + for (var i = 8; i < N - 8; i++) { + matrix[6][i] = matrix[i][6] = i % 2 ? 0x80 : 0x81; + } +} + +// {{{1 Fill reserved areas with zeroes +function fillStub(matrix) { + var N = matrix.length; + for (var i = 0; i < 8; i++) { + if (i != 6) { + matrix[8][i] = matrix[i][8] = 0x80; + } + matrix[8][N - 1 - i] = 0x80; + matrix[N - 1 - i][8] = 0x80; + } + matrix[8][8] = 0x80; + matrix[N - 8][8] = 0x81; + + if (N < 45) return; + + for (var i = N - 11; i < N - 8; i++) { + for (var j = 0; j < 6; j++) { + matrix[i][j] = matrix[j][i] = 0x80; + } + } +} + +// {{{1 Fill reserved areas +var fillReserved = (function() { + var FORMATS = Array(32); + var VERSIONS = Array(40); + + var gf15 = 0x0537; + var gf18 = 0x1f25; + var formats_mask = 0x5412; + + for (var format = 0; format < 32; format++) { + var res = format << 10; + for (var i = 5; i > 0; i--) { + if (res >>> (9 + i)) { + res = res ^ (gf15 << (i - 1)); + } + } + FORMATS[format] = (res | (format << 10)) ^ formats_mask; + } + + for (var version = 7; version <= 40; version++) { + var res = version << 12; + for (var i = 6; i > 0; i--) { + if (res >>> (11 + i)) { + res = res ^ (gf18 << (i - 1)); + } + } + VERSIONS[version] = (res | (version << 12)); + } + + var EC_LEVELS = { L: 1, M: 0, Q: 3, H: 2 }; + + return function fillReserved(matrix, ec_level, mask) { + var N = matrix.length; + var format = FORMATS[EC_LEVELS[ec_level] << 3 | mask]; + function F(k) { return format >> k & 1 ? 0x81 : 0x80 }; + for (var i = 0; i < 8; i++) { + matrix[8][N - 1 - i] = F(i); + if (i < 6) matrix[i][8] = F(i); + } + for (var i = 8; i < 15; i++) { + matrix[N - 15 + i][8] = F(i); + if (i > 8) matrix[8][14 - i] = F(i); + } + matrix[7][8] = F(6); + matrix[8][8] = F(7); + matrix[8][7] = F(8); + + var version = VERSIONS[(N - 17)/4]; + if (!version) return; + function V(k) { return version >> k & 1 ? 0x81 : 0x80 }; + for (var i = 0; i < 6; i++) { + for (var j = 0; j < 3; j++) { + matrix[N - 11 + j][i] = matrix[i][N - 11 + j] = V(i * 3 + j); + } + } + } +})(); + +// {{{1 Fill data +var fillData = (function() { + var MASK_FUNCTIONS = [ + function(i, j) { return (i + j) % 2 == 0 }, + function(i, j) { return i % 2 == 0 }, + function(i, j) { return j % 3 == 0 }, + function(i, j) { return (i + j) % 3 == 0 }, + function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0 }, + function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0 }, + function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0 }, + function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0 } + ]; + + return function fillData(matrix, data, mask) { + var N = matrix.length; + var row, col, dir = -1; + row = col = N - 1; + var mask_fn = MASK_FUNCTIONS[mask]; + var len = data.blocks[data.blocks.length - 1].length; + + for (var i = 0; i < len; i++) { + for (var b = 0; b < data.blocks.length; b++) { + if (data.blocks[b].length <= i) continue; + put(data.blocks[b][i]); + } + } + + len = data.ec_len; + for (var i = 0; i < len; i++) { + for (var b = 0; b < data.ec.length; b++) { + put(data.ec[b][i]); + } + } + + if (col > -1) { + do { + matrix[row][col] = mask_fn(row, col) ? 1 : 0; + } while (next()); + } + + function put(byte) { + for (var mask = 0x80; mask; mask = mask >> 1) { + var pixel = !!(mask & byte); + if (mask_fn(row, col)) pixel = !pixel; + matrix[row][col] = pixel ? 1 : 0; + next(); + } + } + + function next() { + do { + if ((col % 2) ^ (col < 6)) { + if (dir < 0 && row == 0 || dir > 0 && row == N - 1) { + col--; + dir = -dir; + } else { + col++; + row += dir; + } + } else { + col--; + } + if (col == 6) { + col--; + } + if (col < 0) { + return false; + } + } while (matrix[row][col] & 0xf0); + return true; + } + } +})(); + +// {{{1 Calculate penalty +function calculatePenalty(matrix) { + var N = matrix.length; + var penalty = 0; + // Rule 1 + for (var i = 0; i < N; i++) { + var pixel = matrix[i][0] & 1; + var len = 1; + for (var j = 1; j < N; j++) { + var p = matrix[i][j] & 1; + if (p == pixel) { + len++; + continue; + } + if (len >= 5) { + penalty += len - 2; + } + pixel = p; + len = 1; + } + if (len >= 5) { + penalty += len - 2; + } + } + for (var j = 0; j < N; j++) { + var pixel = matrix[0][j] & 1; + var len = 1; + for (var i = 1; i < N; i++) { + var p = matrix[i][j] & 1; + if (p == pixel) { + len++; + continue; + } + if (len >= 5) { + penalty += len - 2; + } + pixel = p; + len = 1; + } + if (len >= 5) { + penalty += len - 2; + } + } + + // Rule 2 + for (var i = 0; i < N - 1; i++) { + for (var j = 0; j < N - 1; j++) { + var s = matrix[i][j] + matrix[i][j + 1] + matrix[i + 1][j] + matrix[i + 1][j + 1] & 7; + if (s == 0 || s == 4) { + penalty += 3; + } + } + } + + // Rule 3 + function I(k) { return matrix[i][j + k] & 1 }; + function J(k) { return matrix[i + k][j] & 1 }; + for (var i = 0; i < N; i++) { + for (var j = 0; j < N; j++) { + if (j < N - 6 && I(0) && !I(1) && I(2) && I(3) && I(4) && !I(5) && I(6)) { + if (j >= 4 && !(I(-4) || I(-3) || I(-2) || I(-1))) { + penalty += 40; + } + if (j < N - 10 && !(I(7) || I(8) || I(9) || I(10))) { + penalty += 40; + } + } + + if (i < N - 6 && J(0) && !J(1) && J(2) && J(3) && J(4) && !J(5) && J(6)) { + if (i >= 4 && !(J(-4) || J(-3) || J(-2) || J(-1))) { + penalty += 40; + } + if (i < N - 10 && !(J(7) || J(8) || J(9) || J(10))) { + penalty += 40; + } + } + } + } + + // Rule 4 + var numDark = 0; + for (var i = 0; i < N; i++) { + for (var j = 0; j < N; j++) { + if (matrix[i][j] & 1) numDark++; + } + } + penalty += 10 * Math.floor(Math.abs(10 - 20 * numDark/(N * N))); + + return penalty; +} + +// {{{1 All-in-one function +function getMatrix(data) { + var matrix = init(data.version); + fillFinders(matrix); + fillAlignAndTiming(matrix); + fillStub(matrix); + + var penalty = Infinity; + var bestMask = 0; + for (var mask = 0; mask < 8; mask++) { + fillData(matrix, data, mask); + fillReserved(matrix, data.ec_level, mask); + var p = calculatePenalty(matrix); + if (p < penalty) { + penalty = p; + bestMask = mask; + } + } + + fillData(matrix, data, bestMask); + fillReserved(matrix, data.ec_level, bestMask); + + return matrix.map(function(row) { + return row.map(function(cell) { + return cell & 1; + }); + }); +} + +// {{{1 export functions +module.exports = { + getMatrix: getMatrix, + init: init, + fillFinders: fillFinders, + fillAlignAndTiming: fillAlignAndTiming, + fillStub: fillStub, + fillReserved: fillReserved, + fillData: fillData, + calculatePenalty: calculatePenalty, +} diff --git a/qr/png.js b/qr/png.js new file mode 100644 index 0000000..20ed92a --- /dev/null +++ b/qr/png.js @@ -0,0 +1,64 @@ +"use strict"; + +var zlib = require('zlib'); + +var crc32 = require('./crc32buffer'); + +var PNG_HEAD = new Buffer([137, 80, 78, 71, 13, 10, 26, 10]); +var PNG_IHDR = new Buffer([0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0]); +var PNG_IDAT = new Buffer([0, 0, 0, 0, 73, 68, 65, 84]); +var PNG_IEND = new Buffer([0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130]); + +function png(bitmap, stream) { + stream.push(PNG_HEAD); + + var IHDR = Buffer.concat([PNG_IHDR]); + IHDR.writeUInt32BE(bitmap.size, 8); + IHDR.writeUInt32BE(bitmap.size, 12); + IHDR.writeUInt32BE(crc32(IHDR.slice(4, -4)), 21); + stream.push(IHDR); + + var IDAT = Buffer.concat([ + PNG_IDAT, + zlib.deflateSync(bitmap.data, { level: 9 }), + new Buffer(4) + ]); + IDAT.writeUInt32BE(IDAT.length - 12, 0); + IDAT.writeUInt32BE(crc32(IDAT.slice(4, -4)), IDAT.length - 4); + stream.push(IDAT); + + stream.push(PNG_IEND); + stream.push(null); +} + +function bitmap(matrix, size, margin) { + var N = matrix.length; + var X = (N + 2 * margin) * size; + var data = new Buffer((X + 1) * X); + data.fill(255); + for (var i = 0; i < X; i++) { + data[i * (X + 1)] = 0; + } + + for (var i = 0; i < N; i++) { + for (var j = 0; j < N; j++) { + if (matrix[i][j]) { + var offset = ((margin + i) * (X + 1) + (margin + j)) * size + 1; + data.fill(0, offset, offset + size); + for (var c = 1; c < size; c++) { + data.copy(data, offset + c * (X + 1), offset, offset + size); + } + } + } + } + + return { + data: data, + size: X + } +} + +module.exports = { + bitmap: bitmap, + png: png +} diff --git a/qr/qr-base.js b/qr/qr-base.js new file mode 100644 index 0000000..808eb39 --- /dev/null +++ b/qr/qr-base.js @@ -0,0 +1,180 @@ +"use strict"; + +var encode = require('./encode'); +var calculateEC = require('./errorcode'); +var matrix = require('./matrix'); + +function _deepCopy(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +var EC_LEVELS = ['L', 'M', 'Q', 'H']; + +// {{{1 Versions +var versions = [ + [], // there is no version 0 + // total number of codewords, (number of ec codewords, number of blocks) * ( L, M, Q, H ) + [26, 7, 1, 10, 1, 13, 1, 17, 1], + [44, 10, 1, 16, 1, 22, 1, 28, 1], + [70, 15, 1, 26, 1, 36, 2, 44, 2], + [100, 20, 1, 36, 2, 52, 2, 64, 4], + [134, 26, 1, 48, 2, 72, 4, 88, 4], // 5 + [172, 36, 2, 64, 4, 96, 4, 112, 4], + [196, 40, 2, 72, 4, 108, 6, 130, 5], + [242, 48, 2, 88, 4, 132, 6, 156, 6], + [292, 60, 2, 110, 5, 160, 8, 192, 8], + [346, 72, 4, 130, 5, 192, 8, 224, 8], // 10 + [404, 80, 4, 150, 5, 224, 8, 264, 11], + [466, 96, 4, 176, 8, 260, 10, 308, 11], + [532, 104, 4, 198, 9, 288, 12, 352, 16], + [581, 120, 4, 216, 9, 320, 16, 384, 16], + [655, 132, 6, 240, 10, 360, 12, 432, 18], // 15 + [733, 144, 6, 280, 10, 408, 17, 480, 16], + [815, 168, 6, 308, 11, 448, 16, 532, 19], + [901, 180, 6, 338, 13, 504, 18, 588, 21], + [991, 196, 7, 364, 14, 546, 21, 650, 25], + [1085, 224, 8, 416, 16, 600, 20, 700, 25], // 20 + [1156, 224, 8, 442, 17, 644, 23, 750, 25], + [1258, 252, 9, 476, 17, 690, 23, 816, 34], + [1364, 270, 9, 504, 18, 750, 25, 900, 30], + [1474, 300, 10, 560, 20, 810, 27, 960, 32], + [1588, 312, 12, 588, 21, 870, 29, 1050, 35], // 25 + [1706, 336, 12, 644, 23, 952, 34, 1110, 37], + [1828, 360, 12, 700, 25, 1020, 34, 1200, 40], + [1921, 390, 13, 728, 26, 1050, 35, 1260, 42], + [2051, 420, 14, 784, 28, 1140, 38, 1350, 45], + [2185, 450, 15, 812, 29, 1200, 40, 1440, 48], // 30 + [2323, 480, 16, 868, 31, 1290, 43, 1530, 51], + [2465, 510, 17, 924, 33, 1350, 45, 1620, 54], + [2611, 540, 18, 980, 35, 1440, 48, 1710, 57], + [2761, 570, 19, 1036, 37, 1530, 51, 1800, 60], + [2876, 570, 19, 1064, 38, 1590, 53, 1890, 63], // 35 + [3034, 600, 20, 1120, 40, 1680, 56, 1980, 66], + [3196, 630, 21, 1204, 43, 1770, 59, 2100, 70], + [3362, 660, 22, 1260, 45, 1860, 62, 2220, 74], + [3532, 720, 24, 1316, 47, 1950, 65, 2310, 77], + [3706, 750, 25, 1372, 49, 2040, 68, 2430, 81] // 40 +]; + +versions = versions.map(function(v, index) { + if (!index) return {}; + + var res = { + } + for (var i = 1; i < 8; i += 2) { + var length = v[0] - v[i]; + var num_template = v[i+1]; + var ec_level = EC_LEVELS[(i/2)|0]; + var level = { + version: index, + ec_level: ec_level, + data_len: length, + ec_len: v[i] / num_template, + blocks: [], + ec: [] + } + + for (var k = num_template, n = length; k > 0; k--) { + var block = (n / k)|0; + level.blocks.push(block); + n -= block; + + } + res[ec_level] = level; + } + return res; +}); + +// {{{1 Get version template +function getTemplate(message, ec_level) { + var i = 1; + var len; + + if (message.data1) { + len = Math.ceil(message.data1.length / 8); + } else { + i = 10; + } + for (/* i */; i < 10; i++) { + var version = versions[i][ec_level]; + if (version.data_len >= len) { + return _deepCopy(version); + } + } + + if (message.data10) { + len = Math.ceil(message.data10.length / 8); + } else { + i = 27; + } + for (/* i */; i < 27; i++) { + var version = versions[i][ec_level]; + if (version.data_len >= len) { + return _deepCopy(version); + } + } + + len = Math.ceil(message.data27.length / 8); + for (/* i */; i < 41; i++) { + var version = versions[i][ec_level]; + if (version.data_len >= len) { + return _deepCopy(version); + } + } + throw new Error("Too much data"); +} + +// {{{1 Fill template +function fillTemplate(message, template) { + var blocks = new Buffer(template.data_len); + blocks.fill(0); + + if (template.version < 10) { + message = message.data1; + } else if (template.version < 27) { + message = message.data10; + } else { + message = message.data27; + } + + var len = message.length; + + for (var i = 0; i < len; i += 8) { + var b = 0; + for (var j = 0; j < 8; j++) { + b = (b << 1) | (message[i + j] ? 1 : 0); + } + blocks[i / 8] = b; + } + + var pad = 236; + for (var i = Math.ceil((len + 4) / 8); i < blocks.length; i++) { + blocks[i] = pad; + pad = (pad == 236) ? 17 : 236; + } + + var offset = 0; + template.blocks = template.blocks.map(function(n) { + var b = blocks.slice(offset, offset + n); + offset += n; + template.ec.push(calculateEC(b, template.ec_len)); + return b; + }); + + return template; +} + +// {{{1 All-in-one +function QR(text, ec_level, parse_url) { + ec_level = EC_LEVELS.indexOf(ec_level) > -1 ? ec_level : 'M'; + var message = encode(text, parse_url); + var data = fillTemplate(message, getTemplate(message, ec_level)); + return matrix.getMatrix(data); +} + +// {{{1 export functions +module.exports = { + QR: QR, + getTemplate: getTemplate, + fillTemplate: fillTemplate, +} diff --git a/qr/qr.js b/qr/qr.js new file mode 100644 index 0000000..9bd792b --- /dev/null +++ b/qr/qr.js @@ -0,0 +1,121 @@ +"use strict"; + +var Readable = require('stream').Readable; + +var QR = require('./qr-base').QR; +var png = require('./png'); +var vector = require('./vector'); + +var fn_noop = function() {}; + +var BITMAP_OPTIONS = { + parse_url: false, + ec_level: 'M', + size: 5, + margin: 4, + customize: null +}; + +var VECTOR_OPTIONS = { + parse_url: false, + ec_level: 'M', + margin: 1, + size: 0 +}; + +function get_options(options, force_type) { + if (typeof options === 'string') { + options = { 'ec_level': options } + } else { + options = options || {}; + } + var _options = { + type: String(force_type || options.type || 'png').toLowerCase() + }; + + var defaults = _options.type == 'png' ? BITMAP_OPTIONS : VECTOR_OPTIONS; + + for (var k in defaults) { + _options[k] = k in options ? options[k] : defaults[k]; + } + + return _options; +} + +function qr_image(text, options) { + options = get_options(options); + + var matrix = QR(text, options.ec_level, options.parse_url); + var stream = new Readable(); + stream._read = fn_noop; + + switch (options.type) { + case 'svg': + case 'pdf': + case 'eps': + process.nextTick(function() { + vector[options.type](matrix, stream, options.margin, options.size); + }); + break; + case 'svgpath': + // deprecated, use svg_object method + process.nextTick(function() { + var obj = vector.svg_object(matrix, options.margin, options.size); + stream.push(obj.path); + stream.push(null); + }); + break; + case 'png': + default: + process.nextTick(function() { + var bitmap = png.bitmap(matrix, options.size, options.margin); + if (options.customize) { + options.customize(bitmap); + } + png.png(bitmap, stream); + }); + } + + return stream; +} + +function qr_image_sync(text, options) { + options = get_options(options); + + var matrix = QR(text, options.ec_level, options.parse_url); + var stream = []; + var result; + + switch (options.type) { + case 'svg': + case 'pdf': + case 'eps': + vector[options.type](matrix, stream, options.margin, options.size); + result = stream.filter(Boolean).join(''); + break; + case 'png': + default: + var bitmap = png.bitmap(matrix, options.size, options.margin); + if (options.customize) { + options.customize(bitmap); + } + png.png(bitmap, stream); + result = Buffer.concat(stream.filter(Boolean)); + } + + return result; +} + +function svg_object(text, options) { + options = get_options(options, 'svg'); + + var matrix = QR(text, options.ec_level); + return vector.svg_object(matrix, options.margin); +} + +module.exports = { + matrix: QR, + image: qr_image, + imageSync: qr_image_sync, + svgObject: svg_object +}; diff --git a/qr/vector.js b/qr/vector.js new file mode 100644 index 0000000..56211ae --- /dev/null +++ b/qr/vector.js @@ -0,0 +1,260 @@ +"use strict"; + +function matrix2path(matrix) { + var N = matrix.length; + var filled = []; + for (var row = -1; row <= N; row++) { + filled[row] = []; + } + + var path = []; + for (var row = 0; row < N; row++) { + for (var col = 0; col < N; col++) { + if (filled[row][col]) continue; + filled[row][col] = 1; + if (isDark(row, col)) { + if (!isDark(row - 1, col)) { + path.push(plot(row, col, 'right')); + } + } else { + if (isDark(row, col - 1)) { + path.push(plot(row, col, 'down')); + } + } + } + } + return path; + + function isDark(row, col) { + if (row < 0 || col < 0 || row >= N || col >= N) return false; + return !!matrix[row][col]; + } + + function plot(row0, col0, dir) { + filled[row0][col0] = 1; + var res = []; + res.push(['M', col0, row0 ]); + var row = row0; + var col = col0; + var len = 0; + do { + switch (dir) { + case 'right': + filled[row][col] = 1; + if (isDark(row, col)) { + filled[row - 1][col] = 1; + if (isDark(row - 1, col)) { + res.push(['h', len]); + len = 0; + dir = 'up'; + } else { + len++; + col++; + } + } else { + res.push(['h', len]); + len = 0; + dir = 'down'; + } + break; + case 'left': + filled[row - 1][col - 1] = 1; + if (isDark(row - 1, col - 1)) { + filled[row][col - 1] = 1; + if (isDark(row, col - 1)) { + res.push(['h', -len]); + len = 0; + dir = 'down'; + } else { + len++; + col--; + } + } else { + res.push(['h', -len]); + len = 0; + dir = 'up'; + } + break; + case 'down': + filled[row][col - 1] = 1; + if (isDark(row, col - 1)) { + filled[row][col] = 1; + if (isDark(row, col)) { + res.push(['v', len]); + len = 0; + dir = 'right'; + } else { + len++; + row++; + } + } else { + res.push(['v', len]); + len = 0; + dir = 'left'; + } + break; + case 'up': + filled[row - 1][col] = 1; + if (isDark(row - 1, col)) { + filled[row - 1][col - 1] = 1; + if (isDark(row - 1, col - 1)) { + res.push(['v', -len]); + len = 0; + dir = 'left'; + } else { + len++; + row--; + } + } else { + res.push(['v', -len]); + len = 0; + dir = 'right'; + } + break; + } + } while (row != row0 || col != col0); + return res; + } +} + +function pushSVGPath(matrix, stream, margin) { + matrix2path(matrix).forEach(function(subpath) { + var res = ''; + for (var k = 0; k < subpath.length; k++) { + var item = subpath[k]; + switch (item[0]) { + case 'M': + res += 'M' + (item[1] + margin) + ' ' + (item[2] + margin); + break; + default: + res += item.join(''); + } + } + res += 'z'; + stream.push(res); + }); +} + +function SVG_object(matrix, margin) { + var stream = []; + pushSVGPath(matrix, stream, margin); + + var result = { + size: matrix.length + 2 * margin, + path: stream.filter(Boolean).join('') + } + + return result; +} + +function SVG(matrix, stream, margin, size) { + var X = matrix.length + 2 * margin; + stream.push(' 0) { + var XY = X * size; + stream.push('width="' + XY + '" height="' + XY + '" '); + } + stream.push('viewBox="0 0 ' + X + ' ' + X + '">'); + stream.push(''); + stream.push(null); +} + +function EPS(matrix, stream, margin) { + var N = matrix.length; + var scale = 9; + var X = (N + 2 * margin) * scale; + stream.push([ + '%!PS-Adobe-3.0 EPSF-3.0', + '%%BoundingBox: 0 0 ' + X + ' ' + X, + '/h { 0 rlineto } bind def', + '/v { 0 exch neg rlineto } bind def', + '/M { neg ' + (N + margin) + ' add moveto } bind def', + '/z { closepath } bind def', + scale + ' ' + scale + ' scale', + '' + ].join('\n')); + + matrix2path(matrix).forEach(function(subpath) { + var res = ''; + for (var k = 0; k < subpath.length; k++) { + var item = subpath[k]; + switch (item[0]) { + case 'M': + res += (item[1] + margin) + ' ' + item[2] + ' M '; + break; + default: + res += item[1] + ' ' + item[0] + ' '; + } + } + res += 'z\n'; + stream.push(res); + }); + + stream.push('fill\n%%EOF\n'); + stream.push(null); +} + +function PDF(matrix, stream, margin) { + // TODO deflate + var N = matrix.length; + var scale = 9; + var X = (N + 2 * margin) * scale; + var data = [ + '%PDF-1.0\n\n', + '1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n', + '2 0 obj << /Type /Pages /Count 1 /Kids [ 3 0 R ] >> endobj\n', + ]; + data.push('3 0 obj << /Type /Page /Parent 2 0 R /Resources <<>> ' + + '/Contents 4 0 R /MediaBox [ 0 0 ' + X + ' ' + X + ' ] >> endobj\n'); + + var path = scale + ' 0 0 ' + scale + ' 0 0 cm\n'; + path += matrix2path(matrix).map(function(subpath) { + var res = ''; + var x, y; + for (var k = 0; k < subpath.length; k++) { + var item = subpath[k]; + switch (item[0]) { + case 'M': + x = item[1] + margin; + y = N - item[2] + margin; + res += x + ' ' + y + ' m '; + break; + case 'h': + x += item[1]; + res += x + ' ' + y + ' l '; + break; + case 'v': + y -= item[1]; + res += x + ' ' + y + ' l '; + break; + } + } + res += 'h'; + return res; + }).join('\n'); + path += '\nf\n'; + data.push('4 0 obj << /Length ' + path.length + ' >> stream\n' + + path + 'endstream\nendobj\n'); + + var xref = 'xref\n0 5\n0000000000 65535 f \n'; + for (var i = 1, l = data[0].length; i < 5; i++) { + xref += ('0000000000' + l).substr(-10) + ' 00000 n \n'; + l += data[i].length; + } + data.push( + xref, + 'trailer << /Root 1 0 R /Size 5 >>\n', + 'startxref\n' + l + '\n%%EOF\n' + ); + stream.push(data.join('')); + stream.push(null); +} + +module.exports = { + svg: SVG, + eps: EPS, + pdf: PDF, + svg_object: SVG_object +}