Initial commit

This commit is contained in:
Jonas L
2019-02-25 00:00:00 +00:00
commit 22c372e246
200 changed files with 27781 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { split } from 'lodash'
export const serializedBitmaskRegex = /^(\d*,\d*(,\d*,\d*)*)?$/
export const validateBitmask = (bitmask: string, maxLength: number) => {
if (!serializedBitmaskRegex.test(bitmask)) {
throw new Error('bitmask does not match regex')
}
if (bitmask === '') {
return
}
const splitpoints = split(bitmask, ',').map((item) => parseInt(item, 10))
if (splitpoints.findIndex((item) => !Number.isSafeInteger(item)) !== -1) {
throw new Error('bitmask contains non-safe integers')
}
if (splitpoints.findIndex((item) => item < 0) !== -1) {
throw new Error('bitmask contains negative integers')
}
if (splitpoints.findIndex((item) => item > maxLength) !== -1) {
throw new Error('bitmask contains integers bigger than maxSize')
}
let previousValue = -1
splitpoints.forEach((item) => {
if (item <= previousValue) {
throw new Error('bitmask numbers are not strictly sorted')
}
previousValue = item
})
}
+20
View File
@@ -0,0 +1,20 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const enumMax = <T> (a: T, b: T, values: Array<T>): T => (
values[Math.max(values.indexOf(a), values.indexOf(b))]
)
+30
View File
@@ -0,0 +1,30 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export function assertIsHexString (value: string) {
if (value.length % 2 !== 0) {
throw new Error('expected hex string but has got uneven length')
}
for (let i = 0; i < value.length; i++) {
const char = value[i]
if ('0123456789abcdef'.indexOf(char) === -1) {
throw new Error('expected hex string but got invalid char')
}
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { uniq } from 'lodash'
export function assertNonEmptyListWithoutDuplicates (list: Array<string>) {
if (list.length === 0) {
throw new Error('expected not empty list')
}
if (uniq(list).length !== list.length) {
throw new Error('expected list without duplicates')
}
}
+83
View File
@@ -0,0 +1,83 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as Email from 'email-templates'
import { join } from 'path'
const mailimprint = process.env.MAIL_IMPRINT || 'not defined'
const email = new Email({
message: {
from: process.env.MAIL_SENDER || ''
},
transport: JSON.parse(process.env.MAIL_TRANSPORT || 'null') || undefined,
views: {
options: {
extension: 'ejs'
}
}
})
export const sendAuthenticationMail = async ({ receiver, code, locale }: {receiver: string, code: string, locale: string}) => {
await email.send({
template: join(__dirname, '../../other/mail/login'),
message: {
to: receiver
},
locals: {
subject: locale === 'de' ? 'Anmeldung bei TimeLimit' : 'Sign in at TimeLimit',
introtext: locale === 'de' ? 'Geben Sie zum Authentifizieren folgenden Code in TimeLimit ein' : 'To authenticate, enter the following code in TimeLimit',
code,
outrotext: locale === 'de' ? 'Geben Sie diesen Code nicht an Dritte weiter.' : 'Do not share this code with third parties.',
mailimprint
}
})
}
export const sendManipulationWarningMail = async ({ receiver, deviceName }: {
receiver: string
deviceName: string
}) => {
await email.send({
template: join(__dirname, '../../other/mail/manipulation'),
message: {
to: receiver
},
locals: {
subject: 'TimeLimit@' + deviceName + ' - Manipulation',
deviceName,
mailimprint
}
})
}
export const sendUninstallWarningMail = async ({ receiver, deviceName }: {
receiver: string
deviceName: string
}) => {
await email.send({
template: join(__dirname, '../../other/mail/uninstall'),
message: {
to: receiver
},
locals: {
subject: 'TimeLimit removed from ' + deviceName,
deviceName,
mailimprint
}
})
}
+19
View File
@@ -0,0 +1,19 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const optionalPasswordRegex = /^(\$2a\$[123][0-9]\$[./A-Za-z0-9]{53})?$/
export const optionalSaltRegex = /^(\$2a\$[123][0-9]\$[./A-Za-z0-9]{22})?$/
+33
View File
@@ -0,0 +1,33 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { readFileSync } from 'fs'
import { range } from 'lodash'
import { resolve } from 'path'
const wordlist = readFileSync(resolve(__dirname, '../../other/wordlist/de.txt'))
.toString()
.split('\n')
.filter((item) => item.trim().length > 0)
const randomWord = () => wordlist[Math.floor(Math.random() * (wordlist.length - 1))]
export const randomWords = (numberOfWords: number) => (
range(numberOfWords)
.map((item) => randomWord())
.join(' ')
)
+64
View File
@@ -0,0 +1,64 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { RateLimiterAbstract, RateLimiterMemory } from 'rate-limiter-flexible'
const globalMailSendLimitMinute: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-auth:global:minute',
points: 5,
duration: 60 // 1 minute
})
const globalMailSendLimitHour: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-auth:global:hour',
points: 30,
duration: 60 * 60 // 1 hour
})
const gloablMailSendLimitDay: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-auth:global:day',
points: 100,
duration: 60 * 60 * 24 // 1 day
})
const checkGlobalMailSendLimit = async () => {
await globalMailSendLimitMinute.consume('global')
await globalMailSendLimitHour.consume('global')
await gloablMailSendLimitDay.consume('global')
}
const individualMailLimitFiveMinutes: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-auth:individual:5minutes',
points: 2,
duration: 60 * 5 // 5 minutes
})
const individualMailLimitDay: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-auth:individual:day',
points: 6,
duration: 60 * 60 * 24 // 1 day
})
const checkIndividualMailSendLimit = async (receiver: string) => {
await individualMailLimitFiveMinutes.consume(receiver)
await individualMailLimitDay.consume(receiver)
}
export const checkMailSendLimit = async (receiver: string) => {
await checkIndividualMailSendLimit(receiver)
await checkGlobalMailSendLimit()
}
+49
View File
@@ -0,0 +1,49 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { RateLimiterAbstract, RateLimiterMemory } from 'rate-limiter-flexible'
const individualMailLimitMinute: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-warning:individual:minute',
points: 2,
duration: 60 // 1 minute
})
const individualMailLimitDay: RateLimiterAbstract = new RateLimiterMemory({
keyPrefix: 'timelimit:sendmail-warning:individual:day',
points: 20,
duration: 60 * 60 * 24 // 1 day
})
const checkIndividualMailSendLimit = async (receiver: string) => {
await individualMailLimitMinute.consume(receiver)
await individualMailLimitDay.consume(receiver)
}
const checkMailSendLimit = async (receiver: string) => {
await checkIndividualMailSendLimit(receiver)
}
export const canSendWarningMail = async (receiver: string) => {
try {
await checkMailSendLimit(receiver)
return true
} catch (ex) {
return false
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* server component for the TimeLimit App
* Copyright (C) 2019 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as TokenGenerator from 'tokgen'
const authTokenGenerator = new TokenGenerator({
length: 32,
chars: 'a-zA-Z0-9'
})
export const generateAuthToken = () => authTokenGenerator.generate()
const idWithinFamilyGenerator = new TokenGenerator({
length: 6,
chars: 'a-zA-Z0-9'
})
export const generateIdWithinFamily = () => idWithinFamilyGenerator.generate()
export const isIdWithinFamily = (id: string) => id.length === 6 && /^[a-zA-Z0-9]+$/.test(id)
export const assertIdWithinFamily = (id: string) => {
if (!isIdWithinFamily(id)) {
throw new Error('invalid id within family')
}
}
const versionIdGenerator = new TokenGenerator({
length: 4,
chars: 'a-zA-Z0-9'
})
export const generateVersionId = () => versionIdGenerator.generate()
const familyIdGenerator = new TokenGenerator({
length: 10,
chars: 'a-zA-Z0-9'
})
export const generateFamilyId = () => familyIdGenerator.generate()