First commit

This commit is contained in:
Félix Aime
2020-11-24 19:45:03 +01:00
parent c042b71634
commit 513f6b1b02
130 changed files with 76873 additions and 0 deletions

24
app/frontend/README.md Normal file
View File

@ -0,0 +1,24 @@
# tinycheck-new
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

12248
app/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
app/frontend/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "tinycheck-new",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve --copy --port=4202",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@fnando/sparkline": "^0.3.10",
"axios": "^0.20.0",
"core-js": "^3.6.5",
"sass": "^1.27.0",
"sass-loader": "^10.0.4",
"simple-keyboard": "^2.30.25",
"vue": "^2.6.12",
"vue-router": "^3.4.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.6",
"@vue/cli-plugin-eslint": "~4.5.6",
"@vue/cli-service": "~4.5.6",
"babel-eslint": "^10.1.0",
"eslint": "^7.9.0",
"eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.12"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}

View File

@ -0,0 +1,3 @@
{
"python.pythonPath": "/usr/local/opt/python@3.8/bin/python3.8"
}

30
app/frontend/src/App.vue Normal file
View File

@ -0,0 +1,30 @@
<template>
<div id="app">
<transition name="fade" mode="out-in">
<router-view />
</transition>
</div>
</template>
<style>
@import './assets/spectre.min.css';
@import './assets/custom.css';
/* Face style for router stuff. */
.fade-enter-active,
.fade-leave-active {
transition-duration: 0.3s;
transition-property: opacity;
transition-timing-function: ease;
}
.fade-enter,
.fade-leave-active {
opacity: 0
}
</style>
<script>
document.title = 'TinyCheck Frontend'
</script>

View File

@ -0,0 +1,636 @@
@font-face {
font-family: 'Lobster';
font-weight: normal;
src: url('fonts/lobster.ttf') format('truetype');
}
@font-face {
font-family: "Roboto-Bold";
src: url("fonts/Roboto-Bold.eot"); /* IE9 Compat Modes */
src: url("fonts/Roboto-Bold.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
url("fonts/Roboto-Bold.otf") format("opentype"), /* Open Type Font */
url("fonts/Roboto-Bold.svg") format("svg"), /* Legacy iOS */
url("fonts/Roboto-Bold.ttf") format("truetype"), /* Safari, Android, iOS */
url("fonts/Roboto-Bold.woff") format("woff"), /* Modern Browsers */
url("fonts/Roboto-Bold.woff2") format("woff2"); /* Modern Browsers */
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: "Roboto-Regular";
src: url("fonts/Roboto-Regular.eot"); /* IE9 Compat Modes */
src: url("fonts/Roboto-Regular.eot?#iefix") format("embedded-opentype"), /* IE6-IE8 */
url("fonts/Roboto-Regular.otf") format("opentype"), /* Open Type Font */
url("fonts/Roboto-Regular.svg") format("svg"), /* Legacy iOS */
url("fonts/Roboto-Regular.ttf") format("truetype"), /* Safari, Android, iOS */
url("fonts/Roboto-Regular.woff") format("woff"), /* Modern Browsers */
url("fonts/Roboto-Regular.woff2") format("woff2"); /* Modern Browsers */
font-weight: normal;
font-style: normal;
}
* {
font-family: "Roboto-Regular";
user-select: none;
}
#qrcode {
width: 150px;
height: 150px;
margin-top:10px;
margin-left:5px;
border:1px #000;
border-radius:5px;
}
.card {
border: 0;
box-shadow: 0 0.25rem 1rem rgba(48,55,66,.15);
height: 100%;
}
.empty-subtitle {
font-family: "Roboto";
}
.btn {
font-family: 'Lobster';
font-size:30px;
}
/*
The min Chrome with is not 480 but 500 so we have to define our app div
with 480 width on a 480 screen.
*/
@media only screen and (max-width: 500px) {
#app {
height: 320px;
display:block;
width: 480px;
overflow-x: hidden;
overflow-y: hidden;
}
#tinycheck-logo {
width:370px;
}
.apcard {
width: 400px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 170px;
display: block;
border-radius: 10px;
}
.sparklines-container {
display:flex;
flex-direction:row;
flex-wrap: wrap;
justify-content:center;
align-items:center;
align-content:center;
width: 480px;
}
.keyboardinput {
padding:15px;
}
* {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.btn {
border-radius: 5px;
}
.warning-title {
font-family: "lobster";
font-weight: lighter;
font-size: 28px;
margin-bottom: .5em;
margin-top: .5em;
color:#FFF;
text-shadow: 1px 2px 3px #0000005c;
font-weight: 300;
}
.report-wrapper {
width:90%;
margin:auto;
}
.device-ctx {
width:90%;
margin:20px 0px 20px 10px;
}
.btn-save {
width:90%;
margin:auto;
}
.high-wrapper {
position: absolute;
width: 100%;
height: 100%;
background: #e53935;
}
.med-wrapper {
position: absolute;
width: 100%;
height: 100%;
background: #f1602b;
}
.low-wrapper, .none-wrapper {
position: absolute;
width: 100%;
height: 100%;
background: #56ab2f;
}
}
@media only screen and (min-width: 501px) {
#app {
height: 100%;
position:absolute;
width: 100%;
overflow-x: hidden;
overflow-y: hidden;
}
.apcard {
width: 500px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: fit-content;
border-radius: 10px;
}
#tinycheck-logo {
width:500px;
}
.warning-title {
font-family: "lobster";
font-weight: lighter;
font-weight: bold;
margin-bottom: .5em;
margin-top: .5em;
color:#FFF;
text-shadow: 1px 2px 3px #0000005c;
font-weight: 300;
}
.report-wrapper {
width:60%;
margin:auto;
}
.device-ctx {
padding:15px;
margin:auto;
}
.btn-save {
margin:auto;
}
.high-wrapper {
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(to right, #e53935, #e35d5b);
}
.med-wrapper {
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(to right, #ff4b1f, #ff9068);
}
.low-wrapper, .none-wrapper {
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(to right, #56ab2f, #a8e063);
}
}
.width-100 {
width: 100%;
}
.center {
width: fit-content;
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: fit-content;
text-align:center;
}
.light-grey {
color:#999;
}
.timer {
font-size:40px;
font-weight: 100;
}
body {
background-color: #f7f8f9;
}
footer {
position: fixed;
bottom: 0;
width: 100%;
}
.container {
padding-left:40px;
padding-right:40px;
padding-bottom:40px;
}
.container-padding-right {
padding-left:40px;
}
.group-title {
text-transform: uppercase;
color : #999;
font-size:12px;
display: block;
padding-bottom:10px;
padding-top:30px;
}
.keyboardinput {
width: 100%;
height: 100px;
padding: 20px;
font-size: 20px;
border: none;
box-sizing: border-box;
}
.legend {
color:#9a9a9a;
margin-bottom: 5px;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #CCC;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Spectre CSS tweaks */
.btn {
height: fit-content;
border-radius: 5px;
padding: .4rem .7rem;
font-size: 1rem;
color: #4c4c4c;
background:#f7f8f9;
border: 1px solid #4c4c4c;
}
.btn:hover {
height: fit-content;
border-radius: 5px;
padding: .4rem .7rem;
font-size: 1rem;
color: #4c4c4c;
background:#FFF;
border: 1px solid #4c4c4c;
}
.btn.active,.btn:active {
height: fit-content;
border-radius: 5px;
padding: .4rem .7rem;
font-size: 1rem;
color: #4c4c4c;
background:#FFF;
border: 1px solid #4c4c4c;
}
.btn.btn-light {
background: #f9f9f9;
border-color: #c1c1c1;
color: #c1c1c1;
}
.loadingsplash {
margin-top:20px;
}
.loading::after {
animation: loading .5s infinite linear;
background: 0 0;
border: .1rem solid #000;
border-radius: 50%;
border-right-color: transparent;
border-top-color: transparent;
content: "";
display: block;
height: .8rem;
left: 50%;
margin-left: -.4rem;
margin-top: -.4rem;
opacity: 1;
padding: 0;
position: absolute;
top: 50%;
width: .8rem;
z-index: 1;
}
.loading.loading-lg::after {
height: 1.6rem;
margin-left: -.8rem;
margin-top: -.8rem;
width: 1.6rem;
}
.divider-vert[data-content]::after, .divider[data-content]::after {
background: #f7f8f9;
}
.white-bg[data-content]::after, .white-bg[data-content]::after {
background: #FFFFFF;
}
#sparkline {
stroke: #e8e8e8;
fill: #f1f1f1;
bottom: 0;
position: fixed;
display: block;
}
.capture-wrapper {
display: block;
height: 100%;
width: 100%;
}
.btn.btn-primary {
border-radius: 5px;
border: 1px solid #333;
color: #FAFAFA;
background-color: #333;
}
.btn.btn-primary:hover {
border-radius: 5px;
border: 1px solid rgb(87, 87, 87);
color: #FAFAFA;
background-color: rgb(87, 87, 87);
}
.btn.btn-primary:active {
border-radius: 5px;
border: 1px solid rgb(87, 87, 87);
color: #FAFAFA;
background-color: rgb(87, 87, 87);
}
.btn-report-high {
border-radius: 5px;
border: 2px solid #ffffff;
color: #e34b49;
background-color: #fff;
}
.btn-report-high:hover {
border-radius: 5px;
border: 2px solid #ffffff;
color: #e34b49;
background-color: #fff;
}
.btn-report-moderate {
border-radius: 5px;
border: 2px solid #ffffff;
color: #ff4b1f;
background-color: #fff;
}
.btn-report-moderate:hover {
border-radius: 5px;
border: 2px solid #ffffff;
color: #ff4b1f;
background-color: #fff;
}
.btn-report-low {
border-radius: 5px;
border: 2px solid #ffffff;
color: #56ab2f;
background-color: #fff;
}
.btn-report-low:hover {
border-radius: 5px;
border: 2px solid #ffffff;
color: #56ab2f;
background-color: #fff;
}
.btn-report-low-light {
border-radius: 5px;
border: 2px solid #ffffff;
color: #ffffff;
background-color:transparent;
margin-right:10px;
}
.btn-report-low-light:hover {
border-radius: 5px;
border: 2px solid #ffffff;
color: #ffffff;
background-color:transparent;
margin-right:10px;
}
.alert-body {
background-color: #FFF;
list-style: none;
padding: 10px;
border-radius: 5px;
border: 1px solid #EEE;
}
.alert-body>.title {
display: block;
padding: 5px 5px 5px 10px;
}
.high-label {
background-color: #e53d38;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 3px 0px 0px 0px;
margin: 0px;
color: #FFF;
margin-left: 10px;
}
.moderate-label {
background-color: #ff7e33eb;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 3px 0px 0px 0px;
margin: 0px;
color: #FFF;
margin-left: 10px;
}
.low-label {
background-color: #4fce0eb8;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 3px 0px 0px 0px;
margin: 0px;
color: #FFF;
margin-left: 10px;
}
.description {
margin: 0;
padding: 10px;
color: #666;
}
.description>i {
font-style: inherit;
color: #353535;
padding: 2px 5px 2px 5px;
border-radius: 5px;
background-color: #F2F2F2;
}
ul {
list-style: none;
margin:0;
padding:0;
}
.alert-id {
background-color: #636363;
padding: 5px;
text-transform: uppercase;
font-size: 10px;
font-weight: bold;
border-radius: 0px 3px 0px 0px;
margin: 0px;
color: #FFF;
margin-right: 10px;
}
.title {
font-weight:500;
}
.btn-save {
margin-top:20px;
margin-bottom:20px;
width: 100%;
}
#controls-analysis {
margin-top: 25px;
margin-bottom: 40px;
}
.lobster {
font-family: 'lobster';
font-size: 40px;
}
.wifi-login {
width: 300px;
}
.frame-download {
width:1px;
height:1px;
border:0;
}
.btn:focus {
border-color: inherit;
-webkit-box-shadow: none;
box-shadow: none;
}
.form-select:focus {
border-color: inherit;
-webkit-box-shadow: none;
box-shadow: none;
}
.form-input:focus {
border-color: inherit;
-webkit-box-shadow: none;
box-shadow: none;
}
/* Used by the legend on analysis */
.fade-in {
animation: fadeIn ease 1s;
}
@keyframes fadeIn {
0% {
opacity:0;
}
100% {
opacity:1;
}
}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 805 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 784 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,4 @@
<svg width="112" height="195" viewBox="0 0 112 195" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="3.5" y1="3.5" x2="3.50001" y2="191.5" stroke="black" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="7" width="105" height="195" fill="#F7F8F9"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@ -0,0 +1,16 @@
<svg version="1.1" id="loader-1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="40px" height="40px" viewBox="0 0 40 40" enable-background="new 0 0 40 40" xml:space="preserve">
<path opacity="0.2" fill="#000" d="M20.201,5.169c-8.254,0-14.946,6.692-14.946,14.946c0,8.255,6.692,14.946,14.946,14.946
s14.946-6.691,14.946-14.946C35.146,11.861,28.455,5.169,20.201,5.169z M20.201,31.749c-6.425,0-11.634-5.208-11.634-11.634
c0-6.425,5.209-11.634,11.634-11.634c6.425,0,11.633,5.209,11.633,11.634C31.834,26.541,26.626,31.749,20.201,31.749z"/>
<path fill="#f7f8f9" d="M26.013,10.047l1.654-2.866c-2.198-1.272-4.743-2.012-7.466-2.012h0v3.312h0
C22.32,8.481,24.301,9.057,26.013,10.047z">
<animateTransform attributeType="xml"
attributeName="transform"
type="rotate"
from="0 20 20"
to="360 20 20"
dur="0.5s"
repeatCount="indefinite"/>
</path>
</svg>

After

Width:  |  Height:  |  Size: 970 B

View File

@ -0,0 +1,5 @@
<svg width="106" height="106" viewBox="0 0 106 106" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="53" cy="53" r="53" fill="#40D8A1"/>
<path d="M29 52.5L47.5 70.5" stroke="white" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="79" y1="40.0711" x2="48.0711" y2="71" stroke="white" stroke-width="10" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1,6 @@
<svg width="548" height="199" viewBox="0 0 548 199" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="403" y="27" width="142" height="145" rx="8" fill="white" stroke="black" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M0 30C0 13.4315 13.4315 0 30 0H428C432.418 0 436 3.58172 436 8V191C436 195.418 432.418 199 428 199H30C13.4315 199 0 185.569 0 169V30Z" fill="black"/>
<rect x="477" y="55" width="26" height="26" fill="white" stroke="black" stroke-width="6"/>
<rect x="477" y="117" width="26" height="26" fill="white" stroke="black" stroke-width="6"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="200px" height="200px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<circle cx="50" cy="50" r="0" fill="none" stroke="#f3f3f3" stroke-width="2">
<animate attributeName="r" repeatCount="indefinite" dur="1.4925373134328357s" values="0;30" keyTimes="0;1" keySplines="0 0.2 0.8 1" calcMode="spline" begin="-0.7462686567164178s"></animate>
<animate attributeName="opacity" repeatCount="indefinite" dur="1.4925373134328357s" values="1;0" keyTimes="0;1" keySplines="0.2 0 0.8 1" calcMode="spline" begin="-0.7462686567164178s"></animate>
</circle>
<circle cx="50" cy="50" r="0" fill="none" stroke="#d8dddf" stroke-width="2">
<animate attributeName="r" repeatCount="indefinite" dur="1.4925373134328357s" values="0;30" keyTimes="0;1" keySplines="0 0.2 0.8 1" calcMode="spline"></animate>
<animate attributeName="opacity" repeatCount="indefinite" dur="1.4925373134328357s" values="1;0" keyTimes="0;1" keySplines="0.2 0 0.8 1" calcMode="spline"></animate>
</circle>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

File diff suppressed because one or more lines are too long

10
app/frontend/src/main.js Normal file
View File

@ -0,0 +1,10 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = true
Vue.config.devtools = true
new Vue({
router,
render: h => h(App)
}).$mount('#app')

View File

@ -0,0 +1,62 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'loader',
component: () => import('../views/splash-screen.vue'),
props: true
},
{
path: '/home',
name: 'home',
component: () => import('../views/home.vue'),
props: true
},
{
path: '/wifi-select',
name: 'wifi-select',
component: () => import('../views/wifi-select.vue'),
props: true
},
{
path: '/generate-ap',
name: 'generate-ap',
component: () => import('../views/generate-ap.vue'),
props: true
},
{
path: '/capture',
name: 'capture',
component: () => import('../views/capture.vue'),
props: true
},
{
path: '/save-capture',
name: 'save-capture',
component: () => import('../views/save-capture.vue'),
props: true
},
{ path: '/analysis',
name: 'analysis',
component: () => import('../views/analysis.vue'),
props: true
},
{ path: '/report',
name: 'report',
component: () => import('../views/report.vue'),
props: true
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

View File

@ -0,0 +1,60 @@
<template>
<div :class="keyboardClass"></div>
</template>
<script>
import Keyboard from "simple-keyboard";
import "simple-keyboard/build/css/index.css";
export default {
name: "SimpleKeyboard",
props: {
keyboardClass: {
default: "simple-keyboard",
type: String
},
input: {
type: String
}
},
data: () => ({
keyboard: null
}),
mounted() {
this.keyboard = new Keyboard({
onChange: this.onChange,
onKeyPress: this.onKeyPress
});
},
methods: {
onChange(input) {
this.$emit("onChange", input);
},
onKeyPress(button) {
this.$emit("onKeyPress", button);
/**
* If you want to handle the shift and caps lock buttons
*/
if (button === "{shift}" || button === "{lock}") this.handleShift();
},
handleShift() {
let currentLayout = this.keyboard.options.layoutName;
let shiftToggle = currentLayout === "default" ? "shift" : "default";
this.keyboard.setOptions({
layoutName: shiftToggle
});
}
},
watch: {
input(input) {
this.keyboard.setInput(input);
}
}
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>

View File

@ -0,0 +1,70 @@
<template>
<div class="center">
<div v-if="question">
<p>Do you want to analyze the captured communications?</p>
<div class="empty-action">
<button class="btn" v-on:click="save_capture()">No, just save them</button> <button class="btn btn-primary" v-on:click="start_analysis()">Yes, let's do it</button>
</div>
</div>
<div v-else-if="running">
<img src="@/assets/loading.svg"/>
<p class="legend" v-if="!long_waiting">Please wait during the analysis...</p>
<p class="legend fade-in" v-if="long_waiting">Yes, it can take some time...</p>
</div>
</div>
</template>
<script>
import router from '../router'
import axios from 'axios'
export default {
name: 'analysis',
data() {
return {
question: true,
running: false,
check_alerts: false,
long_waiting: false
}
},
props: {
capture_token: String
},
methods: {
start_analysis: function() {
this.question = false
this.running = true
setTimeout(function () { this.long_waiting = true }.bind(this), 15000);
axios.get(`/api/analysis/start/${this.capture_token}`, { timeout: 60000 })
.then(response => {
if(response.data.message == "Analysis started")
this.check_alerts = setInterval(() => { this.get_alerts(); }, 500);
})
.catch(error => {
console.log(error);
});
},
get_alerts: function() {
axios.get(`/api/analysis/report/${this.capture_token}`, { timeout: 60000 })
.then(response => {
if(response.data.message != "No report yet"){
clearInterval(this.check_alerts);
this.long_waiting = false;
this.running = false;
router.replace({ name: 'report', params: { alerts : response.data.alerts,
device : response.data.device,
capture_token : this.capture_token } });
}
})
.catch(error => {
console.log(error);
});
},
save_capture: function() {
var capture_token = this.capture_token
router.replace({ name: 'save-capture', params: { capture_token: capture_token } });
}
}
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<div class="capture-wrapper">
<svg id="sparkline" stroke-width="3" :width="sparkwidth" :height="sparkheight" v-if="sparklines"></svg>
<div class="center">
<div class="footer">
<h3 class="timer">{{timer_hours}}:{{timer_minutes}}:{{timer_seconds}}</h3>
<p>Intercepting the communications of {{device_name}}.</p>
<div class="empty-action">
<button class="btn" :class="[ loading ? 'loading' : 'btn-primary', ]" v-on:click="stop_capture()">Stop the capture</button>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import router from '../router'
import sparkline from '@fnando/sparkline'
export default {
name: 'capture',
components: {},
data() {
return {
timer_hours: "00",
timer_minutes: "00",
timer_seconds: "00",
loading: false,
stats_interval: false,
chrono_interval: false,
sparklines: false
}
},
props: {
capture_token: String,
device_name: String
},
methods: {
set_chrono: function() {
this.chrono_interval = setInterval(() => { this.chrono(); }, 10);
},
stop_capture: function() {
this.loading = true
axios.get(`/api/network/ap/stop`, { timeout: 30000 })
axios.get(`/api/capture/stop`, { timeout: 30000 })
.then(response => (this.handle_finish(response.data)))
},
get_stats: function() {
axios.get(`/api/capture/stats`, { timeout: 30000 })
.then(response => (this.handle_stats(response.data)))
},
handle_stats: function(data) {
if (data.packets.length) sparkline(document.querySelector("#sparkline"), data.packets);
},
handle_finish: function(data) {
clearInterval(this.chrono_interval);
clearInterval(this.stats_interval);
if (data.status) {
this.loading = false
var capture_token = this.capture_token
router.replace({ name: 'analysis', params: { capture_token: capture_token } });
}
},
chrono: function() {
var time = Date.now() - this.capture_start
this.timer_hours = Math.floor(time / (60 * 60 * 1000));
this.timer_hours = (this.timer_hours < 10) ? "0" + this.timer_hours : this.timer_hours
time = time % (60 * 60 * 1000);
this.timer_minutes = Math.floor(time / (60 * 1000));
this.timer_minutes = (this.timer_minutes < 10) ? "0" + this.timer_minutes : this.timer_minutes
time = time % (60 * 1000);
this.timer_seconds = Math.floor(time / 1000);
this.timer_seconds = (this.timer_seconds < 10) ? "0" + this.timer_seconds : this.timer_seconds
},
setup_sparklines: function() {
axios.get(`/api/misc/config`, { timeout: 60000 })
.then(response => {
if(response.data.sparklines){
this.sparklines = true
this.sparkwidth = window.screen.width + "px";
this.sparkheight = Math.trunc(window.screen.height / 5) + "px";
this.stats_interval = setInterval(() => { this.get_stats(); }, 500);
}
})
.catch(error => {
console.log(error)
});
}
},
created: function() {
// Get the config for the sparklines.
this.setup_sparklines()
// Start the chrono and get the first stats.
this.capture_start = Date.now()
this.set_chrono();
}
}
</script>

View File

@ -0,0 +1,119 @@
<template>
<div class="center">
<div v-if="(error == false)">
<div v-if="ssid_name">
<div class="card apcard" v-on:click="generate_ap()">
<div class="columns">
<div class="column col-5">
<center><img :src="ssid_qr" id="qrcode"></center>
</div>
<div class="divider-vert white-bg" data-content="OR"></div>
<div class="column col-5"><br />
<span class="light-grey">Network name: </span><br />
<h4>{{ ssid_name }}</h4>
<span class="light-grey">Network password: </span><br />
<h4>{{ ssid_password }}</h4>
</div>
</div>
</div>
<br /><br /><br /><br /> <br /><br /><br /><br /><br /><br />
<!-- Requite a CSS MEME for that shit :) -->
<span class="legend">Tap the white frame to generate a new network.</span>
</div>
<div v-else>
<img src="@/assets/loading.svg"/>
<p class="legend">We generate an ephemeral network for you.</p>
</div>
</div>
<div v-else>
<p>
<strong>Unfortunately, we got some issues.</strong>
<br /><br />
Please verify that you've two Wifi interfaces on your device<br />
and restart it by clicking on the button below.<br />
</p>
<button class="btn" v-on:click="reboot()">Reboot the device</button>
</div>
</div>
</template>
<script>
import axios from 'axios'
import router from '../router'
export default {
name: 'generate-ap',
components: {},
data() {
return {
ssid_name: false,
ssid_qr: false,
ssid_password: false,
capture_token: false,
capture_start: false,
interval: false,
error: false
}
},
methods: {
generate_ap: function() {
clearInterval(this.interval)
this.ssid_name = false
axios.get(`/api/network/ap/start`, { timeout: 30000 })
.then(response => (this.show_ap(response.data)))
},
show_ap: function(data) {
if (data.status) {
this.ssid_name = data.ssid
this.ssid_password = data.password
this.ssid_qr = data.qrcode
this.start_capture() // Start the capture before client connect.
} else {
this.error = true
}
},
start_capture: function() {
axios.get(`/api/capture/start`, { timeout: 30000 })
.then(response => (this.get_capture_token(response.data)))
},
reboot: function() {
axios.get(`/api/misc/reboot`, { timeout: 30000 })
.then(response => { console.log(response)})
},
get_capture_token: function(data) {
if (data.status) {
this.capture_token = data.capture_token
this.capture_start = Date.now()
this.get_device()
}
},
get_device: function() {
this.interval = setInterval(() => {
axios.get(`/api/device/get/${this.capture_token}`, { timeout: 30000 })
.then(response => (this.check_device(response.data)))
}, 500);
},
check_device: function(data) {
if (data.status) {
clearInterval(this.interval);
var capture_token = this.capture_token
var capture_start = this.capture_start
var device_name = data.name
router.replace({
name: 'capture',
params: {
capture_token: capture_token,
capture_start: capture_start,
device_name: device_name
}
});
}
}
},
created: function() {
this.generate_ap();
}
}
</script>

View File

@ -0,0 +1,24 @@
<template>
<div class="center">
<h3 class="lobster">Welcome to TinyCheck.</h3>
<p>We are going to help you to check your device.</p>
<button class="btn btn-primary" v-on:click="next()">Let's start!</button>
</div>
</template>
<script>
import router from '../router'
export default {
name: 'home',
props: { saved_ssid: String, list_ssids: Array, internet: Boolean },
methods: {
next: function() {
var saved_ssid = this.saved_ssid
var list_ssids = this.list_ssids
var internet = this.internet
router.push({ name: 'wifi-select', params: { saved_ssid: saved_ssid, list_ssids: list_ssids, internet:internet } });
}
}
}
</script>

View File

@ -0,0 +1,115 @@
<template>
<div>
<div v-if="results">
<div v-if="alerts.high.length >= 1" class="high-wrapper">
<div class="center">
<h1 class="warning-title">You have {{ nb_translate(alerts.high.length) }} high alert,<br />your device seems to be compromised.</h1>
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
<button class="btn btn-report-high" @click="show_report=true;results=false;">Show the full report</button>
</div>
</div>
<div v-else-if="alerts.moderate.length >= 1" class="med-wrapper">
<div class="center">
<h1 class="warning-title">You have {{ nb_translate(alerts.moderate.length) }} moderate alerts,<br />your device might be compromised.</h1>
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
<button class="btn btn-report-moderate" @click="show_report=true;results=false;">Show the full report</button>
</div>
</div>
<div v-else-if="alerts.low.length >= 1" class="low-wrapper">
<div class="center">
<h1 class="warning-title">You have ony {{ nb_translate(alerts.moderate.low) }} low alerts,<br /> don't hesitate to check them.</h1>
<button class="btn btn-report-low-light" v-on:click="new_capture()">Start a new capture</button>
<button class="btn btn-report-low" @click="show_report=true;results=false;">Show the full report</button>
</div>
</div>
<div v-else class="none-wrapper">
<div class="center">
<h1 class="warning-title">Everything looks fine, zero alerts.</h1>
<button class="btn btn-report-low-light" v-on:click="save_capture()">Save the capture</button>
<button class="btn btn-report-low" v-on:click="new_capture()">Start a new capture</button>
</div>
</div>
</div>
<div v-else-if="show_report" class="report-wrapper">
<div class="device-ctx">
<h3 style="margin: 0;">Report for {{device.name}}</h3>
IP Address: {{device.ip_address}}<br />Mac Address: {{device.mac_address}}
</div>
<ul class="alerts">
<li class="alert" v-for="alert in alerts.high" :key="alert.message">
<span class="high-label">High</span><span class="alert-id">{{ alert.id }}</span>
<div class="alert-body">
<span class="title">{{ alert.title }}</span>
<p class="description">{{ alert.description }}</p>
</div>
</li>
<li class="alert" v-for="alert in alerts.moderate" :key="alert.message">
<span class="moderate-label">Moderate</span><span class="alert-id">{{ alert.id }}</span>
<div class="alert-body">
<span class="title">{{ alert.title }}</span>
<p class="description">{{ alert.description }}</p>
</div>
</li>
<li class="alert" v-for="alert in alerts.low" :key="alert.message">
<span class="low-label">Low</span><span class="alert-id">{{ alert.id }}</span>
<div class="alert-body">
<span class="title">{{ alert.title }}</span>
<p class="description">{{ alert.description }}</p>
</div>
</li>
</ul>
<div class="columns" id="controls-analysis">
<div class="column col-5">
<button class="btn width-100" @click="$router.push('generate-ap')">Start a capture</button>
</div>
<div class="divider-vert column col-2" data-content="OR"></div>
<div class="column col-5">
<button class="btn btn btn-primary width-100" v-on:click="save_capture()">Save the report</button>
</div>
</div>
</div>
</div>
</template>
<style>
#app {
overflow-y: visible;
}
</style>
<script>
import router from '../router'
export default {
name: 'report',
data() {
return {
results: true,
}
},
props: {
device: Object,
alerts: Array,
capture_token: String
},
methods: {
save_capture: function() {
var capture_token = this.capture_token
router.replace({ name: 'save-capture', params: { capture_token: capture_token } });
},
new_capture: function() {
router.push({ name: 'generate-ap' })
},
nb_translate: function(x) {
var nbs = ['zero','one','two','three','four', 'five','six','seven','eight','nine', 'ten', 'eleven']
try {
return nbs[x];
} catch (error)
{
return x;
}
}
}
}
</script>

View File

@ -0,0 +1,200 @@
<template>
<div class="center" v-if="save_usb && init">
<div class="canvas-anim" :class="{'anim-connect': !saved && !usb}" v-on:click="new_capture()">
<div class="icon-spinner" v-if="!saved && usb"></div>
<div class="icon-success" v-if="saved"></div>
<div class="icon-usb"></div>
<div class="icon-usb-plug"></div>
</div>
<p class="legend" v-if="!saved && !usb"><br />Please connect a USB key to save your capture.</p>
<p class="legend" v-if="!saved && usb"><br />We are saving your capture.</p>
<p class="legend" v-if="saved"><br />You can tap the USB key to start a new capture.</p>
</div>
<div class="center" v-else-if="!save_usb && init">
<div>
<p class="legend">The capture download is going to start...<br /><br /><br /></p>
<button class="btn btn-primary" v-on:click="new_capture()">Start another capture</button>
<iframe :src="download_url" class="frame-download"></iframe>
</div>
</div>
</template>
<style lang="scss">
.canvas-anim {
height: 120px;
margin: 0 auto;
position: relative;
width: 205px;
&.anim-connect {
width: 300px;
.icon-usb {
-webkit-animation: slide-right 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite alternate both;
animation: slide-right 1s cubic-bezier(0.455, 0.030, 0.515, 0.955) infinite alternate both;
}
}
}
.icon-usb {
background: url('../assets/icon_usb.svg') no-repeat 0 0;
background-size: 200px auto;
display: block;
height: 120px;
position: absolute;
top: 25px;
left: 0;
width: 200px;
z-index: 8;
}
.icon-usb-plug {
background: url('../assets/icon_plug_usb.svg') no-repeat 0 0;
background-size: cover;
display: block;
height: 120px;
position: absolute;
top: 0;
right: -10px;
width: 55px;
z-index: 9;
}
.icon-success {
background: url('../assets/icon_success.svg') no-repeat 0 0;
background-size: 80px auto;
display: block;
position: absolute;
height: 120px;
top: -25px;
left: -40px;
width: 80px;
z-index: 10;
-webkit-animation: scale-down-center 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
animation: scale-down-center 0.7s cubic-bezier(0.250, 0.460, 0.450, 0.940) both;
}
.icon-spinner {
background: url('../assets/icon_spinner.svg') no-repeat 0 0;
background-color: #f7f8f9;
border-radius: 40px;
display: block;
height: 40px;
position: absolute;
top: 5px;
left: -20px;
width: 40px;
z-index: 10;
}
@-webkit-keyframes slide-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
-webkit-transform: translateX(75px);
transform: translateX(75px);
}
}
@keyframes slide-right {
0% {
-webkit-transform: translateX(0);
transform: translateX(0);
}
100% {
-webkit-transform: translateX(75px);
transform: translateX(75px);
}
}
@-webkit-keyframes scale-down-center {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
100% {
-webkit-transform: scale(0.5);
transform: scale(0.5);
}
}
@keyframes scale-down-center {
0% {
-webkit-transform: scale(1);
transform: scale(1);
}
100% {
-webkit-transform: scale(0.5);
transform: scale(0.5);
}
}
</style>
<script>
import axios from 'axios'
import router from '../router'
export default {
name: 'save-capture',
components: {},
data() {
return {
usb: false,
saved: false,
save_usb: false,
init: false
}
},
props: {
capture_token: String
},
methods: {
check_usb: function() {
axios.get(`/api/save/usb-check`, { timeout: 30000 })
.then(response => {
if(response.data.status) {
this.usb = true
clearInterval(this.interval)
this.save_capture()
}
})
},
save_capture: function() {
var capture_token = this.capture_token
axios.get(`/api/save/save-capture/${capture_token}/usb`, { timeout: 30000 })
.then(response => {
if(response.data.status){
this.saved = true
this.timeout = setTimeout(() => router.push('/'), 60000);
}
})
},
new_capture: function() {
clearTimeout(this.timeout);
router.push({ name: 'generate-ap' })
},
load_config: function() {
axios.get(`/api/misc/config`, { timeout: 60000 })
.then(response => {
if(response.data.download_links){
this.init = true
this.save_usb = false
this.download_url = `/api/save/save-capture/${this.capture_token}/url`
} else {
this.init = true
this.save_usb = true
this.interval = setInterval(() => { this.check_usb() }, 500);
}
})
.catch(error => {
console.log(error)
});
}
},
created: function() {
this.load_config()
}
}
</script>

View File

@ -0,0 +1,53 @@
<template>
<div class="center">
<img src="@/assets/logo.png" id="tinycheck-logo" />
<div class="loading loading-lg loadingsplash"></div>
</div>
</template>
<script>
import router from '../router'
import axios from 'axios'
export default {
name: 'splash-screen',
components: {},
data() {
return {
list_ssids: [],
internet: false
}
},
methods: {
// Check if the device is already connected to internet.
internet_check: function() {
axios.get(`/api/network/status`, { timeout: 10000 })
.then(response => {
if (response.data.internet) this.internet = true
this.get_wifi_networks()
})
.catch(err => (console.log(err)))
},
// Get the WiFi networks around the box.
get_wifi_networks: function() {
axios.get(`/api/network/wifi/list`, { timeout: 10000 })
.then(response => (this.append_ssids(response.data.networks)))
.catch(err => (console.log(err)))
},
// Handle the get_wifi_networks answer and call goto_home.
append_ssids: function(networks) {
this.list_ssids = networks
this.goto_home()
},
// Pass the list of ssids and the internet status as a prop to the home view.
goto_home: function() {
var list_ssids = this.list_ssids
var internet = this.internet
router.replace({ name: 'home', params: { list_ssids: list_ssids, internet: internet } });
}
},
created: function() {
this.internet_check();
}
}
</script>

View File

@ -0,0 +1,166 @@
<template>
<div :class="[ keyboard == false ? 'center' : '' ]">
<div v-if="keyboard == false">
<div v-if="have_internet">
<p>You seem to be already connected to a network.<br />Do you want to use the current connection?</p>
<div class="empty-action">
<button class="btn" @click="have_internet = false">No, use another</button> <button class="btn" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" @click="$router.push({ name: 'generate-ap' })">Yes, use it.</button>
</div>
</div>
<div v-else>
<div v-if="enter_creds" class="wifi-login">
<div class="form-group">
<select class="form-select" id="ssid-select" v-model="ssid">
<option value="" selected>Wifi name</option>
<option v-for="ssid in ssids" v-bind:key="ssid.ssid">
{{ ssid.ssid }}
</option>
</select>
</div>
<div class="form-group">
<input class="form-input" type="password" id="password" v-model="password" placeholder="Wifi password" v-on:click="keyboard = (virtual_keyboard)? true : false">
</div>
<div class="form-group">
<button class="btn width-100" :class="[ connecting ? 'loading' : '', success ? 'btn-success' : 'btn-primary', ]" v-on:click="wifi_setup()">{{ btnval }}</button>
</div>
<div class="form-group">
<button class="btn width-100" :class="[ refreshing ? 'loading' : '' ]" v-on:click="refresh_wifi_list()">Refresh networks list</button>
</div>
</div>
<div v-else>
<p><strong>You seem to not be connected to Internet.</strong><br />Please configure the Wi-Fi connection.</p>
<div class="empty-action">
<button class="btn btn-primary" @click="enter_creds = true">Ok, let's do that.</button>
</div>
</div>
</div>
</div>
<div v-else>
<input :value="input" class="keyboardinput" @input="onInputChange" placeholder="Tap on the virtual keyboard to start">
<SimpleKeyboard @onChange="onChange" @onKeyPress="onKeyPress" :input="input" />
</div>
</div>
</template>
<style>
#app {
overflow-y: hidden;
}
</style>
<script>
import axios from 'axios'
import router from '../router'
import SimpleKeyboard from "./SimpleKeyboard";
export default {
name: 'wifi-select',
components: {
SimpleKeyboard
},
data() {
return {
connecting: false,
error: false,
success: false,
btnval: "Connect to it.",
ssid: "",
selected_ssid: false,
password: "",
keyboard: false,
input: "",
ssids: [],
have_internet: false,
enter_creds: false,
virtual_keyboard: false,
refreshing: false
}
},
props: {
saved_ssid: String,
list_ssids: Array,
internet: Boolean
},
methods: {
wifi_connect: function() {
axios.get(`/api/network/wifi/connect`, { timeout: 60000 })
.then(response => {
if (response.data.status) {
this.success = true
this.connecting = false
this.btnval = "Wifi connected!"
setTimeout(() => router.push('generate-ap'), 1000);
} else {
this.btnval = "Wifi not connected. Please retry."
this.connecting = false
}
})
.catch(error => {
console.log(error)
});
},
wifi_setup: function() {
if (this.ssid.length && this.password.length >= 8 ){
axios.post(`/api/network/wifi/setup`, { ssid: this.ssid, password: this.password }, { timeout: 60000 })
.then(response => {
if(response.data.status) {
this.connecting = true
this.wifi_connect()
} else {
console.log(response.data.message)
}
})
.catch(error => {
console.log(error)
});
}
},
load_config: function() {
axios.get(`/api/misc/config`, { timeout: 60000 })
.then(response => {
this.virtual_keyboard = response.data.virtual_keyboard
})
.catch(error => {
console.log(error)
});
},
onChange(input) {
this.input = input
this.password = this.input;
},
onKeyPress(button) {
if (button == "{enter}")
this.keyboard = false
},
onInputChange(input) {
this.input = input.target.value;
},
append_ssids: function(networks) {
this.ssids = networks
},
refresh_wifi_list: function(){
this.refreshing = true
axios.get(`/api/network/wifi/list`, { timeout: 10000 })
.then(response => {
this.refreshing = false
this.append_ssids(response.data.networks)
}).catch(error => {
this.refreshing = false
console.log(error)
});
}
},
created: function() {
this.load_config()
this.have_internet = (this.internet) ? true : false
this.keyboard = false
if (typeof this.list_ssids == "object" && this.list_ssids.length != 0){
this.ssids = this.list_ssids
} else {
this.refresh_wifi_list()
}
}
}
</script>

View File

@ -0,0 +1,11 @@
module.exports = {
devServer: {
proxy: {
'^/api': {
target: 'http://localhost:8040',
ws: true,
changeOrigin: true
},
}
}
}