├── .dockerignore
├── .github
└── workflows
│ ├── redis.yml
│ ├── release.yml
│ ├── test.yml
│ └── test_docker.yml
├── .gitignore
├── Dockerfile
├── LICENCE
├── README.md
├── bridges.png
├── build
├── bullseye
└── buster
├── config.nims
├── docker-compose.yml
├── install.sh
├── mockups
├── login.nim
├── net.nim
├── stat.nim
└── sys.nim
├── nim.cfg
├── public
├── android-chrome-192x192.png
├── android-chrome-384x384.png
├── apple-touch-icon.png
├── css
│ ├── .gitkeep
│ ├── fontello-codes.css
│ └── fontello.css
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── font
│ ├── fontello.eot
│ ├── fontello.svg
│ ├── fontello.ttf
│ ├── fontello.woff
│ └── fontello.woff2
├── images
│ └── torbox.png
└── site.webmanifest
├── src
├── config.nim
├── lib
│ ├── binascii.nim
│ ├── clib
│ │ ├── c_crypt.nim
│ │ └── shadow.nim
│ ├── crypt.nim
│ ├── fallbacks.nim
│ ├── hostap.nim
│ ├── hostap
│ │ ├── conf.nim
│ │ └── vdom.nim
│ ├── session.nim
│ ├── sys.nim
│ ├── sys
│ │ ├── iface.nim
│ │ ├── service.nim
│ │ ├── sys.nim
│ │ └── vdom.nim
│ ├── tor.nim
│ ├── tor
│ │ ├── bridges.nim
│ │ ├── bridges
│ │ │ ├── bridge.nim
│ │ │ └── vdom.nim
│ │ ├── tor.nim
│ │ ├── torcfg.nim
│ │ ├── torsocks.nim
│ │ └── vdom.nim
│ ├── torbox.nim
│ ├── wifiScanner.nim
│ └── wirelessManager.nim
├── notice.nim
├── query.nim
├── renderutils.nim
├── routes
│ ├── network.nim
│ ├── network
│ │ └── wireless.nim
│ ├── status.nim
│ ├── sys.nim
│ ├── tabs.nim
│ └── tabs
│ │ ├── dsl.nim
│ │ ├── tab.nim
│ │ └── vdom.nim
├── sass
│ ├── box.scss
│ ├── card.scss
│ ├── colours.scss
│ ├── error.scss
│ ├── index.scss
│ ├── loading.scss
│ ├── login.scss
│ ├── menues.scss
│ ├── nav.scss
│ ├── network.scss
│ ├── notify.scss
│ ├── sub-menu.scss
│ ├── table.scss
│ ├── warn.scss
│ └── wireless.scss
├── settings.nim
├── toml.nim
├── torci.nim
├── types.nim
├── utils.nim
└── views
│ ├── login.nim
│ └── network.nim
├── tests
├── local
│ ├── serviceStatus.nim
│ └── sys.nim
├── sandbox
│ ├── Dockerfile
│ ├── docker.nim
│ └── tests
│ │ ├── test_login.nim
│ │ ├── test_redis.nim
│ │ └── test_sys.nim
├── server.nim
├── server
│ ├── client.nim
│ ├── routes
│ │ ├── ap.nim
│ │ ├── status.nim
│ │ └── sys.nim
│ ├── server.nim
│ └── utils.nim
├── test_bridges.nim
├── test_c_crypt.nim
├── test_crypt.nim
├── test_hostname.nim
├── test_iface.nim
├── test_notifies.nim
├── test_routes
│ ├── test_ap.nim
│ ├── test_status.nim
│ └── test_sys.nim
├── test_tabs.nim
├── test_toml.nim
├── test_tor.nim
└── torrc_template.nim
├── tools
└── gencss.nim
├── torci.conf
├── torci.nimble
└── torci.toml
/.dockerignore:
--------------------------------------------------------------------------------
1 | *.png
2 | LICENCE
3 | *.md
4 | .vscode
5 | nimcache
6 | testresults
--------------------------------------------------------------------------------
/.github/workflows/redis.yml:
--------------------------------------------------------------------------------
1 | name: Redis
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'LICENCE'
7 | - '*.md'
8 | - 'buster'
9 | - 'bullseye'
10 | - '.github/workflows/release.yml'
11 | branches:
12 | - main
13 | - devel
14 |
15 | pull_request:
16 | paths-ignore:
17 | - 'LICENCE'
18 | - '*.md'
19 | - 'mockups'
20 | branches:
21 | - main
22 | - devel
23 |
24 | jobs:
25 | test:
26 | runs-on: ${{ matrix.os }}
27 | strategy:
28 | matrix:
29 | os:
30 | - ubuntu-latest
31 | - ubuntu-18.04
32 |
33 | nim-version:
34 | - stable
35 |
36 | redis-version:
37 | [ 7 ]
38 |
39 | steps:
40 | - name: Checkout
41 | uses: actions/checkout@v2
42 |
43 | - name: Cache choosenim
44 | id: cache-choosenim
45 | uses: actions/cache@v2
46 | with:
47 | path: ~/.choosenim
48 | key: ${{ runner.os }}-choosenim-${{ matrix.nim-version}}
49 |
50 | - name: Cache nimble
51 | id: cache-nimble
52 | uses: actions/cache@v2
53 | with:
54 | path: ~/.nimble
55 | key: ${{ runner.os }}-nimble-${{ matrix.nim-version}}-${{ hashFiles('torci.nimble') }}
56 | restore-keys: |
57 | ${{ runner.os }}-nimble-${{ matrix.nim-version}}-
58 |
59 | - name: Setup Nim
60 | uses: jiro4989/setup-nim-action@v1
61 | with:
62 | nim-version: ${{ matrix.nim-version }}
63 |
64 | - name: Start Redis
65 | uses: supercharge/redis-github-action@1.4.0
66 | with:
67 | redis-version: ${{ matrix.redis-version }}
68 | redis-port: 7000
69 |
70 | - name: Install packages
71 | run: nimble install -y
72 |
73 | - name: Test
74 | run: nimble redis -Y
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 | # paths-ignore:
8 | # - 'LICENCE'
9 | # - '*.md'
10 | # branches:
11 | # - main
12 | # - devel
13 |
14 | env:
15 | APP_NAME: 'TorCI'
16 | NIM_VERSION: '1.6.0'
17 | MAINTAINER: 'Luca'
18 | DESC: 'Web-based GUI for TorBox'
19 |
20 | jobs:
21 | build-artefact:
22 | name: Build artefact
23 | runs-on: ubuntu-latest
24 | strategy:
25 | matrix:
26 | dist:
27 | - debian
28 | codename:
29 | - buster
30 | arch:
31 | - amd64
32 | - arm
33 |
34 | steps:
35 | - name: Checkout
36 | uses: actions/checkout@v2
37 | - name: Create artefact
38 | run: |
39 | torci_dir=artefact/torci
40 |
41 | mkdir -p $torci_dir
42 |
43 | # Set env
44 | ARCH=${{ matrix.arch }}
45 |
46 | # Build
47 | docker build -t torci:release -f build/${{ matrix.codename }} build
48 | docker run --rm -v `pwd`:/src/torci -e ARCH=${{ matrix.arch }} torci:release
49 |
50 | # Move binary
51 | mv torci $torci_dir
52 |
53 | # Copy resources
54 | cp -r public $torci_dir/public
55 | cp torci.nimble torci.conf config.nims LICENCE $torci_dir
56 |
57 | archive_name=torci_${{ matrix.arch }}.tar.gz
58 |
59 | tar -czvf $archive_name -C artefact torci
60 |
61 | shell: bash
62 | - uses: actions/upload-artifact@v2
63 | with:
64 | name: artefact-${{ matrix.dist }}_${{ matrix.arch }}
65 | path: torci_*.tar.gz
66 |
67 | create-release:
68 | runs-on: ubuntu-latest
69 | needs:
70 | - build-artefact
71 | outputs:
72 | upload_url: ${{ steps.create_release.outputs.upload_url }}
73 | steps:
74 | - uses: actions/checkout@v2
75 | - name: Create Release
76 | id: create_release
77 | uses: actions/create-release@v1
78 | env:
79 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80 | with:
81 | tag_name: ${{ github.ref }}
82 | release_name: ${{ github.ref }}
83 | body: |
84 | Anyway
85 | draft: false
86 | prerelease: false
87 |
88 | - name: upload_url
89 | run: echo "::set-output name=upload_url::${{ steps.create_release.outputs.upload_url }}"
90 |
91 | upload-release:
92 | runs-on: ubuntu-latest
93 | needs: create-release
94 | strategy:
95 | matrix:
96 | dist:
97 | - debian
98 | arch:
99 | - amd64
100 | - arm
101 | steps:
102 | - uses: actions/download-artifact@v2
103 | with:
104 | name: artefact-${{ matrix.dist }}_${{ matrix.arch }}
105 |
106 | - name: Upload Release Asset
107 | id: upload-release-asset
108 | uses: actions/upload-release-asset@v1
109 | env:
110 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111 | with:
112 | upload_url: ${{ needs.create-release.outputs.upload_url }}
113 | asset_path: torci_${{ matrix.arch }}.tar.gz
114 | asset_name: torci_${{ matrix.arch }}.tar.gz
115 | asset_content_type: application/tar+gzip
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test TorCI
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'LICENCE'
7 | - '*.md'
8 | - 'buster'
9 | - 'bullseye'
10 | - '.github/workflows/release.yml'
11 | branches:
12 | - main
13 | - devel
14 |
15 | pull_request:
16 | paths-ignore:
17 | - 'LICENCE'
18 | - '*.md'
19 | - 'mockups'
20 | branches:
21 | - main
22 | - devel
23 |
24 | jobs:
25 | test:
26 | runs-on: ${{ matrix.os }}
27 | strategy:
28 | matrix:
29 | os:
30 | - ubuntu-latest
31 | - ubuntu-18.04
32 | nim-version:
33 | - stable
34 | # - devel
35 |
36 | steps:
37 | - name: Checkout
38 | uses: actions/checkout@v2
39 |
40 | - name: Cache choosenim
41 | id: cache-choosenim
42 | uses: actions/cache@v2
43 | with:
44 | path: ~/.choosenim
45 | key: ${{ runner.os }}-choosenim-${{ matrix.nim-version}}
46 |
47 | - name: Cache nimble
48 | id: cache-nimble
49 | uses: actions/cache@v2
50 | with:
51 | path: ~/.nimble
52 | key: ${{ runner.os }}-nimble-${{ matrix.nim-version}}-${{ hashFiles('torci.nimble') }}
53 | restore-keys: |
54 | ${{ runner.os }}-nimble-${{ matrix.nim-version}}-
55 |
56 | - name: Setup Nim
57 | uses: jiro4989/setup-nim-action@v1
58 | with:
59 | nim-version: ${{ matrix.nim-version }}
60 |
61 | - name: Install packages
62 | run: nimble install -y
63 |
64 | - name: Test
65 | run: nimble tests -Y
--------------------------------------------------------------------------------
/.github/workflows/test_docker.yml:
--------------------------------------------------------------------------------
1 | name: Test TorCI in Docker container
2 |
3 | on:
4 | push:
5 | paths-ignore:
6 | - 'LICENCE'
7 | - '*.md'
8 | - 'buster'
9 | - 'bullseye'
10 | - '.github/workflows/release.yml'
11 | branches:
12 | - main
13 | - devel
14 |
15 | pull_request:
16 | paths-ignore:
17 | - 'LICENCE'
18 | - '*.md'
19 | branches:
20 | - main
21 | - devel
22 |
23 | jobs:
24 | setup:
25 | runs-on: ${{ matrix.os }}
26 | strategy:
27 | matrix:
28 | os:
29 | - ubuntu-latest
30 |
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v2
34 |
35 | - name: build
36 | run: |
37 | docker build -t torci:test tests/sandbox
38 |
39 | - name: run test
40 | run: |
41 | docker run --rm -v `pwd`:/src/torci torci:test
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | torci
2 | style.css
3 | nimcache/
4 | /testresults
5 | /tools/gencss
6 | ._*.*
7 | /src/views/docs.nim
8 | /src/routes/docs.nim
9 | /src/routes/login.nim
10 | /dokman.nim
11 | /src/views/confs.nim
12 | /src/routes/confs.nim
13 | *.bak
14 | bullseye
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nimlang/nim:alpine as nim
2 | EXPOSE 1984
3 |
4 | RUN USER=root apk --no-cache add libsass-dev libffi-dev pcre-dev openssl-dev openssh-client openssl sudo tor wpa_supplicant dhcpcd openrc bsd-compat-headers
5 |
6 | COPY . /src/torci
7 | WORKDIR /src/torci
8 | # create hostapd environment
9 | RUN mkdir /etc/hostapd
10 | RUN curl -o "/etc/hostapd/hostapd.conf" -A "Mozilla/5.0 (Windows NT 10.0; rv:98.0) Gecko/20100101 Firefox/91.0" https://raw.githubusercontent.com/radio24/TorBox/master/etc/hostapd/hostapd.conf \
11 | && cp /etc/hostapd/hostapd.conf /etc/hostapd/hostapd.conf.tbx
12 |
13 | # add torbox user
14 | RUN adduser --disabled-password --gecos "" torbox && echo "torbox:torbox" | chpasswd
15 |
16 | RUN nimble build -d:release -y && nimble scss
17 | CMD ["./torci"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | 
3 | 
4 |
5 | # TorCI
6 |
7 | TorCI is a Configuration Interface for [TorBox](https://github.com/radio24/torbox). It is implemented in the [Nim](https://nim-lang.org) programming language.
8 |
9 | WARNING: THIS IS A ALPHA VERSION, THEREFORE YOU MAY ENCOUNTER BUGS. IF YOU DO, OPEN AN ISSUE VIA OUR GITHUB REPOSITORY.
10 |
11 | ## Features:
12 |
13 | - [x] Configure [TorBox](https://radio24/torbox) as easy as [OpenWrt](https://github.com/openwrt)'s [LuCI](https://github.com/openwrt/luci)
14 | - [x] ~~JavaScript not required~~
15 | - [x] No Terminal
16 | - [x] Mobile-friendly
17 | - [x] Lightweight
18 |
19 | ## Roadmap
20 |
21 | - [ ] Improving UI
22 | - [ ] All TorBox features support
23 | - [ ] ~~HTTPS support~~
24 | - [ ] Themes support
25 |
26 | ## Screenshots
27 | 
28 | 
29 | 
30 | 
31 |
32 | ## Installation
33 |
34 | ### Docker
35 |
36 | To build and run TorCI in Docker
37 |
38 | ```bash
39 | $ docker build -t torci:debug .
40 | $ docker run --rm -d -p 1984:1984 torci:debug
41 | # See debug logs
42 | $ docker logs `CONTAINER_ID`
43 | ```
44 |
45 | Reach TorCI: `0.0.0.0:1984` (username and password: `torbox`)
46 |
47 | ### Nimble
48 |
49 | To compile the scss files, you need to install `libsass`. On Ubuntu and Debian, you can use `libsass-dev`.
50 |
51 | ```bash
52 | $ git clone https://github.com/nonnil/torci
53 | $ cd torci
54 | $ nimble build
55 | $ nimble scss
56 | ```
57 |
58 | and Run:
59 |
60 | ```bash
61 | $ sudo ./torci
62 | ```
63 |
64 | Then access the following address with a browser:
65 |
66 | ```
67 | http://0.0.0.0:1984
68 | ```
69 | ## SystemD
70 | You can use the SystemD service (install it on `/etc/systemd/system/torci.service`)
71 |
72 | To run TorCI via SystemD you can use this service file:
73 |
74 | ```ini
75 | [Unit]
76 | Description=front-end for TorBox
77 | After=syslog.target
78 | After=network.target
79 |
80 | [Service]
81 | Type=simple
82 |
83 | User=root
84 |
85 | WorkingDirectory=/home/torbox/torci
86 | ExecStart=/home/torbox/torci/torci
87 |
88 | Restart=always
89 | RestartSec=15
90 |
91 | [Install]
92 | WantedBy=multi-user.target
93 | ```
94 |
--------------------------------------------------------------------------------
/bridges.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/bridges.png
--------------------------------------------------------------------------------
/build/bullseye:
--------------------------------------------------------------------------------
1 | FROM debian:bullseye-slim as bullseye
2 | MAINTAINER nilnilnilnil@protonmail.com
3 | EXPOSE 1984
4 | ENV DEBIAN_FRONTEND=noninteractive
5 | # RUN USER=root apk --no-cache add libsass-dev libffi-dev pcre-dev openssl-dev openssh-client openssl sudo tor wpa_supplicant dhcpcd openrc bsd-compat-headers
6 | RUN apt update && \
7 | apt install -y build-essential libsass-dev openssl git curl \
8 | binutils-arm-linux-gnueabi \
9 | gcc-arm-linux-gnueabihf
10 |
11 | RUN curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y
12 |
13 | # COPY . /src/torci
14 | WORKDIR /src/torci
15 |
16 | CMD \
17 | export PATH="${PATH}":$HOME/.nimble/nim/bin:$HOME/.nimble/bin && \
18 | nimble --os:linux --cpu:$ARCH -d:strip -d:release -y build && \
19 | nimble scss
--------------------------------------------------------------------------------
/build/buster:
--------------------------------------------------------------------------------
1 | FROM debian:buster-slim as buster
2 | MAINTAINER nilnilnilnil@protonmail.com
3 | EXPOSE 1984
4 | ENV DEBIAN_FRONTEND=noninteractive
5 | # RUN USER=root apk --no-cache add libsass-dev libffi-dev pcre-dev openssl-dev openssh-client openssl sudo tor wpa_supplicant dhcpcd openrc bsd-compat-headers
6 | RUN apt update && \
7 | apt install -y build-essential libsass-dev openssl git curl \
8 | binutils-arm-linux-gnueabi \
9 | gcc-arm-linux-gnueabihf
10 |
11 | RUN curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y
12 |
13 | # RUN PATH=$HOME/.nimble/bin:$PATH && \
14 | # export PATH="${PATH}":$HOME/.nimble/nim/bin:$HOME/.nimble/bin
15 | # RUN echo "export PATH=/root/.nimble/bin:$PATH" >> /etc/bash.bashrc
16 | # echo PATH=$HOME/.nimble/bin:$PATH >> ~/.profile
17 | # export PATH="$(abspath \"$HOME/.nimble/bin\"):$PATH"
18 | # ENV PATH "/root/.nimlbe/bin:$PATH"
19 |
20 | # COPY . /src/torci
21 | WORKDIR /src/torci
22 |
23 | # RUN nimble install -y
24 |
25 | CMD \
26 | export PATH="${PATH}":$HOME/.nimble/nim/bin:$HOME/.nimble/bin && \
27 | nimble --os:linux --cpu:$ARCH -d:strip -d:release -y build && \
28 | nimble scss
--------------------------------------------------------------------------------
/config.nims:
--------------------------------------------------------------------------------
1 | --define:ssl
2 | --define:useStdLib
3 |
4 | # workaround httpbeast file upload bug
5 | --assertions:off
6 | #--shellNoDebugOutput:off
7 |
8 | # disable annoying warnings
9 | warning("GcUnsafe2", off)
10 | warning("ObservableStores", off)
11 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "0.1"
2 | services:
3 | torci-test:
4 | build: .
5 | ports:
6 | - "1984:1984"
7 | volumes:
8 | - .:/src/torci
9 | depend_on:
10 | - redis
11 | redis:
12 | image: redis:7-alpine
13 | command: redis-server
--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/install.sh
--------------------------------------------------------------------------------
/mockups/login.nim:
--------------------------------------------------------------------------------
1 | import jester
2 | import ../ tests / server / server
3 | import ".." / src / views / login
4 | import ".." / src / renderutils
5 |
6 | router loginPage:
7 | get "/":
8 | resp renderFlat(renderLogin(), "Login")
9 |
10 | serve(loginPage, 1984.Port)
--------------------------------------------------------------------------------
/mockups/net.nim:
--------------------------------------------------------------------------------
1 | import std / [ options, importutils ]
2 | import jester
3 | import karax / [ karaxdsl, vdom ]
4 | import ../ tests / server / server
5 | import ".." / src / notice
6 | import ".." / src / lib / sys
7 | import ".." / src / lib / hostap
8 | import ".." / src / renderutils
9 | import ".." / src / routes / tabs
10 |
11 | router network:
12 | template tab(): Tab =
13 | buildTab:
14 | "Bridges" = "/net" / "bridges"
15 | "Interfaces" = "/net" / "interfaces"
16 | "Wireless" = "/net" / "wireless"
17 |
18 | get "/net/wireless":
19 | privateAccess(HostAp)
20 | privateAccess(HostAp)
21 | privateAccess(HostApConf)
22 | privateAccess(HostApStatus)
23 | privateAccess(Devices)
24 | privateAccess(Device)
25 | let hostap = HostAp(
26 | conf: HostApConf(
27 | iface: some(wlan0),
28 | ssid: "Mirai-bot",
29 | password: "changeme",
30 | band: 'a',
31 | channel: "36",
32 | isHidden: true
33 | ),
34 | status: HostApStatus(isActive: true)
35 | )
36 |
37 | let devs = Devices(
38 | list: @[
39 | Device(
40 | macaddr: "33:cb:49:23:fc",
41 | ipaddr: "192.168.42.11",
42 | signal: "-66 dBm"
43 | ),
44 | Device(
45 | macaddr: "43:3b:dc:c9:a6:f8",
46 | ipaddr: "192.168.42.14",
47 | signal: "-49 dBm"
48 | ),
49 | Device(
50 | macaddr: "cf:f9:d5:f6:91:f9",
51 | ipaddr: "192.168.42.13",
52 | signal: "-41 dBm"
53 | ),
54 | Device(
55 | macaddr: "77:42:4d:c6:90:32",
56 | ipaddr: "192.168.42.12",
57 | signal: "-50 dBm"
58 | )
59 | ]
60 | )
61 | const isModel3 = false
62 |
63 | resp: render "Wireless":
64 | tab: tab
65 | container:
66 | hostap.render(isModel3)
67 | devs.render()
68 |
69 | serve(network, 1984.Port)
--------------------------------------------------------------------------------
/mockups/stat.nim:
--------------------------------------------------------------------------------
1 | import std / [ options, importutils ]
2 | import jester
3 | import results, resultsutils, jsony
4 | import karax / [ karaxdsl, vdom, vstyles ]
5 | import ../ tests / server / server
6 | import ".." / src / notice
7 | import ".." / src / lib / tor {.all.}
8 | import ".." / src / lib / sys
9 | import ".." / src / lib / wirelessManager
10 | import ".." / src / renderutils
11 | import ".." / src / routes / tabs
12 | import std / times
13 | import std / json
14 |
15 | router stat:
16 | get "/api/checktor":
17 | var check = TorStatus.new()
18 | match await checkTor("127.0.0.1", 9050.Port):
19 | Ok(ret): check = ret
20 | Err(): discard
21 |
22 | resp check.toJson().fromJson()
23 |
24 | get "/api/bridgesinfo":
25 | var br: Bridge
26 | match await getBridge():
27 | Ok(ret): br = ret
28 | Err(): discard
29 |
30 | resp br.toJson().fromJson()
31 |
32 | get "/api/sysinfo":
33 | var si = SystemInfo.default()
34 | match await getSystemInfo():
35 | Ok(ret): si = ret
36 | Err(): discard
37 |
38 | resp si.toJson().fromJson()
39 |
40 | get "/api/ioinfo":
41 | var
42 | ii = IoInfo.new()
43 | ap = ConnectedAp.new()
44 | match await getIoInfo():
45 | Ok(ret): ii = ret
46 | Err(): discard
47 | if ii.internet.isSome:
48 | let iface = ii.internet.get
49 | match await getConnectedAp(iface):
50 | Ok(ret): ap = ret
51 | Err(): discard
52 | echo ap.toJson()
53 | var j = ii.toJson().fromJson()
54 | let j2 = ap.toJson().fromJson()
55 | # j.add(ap.toJson().fromJson())
56 | j.add("ap", j2)
57 | echo j
58 | resp j
59 |
60 | # get "/api/ap":
61 | # var ap = ConnectedAp.new()
62 | # match await getConnectedAp():
63 | # Ok(ret): ap = ret
64 | # Err(): discard
65 | # resp ap.toJson().fromJson()
66 |
67 | # get "/io/js2":
68 | # resp: render "JS2":
69 | # container:
70 | # TorInfo.render2()
71 |
72 | get "/io/js":
73 | resp: render "JS":
74 | container:
75 | buildhtml(tdiv(id="ROOT"))
76 | buildHtml(script(`type`="text/javascript", src="/js/status.js"))
77 |
78 | get "/static/io":
79 | privateAccess(TorInfo)
80 | privateAccess(TorStatus)
81 | privateAccess(Bridge)
82 | privateAccess(IoInfo)
83 | privateAccess(SystemInfo)
84 | privateAccess(CpuInfo)
85 | privateAccess(ConnectedAp)
86 | var
87 | ti = TorInfo(
88 | status: TorStatus(isTor: true),
89 | bridge: Bridge(
90 | useBridges: true,
91 | kind: obfs4
92 | )
93 | )
94 |
95 | si = SystemInfo(
96 | cpu: CpuInfo(
97 | model: "Raspberry Pi 4 Model B Rev 1.2",
98 | architecture: "ARMv7 Processor rev 3 (v7l)"
99 | ),
100 | kernelVersion: "5.10.17-v7l+",
101 | torboxVer: "0.5.0"
102 | )
103 |
104 | ii = IoInfo(
105 | internet: some(wlan0),
106 | hostap: some(wlan1)
107 | )
108 |
109 | ap = ConnectedAp(
110 | ssid: "Mirai-bot",
111 | ipaddr: "192.168.19.84"
112 | )
113 |
114 | resp: render "Status":
115 | container:
116 | ti.render()
117 | ii.render(ap)
118 | si.render()
119 |
120 | before "/cube":
121 | let cube = buildHtml(tdiv(class="loading cube")):
122 | tdiv()
123 | tdiv()
124 | tdiv()
125 | tdiv()
126 | tdiv()
127 | tdiv()
128 |
129 | resp: render "Cube":
130 | container:
131 | cube
132 |
133 | patch "/cube":
134 | await sleepAsync(5000)
135 | let greet = buildHtml(tdiv()):
136 | text "I'm wake up"
137 | resp renderFlat(greet, "Cube")
138 |
139 | get "/test":
140 | resp """
141 |
142 |
143 |
144 | Multiple Karax apps
145 |
146 |
147 |
148 |
149 |
154 |
155 | Use multiple karax apps.
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | Some example html
165 |
166 |
167 | Company |
168 | Contact |
169 | Country |
170 |
171 |
172 | Alfreds Futterkiste |
173 | Maria Anders |
174 | Germany |
175 |
176 |
177 | Centro comercial Moctezuma |
178 | Francisco Chang |
179 | Mexico |
180 |
181 |
182 |
183 |
184 | """
185 |
186 | serve(stat, 1984.Port)
--------------------------------------------------------------------------------
/mockups/sys.nim:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/mockups/sys.nim
--------------------------------------------------------------------------------
/nim.cfg:
--------------------------------------------------------------------------------
1 | -d: ssl
2 | -d: strip
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/android-chrome-384x384.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/css/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/css/.gitkeep
--------------------------------------------------------------------------------
/public/css/fontello-codes.css:
--------------------------------------------------------------------------------
1 |
2 | .icon-cog:before { content: '\e800'; } /* '' */
3 | .icon-trash-empty:before { content: '\e801'; } /* '' */
4 | .icon-help-circled:before { content: '\e802'; } /* '' */
5 | .icon-th-large:before { content: '\e803'; } /* '' */
6 | .icon-eye:before { content: '\e804'; } /* '' */
7 | .icon-eye-off:before { content: '\e805'; } /* '' */
8 | .icon-cw:before { content: '\e806'; } /* '' */
9 | .icon-logout:before { content: '\e807'; } /* '' */
10 | .icon-attention:before { content: '\e808'; } /* '' */
11 | .icon-tor:before { content: '\e809'; } /* '' */
12 | .icon-github-circled:before { content: '\f09b'; } /* '' */
13 | .icon-sliders:before { content: '\f1de'; } /* '' */
14 | .icon-wifi:before { content: '\f1eb'; } /* '' */
15 | .icon-user-circle:before { content: '\f2bd'; } /* '' */
16 | .icon-user-circle-o:before { content: '\f2be'; } /* '' */
17 |
--------------------------------------------------------------------------------
/public/css/fontello.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'fontello';
3 | src: url('../font/fontello.eot?45091611');
4 | src: url('../font/fontello.eot?45091611#iefix') format('embedded-opentype'),
5 | url('../font/fontello.woff2?45091611') format('woff2'),
6 | url('../font/fontello.woff?45091611') format('woff'),
7 | url('../font/fontello.ttf?45091611') format('truetype'),
8 | url('../font/fontello.svg?45091611#fontello') format('svg');
9 | font-weight: normal;
10 | font-style: normal;
11 | }
12 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
13 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
14 | /*
15 | @media screen and (-webkit-min-device-pixel-ratio:0) {
16 | @font-face {
17 | font-family: 'fontello';
18 | src: url('../font/fontello.svg?45091611#fontello') format('svg');
19 | }
20 | }
21 | */
22 | [class^="icon-"]:before, [class*=" icon-"]:before {
23 | font-family: "fontello";
24 | font-style: normal;
25 | font-weight: normal;
26 | speak: never;
27 |
28 | display: inline-block;
29 | text-decoration: inherit;
30 | width: 1em;
31 | margin-right: .2em;
32 | text-align: center;
33 | /* opacity: .8; */
34 |
35 | /* For safety - reset parent styles, that can break glyph codes*/
36 | font-variant: normal;
37 | text-transform: none;
38 |
39 | /* fix buttons height, for twitter bootstrap */
40 | line-height: 1em;
41 |
42 | /* Animation center compensation - margins should be symmetric */
43 | /* remove if not needed */
44 | margin-left: .2em;
45 |
46 | /* you can be more comfortable with increased icons size */
47 | /* font-size: 120%; */
48 |
49 | /* Font smoothing. That was taken from TWBS */
50 | -webkit-font-smoothing: antialiased;
51 | -moz-osx-font-smoothing: grayscale;
52 |
53 | /* Uncomment for 3D effect */
54 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
55 | }
56 |
57 | .icon-cog:before { content: '\e800'; } /* '' */
58 | .icon-trash-empty:before { content: '\e801'; } /* '' */
59 | .icon-help-circled:before { content: '\e802'; } /* '' */
60 | .icon-th-large:before { content: '\e803'; } /* '' */
61 | .icon-eye:before { content: '\e804'; } /* '' */
62 | .icon-eye-off:before { content: '\e805'; } /* '' */
63 | .icon-cw:before { content: '\e806'; } /* '' */
64 | .icon-logout:before { content: '\e807'; } /* '' */
65 | .icon-attention:before { content: '\e808'; } /* '' */
66 | .icon-tor:before { content: '\e809'; } /* '' */
67 | .icon-github-circled:before { content: '\f09b'; } /* '' */
68 | .icon-sliders:before { content: '\f1de'; } /* '' */
69 | .icon-wifi:before { content: '\f1eb'; } /* '' */
70 | .icon-user-circle:before { content: '\f2bd'; } /* '' */
71 | .icon-user-circle-o:before { content: '\f2be'; } /* '' */
72 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/favicon.ico
--------------------------------------------------------------------------------
/public/font/fontello.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/font/fontello.eot
--------------------------------------------------------------------------------
/public/font/fontello.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
41 |
--------------------------------------------------------------------------------
/public/font/fontello.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/font/fontello.ttf
--------------------------------------------------------------------------------
/public/font/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/font/fontello.woff
--------------------------------------------------------------------------------
/public/font/fontello.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/font/fontello.woff2
--------------------------------------------------------------------------------
/public/images/torbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nonnil/TorCI/762eb916888382300e66f0c0c57d4da841f9645a/public/images/torbox.png
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TorCI",
3 | "short_name": "TorCI",
4 | "description": "TorBox front-end",
5 | "icons": [
6 | {
7 | "src": "/android-chrome-192x192.png",
8 | "sizes": "192x192",
9 | "type": "image/png"
10 | },
11 | {
12 | "src": "/android-chrome-384x384.png",
13 | "sizes": "384x384",
14 | "type": "image/png"
15 | }
16 | ],
17 | "theme_color": "#333333",
18 | "background_color": "#333333",
19 | "display": "standalone"
20 | }
21 |
--------------------------------------------------------------------------------
/src/config.nim:
--------------------------------------------------------------------------------
1 | import parsecfg except Config
2 | import nativesockets, strutils
3 | import typeinfo
4 |
5 | type
6 | Config* = ref object
7 | address*: string
8 | port*: Port
9 | useHttps*: bool
10 | title*: string
11 | torciVer*: string
12 | torboxVer*: string
13 | hostname*: string
14 | staticDir*: string
15 | torAddress*: string
16 | torPort*: Port
17 |
18 | proc get*[T](config: parseCfg.Config; s, v: string; default: T): T =
19 | let val = config.getSectionValue(s, v)
20 | if val.len == 0: return default
21 |
22 | when T is int: parseInt(val)
23 | elif T is Port: parseInt(val).Port
24 | elif T is bool: parseBool(val)
25 | elif T is string: val
26 |
27 | proc getConfig*(path: string): (Config, parseCfg.Config) =
28 | var
29 | cfg = loadConfig(path)
30 | address: string = "192.168.42.1"
31 | torAddress: string = "127.0.0.1"
32 | torPort = 9050.Port
33 | let conf = Config(
34 | address: cfg.get("Server", "address", address),
35 | port: cfg.get("Server", "port", 1984.Port),
36 | useHttps: cfg.get("Server", "https", true),
37 | title: cfg.get("Server", "title", "TorBox"),
38 | torciVer: cfg.get("Server", "torciVer", "nil"),
39 | torboxVer: cfg.get("Server", "torboxVer", "nil"),
40 | staticDir: cfg.get("Server", "staticDir", "./public"),
41 | torAddress: cfg.get("Server", "torAddress", torAddress),
42 | torPort: cfg.get("Server", "torPort", torPort)
43 | )
44 | return (conf, cfg)
45 |
--------------------------------------------------------------------------------
/src/lib/binascii.nim:
--------------------------------------------------------------------------------
1 | import std / strutils
2 |
3 | proc isAlphaNumeric(s: string): bool
4 |
5 | proc a2bHex*(s: string): string =
6 |
7 | if not s.isAlphaNumeric(): return
8 |
9 | const longDigit: array[256, int] = [
10 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
11 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
12 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
13 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 37, 37, 37, 37, 37, 37,
14 | 37, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
15 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 37, 37, 37, 37,
16 | 37, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
17 | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 37, 37, 37, 37,
18 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
19 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
20 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
21 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
22 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
23 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
24 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
25 | 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
26 | ]
27 |
28 | var i: int
29 |
30 | while i < s.len:
31 | let
32 | top = longDigit[int(s[i])]
33 | bot = longDigit[int(s[i+1])]
34 |
35 | let
36 | base: uint8 = uint8((top shl 4) + bot)
37 | hex = base.toHex
38 | hexStr = hex.parseHexStr()
39 |
40 | result &= hexStr
41 |
42 | i.inc 2
43 |
44 | proc isAlphaNumeric(s: string): bool =
45 | for c in s:
46 | if not c.isAlphaNumeric(): return false
47 |
48 | return true
--------------------------------------------------------------------------------
/src/lib/clib/c_crypt.nim:
--------------------------------------------------------------------------------
1 | {.passL: "-lcrypt".}
2 | proc crypt*(key, salt: cstring): cstring {.importc: "crypt", header: "".}
3 | proc crypt*(key, salt: string): string =
4 | let crypt = crypt(cstring(key), cstring(salt))
5 | $crypt
--------------------------------------------------------------------------------
/src/lib/clib/shadow.nim:
--------------------------------------------------------------------------------
1 | type
2 | Spwd* {.importc: "struct spwd", header: "".} = ptr object
3 | name* {.importc: "sp_namp".}: cstring
4 | passwd* {.importc: "sp_pwdp".}: cstring
5 | sp_lstchg {.importc: "sp_lstchg".}: clong
6 | min {.importc: "sp_min".}: clong
7 | max {.importc: "sp_max".}: clong
8 | warn {.importc: "sp_warn".}: clong
9 | inact {.importc: "sp_inact".}: clong
10 | expire {.importc: "sp_expire".}: clong
11 | flag {.importc: "sp_flag".}: culong
12 |
13 | proc getShadow*(name: cstring): Spwd {.importc: "getspnam", header: "".}
--------------------------------------------------------------------------------
/src/lib/crypt.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | strutils, strformat
3 | ]
4 | import results, resultsutils
5 |
6 | type
7 | CryptPrefix* = enum
8 | yescrypt = "y"
9 | gostYescrypt = "gy"
10 | scrypt = "7"
11 |
12 | bcrypt_a = "2a"
13 | bcrypt_b = "2b"
14 | bcrypt_x = "2x"
15 | bcrypt_y = "2y"
16 |
17 | sha512crypt = "6"
18 | sha256crypt = "5"
19 | sha1crypt = "sha1"
20 |
21 | sunMd5 = "md5"
22 | md5crypt = "1"
23 | bsdicrypt = "_"
24 | bigcrypt, descrypt = ""
25 | nt = "3"
26 |
27 | Shadow* = ref object
28 | prefix: CryptPrefix
29 | salt: string
30 |
31 | proc parsePrefix*(str: string): Result[CryptPrefix, string] =
32 | try:
33 | let ret = parseEnum[CryptPrefix](str)
34 | return ok(ret)
35 |
36 | except: return err("Failure to parse hashing method of shadow")
37 |
38 | proc readAsShadow*(passwd: string): Result[Shadow, string] =
39 | var
40 | passwd = passwd
41 | shadow = new Shadow
42 | expectPrefix: string
43 |
44 | try:
45 | if passwd[0] == '$':
46 | passwd = passwd[1..(passwd.len - 1)]
47 |
48 | # elif passwd[0] == '_':
49 | let columns = passwd.split('$')
50 | expectPrefix = columns[0]
51 |
52 | match parsePrefix(expectPrefix):
53 | Ok(prefix):
54 | case prefix
55 | of yescrypt, gostYescrypt:
56 | if columns.len == 4:
57 | shadow.salt = fmt"{columns[1]}${columns[2]}"
58 | shadow.prefix = prefix
59 |
60 | of sha512crypt, sha256crypt:
61 | if columns.len == 3:
62 | shadow.salt = columns[1]
63 | shadow.prefix = prefix
64 |
65 | else: return err("Invalid hashing method")
66 | Err(msg): return err(msg)
67 |
68 | return ok shadow
69 | except OSError as e: return err(e.msg)
70 | except IOError as e: return err(e.msg)
71 | except ValueError as e: return err(e.msg)
72 | except KeyError as e: return err(e.msg)
73 | except: return err("Something went wrong")
74 |
75 | proc fmtSalt*(shadow: Shadow): string =
76 | result = fmt"${$shadow.prefix}${shadow.salt}"
77 |
--------------------------------------------------------------------------------
/src/lib/fallbacks.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | options,
3 | os, osproc,
4 | re, strutils, strformat,
5 | asyncdispatch, logging ]
6 | import sys, hostap
7 | import sys / [ iface, service ]
8 |
9 | proc hostapdFallback*() {.async.} =
10 | try:
11 | echo("Start hostapd fallback")
12 | if isRouter(wlan1):
13 | echo($wlan1 & " is router")
14 | var f = readFile hostapd
15 | f = f.replace("interface=wlan0", "interface=wlan1")
16 | writeFile hostapd, f
17 |
18 | restartService("hostapd")
19 |
20 | if isRouter(wlan1):
21 | echo($wlan1 & " is router")
22 | var f = readFile hostapd
23 | f = f.replace("interface=wlan1", "interface=wlan0")
24 | writeFile hostapd, f
25 |
26 | let isActive = hostapdIsActive()
27 |
28 | if not isActive:
29 | echo("hostapd is not active")
30 | copyFile hostapdBakup, hostapd
31 |
32 | if hasStaticIp(wlan1):
33 | echo("wlan1 has static ip")
34 | var f = readFile hostapd
35 | f = f.replace("interface=wlan0", "interface=wlan0")
36 | writeFile hostapd, f
37 |
38 | restartService("hostapd")
39 |
40 | if hasStaticIp(wlan1):
41 | echo("wlan1 has static ip")
42 | var f = readFile hostapd
43 | f = f.replace("interface=wlan1", "interface=wlan0")
44 | writeFile hostapd, f
45 | echo("end hostapd fallback")
46 |
47 | except:
48 | return
49 |
50 | proc hostapdFallbackKomplex*(wlan, eth: IfaceKind) =
51 | const rPath = "/etc" / "network" / "interfaces"
52 | let lPath = getHomeDir() / "torbox" / "etc" / "network" / &"interfaces.{$wlan}{$eth}"
53 |
54 | if (not fileExists(lPath)) or (not fileExists(rPath)):
55 | return
56 |
57 | var
58 | newWlan: IfaceKind
59 | newEth: IfaceKind
60 | downedWlan: bool
61 | downedEth: bool
62 | cmd: string
63 |
64 | # wlan and eth are clients - newWlan and newEth are potential Internet sources
65 | newWlan = if wlan == wlan1: wlan0 else: wlan1
66 | newEth = if eth == eth1: eth0 else: eth1
67 |
68 | # First, we have to shutdown the interface with running dhcpclients, before we copy the interfaces file
69 | if dhclientWork(wlan):
70 | refreshdhclient()
71 | ifdown(wlan)
72 | downedwlan = true
73 |
74 | if dhclientWork(eth):
75 | refreshdhclient()
76 | ifdown(eth)
77 | downedeth = true
78 |
79 | copyfile(lpath, rpath)
80 |
81 | if downedwlan:
82 | ifup(wlan)
83 | downedWlan = false
84 |
85 | if downedEth:
86 | ifup(eth)
87 | downedEth = false
88 |
89 | # Is wlan ready?
90 | # If wlan0 or wlan1 doesn't have an IP address then we have to do something about it!
91 | if not hasStaticIp(wlan):
92 | ifdown(wlan)
93 | # Cannot be run in the background because then it jumps into the next if-then-else clause (still missing IP)
94 | ifup(wlan)
95 |
96 | # If wlan0 or wlan1 is not acting as AP then we have to do something about it!
97 | let
98 | conf = waitFor getHostApConf()
99 | iface = conf.iface
100 | if iface.isNone:
101 | try:
102 | var f = readFile(hostapd)
103 | f = f.replace(re"interface=.*", "interface=" & $wlan)
104 | restartService("hostapd")
105 | sleep 5
106 | if not hostapdIsActive():
107 | f = f.multiReplace(
108 | @[
109 | ("hw_mode=a", "hw_mode=g"),
110 | ("channel=.*", "channel=6"),
111 | ("ht_capab=[HT40-][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]", "#ht_capab=[HT40-][HT40+][SHORT-GI-20][SHORT-GI-40][DSSS_CCK-40]"),
112 | ("vht_oper_chwidth=1", "#vht_oper_chwidth=1"),
113 | ("vht_oper_centr_freq_seg0_idx=42", "#vht_oper_centr_freq_seg0_idx=42")
114 | ]
115 | )
116 | writeFile(hostapd, f)
117 | restartService("hostapd")
118 |
119 | except:
120 | return
121 |
122 | # Is eth ready?
123 | if isStateup(eth):
124 | if not isRouter(eth):
125 | ifdown(eth)
126 | ifup(eth)
127 |
128 | else:
129 | ifdown(eth)
130 | ifup(eth)
131 |
132 | # Is newWlan ready?
133 | # Because it is a possible Internet source, the Interface should be up, but
134 | # the IP adress shouldn't be 192.168.42.1 or 192.168.43.1
135 | if isStateup(newWlan):
136 |
137 | if not hasStaticIp(newWlan):
138 | ifdown(newWlan)
139 | ifup(newWlan)
140 |
141 | if isRouter(newWlan):
142 | ifdown(newWlan)
143 | ifup(newWlan)
144 |
145 | else:
146 | ifdown(newWlan)
147 | flush(newWlan)
148 | ifup(newWlan)
149 |
150 | # Is newEth ready?
151 | # Because it is a possible Internet source, the Interface should be up, but
152 | # the IP adress shouldn't be 192.168.42.1 or 192.168.43.1
153 | if isStateup(newEth):
154 |
155 | if not hasStaticIp(newEth):
156 | ifdown(newEth)
157 | ifup(newEth)
158 |
159 | if isRouter(newEth):
160 | ifdown(newEth)
161 | ifup(newEth)
162 |
163 | else:
164 | ifdown(newEth)
165 | flush(newEth)
166 | ifup(newEth)
167 |
168 | # This last part resets the dhcp server and opens the iptables to access TorBox
169 | # This fundtion has to be used after an ifup command
170 | # Important: the right iptables rules to use Tor have to configured afterward
171 | restartDhcpServer()
172 | discard execCmd("sudo /sbin/iptables -F")
173 | discard execCmd("sudo /sbin/iptables -t nat -F")
174 | discard execCmd("sudo /sbin/iptables -P FORWARD DROP")
175 | discard execCmd("sudo /sbin/iptables -P INPUT ACCEPT")
176 | discard execCmd("sudo /sbin/iptables -P OUTPUT ACCEPT")
--------------------------------------------------------------------------------
/src/lib/hostap.nim:
--------------------------------------------------------------------------------
1 | import hostap / [ conf, vdom ]
2 |
3 | export conf, vdom
--------------------------------------------------------------------------------
/src/lib/hostap/vdom.nim:
--------------------------------------------------------------------------------
1 | import std / [ strutils, strformat ]
2 | import karax / [ karaxdsl, vdom ]
3 | import conf
4 | import ../ ../ renderutils
5 |
6 | # procs for front-end
7 | func renderChannelSelect*(band: char, isModel3: bool): VNode =
8 | buildHtml(select(name="channel")):
9 | option(selected="selected"): text "-- Select a channel --"
10 | if isModel3:
11 | option(value="ga"): text "1 at 20 MHz"
12 | option(value="gc"): text "2 at 20 MHz"
13 | option(value="ge"): text "3 at 20 MHz"
14 | option(value="gg"): text "4 at 20 MHz"
15 | option(value="gi"): text "5 at 20 MHz"
16 | option(value="gk"): text "6 at 20 MHz (default)"
17 | option(value="gm"): text "7 at 20 MHz"
18 | option(value="go"): text "8 at 20 MHz"
19 | option(value="gq"): text "9 at 20 MHz"
20 | option(value="gs"): text "10 at 20 MHz"
21 | option(value="gu"): text "11 at 20 MHz"
22 |
23 | elif band == 'g':
24 | option(value="ga"): text "1 at 20 MHz"
25 | option(value="gb"): text "1 at 40 MHz"
26 | option(value="gc"): text "2 at 20 MHz"
27 | option(value="gd"): text "2 at 40 MHz"
28 | option(value="ge"): text "3 at 20 MHz"
29 | option(value="gf"): text "3 at 40 MHz"
30 | option(value="gg"): text "4 at 20 MHz"
31 | option(value="gh"): text "4 at 40 MHz"
32 | option(value="gi"): text "5 at 20 MHz"
33 | option(value="gj"): text "5 at 40 MHz"
34 | option(value="gk"): text "6 at 20 MHz (default)"
35 | option(value="gl"): text "6 at 40 MHz"
36 | option(value="gm"): text "7 at 20 MHz"
37 | option(value="gn"): text "7 at 40 MHz"
38 | option(value="go"): text "8 at 20 MHz"
39 | option(value="gp"): text "8 at 40 MHz"
40 | option(value="gq"): text "9 at 20 MHz"
41 | option(value="gr"): text "9 at 40 MHz"
42 | option(value="gs"): text "10 at 20 MHz"
43 | option(value="gt"): text "10 at 40 MHz"
44 | option(value="gu"): text "11 at 20 MHz"
45 | option(value="gv"): text "11 at 40 MHz"
46 |
47 | elif band == 'a':
48 | option(value="aa"): text "36 at 40 MHz (default)"
49 | option(value="ab"): text "36 at 80 MHz"
50 | option(value="ac"): text "40 at 40 MHz"
51 | option(value="ad"): text "40 at 80 MHz"
52 | option(value="ae"): text "44 at 40 MHz"
53 | option(value="af"): text "44 at 80 MHz"
54 | option(value="ag"): text "48 at 40 MHz"
55 | option(value="ah"): text "48 at 80 MHz"
56 |
57 | func render*(hostap: HostApConf, isModel3: bool, width = 58): VNode =
58 | buildHtml(tdiv(class=fmt"columns width-{$width}")):
59 | tdiv(class="box"):
60 | tdiv(class="box-header"):
61 | text "Config"
62 | tdiv(class="btn edit-button"):
63 | icon "sliders"
64 | input(class="opening-button", `type`="radio", name="popout-button", value="open")
65 | input(class="closing-button", `type`="radio", name="popout-button", value="close")
66 | tdiv(class="shadow")
67 | tdiv(class="editable-box"):
68 | form(`method`="post", action="/net/wireless", enctype="multipart/form-data"):
69 | tdiv(class="card-table"):
70 | label(class="card-title"): text "SSID"
71 | input(`type`="text", name="ssid", placeholder=hostap.ssid)
72 | tdiv(class="card-table"):
73 | label(class="card-title"): text "Band"
74 | input(`type`="radio", name="band", value="g"): text "2.5GHz"
75 | input(`type`="radio", name="band", value="a"): text "5GHz"
76 | tdiv(class="card-table"):
77 | label(class="card-title"): text "Channel"
78 | renderChannelSelect(hostap.band, isModel3)
79 | tdiv(class="card-table"):
80 | if hostap.isHidden:
81 | label(class="card-title"): text "Unhide SSID"
82 | input(`type`="checkbox", name="ssidCloak", value="0")
83 | else:
84 | label(class="card-title"): text "Hide SSID"
85 | input(`type`="checkbox", name="ssidCloak", value="1")
86 | tdiv(class="card-table"):
87 | label(class="card-title"): text "Password"
88 | input(`type`="password", name="password", placeholder="Please enter 8 to 64 characters")
89 | button(`type`="submit", class="btn btn-apply saveBtn", name="saveBtn"):
90 | text "Save"
91 | table(class="full-width box-table"):
92 | tbody():
93 | tr():
94 | td(): text "SSID"
95 | td():
96 | strong():
97 | tdiv():
98 | text hostap.ssid
99 | tr():
100 | td(): text "Band"
101 | td():
102 | strong():
103 | tdiv():
104 | text case hostap.band
105 | of 'g':
106 | "2.5GHz"
107 | of 'a':
108 | "5GHz"
109 | else:
110 | "Unknown"
111 | tr():
112 | td(): text "Channel"
113 | td():
114 | strong():
115 | tdiv():
116 | text hostap.channel
117 | tr():
118 | td(): text "SSID Cloak"
119 | td():
120 | strong():
121 | tdiv():
122 | text if hostap.isHidden: "Hidden" else: "Visible"
123 | tr():
124 | td(): text "Password"
125 | td():
126 | strong():
127 | if not isEmptyOrWhitespace(hostap.password):
128 | tdiv(class="password_field_container"):
129 | tdiv(class="black_circle")
130 | icon "eye-off"
131 | input(class="btn show_password", `type`="radio", name="password_visibility", value="show")
132 | input(class="btn hide_password", `type`="radio", name="password_visibility", value="hide")
133 | tdiv(class="shadow")
134 | tdiv(class="password_preview_field"):
135 | tdiv(class="shown_password"): text hostap.password
136 | else:
137 | tdiv():
138 | text "No password has been set"
139 |
140 | func render*(status: HostApStatus, width = 38): VNode =
141 | buildHtml(tdiv(class=fmt"columns width-{$width}")):
142 | tdiv(class="box"):
143 | tdiv(class="box-header"):
144 | text "Actions"
145 | tdiv(class="card-padding"):
146 | form(`method`="post", action="/net/apctl", enctype="multipart/form-data"):
147 | if status.isActive:
148 | button(`type`="submit", class="btn btn-reload", name="ctl", value="reload"): text "Restart"
149 | button(`type`="submit", class="btn btn-disable", name="ctl", value="disable"): text "Disable"
150 | else:
151 | button(`type`="submit", class="btn btn-enable", name="ctl", value="enable"): text "Enable"
152 |
153 | func render*(self: HostAp, isModel3: bool, confWidth = 58, statusWidth = 38): VNode =
154 | result = newVNode(tdiv)
155 | result.add self.conf.render(isModel3, confWidth)
156 | result.add self.status.render(statusWidth)
--------------------------------------------------------------------------------
/src/lib/session.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | times, random,
3 | strutils
4 | ]
5 | import jester
6 | import redis
7 | import bcrypt
8 | import results, resultsutils
9 | import clib / [ c_crypt, shadow ]
10 | import crypt
11 | when defined(debug):
12 | import std / terminal
13 |
14 | # method username*(req: jester.Request): string {.base.} =
15 | # let token = req.cookies["torci_token"]
16 | # for user in userList.users:
17 | # if user.token == token:
18 | # return user.uname
19 |
20 | proc newExpireTime(): DateTime =
21 | result = getTime().utc + initTimeInterval(hours = 1)
22 |
23 | # method isExpired(user: User): bool {.base.} =
24 | # let now = getTime().utc
25 | # if user.expire < now:
26 | # return true
27 |
28 | # method isExpired(expire: DateTime): bool {.base.} =
29 | # let now = getTime().utc
30 | # if expire < now:
31 | # return true
32 |
33 | proc isLoggedIn*(req: jester.Request): Future[bool] {.async.} =
34 | if not req.cookies.hasKey("torci"): return
35 | let token = req.cookies["torci"]
36 | let red = await openAsync()
37 | return await red.exists(token)
38 |
39 | # method isValidUser*(req: jester.Request): bool {.base.} =
40 | # if (req.isLoggedIn) and (req)
41 |
42 | # Here's code taken from [https://github.com/nim-lang/nimforum/blob/master/src/auth.nim]
43 | proc randomSalt(): string =
44 | result = ""
45 | for i in 0..127:
46 | var r = rand(225)
47 | if r >= 32 and r <= 126:
48 | result.add(chr(rand(225)))
49 |
50 | proc devRandomSalt(): string =
51 | when defined(posix):
52 | result = ""
53 | var f = system.open("/dev/urandom")
54 | var randomBytes: array[0..127, char]
55 | discard f.readBuffer(addr(randomBytes), 128)
56 | for i in 0..127:
57 | if ord(randomBytes[i]) >= 32 and ord(randomBytes[i]) <= 126:
58 | result.add(randomBytes[i])
59 | f.close()
60 | else:
61 | result = randomSalt()
62 |
63 | proc makeSalt(): string =
64 | # Creates a salt using a cryptographically secure random number generator.
65 | #
66 | # Ensures that the resulting salt contains no ``\0``.
67 | try:
68 | result = devRandomSalt()
69 | except IOError:
70 | result = randomSalt()
71 |
72 | var newResult = ""
73 | for i in 0 ..< result.len:
74 | if result[i] != '\0':
75 | newResult.add result[i]
76 | return newResult
77 |
78 | proc makeSessionKey(): string =
79 | # Creates a random key to be used to authorize a session.
80 | let random = makeSalt()
81 | return bcrypt.hash(random, genSalt(8))
82 | # The end of [https://github.com/nim-lang/nimforum/blob/master/src/auth.nim] code
83 |
84 | proc splitShadow(str: string): seq[string] =
85 | let
86 | first = str.split("$")
87 | second = first[3].split(":")
88 | result = first
89 | result[3] = second[0]
90 |
91 | proc getUsername*(r: jester.Request): Future[string] {.async.} =
92 | let token = r.cookies["torci"]
93 | let red = await openAsync()
94 | return await red.get(token)
95 |
96 | proc login*(username, password: string): Future[Result[tuple[token: string, expire: DateTime], string]] {.async.} =
97 |
98 | # Generate a password hash using openssl cli
99 | # let
100 | # shadowCmd = &"sudo cat /etc/shadow | grep \"{username}\""
101 | # shadowOut = execCmdEx(shadowCmd).output
102 | # shadowV = splitShadow(shadowOut)
103 | # passwdCmd = &"openssl passwd -{shadowV[1]} -salt \"{shadowV[2]}\" \"{password}\""
104 | # spawnPasswd = execCmdEx(passwdCmd).output
105 |
106 | if username.isEmptyOrWhitespace: return err("Please set a username")
107 | elif password.isEmptyOrWhitespace: return err("Please set a password")
108 |
109 | try:
110 | let
111 | spwd = getShadow(cstring username)
112 | # splittedShadow = splitShadow($shadow.passwd)
113 | # ret = parseShadow($shadow.passwd)
114 |
115 | # ref: https://forum.nim-lang.org/t/8342
116 | if spwd.isnil: return err(username & " is not found")
117 | var shadow: Shadow
118 | # if ret.isErr:
119 | # result.err ret.error
120 | # return
121 | match readAsShadow($spwd.passwd):
122 | Ok(parse): shadow = parse
123 | Err(msg): return err(msg)
124 |
125 | let crypted: string = crypt(password, fmtSalt(shadow))
126 | when defined(debug):
127 | styledEcho(fgGreen, "Started login...")
128 | styledEcho(fgGreen, "[passwd] ", $spwd.passwd)
129 | styledEcho(fgGreen, "[Generated passwd] ", crypted)
130 | # var passwdV = spawnPasswd.split("$")
131 | # passwdV[3] = passwdV[3].splitWhitespace[0]
132 |
133 | if $spwd.passwd == crypted:
134 | let
135 | token = makeSessionKey()
136 | red = await openAsync()
137 | discard await red.setEx(token, 3600, username)
138 | result.ok (token, newExpireTime())
139 |
140 | except OSError as e: return err(e.msg)
141 | except IOError as e: return err(e.msg)
142 | except ValueError as e: return err(e.msg)
143 | except KeyError as e: return err(e.msg)
144 | except RedisError as e: return err(e.msg)
145 | except CatchableError as e: return err(e.msg)
146 | except NilAccessDefect as e: return err(e.msg)
147 | except: result.err "Something went wrong"
148 |
149 | proc logout*(req: Request): Future[bool] {.async.} =
150 | if not req.cookies.hasKey("torci"): return
151 | let token = req.cookies["torci"]
152 | let red = await openAsync()
153 | let loggedIn = await red.exists(token)
154 | if loggedIn:
155 | let del = await red.del(@[token])
156 | if del == 1: return true
157 | else: return false
158 |
159 | template loggedIn*(node: untyped) =
160 | if await request.isLoggedIn:
161 | node
162 | else:
163 | redirect "/login"
164 |
165 | template notLoggedIn*(node: untyped) =
166 | if not await request.isLoggedIn:
167 | node
168 | else:
169 | redirect "/"
--------------------------------------------------------------------------------
/src/lib/sys.nim:
--------------------------------------------------------------------------------
1 | import sys / [ sys, iface, service, vdom ]
2 |
3 | export sys, iface, service, vdom
--------------------------------------------------------------------------------
/src/lib/sys/iface.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | options, strutils
3 | ]
4 |
5 | type
6 | IfaceKind* = enum
7 | wlan0, wlan1,
8 | eth0, eth1,
9 | ppp0, usb0,
10 | tun0
11 |
12 | Iface* = ref object
13 | kind: IfaceKind
14 | status: IfaceStatus
15 |
16 | IfaceStatus* = ref object
17 | internet: IfaceKind
18 | hostap: IfaceKind
19 | vpnIsActive: bool
20 |
21 | proc isEth*(iface: IfaceKind): bool =
22 | iface == eth0 or iface == eth1
23 |
24 | proc isWlan*(iface: IfaceKind): bool =
25 | iface == wlan0 or iface == wlan1
26 |
27 | proc parseIfaceKind*(iface: string): Option[IfaceKind] =
28 | try:
29 | let ret = parseEnum[IfaceKind](iface)
30 | return some(ret)
31 | except: return none(IfaceKind)
32 |
33 | proc isIface*(iface: IfaceKind): bool =
34 | if iface in { wlan0, wlan1, eth0, eth1, ppp0, usb0, tun0 }:
35 | return true
36 | # let ret = iface.parseIfaceKind()
37 | # ret.isSome:
38 | # return true
--------------------------------------------------------------------------------
/src/lib/sys/service.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | osproc,
3 | asyncdispatch,
4 | strformat, strutils
5 | ]
6 |
7 | proc isActiveService*(service: string): Future[bool] {.async.} =
8 | try:
9 | if service.len >= 0 and
10 | service.contains(IdentChars):
11 | let cmd = fmt("systemctl is-active \"{service}\"")
12 | let
13 | ret = execCmdEx(cmd)
14 | sta = ret.output.splitLines()[0]
15 | if sta == "active":
16 | return true
17 | except OSError: return false
18 | except IOError: return false
19 | except ValueError: return false
20 | except KeyError: return false
21 | except CatchableError: return false
22 | except: return false
23 |
24 | proc startService*(s: string) =
25 | const cmd = "sudo systemctl start "
26 | discard execCmd(cmd & &"\"{s}\"")
27 |
28 | proc stopService*(s: string) =
29 | const cmd = "sudo systemctl stop "
30 | discard execCmd(cmd & &"\"{s}\"")
31 | discard execCmd("sudo systemctl daemon-reload")
32 |
33 | proc restartService*(s: string) =
34 | const cmd = "sudo systemctl restart "
35 | discard execCmd(cmd & &"\"{s}\"")
36 |
--------------------------------------------------------------------------------
/src/lib/sys/vdom.nim:
--------------------------------------------------------------------------------
1 | import std / [ options ]
2 | import karax / [ karaxdsl, vdom ]
3 | import sys
4 | # from ".." / ".." / settings import cfg, sysInfo
5 | import ../ wirelessManager
6 | from ../../ settings import cfg
7 |
8 | method render*(self: SystemInfo): VNode {.base.} =
9 | const defStr = "None"
10 | buildHtml(tdiv(class="columns full-width")):
11 | tdiv(class="card card-padding card-sys"):
12 | tdiv(class="card-header"):
13 | text "System"
14 | table(class="table full-width"):
15 | tbody():
16 | tr():
17 | td(): text "Model"
18 | td():
19 | strong():
20 | tdiv():
21 | text if self.model.len != 0: self.model else: defStr
22 | tr():
23 | td(): text "Kernel"
24 | td():
25 | strong():
26 | tdiv():
27 | text if self.kernelVersion.len != 0: self.kernelVersion else: defStr
28 | tr():
29 | td(): text "Architecture"
30 | td():
31 | strong():
32 | tdiv():
33 | text if self.architecture.len != 0: self.architecture else: defStr
34 | tr():
35 | td(): text "TorBox Version"
36 | td():
37 | strong():
38 | tdiv():
39 | text if self.torboxVersion.len > 0: self.torboxVersion else: "Unknown"
40 | tr():
41 | td(): text "TorCI Version"
42 | td():
43 | strong():
44 | tdiv():
45 | text cfg.torciVer
46 |
47 | func render*(self: IoInfo, ap: ConnectedAp): VNode =
48 | const defStr = "None"
49 | buildHtml(tdiv(class="columns")):
50 | tdiv(class="card card-padding card-sky"):
51 | tdiv(class="card-header"):
52 | text "Network"
53 | table(class="table full-width"):
54 | tbody():
55 | tr():
56 | td(): text "Internet"
57 | td():
58 | strong():
59 | tdiv():
60 | let internet = self.internet
61 | text if internet.isSome: $get(internet)
62 | else: defStr
63 | tr():
64 | td(): text "Host AP"
65 | td():
66 | strong():
67 | tdiv():
68 | # let hostap = io.getHostap
69 | # text if hostap.isSome: $hostap.get else: defStr
70 | let hostap = self.hostap
71 | text if hostap.isSome: $get(hostap)
72 | else: defStr
73 |
74 | tr():
75 | td(): text "SSID"
76 | td():
77 | strong():
78 | tdiv():
79 | text if ap.ssid.len != 0: ap.ssid else: defStr
80 | tr():
81 | td(): text "IP Address"
82 | td():
83 | strong():
84 | tdiv():
85 | text if ap.ipAddr.len != 0: ap.ipaddr else: defStr
86 | tr():
87 | td(): text "VPN"
88 | td():
89 | strong():
90 | tdiv():
91 | text if self.vpnIsActive: "is Up" else: defStr
92 |
93 | func render*(self: Devices): VNode =
94 | buildHtml(tdiv(class="columns full-width")):
95 | tdiv(class="box"):
96 | tdiv(class="box-header"):
97 | text "Connected Devices"
98 | table(class="full-width box-table"):
99 | tbody():
100 | tr():
101 | th(): text "MAC Address"
102 | th(): text "IP Address"
103 | th(): text "Signal"
104 | for v in self.list:
105 | tr():
106 | td(): text if v.macaddr.len != 0: v.macaddr else: "None"
107 | td(): text if v.ipaddr.len != 0: v.ipaddr else: "None"
108 | td(): text if v.signal.len != 0: v.signal else: "None"
109 |
110 | proc renderSys*(): VNode =
111 | buildHtml(tdiv):
112 | tdiv(class="buttons"):
113 | button(): text "Reboot TorBox"
114 | button(): text "Shutdown TorBox"
115 |
116 | proc renderPasswdChange*(): VNode =
117 | buildHtml(tdiv(class="columns")):
118 | tdiv(class="box"):
119 | tdiv(class="box-header"):
120 | text "User password"
121 | form(`method`="post", action="/sys/passwd", enctype="multipart/form-data"):
122 | table(class="full-width box-table"):
123 | tbody():
124 | tr():
125 | td(): text "Current password"
126 | td():
127 | strong():
128 | input(`type`="password", `required`="", name="crPassword")
129 | tr():
130 | td(): text "New password"
131 | td():
132 | strong():
133 | input(`type`="password", `required`="", name="newPassword")
134 | tr():
135 | td(): text "New password (Retype)"
136 | td():
137 | strong():
138 | input(`type`="password", `required`="", name="re_newPassword")
139 | button(class="btn-apply", `type`="submit", name="postType", value="chgPasswd"): text "Apply"
140 |
141 | proc renderChangePassControlPort*(): VNode =
142 | buildHtml(tdiv(class="columns")):
143 | tdiv(class="box"):
144 | tdiv(class="box-header"):
145 | text "Change"
146 |
147 | proc renderLogs*(): VNode =
148 | buildHtml(tdiv(class="")):
149 | form(`method`="post", action="/sys", enctype="multipart/form-data", class="form"):
150 | button(`type`="submit", name="postType", value="eraseLogs", class="eraser"): text "Erase Logs"
--------------------------------------------------------------------------------
/src/lib/tor.nim:
--------------------------------------------------------------------------------
1 | import tor / [ tor, torcfg, bridges, torsocks, vdom ]
2 |
3 | export tor, torcfg, bridges, torsocks, vdom
--------------------------------------------------------------------------------
/src/lib/tor/bridges.nim:
--------------------------------------------------------------------------------
1 | import bridges / [ bridge, vdom ]
2 |
3 | export bridge, vdom
--------------------------------------------------------------------------------
/src/lib/tor/bridges/vdom.nim:
--------------------------------------------------------------------------------
1 | import karax / [ karaxdsl, vdom ]
2 | import bridge
3 |
4 | proc renderObfs4Ctl*(): VNode =
5 | buildHtml(tdiv(class="columns width-50")):
6 | tdiv(class="box"):
7 | tdiv(class="box-header"):
8 | text "Actions"
9 | form(`method`="post", action="/net/bridges", enctype="multipart/form-data"):
10 | table(class="full-width box-table"):
11 | tbody():
12 | tr():
13 | td(): text "All configured Obfs4"
14 | td():
15 | strong():
16 | button(`type`="submit", name="obfs4", value="all"):
17 | text "Activate"
18 | tr():
19 | td(): text "Online Obfs4 only"
20 | td():
21 | strong():
22 | button(`type`="submit", name="obfs4", value="online"):
23 | text "Activate"
24 | tr():
25 | td(): text "Auto Obfs4 "
26 | td():
27 | strong():
28 | button(`type`="submit", name="auto-add-obfs4", value="1"):
29 | text "Add"
30 |
31 | method render*(bridge: Bridge): VNode {.base.} =
32 | buildHtml(tdiv(class="columns width-50")):
33 | tdiv(class="box"):
34 | tdiv(class="box-header"):
35 | text "Actions"
36 | form(`method`="post", action="/net/bridges", enctype="multipart/form-data"):
37 | table(class="full-width box-table"):
38 | tbody():
39 | tr():
40 | td(): text "Obfs4"
41 | td():
42 | strong():
43 | if bridge.kind == obfs4:
44 | button(class="btn-general btn-danger", `type`="submit", name="bridge-action", value="obfs4-deactivate"):
45 | text "Deactivate"
46 | else:
47 | button(class="btn-general btn-safe", `type`="submit", name="bridge-action", value="obfs4-activate-all"):
48 | text "Activate"
49 |
50 | tr():
51 | td(): text "Meek-Azure"
52 | td():
53 | strong():
54 | if bridge.kind == meekAzure:
55 | button(class="btn-general btn-danger", `type`="submit", name="bridge-action", value="meekazure-deactivate"):
56 | text "Deactivate"
57 | else:
58 | button(class="btn-general btn-safe", `type`="submit", name="bridge-action", value="meekazure-activate"):
59 | text "Activate"
60 |
61 | tr():
62 | td(): text "Snowflake"
63 | td():
64 | strong():
65 | if bridge.kind == snowflake:
66 | button(class="btn-general btn-danger", `type`="submit", name="bridge-action", value="snowflake-deactivate"):
67 | text "Deactivate"
68 | else:
69 | button(class="btn-general btn-safe", `type`="submit", name="bridge-action", value="snowflake-activate"):
70 | text "Activate"
71 |
72 | proc renderInputObfs4*(): VNode =
73 | buildHtml(tdiv(class="columns width-50")):
74 | tdiv(class="box"):
75 | tdiv(class="box-header"):
76 | text "Add a bridge"
77 | form(`method`="post", action="/net/bridges", enctype="multipart/form-data"):
78 | textarea(
79 | class="textarea bridge-input",
80 | name="input-bridges",
81 | placeholder="e.g.\n" &
82 | "obfs4 xxx.xxx.xxx.xxx:xxxx FINGERPRINT cert=abcd.. iat-mode=0\n" &
83 | "meek_lite 192.0.2.2:2 FINGERPRINT url=https://meek.torbox.ch/ front=ajax.torbox.ch\n" &
84 | "snowflake 192.0.2.3:1 FINGERPRINT",
85 | required=""
86 | )
87 | button(class="btn-apply", `type`="submit", name="bridges-ctl", value="1"):
88 | text "Enter"
--------------------------------------------------------------------------------
/src/lib/tor/tor.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | os, osproc,
3 | nativesockets, asyncdispatch,
4 | json, strutils,
5 | ]
6 | import results, resultsutils, jsony
7 | import torsocks, torcfg, bridges
8 | import ../ sys / [ service ]
9 | import ../ ../ settings
10 |
11 | export torsocks, bridges
12 |
13 | type
14 | TorInfo* = ref object of RootObj
15 | # setting*: TorSettings
16 | status: TorStatus
17 | bridge: Bridge
18 |
19 | # TorSettings* = ref object
20 | # bridge: Bridge
21 |
22 | TorStatus* = ref object
23 | isTor: bool
24 | isVPN: bool
25 | exitIp: string
26 |
27 | # TorInfo
28 | proc default*(_: typedesc[TorInfo]): TorInfo =
29 | result = TorInfo.new()
30 | result.status = TorStatus.new()
31 | result.bridge = Bridge.new()
32 |
33 | method status*(self: TorInfo): TorStatus {.base.} =
34 | self.status
35 |
36 | method bridge*(self: TorInfo): Bridge {.base.} =
37 | self.bridge
38 |
39 | func status*(self: var TorInfo, ts: TorStatus) =
40 | self.status = ts
41 |
42 | method isTor*(self: TorStatus): bool {.base.} =
43 | self.isTor
44 |
45 | method isVpn*(self: TorStatus): bool {.base.} =
46 | self.isVpn
47 |
48 | method exitIp*(self: TorStatus): string {.base.} =
49 | self.exitIp
50 |
51 | method isEmpty*(self: TorStatus): bool {.base.} =
52 | self.exitIp.len == 0
53 |
54 | method isTor*(self: TorInfo): bool {.base.} =
55 | self.status.isTor
56 |
57 | method isVpn*(self: TorInfo): bool {.base.} =
58 | self.status.isVpn
59 |
60 | method exitIp*(self: TorInfo): string {.base.} =
61 | self.status.exitIp
62 |
63 | method isEmpty*(self: TorInfo): bool {.base.} =
64 | self.status.exitIp.len == 0
65 |
66 | proc checkTor*(torAddr: string, port: Port): Future[Result[TorStatus, string]] {.async.} =
67 | const destHost = "https://check.torproject.org/api/ip"
68 | let checkTor = waitFor destHost.torsocks(torAddr, port)
69 | if checkTor.len == 0: result.err "connection failed"
70 | try:
71 | let jObj = parseJson(checkTor)
72 | if $jObj["IsTor"] == "true":
73 | var ts = TorStatus.new
74 | ts.isTor = true
75 | ts.exitIp = jObj["IP"].getStr()
76 | result.ok ts
77 | except JsonParsingError as e: return err(e.msg)
78 |
79 | proc getTorInfo*(toraddr: string, port: Port): Future[Result[TorInfo, string]] {.async.} =
80 | var
81 | status: TorStatus
82 | bridge: Bridge
83 |
84 | match waitFor checkTor(toraddr, port):
85 | Ok(sta): status = sta
86 | Err(msg): return err(msg)
87 |
88 | match waitFor getBridge():
89 | Ok(ret): bridge = ret
90 | Err(msg): return err(msg)
91 |
92 | var ret = TorInfo.new()
93 | ret.status = status
94 | ret.bridge = bridge
95 | return ok(ret)
96 |
97 | proc renewTorExitIp*(address: string, port: Port): Future[Result[TorStatus, string]] {.async.} =
98 | const cmd = "sudo -u debian-tor tor-prompt --run 'SIGNAL NEWNYM'"
99 | let newIp = execCmdEx(cmd)
100 | if newIp.output == "250 OK":
101 | match await checkTor(address, port):
102 | Ok(stat): return ok(stat)
103 | Err(msg): return err(msg)
104 |
105 | proc restartTor*() {.async.} =
106 | restartService "tor"
107 |
108 | proc newHook*(ts: var TorStatus) =
109 | ts.isTor = false
110 | ts.exitIp = ""
111 |
112 | proc getTorLog*(): Future[string] {.async.} =
113 | if not fileExists(torlog):
114 | return
115 | let f = readFile(torlog)
116 | result = f
117 |
118 | proc spawnTorrc*() =
119 | const
120 | torrcOrig = "/home" / "torbox" / "torbox" / "etc" / "tor" / "torrc"
121 | if not fileExists(torrcOrig):
122 | return
123 | copyFile torrcOrig, torrc
124 |
125 | # when isMainModule:
126 | # let bridgesS = waitFor getBridgeStatuses()
127 | # echo "obfs4: ", bridgesS.obfs4
128 | # echo "meekAzure: ", bridgesS.meekAzure
129 | # echo "snowflake: ", bridgesS.snowflake
130 | # let check = socks5("https://ipinfo.io/products/ip-geolocation-api", "127.0.0.1", 9050.Port, POST, "input=37.228.129.5")
131 | # echo "result: ", check
--------------------------------------------------------------------------------
/src/lib/tor/torcfg.nim:
--------------------------------------------------------------------------------
1 | import std / os
2 |
3 | const
4 | torrc* = "/etc" / "tor" / "torrc"
5 | torrcBak* = "/etc" / "tor" / "torrc.bak"
6 | tmpTorrc* = "/tmp" / "torrc.tmp"
7 | torlog* = "/var" / "log" / "tor" / "notices.log"
--------------------------------------------------------------------------------
/src/lib/tor/torsocks.nim:
--------------------------------------------------------------------------------
1 | import libcurl
2 | import asyncdispatch, strutils
3 | import ../ ../ settings
4 |
5 | type
6 | Protocol* = enum
7 | GET = "get",
8 | POST = "post"
9 |
10 | proc curlWriteFn(
11 | buffer: cstring,
12 | size: int,
13 | count: int,
14 | outstream: pointer): int
15 |
16 | proc socks5(url, address: string, port: Port, prt: Protocol = GET, data: string = ""): Future[string] {.async.}
17 |
18 | proc torsocks*(url: string, cfg: Config, prtc: Protocol = GET): Future[string] {.async.} =
19 | let
20 | address = cfg.torAddress
21 | port = cfg.torPort
22 | result = waitFor url.socks5(address, port, prtc)
23 |
24 | proc torsocks*(url: string, address: string = "127.0.0.1", port: Port = 9050.Port, prtc: Protocol = GET): Future[string] {.async.} =
25 | result = waitFor url.socks5(address, port, prtc)
26 |
27 | proc curlWriteFn(
28 | buffer: cstring,
29 | size: int,
30 | count: int,
31 | outstream: pointer): int =
32 |
33 | let outbuf = cast[ref string](outstream)
34 | outbuf[] &= buffer
35 | result = size * count
36 |
37 | proc socks5(url, address: string, port: Port, prt: Protocol = GET, data: string = ""): Future[string] {.async.} =
38 | let curl = easy_init()
39 | let webData: ref string = new string
40 | discard curl.easy_setopt(
41 | OPT_USERAGENT,
42 | "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
43 | )
44 | case prt
45 | of GET:
46 | discard curl.easy_setopt(OPT_HTTPGET, 1)
47 | of POST:
48 | discard curl.easy_setopt(OPT_HTTPPOST, 10000)
49 | discard curl.easy_setopt(OPT_POSTFIELDS, data)
50 | discard curl.easy_setopt(OPT_WRITEDATA, webData)
51 | discard curl.easy_setopt(OPT_WRITEFUNCTION, curlWriteFn)
52 | discard curl.easy_setopt(OPT_URL, url)
53 | discard curl.easy_setopt(OPT_PROXYTYPE, 5)
54 | discard curl.easy_setopt(OPT_PROXY, address)
55 | discard curl.easy_setopt(OPT_PROXYPORT, port)
56 | discard curl.easy_setopt(OPT_TIMEOUT, 5)
57 |
58 | let ret = curl.easy_perform()
59 | if ret == E_OK:
60 | result = webData[]
61 | else: return
--------------------------------------------------------------------------------
/src/lib/tor/vdom.nim:
--------------------------------------------------------------------------------
1 | # import karax / prelude
2 | import karax / [ karaxdsl, vdom, vstyles ]
3 | import tor, bridges
4 |
5 | method render*(ti: TorInfo): VNode {.base.} =
6 | buildHtml(tdiv(class="columns")):
7 | tdiv(class="card card-padding card-tor"):
8 | tdiv(class="card-header"):
9 | text "Tor"
10 | table(class="table full-width"):
11 | tbody():
12 | tr():
13 | td(): text "Status"
14 | td():
15 | strong(style={display: "flex"}):
16 | tdiv():
17 | text if ti.status.isTor: "Online" else: "Offline"
18 | form(`method`="post", action="/io", enctype="multipart/form-data"):
19 | button(class="btn-flat", `type`="submit", name="tor-request", value="new-circuit"):
20 | svg(class="new-circuit", loading="lazy", alt="new circuit", width="25px", height="25px", viewBox="0 0 16 16", version="1.1"):
21 | title(): text "Enforce a new exit node with a new IP"
22 | path(
23 | d="M13.4411138,10.1446317 L9.5375349,10.1446317 C8.99786512,10.1446317 8.56164018,10.5818326 8.56164018,11.1205264 C8.56164018,11.6592203 8.99786512,12.0964212 9.5375349,12.0964212 L11.4571198,12.0964212 C10.7554515,13.0479185 9.73466563,13.692009 8.60067597,13.9359827 C8.41818366,13.9720908 8.23276366,14.0033194 8.04734366,14.0218614 C7.97219977,14.0277168 7.89803177,14.0306445 7.82288788,14.0335722 C6.07506044,14.137017 4.290149,13.4499871 3.38647049,11.857327 C2.52280367,10.3349312 2.77263271,8.15966189 3.93687511,6.87343267 C5.12453898,5.56183017 7.44814431,5.04363008 8.21226987,3.38558497 C9.01738301,4.92847451 9.60682342,5.02801577 10.853041,6.15029468 C11.2892659,6.54455615 11.9704404,7.55558307 12.1861132,8.10501179 C12.3051723,8.40949094 12.5013272,9.17947187 12.5013272,9.17947187 L14.2862386,9.17947187 C14.2091429,7.59754654 13.439162,5.96877827 12.2261248,4.93628166 C11.279507,4.13116853 10.5065984,3.84718317 9.77662911,2.8088312 C9.63219669,2.60194152 9.59999216,2.4565332 9.56290816,2.21646311 C9.53851079,2.00762164 9.54143848,1.78511764 9.62048595,1.53919218 C9.65952174,1.41720534 9.59804037,1.28545955 9.47702943,1.23764071 L6.40296106,0.0167964277 C6.32391359,-0.0134563083 6.23413128,-0.00272146652 6.16679454,0.0480250584 L5.95502539,0.206120002 C5.85743592,0.280288 5.82815908,0.416913259 5.89159223,0.523285783 C6.70060895,1.92564648 6.36978064,2.82542141 5.8984235,3.20211676 C5.4914754,3.4900057 4.99084141,3.72226864 4.63366394,3.95453159 C3.82367132,4.47956294 3.03222071,5.02508808 2.40374451,5.76774396 C0.434388969,8.09427695 0.519291809,12.0046871 2.77165682,14.1077402 C3.65288975,14.9284676 4.70295247,15.4749686 5.81742423,15.7570022 C5.81742423,15.7570022 6.13556591,15.833122 6.21754107,15.8497122 C7.36616915,16.0829511 8.53529102,16.0146384 9.62243774,15.6672199 C9.67416016,15.6525815 9.77174963,15.620377 9.76784605,15.6154975 C10.7730176,15.2700308 11.7049971,14.7010841 12.4652191,13.90573 L12.4652191,15.0241053 C12.4652191,15.5627992 12.901444,16 13.4411138,16 C13.9798077,16 14.4170085,15.5627992 14.4170085,15.0241053 L14.4170085,11.1205264 C14.4170085,10.5818326 13.9798077,10.1446317 13.4411138,10.1446317",
24 | id="Fill-3",
25 | fill="context-fill",
26 | fill-opacity="context-fill-opacity"
27 | )
28 | path(
29 | d="M5.107,7.462 C4.405,8.078 4,8.946 4,9.839 C4,10.712 4.422,11.57 5.13,12.132 C5.724,12.607 6.627,12.898 7.642,12.949 L7.642,5.8 C7.39,6.029 7.103,6.227 6.791,6.387 C5.993,6.812 5.489,7.133 5.107,7.462",
30 | id="Fill-1",
31 | fill="context-fill",
32 | fill-opacity="context-fill-opacity"
33 | )
34 | tr():
35 | td(): text "Obfs4"
36 | td():
37 | strong():
38 | tdiv():
39 | text if ti.bridge.isObfs4: "On" else: "Off"
40 | tr():
41 | td(): text "Meek-Azure"
42 | td():
43 | strong():
44 | tdiv():
45 | text if ti.bridge.isMeekazure: "On" else: "Off"
46 | tr():
47 | td(): text "Snowflake"
48 | td():
49 | strong():
50 | tdiv():
51 | text if ti.bridge.isSnowflake: "On" else: "Off"
52 |
53 | # method jsRender*(ti: TorInfo): VNode {.base.} =
54 | # buildHtml(tdiv(class="columns")):
55 | # tdiv(class="card card-padding card-tor"):
56 | # tdiv(class="card-header"):
57 | # text "Tor"
58 | # table(class="table full-width"):
59 | # tbody():
60 | # tr():
61 | # td(): text "Status"
62 | # td():
63 | # strong(style={display: "flex"}):
64 | # tdiv():
65 | # text if ti.status.isTor: "Online" else: "Offline"
66 | # form(`method`="post", action="/io", enctype="multipart/form-data"):
67 | # button(class="btn-flat", `type`="submit", name="tor-request", value="new-circuit"):
68 | # svg(class="new-circuit", loading="lazy", alt="new circuit", width="25px", height="25px", viewBox="0 0 16 16", version="1.1"):
69 | # title(): text "Enforce a new exit node with a new IP"
70 | # path(
71 | # d="M13.4411138,10.1446317 L9.5375349,10.1446317 C8.99786512,10.1446317 8.56164018,10.5818326 8.56164018,11.1205264 C8.56164018,11.6592203 8.99786512,12.0964212 9.5375349,12.0964212 L11.4571198,12.0964212 C10.7554515,13.0479185 9.73466563,13.692009 8.60067597,13.9359827 C8.41818366,13.9720908 8.23276366,14.0033194 8.04734366,14.0218614 C7.97219977,14.0277168 7.89803177,14.0306445 7.82288788,14.0335722 C6.07506044,14.137017 4.290149,13.4499871 3.38647049,11.857327 C2.52280367,10.3349312 2.77263271,8.15966189 3.93687511,6.87343267 C5.12453898,5.56183017 7.44814431,5.04363008 8.21226987,3.38558497 C9.01738301,4.92847451 9.60682342,5.02801577 10.853041,6.15029468 C11.2892659,6.54455615 11.9704404,7.55558307 12.1861132,8.10501179 C12.3051723,8.40949094 12.5013272,9.17947187 12.5013272,9.17947187 L14.2862386,9.17947187 C14.2091429,7.59754654 13.439162,5.96877827 12.2261248,4.93628166 C11.279507,4.13116853 10.5065984,3.84718317 9.77662911,2.8088312 C9.63219669,2.60194152 9.59999216,2.4565332 9.56290816,2.21646311 C9.53851079,2.00762164 9.54143848,1.78511764 9.62048595,1.53919218 C9.65952174,1.41720534 9.59804037,1.28545955 9.47702943,1.23764071 L6.40296106,0.0167964277 C6.32391359,-0.0134563083 6.23413128,-0.00272146652 6.16679454,0.0480250584 L5.95502539,0.206120002 C5.85743592,0.280288 5.82815908,0.416913259 5.89159223,0.523285783 C6.70060895,1.92564648 6.36978064,2.82542141 5.8984235,3.20211676 C5.4914754,3.4900057 4.99084141,3.72226864 4.63366394,3.95453159 C3.82367132,4.47956294 3.03222071,5.02508808 2.40374451,5.76774396 C0.434388969,8.09427695 0.519291809,12.0046871 2.77165682,14.1077402 C3.65288975,14.9284676 4.70295247,15.4749686 5.81742423,15.7570022 C5.81742423,15.7570022 6.13556591,15.833122 6.21754107,15.8497122 C7.36616915,16.0829511 8.53529102,16.0146384 9.62243774,15.6672199 C9.67416016,15.6525815 9.77174963,15.620377 9.76784605,15.6154975 C10.7730176,15.2700308 11.7049971,14.7010841 12.4652191,13.90573 L12.4652191,15.0241053 C12.4652191,15.5627992 12.901444,16 13.4411138,16 C13.9798077,16 14.4170085,15.5627992 14.4170085,15.0241053 L14.4170085,11.1205264 C14.4170085,10.5818326 13.9798077,10.1446317 13.4411138,10.1446317",
72 | # id="Fill-3",
73 | # fill="context-fill",
74 | # fill-opacity="context-fill-opacity"
75 | # )
76 | # path(
77 | # d="M5.107,7.462 C4.405,8.078 4,8.946 4,9.839 C4,10.712 4.422,11.57 5.13,12.132 C5.724,12.607 6.627,12.898 7.642,12.949 L7.642,5.8 C7.39,6.029 7.103,6.227 6.791,6.387 C5.993,6.812 5.489,7.133 5.107,7.462",
78 | # id="Fill-1",
79 | # fill="context-fill",
80 | # fill-opacity="context-fill-opacity"
81 | # )
82 | # tr():
83 | # td(): text "Obfs4"
84 | # td():
85 | # strong():
86 | # tdiv():
87 | # text if ti.bridge.isObfs4: "On" else: "Off"
88 | # tr():
89 | # td(): text "Meek-Azure"
90 | # td():
91 | # strong():
92 | # tdiv():
93 | # text if ti.bridge.isMeekazure: "On" else: "Off"
94 | # tr():
95 | # td(): text "Snowflake"
96 | # td():
97 | # strong():
98 | # tdiv():
99 | # text if ti.bridge.isSnowflake: "On" else: "Off"
--------------------------------------------------------------------------------
/src/lib/wifiScanner.nim:
--------------------------------------------------------------------------------
1 | import tables, osproc, strutils, strformat
2 | import re
3 | import asyncdispatch
4 | import ../types
5 | import sys / [ iface ]
6 | # type
7 |
8 | # WlanKind* = enum
9 | # wlan0, wlan1
10 |
11 | # EthKind* = enum
12 | # eth0, eth1
13 |
14 | # ExtInterKind* = enum
15 | # usb0, ppp0
16 |
17 | # Wifi* = object of RootObj
18 | # bssid*: string
19 | # channel*: string
20 | # dbmSignal*: string
21 | # quality*: string
22 | # security*: string
23 | # essid*: string
24 | # isEss*: bool
25 | # isHidden*: bool
26 |
27 | # WifiList* = seq[Wifi]
28 | # Wifi* = object of RootObj
29 | # bssid*: string
30 | # channel*: string
31 | # dbmSignal*: string
32 | # quality*: string
33 | # security*: string
34 | # essid*: string
35 |
36 | # WifiList* = seq[Wifi]
37 | # WifiData = OrderedTable[seq[string]]
38 |
39 | let channelFreq: Table[int, int] = {
40 | 2: 2417,
41 | 3: 2422,
42 | 1: 2412,
43 | 5: 2432,
44 | 6: 2437,
45 | 7: 2442,
46 | 4: 2427,
47 | 8: 2447,
48 | 9: 2452,
49 | 10: 2457,
50 | 11: 2462,
51 | 12: 2467,
52 | 13: 2472,
53 | 14: 2484
54 | }.toTable
55 |
56 | let channelFreq5ghz: Table[int, int] = {
57 | 7: 5035,
58 | 8: 5040,
59 | 9: 5045,
60 | 11: 5055,
61 | 12: 5060,
62 | 16: 5080,
63 | 32: 5160,
64 | 34: 5170,
65 | 36: 5180,
66 | 38: 5190,
67 | 40: 5200,
68 | 42: 5210,
69 | 44: 5220,
70 | 46: 5230,
71 | 48: 5240,
72 | 50: 5250,
73 | 52: 5260,
74 | 54: 5270,
75 | 56: 5280,
76 | 58: 5290,
77 | 60: 5300,
78 | 62: 5310,
79 | 64: 5320,
80 | 68: 5340,
81 | 96: 5480,
82 | 100: 5500,
83 | 102: 5510,
84 | 104: 5520,
85 | 106: 5530,
86 | 108: 5540,
87 | 110: 5550,
88 | 112: 5560,
89 | 114: 5570,
90 | 116: 5580,
91 | 118: 5590,
92 | 120: 5600,
93 | 122: 5610,
94 | 124: 5620,
95 | 126: 5630,
96 | 128: 5640,
97 | 132: 5660,
98 | 134: 5670,
99 | 136: 5680,
100 | 138: 5690,
101 | 140: 5700,
102 | 142: 5710,
103 | 144: 5720,
104 | 149: 5745,
105 | 151: 5755,
106 | 153: 5765,
107 | 155: 5775,
108 | 157: 5785,
109 | 159: 5795,
110 | 161: 5805,
111 | 165: 5825,
112 | 169: 5845,
113 | 173: 5865,
114 | 183: 4915,
115 | 184: 4920,
116 | 185: 4925,
117 | 187: 4935,
118 | 188: 4940,
119 | 189: 4945,
120 | 192: 4960,
121 | 196: 4980
122 | }.toTable
123 |
124 | proc zip[A, B](t1: Table[A, B]; t2: Table[A, B]): Table[A, B] =
125 | result = t1
126 | for k, v in t2.pairs:
127 | result[k] = v
128 |
129 | let channels: Table[int, int] = zip(channelFreq, channelFreq5ghz)
130 |
131 | proc isChannels(ch: int): bool =
132 | # for v in channels.values:
133 | # if ch == v:
134 | # return true
135 | if ch in channels:
136 | return true
137 |
138 | proc exclusion(s: string): bool =
139 | case s
140 | of $eth0: return false
141 | of $eth1: return false
142 | of $wlan0: return true
143 | of $wlan1: return true
144 | of $ppp0: return true
145 | of $usb0: return true
146 | else: return false
147 |
148 | proc sort(strs: seq[string]): WifiList =
149 | for v in strs:
150 | if v.len == 0:
151 | continue
152 | let
153 | lines = v.split("\t")
154 | quality = 2 * (lines[2].parseInt + 100)
155 | result.add Wifi(
156 | bssid: lines[0],
157 | channel: if isChannels(lines[1].parseInt): $lines[1] else: "?",
158 | dbmSignal: $lines[2],
159 | quality: if quality > 100: $100 else: $quality,
160 | security: try: lines[3].findAll(re"\[(.*?)\]")[0] except: "unknown",
161 | essid: try: $lines[4] except: "?"
162 | )
163 |
164 |
165 | proc wifiScan*(wlan: IfaceKind): Future[WifiList] {.async.} =
166 | try:
167 | var wpaScan = execCmdEx(&"wpa_cli -i {wlan} scan")
168 | # sleep 1
169 | if wpaScan.exitCode == 0:
170 | var scanResult = execCmdEx(&"wpa_cli -i {wlan} scan_results")
171 | if scanResult.output.splitLines().len <= 1:
172 | wpaScan = execCmdEx(&"wpa_cli -i {wlan} scan")
173 | scanResult = execCmdEx(&"wpa_cli -i {wlan} scan_results")
174 | var r = scanResult.output.splitLines()
175 | r.delete 0
176 | # return (code: true, msg: "", list: r.sort())
177 | return r.sort()
178 | # result.data = r.sort()
179 | # result.code = true
180 | elif wpaScan.output == "Failed to connect to non-global ctrl_ifname:":
181 | # return (code: false, msg: wpaScan.output, list: @[Wifi()])
182 | return
183 | else:
184 | # return (code: false, msg: wpaScan.output, list: @[Wifi()])
185 | return
186 | except:
187 | # return (code: false, msg: "Something went wrong", list: @[Wifi()])
188 | return
189 | # else: return (code: false, msg: "Invalid interface", data: @[Wifi()])
190 |
191 | # when isMainModule:
192 | # when defined(debugWifiScan):
193 | # var r = wpaResult.splitLines()
194 | # r.delete 0
195 | # var res: WifiList
196 | # res = r.sort()
197 | # for i, el in res:
198 | # if el.essid.contains("\\x00") or el.essid.contains("?") or el.essid == "":
199 | # res[i].essid = "-HIDDEN-"
200 | # echo res
201 | # when defined(debugSsidSecurity):
202 | # var r = wpaResult.splitLines()
203 | # r.delete 0
204 | # var res: WifiList
205 | # res = r.sort()
--------------------------------------------------------------------------------
/src/notice.nim:
--------------------------------------------------------------------------------
1 | # import std / options
2 | import results
3 |
4 | type
5 | State* = enum
6 | success
7 | warn
8 | failure
9 |
10 | Notice = ref object of RootObj
11 | state: State
12 | msg: string
13 |
14 | Notifies* = ref object
15 | notice: seq[Notice]
16 |
17 | method getState*(n: Notice): State {.base.} =
18 | n.state
19 |
20 | method getMsg*(n: Notice): string {.base.} =
21 | n.msg
22 |
23 | method len*(n: Notifies): int {.base.} =
24 | n.notice.len
25 |
26 | method isEmpty*(n: Notifies): bool {.base.} =
27 | n.len == 0
28 |
29 | proc default*(_: typedesc[Notifies]): Notifies =
30 | result = Notifies()
31 |
32 | func add*(n: var Notifies, state: State, msg: string) =
33 | if msg.len == 0: return
34 | let notice = Notice(state: state, msg: msg)
35 | n.notice.add notice
36 |
37 | func add*(n: var Notifies, r: Result[void, string]) =
38 | if r.isErr:
39 | n.add(failure, r.error)
40 |
41 | proc newHook*(nc: var Notice) =
42 | nc.state = success
43 | nc.msg = ""
44 |
45 | iterator items*(n: Notifies): tuple[i: int, n: Notice] {.inline.} =
46 | var i: int
47 | while i < n.len:
48 | yield (i, n.notice[i])
49 | inc i
50 |
51 | import karax / [ karaxdsl, vdom, vstyles ]
52 |
53 | method render*(notifies: Notifies): VNode {.base.} =
54 | const
55 | colourGreen = "#2ECC71"
56 | colourYellow = ""
57 | # colourGray = "#afafaf"
58 | colourRed = "#E74C3C"
59 |
60 | result = new VNode
61 |
62 | for i, n in notifies.notice:
63 | let colour =
64 | case n.getState
65 | of success:
66 | colourGreen
67 |
68 | of warn:
69 | colourYellow
70 |
71 | of failure:
72 | colourRed
73 |
74 | result = buildHtml(tdiv(class="notify-bar")):
75 | input(`for`="notify-msg" & $i, class="ignore-notify", `type`="checkbox", name="ignoreNotify")
76 | tdiv(id="notify-msg" & $i, class="notify-message", style={backgroundColor: colour}):
77 | text n.getMsg
--------------------------------------------------------------------------------
/src/query.nim:
--------------------------------------------------------------------------------
1 | import std / [ options, tables ]
2 |
3 | import lib / sys / iface
4 |
5 | type
6 | Query* = ref object
7 | iface*: IfaceKind
8 | withCaptive*: bool
9 |
10 | template `@`(param: string): untyped =
11 | if param in pms: pms[param]
12 | else: ""
13 |
14 | proc initQuery*(pms: Table[string, string]): Query =
15 | result = Query(
16 | iface: parseIfaceKind(@"iface").get,
17 | withCaptive: if @"captive" == "1": true else: false
18 | )
--------------------------------------------------------------------------------
/src/renderutils.nim:
--------------------------------------------------------------------------------
1 | import std / [ macros, strutils, re, httpcore ]
2 | import jester, karax / [ karaxdsl, vdom ]
3 | import routes / tabs
4 | import ./ notice, settings
5 | import lib / [ session ]
6 |
7 | const doctype = "\n"
8 |
9 | proc getCurrentTab*(r: Request): string =
10 | const tabs = @[
11 | (name: "/io", text: "Status"),
12 | (name: "/net", text: "Network"),
13 | (name: "/tor", text: "Tor"),
14 | (name: "/sys", text: "System")
15 | ]
16 |
17 | for v in tabs:
18 | if r.pathInfo.startsWith(v.name): return v.text
19 |
20 | proc getNavClass*(path: string; text: string): string =
21 | result = "linker"
22 | if match(path, re("^" & text)):
23 | result &= " current"
24 |
25 | proc icon*(icon: string; text=""; title=""; class=""; href=""): VNode =
26 | var c = "icon-" & icon
27 | if class.len > 0: c = c & " " & class
28 | buildHtml(tdiv(class="icon-container")):
29 | if href.len > 0:
30 | a(class=c, title=title, href=href)
31 | else:
32 | span(class=c, title=title)
33 |
34 | if text.len > 0:
35 | text " " & text
36 |
37 | proc renderHead(cfg: Config, title: string = ""): VNode =
38 | buildHtml(head):
39 | link(rel="stylesheet", `type`="text/css", href="/css/style.css")
40 | link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2")
41 | link(rel="apple-touch-icon", sizes="180x180", href="/apple-touch-icon.png")
42 | link(rel="icon", type="image/png", sizes="32x32", href="/favicon-32x32.png")
43 | link(rel="icon", type="image/png", sizes="16x16", href="/favicon-16x16.png")
44 | link(rel="manifest", href="/site.webmanifest")
45 | title:
46 | if title.len > 0:
47 | text title & " | " & cfg.title
48 | else:
49 | text cfg.title
50 | meta(name="viewport", content="width=device-width, initial-scale=1.0")
51 |
52 | proc renderNav(req: Request; username: string; tab: Tab = new Tab): VNode =
53 | result = buildHtml(header(class="headers")):
54 | nav(class="nav-container"):
55 | tdiv(class="inner-nav"):
56 | tdiv(class="linker-root"):
57 | a(class="", href="/"):
58 | img(class="logo-file", src="/images/torbox.png")
59 | tdiv(class="service-name"):text cfg.title
60 | tdiv(class="center-title"):
61 | text req.getCurrentTab()
62 | tdiv(class="tabs"):
63 | a(class=getNavClass(req.pathInfo, "/io"), href="/io"):
64 | icon "th-large", class="tab-icon"
65 | tdiv(class="tab-name"):
66 | text "Status"
67 | a(class=getNavClass(req.pathInfo, "/net"), href="/net"):
68 | icon "wifi", class="tab-icon"
69 | tdiv(class="tab-name"):
70 | text "Network"
71 | a(class=getNavClass(req.pathInfo, "/tor"), href="/tor"):
72 | icon "tor", class="tab-icon"
73 | tdiv(class="tab-name"):
74 | text "Tor"
75 | a(class=getNavClass(req.pathInfo, "/sys"), href="/sys"):
76 | icon "cog", class="tab-icon"
77 | tdiv(class="tab-name"):
78 | text "System"
79 | tdiv(class="user-drop"):
80 | icon "user-circle-o"
81 | input(class="popup-btn", `type`="radio", name="popup-btn", value="open")
82 | input(class="popout-btn", `type`="radio", name="popup-btn", value="close")
83 | tdiv(class="dropdown"):
84 | tdiv(class="panel"):
85 | tdiv(class="line"):
86 | icon "user-o"
87 | tdiv(class="username"): text "Username: " & username
88 | form(`method`="post", action="/io", enctype="multipart/form-data"):
89 | button(`type`="submit", name="tor-request", value="restart-tor"):
90 | icon "cw"
91 | tdiv(class="btn-text"): text "Restart Tor"
92 | form(`method`="post", action="/logout", enctype="multipart/form-data"):
93 | button(`type`="submit", name="signout", value="1"):
94 | icon "logout"
95 | tdiv(class="btn-text"): text "Log out"
96 | # tdiv(class="logout-button"):
97 | # icon "logout"
98 | if not tab.isEmpty:
99 | tab.render(req.pathInfo)
100 |
101 | proc renderMain*(
102 | v: VNode;
103 | req: Request;
104 | username: string;
105 | title: string = "",
106 | tab: Tab = Tab.new();
107 | notifies: Notifies = Notifies.new()): string =
108 |
109 | let node = buildHtml(html(lang="en")):
110 | renderHead(cfg, title)
111 |
112 | body:
113 | if not tab.isEmpty: renderNav(req, username, tab)
114 | else: renderNav(req, username)
115 |
116 | if not notifies.isEmpty: notifies.render()
117 |
118 | tdiv(class="container"):
119 | v
120 |
121 | result = doctype & $node
122 |
123 | macro render*(title: string, body: untyped): string =
124 | # expectKind(body.children, nnkCall)
125 | proc container(n: NimNode): NimNode =
126 | expectKind(n, { nnkStmtList })
127 | result = nnkStmtListExpr.newTree()
128 | let
129 | call = newCall(
130 | ident"buildHtml",
131 | newCall(
132 | ident"tdiv",
133 | nnkExprEqExpr.newTree(
134 | ident"class",
135 | newLit("cards")
136 | )
137 | ),
138 | n
139 | )
140 | result.add call
141 |
142 | var
143 | n: NimNode = nnkStmtListExpr.newTree()
144 | c: NimNode = nil
145 | t: NimNode = nil
146 | nc: NimNode = nil
147 |
148 | for child in body:
149 | expectKind(child, { nnkCall, nnkCommand, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
150 | case $child[0]
151 | of "container":
152 | expectKind(child[1], { nnkStmtList, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
153 | c = container(child[1])
154 | of "notice":
155 | expectKind(child[1], { nnkStmtList, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
156 | let notice = child[1][0]
157 | expectKind(notice, { nnkCall, nnkIdent, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
158 | nc = nnkStmtListExpr.newTree()
159 | nc.add notice
160 | of "tab":
161 | expectKind(child[1], { nnkStmtList, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
162 | let tab = child[1][0]
163 | expectKind(tab, { nnkCall, nnkIdent, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
164 | t = nnkStmtListExpr.newTree()
165 | t.add tab
166 |
167 | if t.isNil: t = newCall(newIdentNode"new", newIdentNode("Tab"))
168 | if nc.isNil: nc = newCall(ident"default", ident"Notifies")
169 |
170 | n.add newCall(
171 | ident"renderMain",
172 | c,
173 | ident"request",
174 | # newDotExpr(ident"request", ident"getUserName"),
175 | newStrLitNode"Tor-chan",
176 | title,
177 | t,
178 | nc
179 | )
180 | n
181 |
182 | proc renderError*(e: string): VNode =
183 | buildHtml():
184 | tdiv(class="content"):
185 | tdiv(class="panel-container"):
186 | tdiv(class="logo-container"):
187 | img(class="logo", src="/images/torbox.png", alt="TorBox")
188 | tdiv(class="error-panel"):
189 | span(): text e
190 |
191 | proc renderClosed*(): VNode =
192 | buildHtml():
193 | tdiv(class="warn-panel"):
194 | icon "attention", class="warn-icon"
195 | tdiv(class="warn-subject"): text "Sorry..."
196 | tdiv(class="warn-description"):
197 | text "This feature is currently closed as it is under development and can cause bugs"
198 |
199 | proc renderPanel*(v: VNode): VNode =
200 | buildHtml(tdiv(class="main-panel")):
201 | v
202 |
203 | proc renderFlat*(v: VNode, title: string = ""): string =
204 | let ret = buildHtml(html(lang="en")):
205 | renderHead(cfg, title)
206 | body:
207 | v
208 | result = doctype & $ret
209 |
210 | proc renderFlat*(v: VNode, title: string = "", notifies: Notifies): string =
211 | let ret = buildHtml(html(lang="en")):
212 | renderHead(cfg, title)
213 | body:
214 | notifies.render
215 | v
216 | result = doctype & $ret
217 |
218 | template loggedIn*(code: HttpCode = Http403, node: untyped) =
219 | if await request.isLoggedIn:
220 | node
221 | else:
222 | resp code, "", "application/json"
223 |
224 | template loggedIn*(code: HttpCode = Http403, con: string = "", node: untyped) =
225 | if await request.isLoggedIn:
226 | node
227 | else:
228 | resp code, con, "application/json"
--------------------------------------------------------------------------------
/src/routes/network.nim:
--------------------------------------------------------------------------------
1 | import std / [ options, strutils, asyncdispatch ]
2 | import results, resultsutils
3 | import jester, karax / [ karaxdsl, vdom]
4 | import ../ renderutils
5 | import ".." / [ types, notice ]
6 | import ".." / lib / [tor, sys, session, hostap, fallbacks ]
7 | import network / [ wireless ]
8 | import tabs
9 |
10 | export wireless
11 |
12 | routerWireless()
13 |
14 | # routerwireless()
15 | proc routingNet*() =
16 | router network:
17 | template respMaintenance() =
18 | resp renderMain(renderClosed(), request, await request.getUserName, "Under maintenance", tab())
19 |
20 | template tab(): Tab =
21 | buildTab:
22 | "Bridges" = "/net" / "bridges"
23 | "Interfaces" = "/net" / "interfaces"
24 | "Wireless" = "/net" / "wireless"
25 |
26 | # extend wireless, ""
27 | # let tab = netTab()
28 | extend wireless, "/net"
29 |
30 | get "/bridges":
31 | loggedIn:
32 | var
33 | bridge = Bridge.new()
34 | nc = Notifies.default()
35 |
36 | match await getBridge():
37 | Ok(ret): bridge = ret
38 | Err(msg): nc.add(failure, msg)
39 |
40 | resp: render "Bridges":
41 | tab: tab
42 | notice: nc
43 | container:
44 | bridge.render()
45 | renderInputObfs4()
46 |
47 | get "/interfaces":
48 | loggedIn:
49 | respMaintenance()
50 | # resp renderNode(renderInterfaces(), request, request.getUserName, "Interfaces", tab)
51 |
52 | get "/interfaces/set/?":
53 | loggedIn:
54 | respMaintenance()
55 | # let
56 | # query = initQuery(request.params)
57 | # iface = query.iface
58 |
59 | # var clientWln, clientEth: IfaceKind
60 | # # let query = initQuery(params(request))
61 |
62 | # if not ifaceExists(iface):
63 | # redirect "interfaces"
64 |
65 | # case iface
66 | # of wlan0, wlan1:
67 |
68 | # case iface
69 | # of wlan0:
70 | # clientWln = wlan1
71 | # clientEth = eth0
72 |
73 | # of wlan1:
74 | # clientWln = wlan0
75 | # clientEth = eth0
76 |
77 | # else: redirect "interfaces"
78 |
79 | # hostapdFallbackKomplex(clientWln, clientEth)
80 | # editTorrc(iface, clientWln, clientEth)
81 | # restartDhcpServer()
82 |
83 | # # net.scanned = true
84 | # # if wifiScanResult.code:
85 | # # resp renderNode(renderWifiConfig(@"interface", wifiScanResult, currentNetwork), request, cfg, tab)
86 | # if query.withCaptive:
87 | # redirect "interfaces/join/?iface=" & $iface & "&captive=1"
88 |
89 | # redirect "interfaces/join/?iface=" & $iface
90 | # else: redirect "interfaces"
91 |
92 | get "/interfaces/join/?":
93 | # let user = await getUser(request)
94 | # if user.isLoggedIn:
95 | loggedIn:
96 | respMaintenance()
97 | # let
98 | # query = initQuery(request.params)
99 | # iface = query.iface
100 | # withCaptive = query.withCaptive
101 |
102 | # case iface
103 | # of wlan0, wlan1:
104 | # var wpa = await newWpa(iface)
105 | # let
106 | # wifiScanResult = await networkList(wpa)
107 | # currentNetwork = await currentNetwork(wpa.wlan)
108 | # net = wpa
109 | # respNetworkManager(wifiScanResult, currentNetwork)
110 |
111 | # else:
112 | # redirect "interfaces"
113 |
114 | post "/apctl":
115 | loggedIn:
116 | let ctl = request.formData.getOrDefault("ctl").body
117 |
118 | case ctl
119 | of "reload":
120 | await hostapdFallback()
121 |
122 | of "disable":
123 | await disableAp()
124 |
125 | of "enable":
126 | await enableWlan()
127 |
128 | else:
129 | redirect "wireless"
130 |
131 | redirect "wireless"
132 |
133 | post "/interfaces/join/@wlan":
134 | loggedIn:
135 | respMaintenance()
136 | # var clientWln, clientEth: IfaceKind
137 | # let
138 | # iface = parseIface(@"wlan")
139 | # captive = request.formData.getOrDefault("captive").body
140 | # withCaptive = if captive == "1": true else: false
141 |
142 | # if not ifaceExists(iface):
143 | # redirect "interfaces"
144 |
145 | # elif not net.scanned:
146 | # redirect "interfaces"
147 |
148 | # case iface
149 | # of wlan0, wlan1:
150 |
151 | # case iface
152 | # of wlan0:
153 | # clientWln = wlan1
154 | # clientEth = eth0
155 |
156 | # of wlan1:
157 | # clientWln = wlan0
158 | # clientEth = eth0
159 |
160 | # else: redirect "interfaces"
161 |
162 | # let
163 | # essid = request.formData.getOrDefault("essid").body
164 | # bssid = request.formData.getOrDefault("bssid").body
165 | # password = request.formData.getOrDefault("password").body
166 | # # sec = request.formData.getOrDefault("security").body
167 | # cloak = request.formData.getOrDefault("cloak").body
168 | # ess = request.formData.getOrDefault("ess").body
169 |
170 | # net.isHidden = if cloak == "0": true else: false
171 | # net.isEss = if ess == "0": true else: false
172 |
173 | # if not net.isEss:
174 | # if password.len == 0:
175 | # redirect "interfaces"
176 |
177 | # if (essid.len != 0) or (bssid.len != 0):
178 | # let con = await connect(net, (essid: essid, bssid: bssid, password: password))
179 |
180 | # if con.code:
181 | # if withCaptive:
182 | # setCaptive(iface, clientWln, clientEth)
183 | # setInterface(iface, clientWln, clientEth)
184 | # saveIptables()
185 | # redirect "interfaces"
186 | # net = new Network
187 |
188 | # redirect "interfaces"
189 | # else:
190 | # redirect "interfaces"
191 | # newConnect()
192 |
193 | post "/bridges":
194 | loggedIn:
195 | var nc = Notifies.default()
196 | let
197 | input: string = request.formData.getOrDefault("input-bridges").body
198 | action = request.formData.getOrDefault("bridge-action").body
199 |
200 | if input.len > 0:
201 | let (failure, success) = await addBridges(input)
202 | if failure == 0 and success > 0:
203 | nc.add State.success, "Bridge has been added"
204 |
205 | elif failure > 0 and success > 0:
206 | nc.add State.warn, "Some bridges failed to add"
207 |
208 | else: nc.add State.failure, "Failed to bridge add"
209 |
210 | if action.len > 0:
211 | case action
212 |
213 | of "obfs4-activate-all":
214 | await activateObfs4(ActivateObfs4Kind.all)
215 |
216 | of "obfs4-activate-online":
217 | await activateObfs4(ActivateObfs4Kind.online)
218 |
219 | of "obfs4-deactivate":
220 | await deactivateObfs4()
221 |
222 | of "meekazure-activate":
223 | await activateMeekazure()
224 |
225 | of "meekazure-deactivate":
226 | await deactivateMeekazure()
227 |
228 | of "snowflake-activate":
229 | await activateSnowflake()
230 |
231 | of "snowflake-deactivate":
232 | await deactivateSnowflake()
233 |
234 | if not nc.isEmpty:
235 | var bridge = Bridge.new()
236 | match await getBridge():
237 | Ok(ret): bridge = ret
238 | Err(msg): nc.add(failure, msg)
239 | resp: render "Bridges":
240 | tab: tab
241 | notice: nc
242 | container:
243 | bridge.render()
244 | else:
245 | redirect "bridges"
--------------------------------------------------------------------------------
/src/routes/network/wireless.nim:
--------------------------------------------------------------------------------
1 | import std / options
2 | import results, resultsutils
3 | import jester, karax / [ karaxdsl, vdom]
4 | import ".." / ".." / lib / [ sys, session, hostap ]
5 | import ".." / ".." / [ renderutils, notice ]
6 | import ../ tabs
7 |
8 | proc routerWireless*() =
9 | router wireless:
10 | template tab(): Tab =
11 | buildTab:
12 | "Bridges" = "/net" / "bridges"
13 | "Interfaces" = "/net" / "interfaces"
14 | "Wireless" = "/net" / "wireless"
15 |
16 | get "/wireless/@hash":
17 | loggedIn:
18 | var
19 | hostap: HostAp = HostAp.default()
20 | # iface = conf.iface
21 | devs = Devices.default()
22 | nc = Notifies.default()
23 |
24 | hostap = await getHostAp()
25 | let
26 | isModel3 = await rpiIsModel3()
27 | iface = hostap.conf.iface
28 |
29 | if iface.isSome:
30 | match await getDevices(iface.get):
31 | Ok(ret): devs = ret
32 | Err(msg): nc.add(failure, msg)
33 |
34 | resp: render "Wireless":
35 | tab: tab
36 | notice: nc
37 | container:
38 | hostap.render(isModel3)
39 | devs.render()
40 |
41 | get "/wireless":
42 | loggedIn:
43 | var
44 | hostap: HostAp = HostAp.default()
45 | # iface = conf.iface
46 | devs = Devices.default()
47 | nc = Notifies.default()
48 |
49 | hostap = await getHostAp()
50 | let
51 | isModel3 = await rpiIsModel3()
52 | iface = hostap.conf.iface
53 |
54 | match await getDevices(iface.get):
55 | Ok(ret): devs = ret
56 | Err(msg): nc.add(failure, msg)
57 |
58 | resp: render "Wireless":
59 | tab: tab
60 | notice: nc
61 | container:
62 | hostap.render(isModel3)
63 | devs.render()
64 |
65 | post "/wireless":
66 | loggedIn:
67 | let
68 | isModel3 = await rpiIsModel3()
69 | band = if isModel3: "g"
70 | else: request.formData.getOrDefault("band").body
71 | channel = request.formData.getOrDefault("channel").body
72 | ssid = request.formData.getOrDefault("ssid").body
73 | cloak = request.formData.getOrDefault("ssidCloak").body
74 | password = request.formData.getOrDefault("password").body
75 |
76 | var
77 | nc = Notifies.default()
78 | hostapConf: HostApConf = HostApConf.new
79 |
80 | nc.add(hostapConf.ssid(ssid))
81 | nc.add(hostapConf.band(band))
82 | nc.add(hostapConf.channel(channel))
83 | nc.add(hostapConf.password(password))
84 |
85 | hostapConf.cloak if cloak == "1": true else: false
86 |
87 | hostapConf.write()
88 |
89 | if nc.isEmpty:
90 | nc.add success, "configuration successful. please restart the access point to apply the changes"
91 |
92 | var
93 | hostap: HostAp = HostAp.new()
94 | conf = await getHostApConf()
95 | devs = Devices.default()
96 |
97 | match await getDevices(conf.iface.get):
98 | Ok(ret): devs = ret
99 | Err(msg): nc.add(failure, msg)
100 |
101 | let isActive = hostapdIsActive()
102 | hostap.active(isActive)
103 |
104 | # resp renderNode(renderHostApPane(hostap, isModel3, devs), request, request.getUserName, "Wireless", netTab(), notifies=notifies)
105 | resp: render "Wireless":
106 | tab: tab
107 | notice: nc
108 | container:
109 | hostap.conf.render(isModel3)
110 | hostap.status.render()
111 | devs.render()
--------------------------------------------------------------------------------
/src/routes/status.nim:
--------------------------------------------------------------------------------
1 | import std / [ options, asyncdispatch ]
2 | import results, resultsutils
3 | import jester, karax / [ karaxdsl, vdom ]
4 | import ".." / [ notice, settings ]
5 | import ../ renderutils
6 | import ".." / lib / [ session, sys, wirelessManager ]
7 | import ../ lib / tor
8 | import tabs
9 | # import sugar
10 |
11 | proc routingStatus*() =
12 | router status:
13 |
14 | before "/io":
15 | resp "Loading"
16 |
17 | get "/io":
18 | loggedIn:
19 | var
20 | ti = TorInfo.default()
21 | si = SystemInfo.default()
22 | ii = IoInfo.new()
23 | ap = ConnectedAp.new()
24 | nc = Notifies.default()
25 |
26 | match await getTorInfo(cfg.torAddress, cfg.torPort):
27 | Ok(ret): ti = ret
28 | Err(msg): nc.add(failure, msg)
29 |
30 | match await getSystemInfo():
31 | Ok(ret): si = ret
32 | Err(msg): nc.add(failure, msg)
33 |
34 | match await getIoInfo():
35 | Ok(ret):
36 | ii = ret
37 | if isSome(ii.internet):
38 | let wlan = ii.internet.get
39 | match await getConnectedAp(wlan):
40 | Ok(ret): ap = ret
41 | Err(msg): nc.add(failure, msg)
42 | Err(msg): nc.add(failure, msg)
43 |
44 | resp: render "Status":
45 | notice: nc
46 | container:
47 | ti.render()
48 | ii.render(ap)
49 | si.render()
50 |
51 | post "/io":
52 | loggedIn:
53 | # let req = r.formData.getOrDefault("tor-request").body
54 | let req = request.formData.getOrDefault("tor-request").body
55 | case req
56 | of "new-circuit":
57 | var
58 | ti = TorInfo.default()
59 | ii = IoInfo.new()
60 | si = SystemInfo.default()
61 | ap = ConnectedAp.new()
62 | nc = Notifies.default()
63 |
64 | match await getTorInfo(cfg.torAddress, cfg.torPort):
65 | Ok(ret): ti = ret
66 | Err(msg): nc.add(failure, msg)
67 |
68 | match await renewTorExitIp(cfg.torAddress, cfg.torPort):
69 | Ok(ret):
70 | ti.status(ret)
71 | nc.add success, "Exit node has been changed."
72 | Err(msg):
73 | nc.add(failure, msg)
74 | nc.add failure, "Request new exit node failed. Please try again later."
75 |
76 | match await getSystemInfo():
77 | Ok(ret): si = ret
78 | Err(msg): nc.add failure, msg
79 |
80 | match await getIoInfo():
81 | Ok(ret):
82 | ii = ret
83 | if isSome(ii.internet):
84 | let wlan = ii.internet.get
85 | match await getConnectedAp(wlan):
86 | Ok(ret): ap = ret
87 | Err(msg): nc.add failure, msg
88 | Err(msg): nc.add failure, msg
89 |
90 | resp: render "Status":
91 | notice: nc
92 | container:
93 | ti.render()
94 | ii.render(ap)
95 | si.render()
96 |
97 | of "restart-tor":
98 | await restartTor()
99 | redirect "/io"
100 |
101 | else:
102 | redirect "/io"
103 | # redirect "/io"
104 | # let notifies = await postIO(request)
105 | # if notifies.isSome:
106 | # respIO(notifies.get)
107 | # redirect "/io"
--------------------------------------------------------------------------------
/src/routes/sys.nim:
--------------------------------------------------------------------------------
1 | import std / [ os, options, asyncdispatch ]
2 | import results, resultsutils
3 | import jester
4 | import karax / [ karaxdsl, vdom ]
5 | import ./ tabs
6 | import ".." / [ renderutils, notice ]
7 | import ".." / lib / sys as libsys
8 | import ".." / lib / session
9 |
10 | export sys
11 |
12 | template tab(): Tab =
13 | buildTab:
14 | "Password" = "/sys" / "passwd"
15 | "Logs" = "/sys" / "logs"
16 | "Update" = "/sys" / "update"
17 |
18 | proc routingSys*() =
19 | router sys:
20 | get "/sys":
21 | redirect "/sys/passwd"
22 |
23 | post "/sys":
24 | loggedIn:
25 | case request.formData["postType"].body
26 | of "chgPasswd":
27 | let
28 | oldPasswd = request.formData["crPassword"].body
29 | newPasswd = request.formData["newPasswd"].body
30 | rePasswd = request.formData["re_newPasswd"].body
31 | match await changePasswd(oldPasswd, newPasswd, rePasswd):
32 | Ok(): redirect "/login"
33 | Err(): redirect "/login"
34 |
35 | post "/sys/logs":
36 | loggedIn:
37 | let ops = request.formData["ops"].body
38 | case ops
39 | of "eraseLogs":
40 | var nc = Notifies.default()
41 |
42 | match await eraseLogs():
43 | Ok(): nc.add success, "Complete erase logs"
44 | Err(): nc.add failure, "Failure erase logs"
45 |
46 | resp: render "Logs":
47 | notice: nc
48 | tab: tab
49 | container:
50 | renderLogs()
51 |
52 | get "/sys/passwd":
53 | loggedIn:
54 | resp: render "Passwd":
55 | tab: tab
56 | container:
57 | renderPasswdChange()
58 |
59 | get "/sys/eraselogs":
60 | loggedIn:
61 | resp: render "Logs":
62 | tab: tab
63 | container:
64 | renderLogs()
--------------------------------------------------------------------------------
/src/routes/tabs.nim:
--------------------------------------------------------------------------------
1 | import tabs / [ tab, dsl, vdom ]
2 | export tab, dsl, vdom
--------------------------------------------------------------------------------
/src/routes/tabs/dsl.nim:
--------------------------------------------------------------------------------
1 | import std / [ macros, strformat ]
2 | import tab
3 |
4 | proc joinPath(node: NimNode): string =
5 | expectKind(node, nnkInfix)
6 | let (left, op, right) = node.unpackInfix()
7 | if eqIdent(op, "/"):
8 | case left.kind
9 | of nnkStrLit:
10 | result = fmt"{left}/{right}"
11 | of nnkInfix:
12 | result = fmt"{joinPath(left)}/{right}"
13 | else: return
14 |
15 | proc createTab(node: NimNode): NimNode =
16 | expectKind(node, nnkStmtList)
17 |
18 | result = newTree(nnkStmtListExpr)
19 | let
20 | tmp = genSym(nskLet, "tab")
21 | call = newCall(bindSym"new", ident("Tab"))
22 | # let tmp = new Tab
23 | result.add newTree(nnkStmtList, newLetStmt(tmp, call))
24 |
25 | for asgn in node.children:
26 | expectKind(asgn, nnkAsgn)
27 | expectKind(asgn[0], nnkStrLit)
28 | # expectKind(asgn[1], nnkStrLit)
29 | # let op = newAssignment(nnkBracketExpr.newTree(ident, asgn[0]), asgn[1])
30 | # let right = newAssignment(ident("str"), asgn[1])
31 | var right: string
32 | case asgn[1].kind
33 | of nnkStrLit: right = $asgn[1]
34 | of nnkInfix: right = joinPath(asgn[1])
35 | else: return
36 |
37 | # Represent
38 | # result.add "Tor", "/tor" / "projet"
39 | let command = newCall(bindSym("add"), tmp, asgn[0], newLit(right))
40 | result.add command
41 | # final value
42 | result.add tmp
43 |
44 | macro buildTab*(node: untyped): Tab =
45 | result = createTab(node)
46 |
47 | when defined(debugTabs):
48 | echo repr result
--------------------------------------------------------------------------------
/src/routes/tabs/tab.nim:
--------------------------------------------------------------------------------
1 | type
2 | Tab* = ref object
3 | tab: TabList
4 |
5 | TabList* = seq[TabField]
6 |
7 | TabField* = ref object
8 | label: string
9 | link: string
10 |
11 | # method newTab*()
12 |
13 | method len*(tab: Tab): int {.base.} =
14 | tab.tab.len
15 |
16 | method isEmpty*(tab: Tab): bool {.base.} =
17 | tab.len == 0
18 |
19 | proc label*(tab: Tab, i: int): string =
20 | tab.tab[i].label
21 |
22 | proc link*(tab: Tab, i: int): string =
23 | tab.tab[i].link
24 |
25 | proc `[]`*(tab: Tab, i: int): TabField =
26 | # if not tab.tab[i].isNil:
27 | tab.tab[i]
28 |
29 | method label*(self: TabField): string {.base.} =
30 | self.label
31 |
32 | method link*(self: TabField): string {.base.} =
33 | self.link
34 |
35 | proc add*(tab: var Tab, label, link: string) =
36 | let field: TabField = TabField(label: label, link: link)
37 | tab.tab.add field
38 |
39 | proc add*(tab: Tab, label, link: string) =
40 | let field: TabField = TabField(label: label, link: link)
41 | tab.tab.add field
42 |
43 | method list*(self: Tab): TabList {.base.} =
44 | self.tab
45 |
46 | # iterator items*(tab: Tab): tuple[i: int, label, link: string] {.inline.} =
47 | # var i: int
48 | # while i < tab.len:
49 | # let f = tab[i]
50 | # yield (i, f.label, f.link)
51 |
--------------------------------------------------------------------------------
/src/routes/tabs/vdom.nim:
--------------------------------------------------------------------------------
1 | import std / [ strutils ]
2 | import karax / [ karaxdsl, vdom ]
3 | import tab
4 |
5 | func render*(self: Tab, currentPath: string): VNode =
6 | buildHtml(tdiv(class="sub-menu")):
7 | ul(class="menu-table"):
8 | for i, v in self.list:
9 | let class = if currentPath.startsWith(v.link): "menu-item current"
10 | else: "menu-item"
11 | li(class=class):
12 | a(class="menu-link", href=v.link):
13 | text v.label
--------------------------------------------------------------------------------
/src/sass/box.scss:
--------------------------------------------------------------------------------
1 | .box {
2 | margin-bottom: 16px;
3 | border: solid 1px;
4 | border-radius: 6px;
5 | border-color: #d0d7de;
6 | }
7 |
8 | .box-header {
9 | padding: 16px;
10 | margin: -1px -1px 0;
11 | border: solid 1px;
12 | border-top-left-radius: 6px;
13 | border-top-right-radius: 6px;
14 | // relative
15 | position: relative;
16 | // background color
17 | background-color: rgba(234,238,242,0.5);
18 | border-color: #d0d7de;
19 | color: #000;
20 | display: flex;
21 | // padding: 8px !important;
22 | align-items: center;
23 | justify-content: space-between !important;
24 | }
25 |
26 | .box-table {
27 | padding: 20px;
28 | position: relative;
29 | overflow-y: auto;
30 | }
31 |
32 | .edit-btn {
33 | float: right;
34 | position: relative;
35 | padding: 8px !important;
36 | margin-left: 5px;
37 | line-height: 1;
38 | background: transparent;
39 | border: 0;
40 | box-shadow: none;
41 | }
42 |
43 | .btn-apply {
44 | color: #fff;
45 | background: linear-gradient(to bottom, $torbox-green, $torbox-green) no-repeat;
46 | text-shadow: 0 -1px 0 rgba(0,0,0,0.25);
47 | width: 100%;
48 | display: block;
49 | height: 40px;
50 | line-height: 40px;
51 | text-align: center;
52 | border-radius: 5px;
53 | font-size: 16px;
54 | border: hidden;
55 | margin: 10px 0 0 0;
56 | padding: 0;
57 | }
58 |
59 | .btn-apply:hover {
60 | background: linear-gradient(to bottom, $torbox-dark-green, $torbox-dark-green) no-repeat;
61 | }
62 |
63 | .btn-apply:focus {
64 | background: linear-gradient(to bottom, $torbox-dark-green $torbox-dark-green) no-repeat;
65 | }
66 |
67 | .password_field_container {
68 | display: flex;
69 | position: relative;
70 | }
71 |
72 | .hidden_password {
73 | display: block;
74 | }
75 |
76 | .shown_password {
77 | display: block;
78 | }
79 |
80 | .show_password {
81 | position: absolute;
82 | top: 0;
83 | left: 0;
84 | width: 100%;
85 | height: 100%;
86 | cursor: pointer;
87 | opacity: 0;
88 | margin: 0;
89 | padding: 0;
90 | }
91 |
92 | input[name="password_visibility"][value="show"]:checked ~ input[name="password_visibility"][value="hide"] {
93 | display: block;
94 | width: 100%;
95 | height: 100%;
96 | left: 0;
97 | top: 0;
98 | z-index: 999998;
99 | position: fixed;
100 | opacity: 0;
101 | margin: 0;
102 | padding: 0;
103 | }
104 |
105 | .show_password:checked ~ .hide_password_icon {
106 | display: block;
107 | }
108 |
109 | input[name="password_visibility"][value="show"]:checked ~ .password_preview_field {
110 | display: block;
111 | }
112 |
113 | input[name="password_visibility"][value="show"]:checked ~ .black_circle {
114 | display: none;
115 | }
116 |
117 | .hide_password {
118 | display: none;
119 | }
120 |
121 | .hide_password_icon {
122 | display: none;
123 | }
124 |
125 | input[name="password_visibility"][value="show"]:checked ~ .shadow {
126 | opacity: .3;
127 | display: block;
128 | }
129 |
130 | .black_circle:after {
131 | content: "\25CF\25CF\25CF\25CF\25CF";
132 | }
133 |
134 | .icon-eye {
135 | margin-left: 5px;
136 | }
137 |
138 | .icon-eye-off {
139 | margin-left: 5px;
140 | }
141 |
142 | .password_preview_field {
143 | width: auto;
144 | height: auto;
145 | display: block;
146 | position: fixed;
147 | z-index: 999999;
148 | // width: 500px;
149 | // height: auto;
150 | padding: 5px 20px 5px 20px;
151 | background: #fff;
152 | color: #000;
153 | display: none;
154 | -webkit-transform: translate(-50%, -50%);
155 | -moz-transform: translate(-50%, -50%);
156 | transform: translate(-50%, -50%);
157 | top: 50%;
158 | left: 50%;
159 | margin: 0 auto;
160 | cursor: initial;
161 | overflow-y: auto;
162 | text-align: left;
163 | font-size: 14px;
164 | font-weight: normal;
165 | max-height: 80%;
166 | max-width: 90%;
167 | overflow-x: hidden;
168 | text-transform: none;
169 | line-height: 1;
170 | }
171 |
172 | .card-table > input[type="radio"] {
173 | width: auto;
174 | }
175 |
176 | .card-table > input[type="checkbox"] {
177 | width: auto;
178 | }
179 |
180 | .textarea {
181 | border: none;
182 | width: 100%;
183 | resize: vertical;
184 | padding: 6px;
185 | }
186 |
187 | .btn-general {
188 | width: auto;
189 | padding: 0 10px;
190 | line-height: 32px;
191 | font-size: 18px;
192 | border-radius: .4rem;
193 | }
194 |
195 | .btn-danger {
196 | background-color: $red-btn-bg;
197 | color: #fff;
198 | }
199 |
200 | .btn-warn {
201 | background-color: $yellow-btn-bg;
202 | color: #000;
203 | }
204 |
205 | .btn-safe {
206 | background-color: $green-btn-bg;
207 | color: #fff;
208 | }
209 |
210 | @media (max-width: 800px) {
211 | .box-table {
212 | display: inline-block;
213 | }
214 | }
--------------------------------------------------------------------------------
/src/sass/card.scss:
--------------------------------------------------------------------------------
1 | .columns {
2 | width: 48%;
3 | box-sizing: border-box;
4 | float: left;
5 | margin-left: 4%;
6 | }
7 |
8 | .columns:first-child {
9 | margin-left: 0;
10 | }
11 |
12 | // .columns:last-child {
13 | // width: 100%;
14 | // margin-left: 0;
15 | // }
16 |
17 | .card {
18 | background-color: #fff;
19 | margin-bottom: 20px;
20 | position: relative;
21 | box-shadow: 0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);
22 | }
23 |
24 | .card-tor {
25 | border-top: 2px solid $tor-purple;
26 | }
27 |
28 | .card-sky {
29 | border-top: 2px solid $sky-blue;
30 | }
31 |
32 | .card-sys {
33 | border-top: 2px solid $torbox-green;
34 | }
35 |
36 | .card-header {
37 | color: #000;
38 | margin-bottom: 2rem;
39 | font-size: 24px;
40 | }
41 |
42 | .card-header em {
43 | font-style: normal;
44 | font-weight: 700;
45 | padding: 0 5px;
46 | }
47 |
48 | .card-padding {
49 | padding: 20px;
50 | }
51 |
52 | .table {
53 | box-sizing: border-box;
54 | margin-bottom: 2.5rem;
55 | border-collapse: collapse;
56 | border-spacing: 0;
57 | }
58 |
59 | .full-width {
60 | width: 100%;
61 | max-width: none;
62 | margin-left: 0;
63 | }
64 |
65 | td, th {
66 | padding: 12px 15px;
67 | text-align: left;
68 | border-bottom: 1px solid #e1e1e1;
69 | }
70 |
71 | td:first-child, th:first-child {
72 | padding-left: 0;
73 | }
74 |
75 | td:last-child, th:last-child {
76 | padding-right: 0;
77 | }
78 |
79 | // Included from status.scss
80 | .cards {
81 | // display: flex;
82 | }
83 |
84 | @media screen and (max-width: 800px) {
85 | .card-header {
86 | width: 100%;
87 | }
88 | }
89 |
90 | .card-table {
91 | display: table-row-group;
92 | box-sizing: border-box;
93 | }
94 | .card-title {
95 | display: table-cell;
96 | box-sizing: border-box;
97 | width: 170px;
98 | line-height: 22px;
99 | //text-align: left;
100 | //padding-right: 10px;
101 | color: #afafaf;
102 | }
103 | .card-text {
104 | font-size: 20px;
105 | display: table-cell;
106 | padding: 0 10px;
107 | word-break: break-all;
108 | }
109 | // end at status.scss
110 |
111 | #editable {
112 | display: none;
113 | }
114 |
115 | .editable-box {
116 | display: none;
117 | }
118 | .opening-button {
119 | //display: block;
120 | position: absolute;
121 | width: 100%;
122 | height: 100%;
123 | cursor: pointer;
124 | opacity: 0;
125 | margin: 0;
126 | padding: 0;
127 | top: 0;
128 | right: 0;
129 | }
130 |
131 | .closing-button {
132 | display: none;
133 | }
134 |
135 | .shadow {
136 | position: fixed;
137 | top: 0;
138 | left: 0;
139 | width: 100%;
140 | height: 100%;
141 | z-index: 999997;
142 | background-color: #000;
143 | cursor: pointer;
144 | display: none;
145 | }
146 |
147 | .editable-box{
148 | position: fixed;
149 | z-index: 999999;
150 | width: 500px;
151 | height: auto;
152 | padding: 20px 20px 20px 20px;
153 | background: #fff;
154 | color: #000;
155 | display: none;
156 | -webkit-transform: translate(-50%, -50%);
157 | -moz-transform: translate(-50%, -50%);
158 | transform: translate(-50%, -50%);
159 | top: 50%;
160 | left: 50%;
161 | margin: 0 auto;
162 | cursor: initial;
163 | overflow-y: auto;
164 | text-align: left;
165 | font-size: 14px;
166 | font-weight: normal;
167 | max-height: 80%;
168 | max-width: 90%;
169 | overflow-x: hidden;
170 | text-transform: none;
171 | line-height: 1;
172 | }
173 |
174 | .editable-box>form{
175 | //position: absolute;
176 | //right: 0;
177 | //top: 50%;
178 | width: 100%;
179 | //padding: 0 30px;
180 | //transform: translateY(-50%);
181 | display: table;
182 | box-sizing: border-box;
183 | table-layout: fixed;
184 | border-collapse: separate;
185 | border-spacing: 0px 0.996553px;
186 | }
187 |
188 | .editable-box>form>div{
189 | //display: block;
190 | //margin-bottom: 15px;
191 | }
192 |
193 | .editable-box>form>div>label{
194 | //font-size: 16px;
195 | //margin-bottom: 5px;
196 | //display: block;
197 | }
198 |
199 | .editable-box>form>div>input{
200 | //display: block;
201 | display: table-cell;
202 | width: 100%;
203 | font-size: 16px;
204 | box-sizing: border-box;
205 | //height: 40px;
206 | //line-height: 40px;
207 | padding: 0 15px 0 15px;
208 | margin: 0;
209 | border-top-style: hidden;
210 | border-right-style: hidden;
211 | border-bottom-style: groove;
212 | border-left-style: hidden;
213 | }
214 |
215 | .editable-box>form>div>input:focus {
216 | outline: none;
217 | }
218 |
219 | // .editable-box>form>.saveBtn {
220 | // width: 100%;
221 | // display: block;
222 | // height: 40px;
223 | // line-height: 40px;
224 | // text-align: center;
225 | // border-radius: 5px;
226 | // font-size: 16px;
227 | // border: hidden;
228 | // color: white;
229 | // background-color: rgb(127, 187, 35);
230 | // margin: 10px 0 0 0;
231 | // padding: 0;
232 | // }
233 |
234 | .editable-box>form>.saveBtn:focus {
235 | color: #fff;
236 | background-color: #6ba610;
237 | }
238 |
239 | .editable-box>form>.saveBtn:hover {
240 | color: #fff;
241 | background-color: #6ba610;
242 | }
243 |
244 | .opening-button:checked ~ .closing-button {
245 | display: block;
246 | width: 100%;
247 | height: 100%;
248 | right: 0;
249 | top: 0;
250 | z-index: 999998;
251 | position: fixed;
252 | opacity: 0;
253 | margin: 0;
254 | padding: 0;
255 | }
256 |
257 | .opening-button:checked ~ .shadow {
258 | opacity: .3;
259 | display: block;
260 | }
261 |
262 | .opening-button:checked ~ .editable-box {
263 | display: block;
264 | height: 500px;
265 | width: 700px;
266 | border-radius: .25rem;
267 | border: 2px solid rgba(0, 0, 0, 0.125);
268 | //border: 2px solid #ccc;
269 | }
270 |
271 | #editable:target {
272 | display: block;
273 | }
274 | #editable:not(target){
275 | display: none;
276 | }
277 |
278 | .btn {
279 | cursor: pointer;
280 | display: inline-block;
281 | position: relative;
282 | font-family: Arial;
283 | // text-shadow: 0 -1px 0 rgba(165, 151, 151, 0.75);
284 | // text-shadow: 0 1px 1px rgba(255,255,255,0.75);
285 | // background: linear-gradient(#fff,#fff 25%,#e6e6e6);
286 | // box-shadow: inset 0 1px 0 rgba(255,255,255,0.2),0 1px 2px rgba(0,0,0,0.05);
287 | // border: 1px solid #ccc;
288 | // border-bottom-color: #bbb;
289 | }
290 |
291 | .width-58 {
292 | width: 58%;
293 | }
294 |
295 | .width-38 {
296 | width: 38%;
297 | }
298 |
299 | @media (max-width: 800px) {
300 | .columns {
301 | width: 100%;
302 | margin: 0;
303 | }
304 | }
--------------------------------------------------------------------------------
/src/sass/colours.scss:
--------------------------------------------------------------------------------
1 | $tor-purple: #7D4698;
2 |
3 | // torbox colours
4 | $torbox-green: #7fbb23;
5 | $torbox-dark-green: #6ba610;
6 |
7 | $green-btn-bg: #2da44e;
8 | $blue-btn-bg: #1d70b8;
9 | $red-btn-bg: #d4351c;
10 | $yellow-btn-bg: #faee1c;
11 |
12 | $sky-blue: #5AEDFA;
13 | $blue: #41a4db;
14 |
15 | $silver: #848999;
16 |
--------------------------------------------------------------------------------
/src/sass/error.scss:
--------------------------------------------------------------------------------
1 | .panel-container {
2 | margin: auto;
3 | font-size: 130%;
4 | padding: 120px 0;
5 | }
6 |
7 | .logo-container {
8 | text-align: center;
9 | }
10 |
11 | .error-panel {
12 | padding: 14px;
13 | border-radius: 8px;
14 | background-color: #fff;
15 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1), 0 8px 16px rgba(0, 0, 0, 0.1);
16 | margin: auto;
17 | margin-top: 50px;
18 | text-align: center;
19 | width: 396px;
20 | }
--------------------------------------------------------------------------------
/src/sass/index.scss:
--------------------------------------------------------------------------------
1 | @import 'colours';
2 | @import 'menues';
3 | @import 'nav';
4 | @import 'notify';
5 | @import 'login';
6 | // @import 'status';
7 | @import 'sub-menu';
8 | @import 'card';
9 | @import 'box';
10 | @import 'table';
11 | @import 'network';
12 | @import 'wireless';
13 | @import 'error';
14 | @import 'warn';
15 | @import 'loading';
16 |
17 | * {
18 | outline: none;
19 | }
20 |
21 | *::selection {
22 | background-color: $torbox-green;
23 | color: #fff;
24 | }
25 |
26 | body {
27 | color: #4a4a4a;
28 | //background-color: #fafafa;
29 | //background-color: #EFEFED;
30 | background-color: #fff;
31 | margin: 0;
32 | // font-family: Consolas,Liberation Mono,Menlo,monospace,monospace;
33 | font-family: Helvetica Neue, Helvetica, Arial, sans-serif;
34 | }
35 |
36 | a {
37 | text-decoration: none;
38 | color: $tor-purple;
39 | }
40 | a:hover {
41 | color: #ddd;
42 | }
43 | .container {
44 | color: #4a4a4a;
45 | //width: 100%;
46 | font-size: 1.3rem;
47 | //float: right;
48 | width: 80% !important;
49 | max-width: unset;
50 | display: flex;
51 | margin: 0px auto;
52 | margin-top: 120px;
53 | flex-direction: column;
54 | }
55 |
56 | .container-inside {
57 | display: flex;
58 | flex-direction: row;
59 | background-color: #fff;
60 | }
61 |
62 | .wrap-container {
63 | display: flex;
64 | margin: 50px auto 0 auto;
65 | background-color: #fff;
66 | max-width: 1200px;
67 | }
68 |
69 | .headers {
70 | position: fixed;
71 | width: 100%;
72 | z-index: 1000;
73 | //margin-bottom: 80px;
74 | top: 0;
75 | right: 0;
76 | left: 0;
77 | }
78 |
79 | .flag {
80 | // width: 50px;
81 | height: 23px;
82 | border-radius: 4px;
83 | // margin-bottom: 10px;
84 | // margin-left: 15px;
85 | margin-right: 15px;
86 | }
87 |
88 | .new-circuit {
89 | width: 19px;
90 | height: 19px;
91 | cursor: pointer;
92 | margin-left: 10px;
93 | }
94 |
95 | .new-circuit:hover {
96 | color: gray;
97 | }
98 |
99 | .btn-flat {
100 | border: none;
101 | background: none;
102 | margin: 0;
103 | padding: 0;
104 | }
105 |
106 | @media (max-width: 800px) {
107 | .container {
108 | width: 95% !important;
109 | font-size: 1.1rem;
110 | margin-bottom: 70px;
111 | }
112 | }
--------------------------------------------------------------------------------
/src/sass/loading.scss:
--------------------------------------------------------------------------------
1 | .loading {
2 | margin-right: auto;
3 | margin-left: auto;
4 | z-index: 999999;
5 | position: fixed;
6 | left: 50%;
7 | transform: translate(-50%, -50%);
8 | cursor: initial;
9 | }
10 | @-webkit-keyframes cube {
11 | 0% {
12 | -webkit-transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
13 | transform: rotate(45deg) rotateX(-25deg) rotateY(25deg); }
14 | 50% {
15 | -webkit-transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
16 | transform: rotate(45deg) rotateX(-385deg) rotateY(25deg); }
17 | 100% {
18 | -webkit-transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
19 | transform: rotate(45deg) rotateX(-385deg) rotateY(385deg); } }
20 | @keyframes cube {
21 | 0% {
22 | -webkit-transform: rotate(45deg) rotateX(-25deg) rotateY(25deg);
23 | transform: rotate(45deg) rotateX(-25deg) rotateY(25deg); }
24 | 50% {
25 | -webkit-transform: rotate(45deg) rotateX(-385deg) rotateY(25deg);
26 | transform: rotate(45deg) rotateX(-385deg) rotateY(25deg); }
27 | 100% {
28 | -webkit-transform: rotate(45deg) rotateX(-385deg) rotateY(385deg);
29 | transform: rotate(45deg) rotateX(-385deg) rotateY(385deg); } }
30 |
31 | .cube {
32 | -webkit-animation: cube 2s infinite ease;
33 | animation: cube 2s infinite ease;
34 | height: 40px;
35 | -webkit-transform-style: preserve-3d;
36 | transform-style: preserve-3d;
37 | width: 40px; }
38 | .cube div {
39 | background-color: rgba(127, 187, 35, 0.25);
40 | height: 100%;
41 | position: absolute;
42 | width: 100%;
43 | border: 2px solid #7fbb23; }
44 | .cube div:nth-of-type(1) {
45 | -webkit-transform: translateZ(-20px) rotateY(180deg);
46 | transform: translateZ(-20px) rotateY(180deg); }
47 | .cube div:nth-of-type(2) {
48 | -webkit-transform: rotateY(-270deg) translateX(50%);
49 | transform: rotateY(-270deg) translateX(50%);
50 | -webkit-transform-origin: top right;
51 | transform-origin: top right; }
52 | .cube div:nth-of-type(3) {
53 | -webkit-transform: rotateY(270deg) translateX(-50%);
54 | transform: rotateY(270deg) translateX(-50%);
55 | -webkit-transform-origin: center left;
56 | transform-origin: center left; }
57 | .cube div:nth-of-type(4) {
58 | -webkit-transform: rotateX(90deg) translateY(-50%);
59 | transform: rotateX(90deg) translateY(-50%);
60 | -webkit-transform-origin: top center;
61 | transform-origin: top center; }
62 | .cube div:nth-of-type(5) {
63 | -webkit-transform: rotateX(-90deg) translateY(50%);
64 | transform: rotateX(-90deg) translateY(50%);
65 | -webkit-transform-origin: bottom center;
66 | transform-origin: bottom center; }
67 | .cube div:nth-of-type(6) {
68 | -webkit-transform: translateZ(20px);
69 | transform: translateZ(20px); }
--------------------------------------------------------------------------------
/src/sass/login.scss:
--------------------------------------------------------------------------------
1 | .main-panel {
2 | margin: 100px auto auto auto;
3 | max-width: 900px;
4 | background-color: #fff;
5 | padding: 50px 30px;
6 | width: 100%;
7 | }
8 | button {
9 | cursor: pointer;
10 | }
11 | .loginBtn {
12 | font-size: 18px;
13 | font-weight: bold;
14 | color: #000;
15 | background-color: #ffffff;
16 | border: none;
17 | border-radius: 50px;
18 | box-shadow: 12px 12px 24px 0 rgba(0, 0, 0, 0.2), -12px -12px 24px 0 rgba(255, 255, 255, 0.5);
19 | height: 50px;
20 | width: 100%;
21 | }
22 | .loginPanel>form>.inp {
23 | font-size: 14px;
24 | border: none;
25 | border-radius: 5px;
26 | box-shadow: inset 6px 6px 10px 0 rgba(0, 0, 0, 0.2), inset -6px -6px 10px 0 rgba(255, 255, 255, 0.5);;
27 | height: 30px;
28 | width: 100%;
29 | }
30 |
31 | .content {
32 | background-color: #f0f2f5;
33 | height: 100vh;
34 | }
35 |
36 | .login-pane {
37 | padding: 120px 0;
38 | }
39 |
40 | .login-header {
41 | text-align: center;
42 | }
43 |
44 | .logo {
45 | width: 120px;
46 | height: 120px;
47 | margin: -44px 0 -4px 0;
48 | }
49 |
50 | .form-box {
51 | text-align: center;
52 | box-shadow: 0 2px 4px rgba(0, 0, 0, .1), 0 8px 16px rgba(0, 0, 0, .1);
53 | margin: auto;
54 | padding: 0 0 14px 0;
55 | border-radius: 8px;
56 | background-color: #fff;
57 | width: 396px;
58 | }
59 |
60 | .login-text {
61 | padding: 24px 0 16px 0;
62 | margin-top: 20px;
63 | }
64 |
65 | .form-section {
66 | padding: 6px 0;
67 | width: 330px;
68 | margin: auto;
69 | }
70 |
71 | .form-section>label {
72 | display: block;
73 | }
74 |
75 | .form-section>input {
76 | // border-radius: 6px;
77 | // font-size: 17px;
78 | // padding: 14px 16px;
79 | // width: 330px;
80 | border: 1px solid #dddfe2;
81 | color: #1d2129;
82 | font-family: Helvetica, Arial, sans-serif;
83 | font-size: 12px;
84 | height: 22px;
85 | line-height: 16px;
86 | padding: 0 8px;
87 | vertical-align: middle;
88 | }
89 |
90 | .login-btn {
91 | padding: 6px 0;
92 | }
93 |
94 | .login-btn>button {
95 | border: none;
96 | background-color: $torbox-green;
97 | border-radius: 6px;
98 | font-size: 20px;
99 | line-height: 48px;
100 | padding: 0 16px;
101 | width: 332px;
102 | cursor: pointer;
103 | display: inline-block;
104 | color: white;
105 | white-space: nowrap;
106 | text-decoration: none;
107 | // padding-top: 24px;
108 | }
109 |
110 | .login-btn>button:hover {
111 | background-color: $torbox-dark-green;
112 | }
--------------------------------------------------------------------------------
/src/sass/menues.scss:
--------------------------------------------------------------------------------
1 | .menues-container {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 | .tor-status-active {
6 | color: #2ECC71;
7 | }
8 | .tor-status-deactive {
9 | color: #E74C3C;
10 | }
11 | .status-ready {
12 | padding: 0 20px 0 0;
13 | color: #ccc;
14 | }
15 | .menues {
16 | display: flex;
17 | //float: right;
18 | flex-direction: column;
19 | margin: 10px 10px 10px 10px;
20 | }
21 | .status-bar {
22 | display: flex;
23 | }
24 | .logs-container {
25 | padding: 20px 20px 10px 20px;
26 | border: 1px solid #ccc;
27 | margin: 60px 0 0 0;
28 | }
29 | .logs-text {
30 | font-size: 0.5em;
31 | overflow-y: hidden;
32 | overflow-x: visible;
33 | margin: 0;
34 | }
35 | .doc-bridges {
36 | background-color: #fff;
37 | overflow-y: hidden;
38 | overflow-x: visible;
39 | border: 1px solid #ccc;
40 | font-size: 0.5em;
41 | width: 100%;
42 | margin: 0;
43 | }
44 | .sub-menu {
45 | //margin: 100px auto 100px auto;
46 | width: 16%;
47 | //background-color: #fff;
48 | padding: 10px 10px 10px 10px;
49 | color: #444;
50 | float: left;
51 | }
52 | .main-panel {
53 | width: 94% !important;
54 | margin: auto;
55 | position: relative;
56 | }
57 | .subject-title {
58 | margin: 0;
59 | border-bottom: 1px solid #444;
60 | }
61 | .status-list {
62 | display: flex;
63 | flex-direction: row;
64 | border-bottom: 1px solid #444;
65 | padding: 10px 0;
66 | }
67 | .status-text {
68 | position: absolute;
69 | right: 0;
70 | }
--------------------------------------------------------------------------------
/src/sass/nav.scss:
--------------------------------------------------------------------------------
1 | .nav-container {
2 | width: 100%;
3 | height: 50px;
4 | margin: auto;
5 | background-color: #444;
6 | display: flex;
7 | //padding: 0 20px 0 0;
8 | position: sticky;
9 | align-items: center;
10 | box-shadow: inset 0 -3em 3em rgba(0,0,0,0.1),0.3em 0.3em 1em rgba(0,0,0,0.3);
11 | }
12 |
13 | .nav-contents {
14 | display: flex;
15 | }
16 |
17 | .inner-nav {
18 | display: flex;
19 | height: 50px;
20 | box-sizing: border-box;
21 | align-items: center;
22 | flex-basis: 90%;
23 | }
24 |
25 | .top {
26 | text-align: center;
27 | }
28 |
29 | .linker-root {
30 | display: flex;
31 | flex-direction: row;
32 | align-items: center;
33 | margin-right: 30px;
34 | padding-left: 12px;
35 | }
36 |
37 | .logo-file {
38 | width: 2rem;
39 | height: 2rem;
40 | margin: 10px 10px 10px 10px;
41 | }
42 |
43 | .service-name {
44 | font-size: 24px;
45 | color: #fff;
46 | }
47 |
48 | .caracteres-version {
49 | color: #fff;
50 | font-size: 14px;
51 | }
52 |
53 | .center-title {
54 | display: none;
55 | }
56 |
57 | .tabs {
58 | display: flex;
59 | font-size: 20px;
60 | /*
61 | flex-direction: column;
62 | margin: 20px 0 20px 0px;
63 | padding: 20px 10px 0 10px;
64 | */
65 | }
66 |
67 | .linker {
68 | display: flex;
69 | margin: 12px 0px 12px 0px;
70 | color: white;
71 | padding: 5px 20px 5px 20px;
72 | //box-shadow: inset 0 -3em 3em rgba(0,0,0,0.1),0.3em 0.3em 1em rgba(0,0,0,0.3);
73 | flex-wrap: wrap;
74 | //border-radius: 10px;
75 | }
76 |
77 | .linker-root > a {
78 | display: flex;
79 | align-items: center;
80 | }
81 |
82 | .linker > .tab-name {
83 | margin-left: 5px;
84 | }
85 |
86 | .tabs > .current {
87 | color: $torbox-green;
88 | }
89 |
90 | .user-drop {
91 | display: flex;
92 | color: #fff;
93 | // right: 10%;
94 | // position: absolute;
95 | font-size: 1.3rem;
96 | flex: 1;
97 | justify-content: flex-end;
98 | padding-right: 12px;
99 | }
100 |
101 | .user-drop > .user-status {
102 | color: #fff;
103 | display: flex;
104 | align-items: center;
105 | margin-left: 30px;
106 | }
107 |
108 | .user-drop > .user-status > .username {
109 | letter-spacing: 1px;
110 | margin-left: 5px;
111 | font-size: 1rem;
112 | }
113 |
114 | .user-drop > .user-status > .icon-container:last-child {
115 | margin-left: 50px;
116 | }
117 |
118 | .user-drop > .dropdown {
119 | display: none;
120 | width: 200px;
121 | position: absolute;
122 | top: 100%;
123 | padding-top: 10px;
124 | z-index: 999999;
125 | }
126 |
127 | .user-drop > .dropdown > .panel {
128 | display: block;
129 | width: 100%;
130 | background-color: #fff;
131 | border-radius: 3px;
132 | box-shadow: 0 2px 4px 0 rgb(0, 0, 0 / 20%);
133 | // box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
134 | overflow: hidden;
135 | }
136 |
137 | .dropdown > .panel > .line {
138 | display: flex;
139 | background-color: #fff;
140 | color: #000;
141 | height: 40px;
142 | line-height: 40px;
143 | font-size: 14px;
144 | padding: 0 15px 0 15px;
145 | cursor: default;
146 | }
147 |
148 | .user-drop > .dropdown > .panel > form > button {
149 | display: flex;
150 | border: none;
151 | background-color: #fff;
152 | color: #000;
153 | height: 40px;
154 | line-height: 40px;
155 | font-size: 14px;
156 | width: 100%;
157 | padding: 0 15px 0 15px;
158 | }
159 |
160 | .user-drop > .dropdown:hover, .user-drop > .user-status:hover ~
161 | .dropdown {
162 | display: block;
163 | }
164 |
165 | .dropdown > .panel > .line:hover {
166 | color: #fff;
167 | background-color: $torbox-green;
168 | }
169 |
170 | .dropdown > .panel > form > button:hover {
171 | color: #fff;
172 | background-color: $torbox-green;
173 | }
174 |
175 | .user-drop > .popup-btn {
176 | position: absolute;
177 | width: 1.3rem;
178 | height: 100%;
179 | cursor: pointer;
180 | opacity: 0;
181 | margin: 0;
182 | padding: 0;
183 | top: 0;
184 | }
185 |
186 | .user-drop > .popout-btn {
187 | display: none;
188 | }
189 |
190 | .invisible {
191 | background-color: none;
192 | }
193 |
194 | .user-drop > .popup-btn:checked ~ .popout-btn {
195 | display: block;
196 | width: 100%;
197 | height: 100%;
198 | right: 0;
199 | top: 0;
200 | z-index: 999998;
201 | position: fixed;
202 | opacity: 0;
203 | margin: 0;
204 | padding: 0;
205 | }
206 |
207 | .user-drop > .popup-btn:checked ~ .shadow {
208 | opacity: .3;
209 | display: block;
210 | }
211 |
212 | .user-drop > .popup-btn:checked ~ .dropdown {
213 | display: block;
214 | }
215 |
216 | @media (max-width: 800px) {
217 | .inner-nav {
218 | flex-basis: 900px;
219 | }
220 |
221 | .tabs {
222 | flex-direction: row;
223 | bottom: 0;
224 | position: fixed;
225 | font-size: 1.5rem;
226 | margin: 0;
227 | width: 100%;
228 | background-color: #444;
229 | height: 50px;
230 | align-items: center;
231 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
232 | }
233 |
234 | .tab-name {
235 | display: none;
236 | }
237 |
238 | .nav-container {
239 | float: bottom;
240 | }
241 |
242 | .top {
243 | top: 0;
244 | left: 0;
245 | position: fixed;
246 | }
247 |
248 | .linker-root {
249 | display: flex;
250 | flex: 1;
251 | margin: 0;
252 | }
253 |
254 | .center-title {
255 | display: block;
256 | font-weight: bold;
257 | font-size: 1.5rem;
258 | color: #fff;
259 | align-items: center;
260 | margin: auto;
261 | }
262 |
263 | .linker {
264 | border-radius: 0;
265 | margin: auto;
266 | padding: 0;
267 | }
268 |
269 | .service-name {
270 | display: none;
271 | }
272 |
273 | .caracteres-version {
274 | display: none;
275 | }
276 |
277 | .top {
278 | top: 0;
279 | left: 0;
280 | position: fixed;
281 | }
282 |
283 | .logo {
284 | width: 50px;
285 | height: 50px;
286 | }
287 | }
288 |
289 | .linker:hover {
290 | color: #ddd;
291 | }
292 |
--------------------------------------------------------------------------------
/src/sass/network.scss:
--------------------------------------------------------------------------------
1 |
2 | // .ap-list>.button {
3 | .button {
4 | float: right;
5 | font-size: 18px;
6 | border: #ccc solid 2px;
7 | border-radius: .5rem;
8 | padding: 5px 10px;
9 | position: relative;
10 | background-color: $torbox-green;
11 | color: #fff;
12 | margin: 10px 10px;
13 | }
14 |
15 | .button>.popup-button {
16 | position: absolute;
17 | width: 100%;
18 | height: 100%;
19 | cursor: pointer;
20 | opacity: 0;
21 | margin: 0;
22 | padding: 0;
23 | top: 0;
24 | right: 0;
25 | }
26 |
27 | .button>.popout-button {
28 | display: none;
29 | }
30 |
31 | .popup-button:checked ~ .popout-button {
32 | display: block;
33 | width: 100%;
34 | height: 100%;
35 | right: 0;
36 | top: 0;
37 | z-index: 999998;
38 | position: fixed;
39 | opacity: 0;
40 | margin: 0;
41 | padding: 0;
42 | }
43 |
44 | .popup-button:checked ~ .shadow {
45 | opacity: .3;
46 | display: block;
47 | }
48 |
49 | .popup-button:checked ~ .editable-box {
50 | display: block;
51 | height: 500px;
52 | width: 700px;
53 | border-radius: .25rem;
54 | border: 2px solid rgba(0, 0, 0, 0.125);
55 | //border: 2px solid #ccc;
56 | }
57 |
58 | .bridge-input {
59 | min-height: 180px;
60 | }
--------------------------------------------------------------------------------
/src/sass/notify.scss:
--------------------------------------------------------------------------------
1 | .notify-bar {
2 | position: relative;
3 | width: 60%;
4 | text-align: center;
5 | margin: 110px auto 0px auto;
6 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
7 | }
8 | .notify-message {
9 | width: 100%;
10 | color: white;
11 | padding: 15px 0 15px 0;
12 | font-size: 1.3rem;
13 | background-color: #E74C3C;
14 | cursor: pointer;
15 | margin-bottom: -98px;
16 | }
17 |
18 | .ignore-notify {
19 | opacity: 0;
20 | position: absolute;
21 | cursor: pointer;
22 | top: 0;
23 | right: 0;
24 | width: 100%;
25 | height: 100%;
26 | padding: 0;
27 | margin: 0;
28 | }
29 |
30 | .ignore-notify:checked ~ .notify-message{
31 | display: none;
32 | }
33 |
34 | @media (max-width: 800px) {
35 | .notify-bar {
36 | width: 95%;
37 | }
38 | .notify-message {
39 | font-size: 1.1rem;
40 | }
41 | }
--------------------------------------------------------------------------------
/src/sass/sub-menu.scss:
--------------------------------------------------------------------------------
1 |
2 | .sub-menu {
3 | width: 100%;
4 | visibility: visible;
5 | background-color: #4a4a4a;
6 | padding: 0px 0px;
7 | margin: 0;
8 | overflow: auto;
9 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.6);
10 | }
11 |
12 | .menu-table {
13 | width: auto;
14 | height: auto;
15 | white-space: nowrap;
16 | list-style-type: none;
17 | padding: 0;
18 | margin: 0;
19 | }
20 |
21 | .menu-table > .current {
22 | border-bottom: solid 5px $torbox-green;
23 | }
24 |
25 | .menu-item {
26 | display: inline-block;
27 | margin: 0 20px;
28 | text-align: center;
29 | letter-spacing: .05em;
30 | background-color: #4a4a4a;
31 | height: 100%;
32 | padding: 10px 10px;
33 | }
34 |
35 | .current > .menu-link {
36 | font-weight: bold;
37 | }
38 |
39 | .menu-link {
40 | color: #fff;
41 | font-size: 16px;
42 | }
--------------------------------------------------------------------------------
/src/sass/table.scss:
--------------------------------------------------------------------------------
1 | *, ::after, ::before {
2 | box-sizing: border-box;
3 | }
4 | section .table {
5 | display: table;
6 | font-size: 12px;
7 | width: 100%;
8 | overflow: hidden;
9 | position: relative;
10 | table-layout: fixed;
11 | }
12 |
13 | .table {
14 | max-width: 100%;
15 | margin-bottom: 1rem;
16 | background-color: transparent;
17 | }
18 |
19 | .thead {
20 | // background-color: #526474;
21 | background-color: #7fbb23;
22 | color: #fff;
23 | letter-spacing: .05rem;
24 | }
25 |
26 | .table-row {
27 | display: block;
28 | width: 100%;
29 | font-size: 0;
30 | border-top: 1px solid #dde2e6;
31 | }
32 |
33 | .table-row .table-header {
34 | padding: .3rem;
35 | border-bottom: 1px solid #2d3f4e;
36 | font-weight: 700;
37 | line-height: 15px;
38 | }
39 | .table-row .table-header, .table-row .table-item {
40 | display: inline-block;
41 | font-size: 12px;
42 | vertical-align: top;
43 | word-break: break-word;
44 | min-height: 18px;
45 | box-sizing: border-box;
46 | }
47 |
48 | .tbody {
49 | display: table-row-group;
50 | width: 100%;
51 | }
52 |
53 | .table-row .table-item {
54 | padding: .3rem .35rem;
55 | }
56 |
57 | .table-row .buttons {
58 | width: 150px;
59 | float: right;
60 | }
61 |
62 | .table-header.signal {
63 | width: 100px;
64 | }
65 |
66 | .table-header.essid {
67 | width: 150px;
68 | }
69 |
70 | .table-header.channel {
71 | width: 100px;
72 | }
73 |
74 | .table-header.bssid {
75 | width: 150px;
76 | }
77 |
78 | .table-header.security {
79 | width: 150px;
80 | }
81 |
82 | .table-item.signal {
83 | width: 100px;
84 | }
85 |
86 | .table-item.essid {
87 | width: 150px;
88 | }
89 |
90 | .table-item.channel {
91 | width: 100px;
92 | }
93 |
94 | .table-item.bssid {
95 | width: 150px;
96 | }
97 |
98 | .table-item.security {
99 | width: 150px;
100 | }
--------------------------------------------------------------------------------
/src/sass/warn.scss:
--------------------------------------------------------------------------------
1 | .warn-panel {
2 | text-align: center;
3 | color: $silver;
4 | }
5 |
6 | .warn-icon {
7 | font-size: 200px;
8 | }
9 |
10 | .warn-subject {
11 | font-size: 36px;
12 | letter-spacing: 1px;
13 | }
14 |
15 | .warn-description {
16 | margin-top: 20px;
17 | }
--------------------------------------------------------------------------------
/src/sass/wireless.scss:
--------------------------------------------------------------------------------
1 | .btn-reload {
2 | // color: #fc0;
3 | color: #000;
4 | font-size: 18px;
5 | width: auto;
6 | padding: 0 10px;
7 | line-height: 42px;
8 | margin: 10px 0;
9 | margin-right: 10px;
10 | text-align: center;
11 | border-radius: .4rem;
12 | vertical-align: middle;
13 | background-color: $yellow-btn-bg;
14 | }
15 |
16 | .btn-enable {
17 | // color: #339900;
18 | color: #fff;
19 | font-size: 18px;
20 | width: auto;
21 | padding: 0 10px;
22 | line-height: 42px;
23 | margin: 10px 0;
24 | text-align: center;
25 | border-radius: .4rem;
26 | vertical-align: middle;
27 | border-radius: .4rem;
28 | background-color: $green-btn-bg;
29 | }
30 |
31 | .btn-disable{
32 | // background-color: #cc3300;
33 | // color: #cc3300;
34 | color: #fff;
35 | font-size: 18px;
36 | width: auto;
37 | padding: 0 10px;
38 | line-height: 42px;
39 | margin: 10px 0;
40 | text-align: center;
41 | border-radius: .4rem;
42 | vertical-align: middle;
43 | border-radius: .4rem;
44 | background-color: $red-btn-bg;
45 | }
--------------------------------------------------------------------------------
/src/settings.nim:
--------------------------------------------------------------------------------
1 | import config
2 | # import lib / [ torbox ]
3 | # import lib / tor / tor
4 |
5 | export config
6 |
7 | const configPath {.strdefine.} = "./torci.conf"
8 | let (cfg*, _) = getConfig(configpath)
9 | # var sysInfo* = getSystemInfo()
10 | # let torboxVer = getTorboxVersion()
11 | # sysInfo.torboxVer = torboxVer
--------------------------------------------------------------------------------
/src/toml.nim:
--------------------------------------------------------------------------------
1 | import std / [ nativesockets, sugar, strutils ]
2 | import toml_serialization
3 |
4 | export toml_serialization
5 |
6 | type
7 | TorCi* = object
8 | version*: string
9 | staticDir*: string
10 | address*: string
11 | port*: Port
12 | # port*: int
13 | # port*: string
14 |
15 | # proc readValue(r: var TomlReader, v: var TorCi) =
16 | # r.parseTable(k):
17 |
18 | proc readValue*(r: var TomlReader, p: var Port) =
19 | p = r.parseInt(int)
20 | .Port
21 |
22 |
23 | proc load*(_: typedesc[TorCi], filename: string = "torci.toml"): TorCi =
24 | # func load[T](con, key: string, default: T): T =
25 | # # let n = (n: string) => when T is int: parseInt(n).Port
26 | # # elif T is string: n
27 | # let t = Toml.decode(con, string, key)
28 | # when T is Port: parseInt(t).Port
29 | # elif T is int: parseInt(t)
30 | # elif T is string: t
31 | # result = new TorCi
32 | # result.version = Toml
33 | # .loadFile(filename, TorCi, "TorCI.version")
34 | const n = (n: string) => Toml.decode(n, TorCi, "TorCI")
35 | slurp(filename)
36 | .n()
37 | # TorCi(
38 | # version: t.load("TorCI.version", "0.0.0"),
39 | # staticDir: t.load("TorCI.staticDir", "./public"),
40 | # address: t.load("TorCI.address", "0.0.0.0"),
41 | # port: t.load("TorCI.port", 1984)
42 | # )
43 | # .Toml.decode(TorCi, "TorCI")
44 | # Toml.loadFile(filename, TorCi, "TorCI")
45 |
46 | # result.port = Toml
47 | # .loadFile(filename, int, "TorCI.port")
48 | # .Port
--------------------------------------------------------------------------------
/src/torci.nim:
--------------------------------------------------------------------------------
1 | import std / [ strutils, options, asyncdispatch ]
2 | import jester, karax / [ karaxdsl, vdom]
3 | import results, resultsutils
4 |
5 | import views / [ login ]
6 | import routes / [ status, network, sys, tabs ]
7 | import ./ renderutils, types, config, query, utils, notice
8 | import settings as torciSettings
9 | import lib / [ tor, session, torbox, hostap, fallbacks, wifiScanner, wirelessManager ]
10 | import lib / sys as libsys
11 |
12 | {.passL: "-flto", passC: "-flto", optimization: size.}
13 | # {.passC: "/usr/include/x86_64-linux-musl".}
14 | # {.passL: "-I/usr/include/x86_64-linux-musl".}
15 |
16 | routingStatus()
17 | # routerWireless()
18 | routingNet()
19 | routingSys()
20 |
21 | settings:
22 | port = cfg.port
23 | staticDir = cfg.staticDir
24 | bindAddr = cfg.address
25 |
26 | routes:
27 | get "/":
28 | loggedIn:
29 | redirect "/io"
30 |
31 | get "/login":
32 | notLoggedIn:
33 | resp renderFlat(renderLogin(), "Login")
34 |
35 | post "/login":
36 | template respLogin() =
37 | resp renderFlat(renderLogin(), "Login", notifies = nc)
38 |
39 | notLoggedIn:
40 | let
41 | username = request.formData.getOrDefault("username").body
42 | password = request.formData.getOrDefault("password").body
43 | var nc = Notifies.default()
44 |
45 | match await login(username, password):
46 | Ok(res):
47 | setCookie("torci", res.token, expires = res.expire, httpOnly = true)
48 | redirect "/"
49 | Err(msg):
50 | nc.add(failure, msg)
51 | respLogin()
52 |
53 | # respLogin()
54 |
55 | post "/logout":
56 | loggedIn:
57 | let signout = request.formData.getOrDefault("signout").body
58 | if signout == "1":
59 | if await logout(request):
60 | redirect "/login"
61 |
62 | redirect "/"
63 |
64 | get "/net":
65 | redirect "/net/bridges"
66 |
67 | error Http404:
68 | resp renderFlat(renderError("404 Not Found"), "404 Not Found")
69 |
70 | error Exception:
71 | resp renderFlat(renderError("Something went wrong"), "Error")
72 |
73 | extend status, ""
74 | extend network, "/net"
75 | # extend wireless, "/net"
76 | extend sys, ""
--------------------------------------------------------------------------------
/src/types.nim:
--------------------------------------------------------------------------------
1 | import std / [ uri ]
2 | import std / [ nativesockets ]
3 | import lib / hostap
4 | import lib / sys / [ iface ]
5 |
6 | type
7 | ActivateObfs4Kind* {.pure.} = enum
8 | all, online, select
9 |
10 | BridgeKind* = enum
11 | obfs4 = "obfs4",
12 | meekazure = "meek_lite",
13 | snowflake = "snowflake"
14 |
15 | Obfs4* = object
16 | ipaddr*: string
17 | port*: Port
18 | fingerprint*, cert*, iatMode*: string
19 |
20 | Meekazure* = object
21 | ipaddr*: string
22 | port*: Port
23 | fingerprint*: string
24 | meekazureUrl*: Uri
25 | front*: Uri
26 |
27 | Snowflake* = object
28 | ipaddr*: string
29 | port*: Port
30 | fingerprint*: string
31 |
32 | Wifi* = object of RootObj
33 | bssid*: string
34 | channel*: string
35 | dbmSignal*: string
36 | quality*: string
37 | security*: string
38 | essid*: string
39 | isEss*: bool
40 | isHidden*: bool
41 |
42 | WifiList* = seq[Wifi]
43 |
44 | Network* = ref object of Wifi
45 | # wifiList: WifiList
46 | wlan*: IfaceKind
47 | networkId*: int
48 | password*: string
49 | hasNetworkId*: bool
50 | connected*: bool
51 | scanned*: bool
52 | logFile*: string
53 | configFile*: string
54 |
55 | BridgeStatuses* = object
56 | useBridges*: bool
57 | obfs4*: bool
58 | meekAzure*: bool
59 | snowflake*: bool
--------------------------------------------------------------------------------
/src/utils.nim:
--------------------------------------------------------------------------------
1 | template test*(nim: untyped) =
2 | when defined test:
3 | nim
4 |
5 | template test*(nim: untyped) =
6 | when defined test:
7 | nim
8 | else:
9 | quit(QuitFailure)
--------------------------------------------------------------------------------
/src/views/login.nim:
--------------------------------------------------------------------------------
1 | import karax / [ karaxdsl, vdom ]
2 |
3 | proc renderLogin*(): VNode =
4 | buildHtml(tdiv(class="content")):
5 | tdiv(class="login-pane"):
6 | tdiv(class="login-header"):
7 | img(class="logo", src="/images/torbox.png", alt="TorBox")
8 | tdiv(class="form-box"):
9 | tdiv(class="login-text"): text "Log Into TorBox"
10 | form(`method`="post", action="/login", enctype="multipart/form-data", class=""):
11 | tdiv(class="form-section username"):
12 | label(class=""):text "Username"
13 | input(`type`="text", `required`="", name="username", placeholder="torbox", class="inp")
14 | tdiv(class="form-section username"):
15 | label(class=""):text "Password"
16 | input(`type`="password", `required`="", name="password", class="inp")
17 | tdiv(class="login-btn"):
18 | button(`type`="submit", name="loginBtn", class=""):text "Login"
--------------------------------------------------------------------------------
/src/views/network.nim:
--------------------------------------------------------------------------------
1 | import karax / [karaxdsl, vdom, vstyles]
2 | import strformat
3 | import ../ types
4 | import ../ lib / [ sys ]
5 | import ../ lib / sys / [ iface ]
6 |
7 | proc renderInterfaces*(): VNode =
8 | buildHtml(tdiv(class="card")):
9 | tdiv(class="card-header"):
10 | text "Interfaces"
11 | tdiv(class="table table-striped"):
12 | tdiv(class="table-row thead"):
13 | tdiv(class="table-header name"): text "Name"
14 | tdiv(class="table-header status"): text "Status"
15 | tdiv(class="tbody"):
16 | tdiv(class="table-row", style={display: "table-row"}):
17 | tdiv(class="table-item"): text "eth0"
18 | tdiv(class="buttons"):
19 | a(href="/net/interfaces/connect/eth0"):
20 | button(): text "Connect"
21 | tdiv(class="table-row", style={display: "table-row"}):
22 | tdiv(class="table-item"): text "eth1"
23 | tdiv(class="buttons"):
24 | a(href="/net/interfaces/connect/eth1"):
25 | button(): text "Connect"
26 | tdiv(class="table-row", style={display: "table-row"}):
27 | tdiv(class="table-item"): text "wlan0"
28 | tdiv(class="buttons"):
29 | a(href="/net/interfaces/join/?iface=wlan0"):
30 | button(): text "Open Access"
31 | a(href="/net/interfaces/join/?iface=wlan0&captive=1"):
32 | button(): text "Captive Access"
33 | tdiv(class="table-row", style={display: "table-row"}):
34 | tdiv(class="table-item"): text "wlan1"
35 | tdiv(class="buttons"):
36 | a(href="/net/interfaces/set/?iface=wlan1"):
37 | button(): text "Open Access"
38 | a(href="/net/interfaces/join/?iface=wlan1&captive=1"):
39 | button(): text "Captive Access"
40 | tdiv(class="table-row", style={display: "table-row"}):
41 | tdiv(class="table-item"): text "ppp0 or usb0"
42 | tdiv(class="buttons"):
43 | a(href="/net/interfaces/connect/usb0"):
44 | button(): text "Connect"
45 | tdiv(class="table-row", style={display: "table-row"}):
46 | tdiv(class="table-item"): text "tun0"
47 | tdiv(class="buttons"):
48 | a(href="/net/interfaces/join/tun0"):
49 | button(): text "Scan"
50 |
51 | proc renderWifiConfig*(wlan: IfaceKind, withCaptive: bool; wifiInfo: WifiList; currentNetwork: tuple[ssid, ipAddr: string]): VNode =
52 | buildHtml(tdiv(class="card")):
53 | tdiv(class="card-header"):
54 | text "Nearby APs"
55 | tdiv(class="ap-list"):
56 | tdiv(class="table table-striped"):
57 | tdiv(class="table-row thead"):
58 | tdiv(class="table-header signal"): text "Signal"
59 | tdiv(class="table-header essid"): text "ESSID"
60 | tdiv(class="table-header channel"): text "Channel"
61 | tdiv(class="table-header bssid"): text "BSSID"
62 | tdiv(class="table-header security"): text "Security"
63 | tdiv(class="tbody"):
64 | for i, v in wifiInfo:
65 | # tdiv(class="ap-table"):
66 | tdiv(class="table-row", style={display: "table-row"}):
67 | tdiv(class="table-item signal"): text v.quality
68 | tdiv(class="table-item essid"): text v.essid
69 | tdiv(class="table-item channel"): text v.channel
70 | tdiv(class="table-item bssid"): text v.bssid
71 | tdiv(class="table-item security"): text v.security
72 | # button(`type`="submit", name="ap", value=v.essid): text "Join"
73 | tdiv(class="button"):
74 | label(): text "Join"
75 | input(class="popup-button", `type`="radio", name="select-network", value="open")
76 | input(class="popout-button", `type`="radio", name="select-network", value="close")
77 | tdiv(class="shadow")
78 | tdiv(class="editable-box"):
79 | form(`method`="post", action="/net/interfaces/join/" & $wlan, enctype="multipart/form-data"):
80 | # tdiv(class="card-table", style=style {visibility: "hidden"}):
81 | # label(class="card-title"): text "Interface"
82 | # select(name="wlan"):
83 | # option(value=wlan): text wlan
84 |
85 | tdiv(class="card-table bssid"):
86 | input(`type`="hidden", name="bssid", value=v.bssid)
87 |
88 | tdiv(class="card-table essid"):
89 | label(class="card-title"): text "SSID"
90 | if v.isHidden:
91 | input(`type`="text", name="essid", placeholder="ESSID of a Hidden Access Point")
92 | input(`type`="hidden", name="cloak", value="1")
93 | else:
94 | tdiv(): text v.essid
95 | input(`type`="hidden", name="essid", value=v.essid)
96 | input(`type`="hidden", name="cloak", value="0")
97 |
98 | tdiv(class="card-table"):
99 | label(class="card-title"): text "Password"
100 | if v.isEss:
101 | tdiv(): text "ESS does not require a password"
102 | input(`type`="hidden", name="password", value="")
103 | input(`type`="hidden", name="ess", value="1")
104 | else:
105 | input(`type`="password", name="password")
106 | input(`type`="hidden", name="ess", value="0")
107 |
108 | tdiv(class="card-table", style={display: "none"}):
109 | label(class="card-title"): text "Connect to with a Captive portal or not"
110 | if withCaptive:
111 | input(`type`="checkbox", name="captive", value="1", checked="")
112 | else:
113 | input(`type`="checkbox", name="captive", value="0")
114 |
115 | button(`type`="submit", class="btn-join"): text "Join Network"
116 | if currentNetwork.ssid != "":
117 | tdiv(class="current-network"):
118 | span(): text "Connected:"
119 | tdiv(class="cr-net-ssid"): text currentNetwork.ssid
120 | tdiv(class="cr-net-ipaddr"): text &"[{currentNetwork.ipAddr}]"
121 | # tdiv(class="button"):
122 | # label(): text "Select Network"
123 | # input(class="popup-button", `type`="radio", name="select-network", value="open")
124 | # input(class="popout-button", `type`="radio", name="select-network", value="close")
125 | # tdiv(class="shadow")
126 | # tdiv(class="editable-box"):
127 | # form(`method`="post", action="/net/interfaces/join/" & wlan, enctype="multipart/form-data"):
128 | # # tdiv(class="card-table", style=style {visibility: "hidden"}):
129 | # # label(class="card-title"): text "Interface"
130 | # # select(name="wlan"):
131 | # # option(value=wlan): text wlan
132 | # tdiv(class="card-table essid"):
133 | # label(class="card-title"): text "SSID"
134 | # select(name="essid"):
135 | # for v in wifiInfo:
136 | # option(value=v.essid): text v.essid
137 | # tdiv(class="card-table"):
138 | # label(class="card-title"): text "Password"
139 | # input(`type`="password", name="wifi-password")
140 | # button(`type`="submit", class="btn-join"): text "Join Network"
--------------------------------------------------------------------------------
/tests/local/serviceStatus.nim:
--------------------------------------------------------------------------------
1 | import osproc, strutils
2 |
3 | discard """
4 |
5 | output: '''
6 | active
7 | inactive
8 | activating
9 | '''
10 |
11 | """
12 |
13 | proc isActive(name: string): string =
14 | const cmd = "sudo systemctl is-active "
15 | var ret = execCmdEx(cmd & name).output
16 | ret = splitLines(ret)[0]
17 | return ret
18 |
19 | var ret: string
20 | ret = isActive "wpa_supplicant"
21 | echo "result: ", "\"", ret, "\""
22 | ret = isActive "nim"
23 | echo "result: ", "\"", ret, "\""
--------------------------------------------------------------------------------
/tests/local/sys.nim:
--------------------------------------------------------------------------------
1 | import ../src/libs/syslib
2 | import ../src/types
3 |
4 | const
5 | torci: string = "torci"
6 | torbox: string = "torbox"
7 |
8 | # let
9 | # cs = psExists(wlan0)
10 | # csS = if cs: "working" else: "not found"
11 | # echo torci, " is ", csS
12 | # let
13 | # bs = psExists(torbox)
14 | # bsS = if bs: "working" else: "not found"
15 | # echo torbox, " is ", bsS
16 |
17 | var s = dhclientWork(wlan0)
18 | echo if s: "found " else: "not found ", "dhclient ", $wlan0
19 | s = dhclientWork(wlan1)
20 | echo if s: "found " else: "not found ", "dhclient ", $wlan1
21 | s = dhclientWork(eth0)
22 | echo if s: "found " else: "not found ", "dhclient ", $eth0
23 | s = dhclientWork(eth1)
24 | echo if s: "found " else: "not found ", "dhclient ", $eth1
25 |
26 | for v in IfaceKind:
27 | let r = ifaceExists(v)
28 | echo $v, " exists: ", $r
29 | if r:
30 | let s = isStateup(v)
31 | echo " ", $v, " stateup: ", $s
32 | let ip = hasStaticIp(v)
33 | echo " ", $v, " has ip address: ", $ip
34 | let rt = isRouter(v)
35 | echo " ", $v, " is ", if rt: "a Router" else: "not a Router"
36 |
--------------------------------------------------------------------------------
/tests/sandbox/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bullseye-slim as bullseye
2 | ENV DEBIAN_FRONTEND=noninteractive
3 | # ENV PATH="$PATH:$HOME/.nimble/nim/bin:$HOME/.nimble/bin"
4 | # ENV PATH=$PATH:$HOME/.nimble/nim/bin:$HOME/.nimble/bin
5 | # ENV PATH=$PATH:$HOME/.nimble/nim/bin:$HOME/.nimble/bin
6 | RUN dpkg --add-architecture armhf && \
7 | apt update && \
8 | apt install -y build-essential openssl git curl \
9 | binutils-arm-linux-gnueabi \
10 | gcc-arm-linux-gnueabihf \
11 | hostapd wpasupplicant && \
12 | apt install -y libcrypt-dev:armhf
13 |
14 | # Create user
15 | RUN adduser --disabled-password --gecos "" tor-chan && echo "tor-chan:tor-chan" | chpasswd
16 |
17 | # install nim-lang binaries
18 | RUN curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y
19 | # RUN echo "export PATH=$PATH:$HOME/.nimble/nim/bin:$HOME/.nimble/bin" >> /root/.bashrc
20 |
21 | # install dependencies for TorCI
22 | WORKDIR /src/torci
23 |
24 | CMD \
25 | export PATH="${PATH}":$HOME/.nimble/nim/bin:$HOME/.nimble/bin && \
26 | nimble -y install && \
27 | nimble sandbox
28 | # nim r tests/sandbox/tests/test_sys.nim
29 | # CMD ["nimble", "test"]
--------------------------------------------------------------------------------
/tests/sandbox/docker.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | os, osproc, terminal,
3 | strutils
4 | ]
5 |
6 | const
7 | atme = "tests" / "sandbox"
8 | # dockerFile = "Dockerfile"
9 |
10 | # proc imageExists(label: string): bool =
11 | # const prefix = "sudo docker images "
12 |
13 | # let res = execCmdEx(prefix & label)
14 |
15 | # for line in splitLines(res.output):
16 | # proc(x: string): bool: startsWith
17 |
18 | func build(imageLabel: string = ""): int =
19 | const prefix = "sudo docker build"
20 |
21 | let
22 | label = if imageLabel.len == 0: " -t torci:test"
23 | else: " -t " & imageLabel
24 |
25 | path = "-f "
26 | cm = prefix & label & path
27 |
28 | # sudo docker build -t {{ a label }} -f tests/docker/Dockerfile
29 | result = execCmd(cm & atme)
30 |
31 | proc run(imageLabel: string = ""): Process =
32 | const prefix = "sudo docker run --rm -v `pwd`:/src/torci"
33 |
34 | let
35 | label = if imageLabel.len == 0: " torci:test"
36 | else: imageLabel
37 |
38 | cm = prefix & label
39 |
40 | result = startProcess(cm)
41 |
42 | when isMainModule:
43 | let code = build()
44 |
45 | if code != 0:
46 | styledEcho fgRed, "[Docker build] ", fgWhite, "failure."
47 | quit()
48 |
49 | styledEcho fgGreen, "[Docker build] ", fgWhite, "build successfully."
50 |
51 | let process = run()
52 |
53 | styledEcho fgGreen, "[Ok] ", fgWhite, "tests successfully in Docker container."
54 |
55 | kill process
--------------------------------------------------------------------------------
/tests/sandbox/tests/test_login.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest,
3 | asyncdispatch, nativesockets,
4 | times, terminal
5 | ]
6 | import redis
7 | import results, resultsutils
8 | import ../ ../ ../ src / lib / session {.all.}
9 |
10 | suite "Login...":
11 | test "logged in...":
12 | # privateAccess()
13 | proc isLoggedIn(key: string): Future[bool] {.async.} =
14 | let red = await openAsync(port=7000.Port)
15 | return await red.exists(key)
16 |
17 | proc makeUser(token: string, name: string) {.async.} =
18 | let red = await openAsync(port=7000.Port)
19 | discard await red.setEx(token, 10, name)
20 |
21 | proc getUsername(token: string): Future[string] {.async.} =
22 | let red = await openAsync(port=7000.Port)
23 | return await red.get(token)
24 |
25 | let ses = makeSessionKey()
26 | waitFor makeUser(ses, "tor-chan")
27 | check: waitFor isLoggedIn(ses); "tor-chan" == waitFor getUsername(ses)
28 |
29 | test "try login":
30 | match waitFor login("tor-chan", "tor-chan"):
31 | Ok(login):
32 | styledEcho fgGreen, " [Username] ", fgWhite, login.token
33 | styledEcho fgGreen, " [Expire at] ", fgWhite, $login.expire
34 | Err(msg):
35 | styledEcho fgRed, " [Error] ", fgWhite, msg
--------------------------------------------------------------------------------
/tests/sandbox/tests/test_redis.nim:
--------------------------------------------------------------------------------
1 | import std / [ times, unittest, asyncdispatch, nativesockets ]
2 | import redis
3 |
4 | converter toInt(x: int64): int = cast[int](x)
5 |
6 | proc main() {.async.} =
7 | let client = await openAsync(port=7000.Port)
8 |
9 | let token = "randomness0"
10 | let strct = @[
11 | ("username", "Tor-chan")
12 | ]
13 | echo "is nil? ", await client.hGet(token, "username")
14 |
15 | let res = await client.setEx(token, 3, "Tor-chan")
16 | echo "res: ", res
17 | echo "username before expire: ", await client.get(token)
18 | echo "sleeping..."
19 |
20 | await sleepAsync(300)
21 | echo "username after expire: ", await client.get(token)
22 |
23 | # let res = await client.flushPipeline()
24 | # echo res
25 | # echo get
26 |
27 | proc normal() {.async.} =
28 | let red = await openAsync(port=7000.Port)
29 |
30 | block:
31 | let res = await red.ping()
32 | echo res
33 |
34 | block:
35 | let res = await red.ping()
36 | echo res
37 |
38 | block:
39 | let res = await red.ping()
40 | echo res
41 |
42 | block:
43 | let res = await red.ping()
44 | echo res
45 |
46 | proc pipe() {.async.} =
47 | let red = await openAsync(port=7000.Port)
48 | red.startPipelining()
49 |
50 | block:
51 | let res = await red.ping()
52 | echo res
53 |
54 | block:
55 | let res = await red.ping()
56 | echo res
57 |
58 | block:
59 | let res = await red.ping()
60 | echo res
61 |
62 | block:
63 | let res = await red.ping()
64 | echo res
65 |
66 | let flushed = await red.flushPipeline()
67 | echo flushed
68 |
69 | suite "Redis":
70 | waitFor main()
71 | test "normal":
72 | waitFor normal()
73 | test "pipe":
74 | waitFor pipe()
--------------------------------------------------------------------------------
/tests/sandbox/tests/test_sys.nim:
--------------------------------------------------------------------------------
1 | import std / [ unittest, asyncdispatch, terminal ]
2 | import results, resultsutils
3 | import ../ ../ ../ src / lib / sys
4 |
5 | suite "system in Docker container":
6 | test "getting system info...":
7 | match waitFor getSystemInfo():
8 | Ok(info):
9 | check:
10 | info.architecture.len > 0
11 |
12 | Err(msg):
13 | styledEcho fgRed, "[Err] ", fgWhite, msg
--------------------------------------------------------------------------------
/tests/server.nim:
--------------------------------------------------------------------------------
1 | import server / [ server, client ]
2 |
3 | export server, client
--------------------------------------------------------------------------------
/tests/server/client.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | macros, terminal, unittest,
3 | httpclient, httpcore ,nativesockets, asyncdispatch,
4 | strformat, strutils, tables, json,
5 | os, osproc
6 | ]
7 | import jester #validateip
8 | import utils
9 |
10 | export httpClient, httpcore, utils
11 |
12 | type
13 | Routes* = ref object
14 | # : OrderedTableRef[HttpMethod, seq[string]]
15 | entries: seq[RouteEntry]
16 |
17 | RouteEntry* = ref object
18 | path: string
19 | expect: JsonNode
20 | case kind: HttpMethod
21 | of HttpPost:
22 | data: MultipartData
23 | else: discard
24 |
25 | proc start*(address: string, port: Port, routes: Routes) {.async.} =
26 | for entry in routes.entries:
27 | for i in 0..20:
28 | var
29 | client: AsyncHttpClient
30 | res: Future[AsyncResponse]
31 |
32 | let address = if entry.path.startsWith('/'): fmt"http://{address}:{$port}{entry.path}"
33 | else: fmt"http://{address}:{$port}/{entry.path}"
34 |
35 | case entry.kind
36 | of HttpGet:
37 | client = newAsyncHttpClient()
38 | res = client.get(address)
39 | styledEcho fgBlue, "[GET] ", fgWhite, address
40 |
41 | of HttpPost:
42 | client = newAsyncHttpClient()
43 | client.headers = newHttpHeaders({"Content-Type": "multipart/form-data; boundary=boundary"})
44 | res = client.post(address, multipart = entry.data)
45 | styledEcho fgBlue, "[POST] ", fgWhite, address
46 | # styledEcho fgBlue, "[POST] ", fgWhite, $entry.data
47 |
48 | else: return
49 |
50 | yield res or sleepAsync(4000)
51 |
52 | if not res.finished:
53 | styledEcho(fgYellow, "Timed out")
54 | continue
55 |
56 | elif not res.failed:
57 | let res = await res
58 |
59 | if res.code.is2xx:
60 | styledEcho fgBlue, "[Status] ", fgWhite, res.status
61 |
62 | let
63 | body = await res.body
64 | headers = res.headers
65 |
66 | if headers["Content-Type"] == "text/html;charset=utf-8":
67 | styledEcho fgGreen, "[Content-Type] ", fgWhite, headers["Content-Type"]
68 |
69 | else:
70 | styledEcho fgGreen, "[Content-Type] ", fgWhite, headers["Content-Type"]
71 | styledEcho fgGreen, "[Response body] ", fgWhite, body
72 |
73 | elif res.code.is4xx:
74 | styledEcho fgBlue, "[Status] ", fgRed, res.status
75 |
76 | echo ""
77 | break
78 |
79 | else: echo res.error.msg
80 | client.close()
81 |
82 | proc createPostBody*(node: NimNode): NimNode =
83 | expectKind(node, { nnkStmtList, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
84 | expectKind(node[0], { nnkTableConstr, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
85 | result = newStmtList()
86 | let
87 | tmp = genSym(nskVar)
88 | init = newCall(bindSym"newMultipartData")
89 |
90 | result.add newVarStmt(tmp, init)
91 | result.add newCall(bindSym"add", tmp, nnkTableConstr.newTree(
92 | nnkExprColonExpr.newTree(
93 | newStrLitNode("Content-Disposition"),
94 | newStrLitNode("form-data")
95 | )
96 | ))
97 |
98 | for child in node[0]:
99 | expectKind(child, { nnkExprColonExpr, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
100 | expectKind(child[0], { nnkStrLit, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
101 | expectKind(child[1], { nnkStrLit, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
102 |
103 | let
104 | tup = nnkTupleConstr.newTree(child[0], child[1])
105 | entries = nnkPrefix.newTree(ident"@", nnkBracket.newTree(tup))
106 | result.add newCall(bindSym"add", tmp, entries)
107 |
108 | result.add tmp
109 |
110 | macro routerTest*(routerName: string, node: untyped): untyped =
111 | expectKind(node, { nnkStmtList, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
112 | result = newStmtList()
113 |
114 | let
115 | routes = genSym(nskLet)
116 | init = newCall(bindSym"new", ident("Routes"))
117 |
118 | result.add newTree(nnkStmtList, newLetStmt(routes, init))
119 |
120 | for child in node:
121 | expectKind(child, { nnkCall, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
122 | # expect httpMethod
123 | expectKind(child[0], { nnkIdent, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
124 |
125 | expectKind(child[1], nnkStmtList)
126 |
127 | let
128 | httpMethod = parseEnum[HttpMethod]($child[0])
129 | body = child[1]
130 |
131 | case httpMethod
132 | of HttpGet:
133 | for path in body:
134 | expectKind(path, { nnkStrLit, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
135 |
136 | result.add nnkCommand.newTree(
137 | bindSym("add"),
138 | nnkDotExpr.newTree(
139 | routes,
140 | ident"entries"
141 | ),
142 | nnkObjConstr.newTree(
143 | newIdentNode("RouteEntry"),
144 | nnkExprColonExpr.newTree(
145 | newIdentNode("kind"),
146 | ident("HttpGet")
147 | ),
148 | nnkExprColonExpr.newTree(
149 | newIdentNode("path"),
150 | path
151 | )
152 | )
153 | )
154 |
155 | of HttpPost:
156 | for pair in body:
157 | expectKind(pair, { nnkCall, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
158 | # a path
159 | expectKind(pair[0], { nnkStrLit, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice })
160 | let
161 | path = pair[0]
162 | data = createPostBody(pair[1])
163 |
164 | result.add newCall(
165 | bindSym("add"),
166 | nnkDotExpr.newTree(
167 | routes,
168 | ident"entries"
169 | ),
170 | nnkObjConstr.newTree(
171 | newIdentNode("RouteEntry"),
172 | nnkExprColonExpr.newTree(
173 | newIdentNode("kind"),
174 | newIdentNode("HttpPost")
175 | ),
176 | nnkExprColonExpr.newTree(
177 | newIdentNode("path"),
178 | path
179 | ),
180 | nnkExprColonExpr.newTree(
181 | ident("data"),
182 | data
183 | )
184 | )
185 | )
186 |
187 | else: error("Invalid HttpMethod")
188 |
189 | let
190 | tmpProcess = genSym(nskLet)
191 | startProcess = newLetStmt(tmpProcess, newCall(ident("start"), routerName))
192 | clientStart = newCall(
193 | bindSym"waitFor",
194 | newCall(
195 | bindSym"start",
196 | newLit("0.0.0.0"),
197 | newCall(
198 | ident"Port",
199 | newIntLitNode(1984)
200 | ),
201 | routes
202 | )
203 | )
204 |
205 | kill = newCall(ident"kill", tmpProcess)
206 |
207 | result.add startProcess
208 | result.add clientStart
209 | result.add kill
--------------------------------------------------------------------------------
/tests/server/routes/ap.nim:
--------------------------------------------------------------------------------
1 | import std / options
2 | import jester, results, resultsutils
3 | import karax / [ karaxdsl, vdom ]
4 | import ../ server
5 | import ".." / ".." / ".." / src / lib / [ hostap, sys ]
6 | import ".." / ".." / ".." / src / renderutils
7 | import ".." / ".." / ".." / src / routes / tabs
8 | import ".." / ".." / ".." / src / notice
9 |
10 | template tab(): Tab =
11 | buildTab:
12 | "Ap" = "/ap"
13 | "Def / Ap" = "/default" / "ap"
14 | "Conf" = "/conf"
15 | "Def / Conf" = "/default" / "conf"
16 |
17 | router ap:
18 | get "/hostap":
19 | var
20 | hostap: HostAp = HostAp.default()
21 | # iface = conf.iface
22 | devs = Devices.new()
23 | nc = Notifies.default()
24 |
25 | hostap = await getHostAp()
26 | let
27 | isModel3 = await rpiIsModel3()
28 | iface = hostap.conf.iface
29 |
30 | if iface.isSome:
31 | match await getDevices(iface.get):
32 | Ok(ret): devs = ret
33 | Err(msg): nc.add(failure, msg)
34 |
35 | resp: render "Wireless":
36 | tab: tab
37 | notice: nc
38 | container:
39 | hostap.render(isModel3)
40 | devs.render()
41 |
42 | get "/default/hostap":
43 | var
44 | hostap: HostAp = HostAp.default()
45 | # iface = conf.iface
46 | devs = Devices.new()
47 | nc = Notifies.default()
48 |
49 | resp: render "Wireless":
50 | tab: tab
51 | notice: nc
52 | container:
53 | # hostap.conf.render(false)
54 | # hostap.status.render()
55 | hostap.render(false)
56 | devs.render()
57 |
58 | get "/conf":
59 | let cf = await getHostApConf()
60 | resp $cf.render(false)
61 |
62 | get "/default/conf":
63 | let cf = HostApConf.new()
64 | resp $cf.render(false)
65 |
66 | get "/status":
67 | var sta = await getHostApStatus()
68 | resp $sta.render()
69 |
70 | get "/default/status":
71 | let sta = HostApStatus.new()
72 | resp $sta.render()
73 |
74 | get "/ap":
75 | var hostap = HostAp.default()
76 | hostap = await getHostAp()
77 |
78 | resp: render "Access Point":
79 | tab: tab
80 | container:
81 | hostap.conf.render(false)
82 | hostap.status.render()
83 |
84 | get "/default/ap":
85 | let hostap = HostAp.default()
86 | resp $hostap.conf
87 | .render(false)
88 |
89 | serve(ap, 1984.Port)
--------------------------------------------------------------------------------
/tests/server/routes/status.nim:
--------------------------------------------------------------------------------
1 | import std / options
2 | import jester
3 | import ../ server
4 | import results, resultsutils
5 | import karax / [ karaxdsl, vdom ]
6 | import ".." / ".." / ".." / src / notice
7 | import ".." / ".." / ".." / src / lib / tor
8 | import ".." / ".." / ".." / src / lib / sys
9 | import ".." / ".." / ".." / src / lib / wirelessManager
10 | import ".." / ".." / ".." / src / renderutils
11 | import ".." / ".." / ".." / src / routes / tabs
12 |
13 | router status:
14 | get "/status":
15 | var
16 | ti = TorInfo.default()
17 | si = SystemInfo.default()
18 | ii = IoInfo.new()
19 | ap = ConnectedAp.new()
20 | nc = Notifies.default()
21 |
22 | match await getTorInfo("127.0.0.1", 9050.Port):
23 | Ok(ret): ti = ret
24 | Err(msg): nc.add(failure, msg)
25 |
26 | match await getSystemInfo():
27 | Ok(ret): si = ret
28 | Err(msg): nc.add(failure, msg)
29 |
30 | match await getIoInfo():
31 | Ok(ret):
32 | ii = ret
33 | if isSome(ii.internet):
34 | let wlan = ii.internet.get
35 | match await getConnectedAp(wlan):
36 | Ok(ret): ap = ret
37 | Err(msg): nc.add(failure, msg)
38 | Err(msg): nc.add(failure, msg)
39 |
40 | resp: render "Status":
41 | notice: nc
42 | container:
43 | ti.render()
44 | ii.render(ap)
45 | si.render()
46 |
47 | get "/default/status":
48 | var
49 | ti = TorInfo.default()
50 | si = SystemInfo.default()
51 | ii = IoInfo.new()
52 | ap = ConnectedAp.new()
53 | nc = Notifies.default()
54 |
55 | resp: render "Status":
56 | notice: nc
57 | container:
58 | ti.render()
59 | ii.render(ap)
60 | si.render()
61 |
62 | get "/default/tor":
63 | # empty object
64 | var
65 | torInfo = TorInfo.default()
66 |
67 | resp $torInfo.render()
68 |
69 | get "/tor":
70 | var
71 | ti: TorInfo = TorInfo.default()
72 | nc: Notifies = Notifies.default()
73 |
74 | match await getTorInfo("127.0.0.1", 9050.Port):
75 | Ok(ret): ti = ret
76 | Err(msg): nc.add(failure, msg)
77 |
78 | resp: render "Tor":
79 | notice: nc
80 | container:
81 | ti.render()
82 |
83 | get "/iface":
84 | var
85 | ioInfo: IoInfo = IoInfo.new()
86 | connectedAp = ConnectedAp.new()
87 | nc = Notifies.default()
88 |
89 | match await getIoInfo():
90 | Ok(iface):
91 | ioInfo = iface
92 | if isSome(ioInfo.internet):
93 | let wlan = ioInfo.internet.get
94 |
95 | match await getConnectedAp(wlan):
96 | Ok(ap): connectedAp = ap
97 | Err(msg): nc.add failure, msg
98 | Err(msg): nc.add failure, msg
99 |
100 | resp: render "I/O":
101 | notice: nc
102 | container:
103 | ioInfo.render(connectedAp)
104 |
105 | get "/default/iface":
106 | let
107 | ioInfo: IoInfo = IoInfo.new()
108 | ap: ConnectedAp = ConnectedAp.new()
109 |
110 | resp $ioInfo.render(ap)
111 |
112 | get "/default/sys":
113 | let sysInfo = SystemInfo.default()
114 | resp $sysInfo.render()
115 |
116 | get "/sys":
117 | var
118 | sysInfo = SystemInfo.default()
119 | nc = Notifies.default()
120 |
121 | match await getSystemInfo():
122 | Ok(ret): sysInfo = ret
123 | Err(msg): nc.add(failure, msg)
124 |
125 | resp: render "System":
126 | notice: nc
127 | container:
128 | sysInfo.render()
129 |
130 | post "/io":
131 | let val = request.formData.getOrDefault("tor-request").body
132 | echo "hey"
133 | resp Http200, val
134 |
135 | serve(status, 1984.Port)
--------------------------------------------------------------------------------
/tests/server/routes/sys.nim:
--------------------------------------------------------------------------------
1 | import jester
2 | import karax / [ karaxdsl, vdom ]
3 | import ../server
4 | import ".." / ".." / ".." / src / [ renderutils, notice ]
5 | import ".." / ".." / ".." / src / lib / sys as libsys
6 | import ".." / ".." / ".." / src / routes / tabs
7 |
8 | template tab(): Tab =
9 | buildTab:
10 | "Passwd" = "/passwd"
11 |
12 | router sys:
13 | get "/passwd":
14 | resp: render "Passwd":
15 | tab: tab
16 | container:
17 | renderPasswdChange()
18 |
19 | get "/logs":
20 | resp: render "Logs":
21 | tab: tab
22 | container:
23 | renderLogs()
24 |
25 | serve(sys, 1984.Port)
--------------------------------------------------------------------------------
/tests/server/server.nim:
--------------------------------------------------------------------------------
1 | import jester
2 |
3 | proc serve*(match: proc, port: Port) =
4 | let settings = newSettings(port=port)
5 | var jester = initJester(match, settings=settings)
6 | jester.serve()
--------------------------------------------------------------------------------
/tests/server/utils.nim:
--------------------------------------------------------------------------------
1 | import std / [ os, osproc ]
2 |
3 | proc compile*(path: string): int =
4 | const prefix = "nim c "
5 | execCmd(prefix & path)
6 |
7 | template start*(name: string): Process =
8 | let path = "tests" / "server" / "routes" / name
9 |
10 | if not fileExists(path):
11 | let code = compile(path)
12 | if code != 0:
13 | raise newException(IOError, "Can't compile " & path)
14 |
15 | startProcess(expandFilename(path))
16 |
17 | when isMainModule:
18 | let pro = start("status")
--------------------------------------------------------------------------------
/tests/test_bridges.nim:
--------------------------------------------------------------------------------
1 | import std / unittest
2 | import std / [ strutils, re ]
3 | import std / [ sha1, json ]
4 | import std / [ httpclient, asyncdispatch ]
5 | import ../ src / lib / [ binascii ]
6 | import ../ src / lib / tor / bridges
7 | import ../ src / types
8 | import std / nativesockets
9 | import torrc_template
10 |
11 | suite "Bridges parse":
12 | const
13 | o = "obfs4 122.148.194.24:993 07784768F54CF66F9D588E19E8EE3B0FA702711B cert=m3jPGnUyZMWHT9Riioob95s1czvGs3HiZ64GIT3QbH/AZDVlF/YEXu/OtyYZ1eObKnTjcg iat-mode=0"
14 | m = "meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com"
15 | s = "snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72"
16 |
17 | let obfs4: Obfs4 = parseObfs4(o)
18 | let meekazure: Meekazure = parseMeekazure(m)
19 | let snowflake: Snowflake = parseSnowflake(s)
20 |
21 | test "obfs4 parse":
22 | check:
23 | obfs4.ipaddr == "122.148.194.24"
24 | obfs4.port == 993.Port
25 | obfs4.fingerprint == "07784768F54CF66F9D588E19E8EE3B0FA702711B"
26 | obfs4.cert == "m3jPGnUyZMWHT9Riioob95s1czvGs3HiZ64GIT3QbH/AZDVlF/YEXu/OtyYZ1eObKnTjcg"
27 | obfs4.iatMode == "0"
28 |
29 | test "meekazure parse":
30 | check:
31 | meekazure.ipaddr == "192.0.2.2"
32 | meekazure.port == 2.Port
33 | meekazure.fingerprint == "97700DFE9F483596DDA6264C4D7DF7641E1E39CE"
34 |
35 | test "snowflake parse":
36 | check:
37 | snowflake.ipaddr == "192.0.2.3"
38 | snowflake.port == 1.Port
39 | snowflake.fingerprint == "2B280B23E1107BB62ABFC40DDCC8824814F80A72"
40 |
41 | suite "Bridges validity":
42 | const
43 | o = "obfs4 122.148.194.24:993 07784768F54CF66F9D588E19E8EE3B0FA702711B cert=m3jPGnUyZMWHT9Riioob95s1czvGs3HiZ64GIT3QbH/AZDVlF/YEXu/OtyYZ1eObKnTjcg iat-mode=0"
44 | m = "meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com"
45 | s = "snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72"
46 |
47 | test "obfs4 validity":
48 | check:
49 | o.isObfs4()
50 | not m.isObfs4()
51 |
52 | test "meekazure validity":
53 | check:
54 | m.isMeekazure()
55 | not o.isMeekazure()
56 |
57 | test "snowflake validity":
58 | check:
59 | s.isSnowflake()
60 | not s.isMeekazure()
61 |
62 | suite "Check fingerprint of Tor bridges":
63 | test "Fingerprint hashing":
64 | let
65 | o4Fp = "07784768F54CF66F9D588E19E8EE3B0FA702711B"
66 | o4Hashed = "581674112383BEBF88E79C3328B71ADF79365B45"
67 |
68 | sfFp = "2B280B23E1107BB62ABFC40DDCC8824814F80A72"
69 | sfHashed = "5481936581E23D2D178105D44DB6915AB06BFB7F"
70 |
71 |
72 | sfHash = secureHash(a2bHex(sfFp))
73 | o4Hash = secureHash(a2bHex(o4Fp))
74 |
75 | check:
76 | $sfHash == sfHashed
77 | $o4Hash == o4Hashed
78 |
79 | suite "Request to Onionoo":
80 | proc isFound(fp: string): bool =
81 | const
82 | destHost = "https://onionoo.torproject.org/details?lookup="
83 | userAgent = "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
84 |
85 | var client = newHttpClient(userAgent = userAgent)
86 |
87 | let res = client.get(destHost & fp)
88 |
89 | if res.code == Http200:
90 | let
91 | j = parseJson(res.body)
92 | b = j["bridges"]
93 |
94 | if b.len > 0:
95 | let hashedFP = b[0]{"hashed_fingerprint"}.getStr
96 | if fp == hashedFP: return true
97 |
98 | test "Get bridges data":
99 | const
100 | fp = "07784768F54CF66F9D588E19E8EE3B0FA702711B"
101 | hfp = "5481936581E23D2D178105D44DB6915AB06BFB7F"
102 |
103 | check:
104 | hfp.isFound()
105 | not fp.isFound()
106 | not "123456FF".isFound()
107 |
108 | suite "Bridge actions":
109 | test "Activate obfs4":
110 | proc activateObfs4(torrc: string, kind: ActivateObfs4Kind): string =
111 | var rc = torrc
112 | rc = rc.replacef(re"#UseBridges\s(\d+)", "UseBridges $1")
113 | rc = rc.replacef(re"#UpdateBridgesFromAuthority\s(\d+)", "UpdateBridgesFromAuthority $1")
114 | rc = rc.replacef(re"#ClientTransportPlugin meek_lite,obfs4\s(.*)", "ClientTransportPlugin meek_lite,obfs4 $1")
115 | rc = rc.replacef(re"[^#]ClientTransportPlugin snowflake\s(.*)", "\n#ClientTransportPlugin snowflake $1")
116 | rc = rc.replacef(re"[^#]Bridge snowflake\s(.*)", "\n#Bridge snowflake $1")
117 | rc = rc.replacef(re"[^#]Bridge meek_lite\s(.*)", "\n#Bridge meek_lite $1")
118 |
119 | case kind
120 | of ActivateObfs4Kind.all:
121 | rc = rc.replacef(re"#Bridge obfs4\s(.*)", "Bridge obfs4 $1")
122 |
123 | of ActivateObfs4Kind.online:
124 | rc = rc.replacef(re"#Bridge obfs4\s(.*)", "Bridge obfs4 $1")
125 |
126 | of ActivateObfs4Kind.select:
127 | rc = rc.replacef(re"#Bridge obfs4\s(.*)", "Bridge obfs4 $1")
128 |
129 | return rc
130 |
131 | check:
132 | torrc_activated_obfs4 == torrc.activateObfs4(ActivateObfs4Kind.all)
133 |
134 | test "Deactivate obfs4":
135 | proc deactivateObfs4(torrc: string): string =
136 | var rc = torrc
137 | rc = rc.replacef(re"[^#]UseBridges\s(\d+)", "\n#UseBridges $1")
138 | rc = rc.replacef(re"[^#]UpdateBridgesFromAuthority\s(\d+)", "\n#UpdateBridgesFromAuthority $1")
139 | rc = rc.replacef(re"[^#]ClientTransportPlugin meek_lite,obfs4\s(.*)", "\n#ClientTransportPlugin meek_lite,obfs4 $1")
140 | rc = rc.replacef(re"[^#]Bridge obfs4\s(.*)", "\n#Bridge obfs4 $1")
141 |
142 | return rc
143 |
144 | check:
145 | torrc == torrc_activated_obfs4.deactivateObfs4()
146 |
147 | test "Activate meekazure":
148 | proc activateMeekazure(torrc: string): string =
149 | var rc = torrc
150 |
151 | rc = rc.replacef(re"[^#]Bridge obfs4\s(.*)", "\n#Bridge obfs4 $1")
152 | rc = rc.replacef(re"[^#]Bridge snowflake\s(.*)", "\n#Bridge snowflake $1")
153 | rc = rc.replacef(re"[^#]ClientTransportPlugin snowflake\s(.*)", "\n#ClientTransportPlugin snowflake $1")
154 | rc = rc.replacef(re"#UseBridges\s(\d+)", "UseBridges $1")
155 | rc = rc.replacef(re"#UpdateBridgesFromAuthority\s(\d+)", "UpdateBridgesFromAuthority $1")
156 | rc = rc.replacef(re"#ClientTransportPlugin meek_lite,obfs4\s(.*)", "ClientTransportPlugin meek_lite,obfs4 $1")
157 | rc = rc.replacef(re"#Bridge meek_lite\s(.*)", "Bridge meek_lite $1")
158 |
159 | return rc
160 |
161 | check:
162 | torrc_activated_meekazure == torrc.activateMeekazure()
163 |
164 | test "Deactivate meekazure":
165 | proc deactivateMeekazure(torrc: string): string =
166 | var rc = torrc
167 |
168 | rc = rc.replacef(re"[^#]Bridge obfs4\s(.*)", "\n#Bridge obfs4 $1")
169 | rc = rc.replacef(re"[^#]Bridge snowflake\s(.*)", "\n#Bridge snowflake $1")
170 | rc = rc.replacef(re"[^#]ClientTransportPlugin snowflake\s(.*)", "\n#ClientTransportPlugin snowflake $1")
171 | rc = rc.replacef(re"[^#]UseBridges\s(\d+)", "\n#UseBridges $1")
172 | rc = rc.replacef(re"[^#]UpdateBridgesFromAuthority\s(\d+)", "\n#UpdateBridgesFromAuthority $1")
173 | rc = rc.replacef(re"[^#]ClientTransportPlugin meek_lite,obfs4\s(.*)", "\n#ClientTransportPlugin meek_lite,obfs4 $1")
174 | rc = rc.replacef(re"[^#]Bridge meek_lite\s(.*)", "\n#Bridge meek_lite $1")
175 |
176 | return rc
177 |
178 | check:
179 | torrc == torrc_activated_meekazure.deactivateMeekazure()
180 |
181 | test "Activate snowflake":
182 | proc activateSnowflake(torrc: string): string =
183 | var rc = torrc
184 |
185 | rc = rc.replacef(re"[^#]Bridge obfs4\s(.*)", "\n#Bridge obfs4 $1")
186 | rc = rc.replacef(re"[^#]Bridge meek_lite\s(.*)", "\n#Bridge meek_lite $1")
187 | rc = rc.replacef(re"#UseBridges\s(\d+)", "UseBridges $1")
188 | rc = rc.replacef(re"#UpdateBridgesFromAuthority\s(\d+)", "UpdateBridgesFromAuthority $1")
189 | rc = rc.replacef(re"#ClientTransportPlugin snowflake\s(.*)", "ClientTransportPlugin snowflake $1")
190 | rc = rc.replacef(re"#Bridge snowflake\s(.*)", "Bridge snowflake $1")
191 |
192 | return rc
193 |
194 | check:
195 | torrc_activated_snowflake == torrc.activateSnowflake()
196 |
197 | test "Deactivate snowflake":
198 | proc deactivateSnowflake(torrc: string): string =
199 | var rc = torrc
200 |
201 | rc = rc.replacef(re"[^#]Bridge obfs4\s(.*)", "\n#Bridge obfs4 $1")
202 | rc = rc.replacef(re"[^#]Bridge meek_lite\s(.*)", "\n#Bridge meek_lite $1")
203 | rc = rc.replacef(re"[^#]UseBridges\s(\d+)", "\n#UseBridges $1")
204 | rc = rc.replacef(re"[^#]UpdateBridgesFromAuthority\s(\d+)", "\n#UpdateBridgesFromAuthority $1")
205 | rc = rc.replacef(re"[^#]ClientTransportPlugin snowflake\s(.*)", "\n#ClientTransportPlugin snowflake $1")
206 | rc = rc.replacef(re"[^#]Bridge snowflake\s(.*)", "\n#Bridge snowflake $1")
207 |
208 | return rc
209 |
210 | check:
211 | torrc == torrc_activated_snowflake.deactivateSnowflake()
212 |
213 | test "Add bridges":
214 | const newBridges: string = """
215 | obfs4 185.220.101.221:38395 1BBABB8B42EF34BA93D0D4F37F7CAAAAF9EAA512 cert=p9L6+25s8bnfkye1ZxFeAE4mAGY7DH4Gaj7dxngIIzP9BtqrHHwZXdjMK0RVIQ34C7aqZw iat-mode=0
216 |
217 | meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com
218 | snowflake 192.0.2.3:1 2B280B23E1107BB62ABFC40DDCC8824814F80A72
219 | """
220 | proc addBridges(bridges: string): string =
221 | result = torrc
222 | for bridge in bridges.splitLines:
223 | if (bridge.len > 0) and
224 | bridge.isObfs4() or
225 | bridge.isMeekazure() or
226 | bridge.isSnowflake():
227 | result &= "Bridge " & bridge & "\n"
228 |
229 | check:
230 | torrc_added_bridges == newBridges.addBridges()
--------------------------------------------------------------------------------
/tests/test_c_crypt.nim:
--------------------------------------------------------------------------------
1 | import std / unittest, system
2 | import strformat
3 | import ../ src / lib / clib / c_crypt
4 |
5 | suite "Encrypt password":
6 | test "do crypt":
7 | const
8 | shadow = "$6$FRuqFx.gDQotf$xph8gaXXM2D1Y8WMYPfUgLUlQivlc/cAZtB2x.xZbIACrlfqnZtgeVAGcVwvV/embpKisdSKSlVkhrEVR0H3X."
9 | salt = "FRuqFx.gDQotf"
10 |
11 | check:
12 | shadow == $crypt("nim", fmt"$6${cstring salt}")
13 | shadow == crypt("nim", fmt"$6${salt}")
--------------------------------------------------------------------------------
/tests/test_crypt.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest, importutils,
3 | terminal
4 | ]
5 | import results, resultsutils
6 | import ../ src / lib / crypt {.all.}
7 | import ../ src / lib / clib / c_crypt
8 |
9 | suite "Cryptgraphics":
10 | test "Test prefix parse":
11 | privateAccess(CryptPrefix)
12 | let yescrypt_prefix = "y"
13 |
14 | match parsePrefix(yescrypt_prefix):
15 | Ok(prefix):
16 | check:
17 | prefix == yescrypt
18 | Err(msg): styledEcho(fgRed, "[Error] ", fgWhite, msg)
19 |
20 | test "Test parse on yescrypt":
21 | privateAccess(Shadow)
22 | let passwd = "$y$j9T$C5QGAtTr38W/K2jMJ3uTV/$Z5uBSxY.JoKyWiSfUumJTKjiJQFAlAuMfY9YHvAyBmB:19076:0:99999:7:::"
23 | let ret = readAsShadow(passwd)
24 | if ret.isErr:
25 | styledEcho(fgRed, "[Error] ", fgWhite, "parseShadow returned err.")
26 |
27 | let
28 | prefix = ret.get.prefix
29 | salt = ret.get.salt
30 |
31 | styledEcho(fgBlue, "[Prefix] ", fgWhite, $prefix)
32 | styledEcho(fgBlue, "[Salt] ", fgWhite, salt)
33 | styledEcho(fgGreen, "[Success encryption] ", fgWhite, crypt("trickleaks", fmtSalt(ret.get)))
34 | # check:
35 | test "Test parse on sha512crypt":
36 | privateAccess(Shadow)
37 | let passwd = "$6$D.3Q1uJwc5TIs.g3$VnU8JwwjxWN15Vo2M1CCcf3dr5FJUN9cPUNls0DKW9pknjEwrESA0uGdxMpB735uYJbYBMz86GbkliwrhJVWo.:19068:0:99999:7:::"
38 | let ret = readAsShadow(passwd)
39 | if ret.isErr:
40 | styledEcho(fgRed, "[Error] ", fgWhite, "returned err on parse sha512crypt.")
41 |
42 | let
43 | prefix = ret.get.prefix
44 | salt = ret.get.salt
45 |
46 | styledEcho(fgBlue, "[Prefix] ", fgWhite, $prefix)
47 | styledEcho(fgBlue, "[Salt] ", fgWhite, salt)
48 | styledEcho(fgGreen, "[Success encryption] ", fgWhite, crypt("trickleaks", fmtSalt(ret.get)))
--------------------------------------------------------------------------------
/tests/test_hostname.nim:
--------------------------------------------------------------------------------
1 | import std / [ unittest, re, strutils ]
2 |
3 | suite "torbox":
4 | proc getTorboxVersion(hname: string): string =
5 | if hname.match(re"TorBox(\d){3}"):
6 | # hname.delete(0, 5)
7 | let version = hname[6..8]
8 | result = version.insertSep('.', 1)
9 |
10 | test "Check TorBox version":
11 | var hostname = "TorBox050"
12 | let h = getTorboxVersion(hostname)
13 |
14 | check:
15 | "0.5.0" == h
16 |
17 | # proc getTorboxVersion*(): string =
18 | # var hname = $getHostname()
19 | # if hname.match(re"TorBox(\d){3}"):
20 | # hname.delete(0..5)
21 | # result = hname.insertSep('.', 1)
22 |
23 |
--------------------------------------------------------------------------------
/tests/test_iface.nim:
--------------------------------------------------------------------------------
1 | import std / [ unittest, options ]
2 | import ".." / src / lib / sys / iface
3 |
4 | suite "iface":
5 | test "parse Iface":
6 | let
7 | n = parseIfaceKind("nil")
8 | wlan0 = parseIfaceKind("wlan0")
9 | check:
10 | n.isNone
11 | wlan0.isSome
--------------------------------------------------------------------------------
/tests/test_notifies.nim:
--------------------------------------------------------------------------------
1 | import std / [ unittest, terminal ]
2 | import karax / [ vdom ]
3 | import ../ src / notice
4 |
5 | suite "Notifies":
6 | var nt = new Notifies
7 | nt.add success, "Some notifies!"
8 | styledEcho(fgGreen, "render: ", fgWhite, $nt.render())
9 | test "notifies render":
10 | check:
11 | nt.render().len >= 0
--------------------------------------------------------------------------------
/tests/test_routes/test_ap.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest, os, osproc,
3 | asyncdispatch, nativesockets,
4 | ]
5 | import ../ server / client
6 |
7 | suite "route AP":
8 | routerTest "ap":
9 | GET:
10 | "/hostap"
11 | "/default/hostap"
12 | "/ap"
13 | "/default/ap"
14 | "/conf"
15 | "/default/conf"
16 | "/status"
17 | "/default/status"
--------------------------------------------------------------------------------
/tests/test_routes/test_status.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest, os, osproc,
3 | asyncdispatch, nativesockets,
4 | ]
5 | import ../ server / client
6 |
7 | suite "route status":
8 | routerTest "status":
9 | GET:
10 | "/status"
11 | "/default/status"
12 | "/tor"
13 | "/default/tor"
14 | "/iface"
15 | "/default/iface"
16 | "/sys"
17 | "/default/sys"
18 |
19 | # POST:
20 | # "/io": {"tor-request": "renew"}
21 |
--------------------------------------------------------------------------------
/tests/test_routes/test_sys.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest,
3 | os, osproc,
4 | asyncdispatch, nativesockets
5 | ]
6 | import ../ server / client
7 |
8 | suite "route Sys":
9 | routerTest "sys":
10 | GET:
11 | "/passwd"
--------------------------------------------------------------------------------
/tests/test_tabs.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest
3 | ]
4 | import ../ src / routes / tabs
5 |
6 | suite "Tabs":
7 | test "Test some method is work correctly.":
8 | let tab = Tab.new
9 | check:
10 | tab.isEmpty
11 |
12 | test "build macro":
13 | let tab = buildTab:
14 | # "Bridges" = "/net" / "bridges"
15 | "Bridges" = "/net" / "bridges"
16 | "Tor" = "/tor" / "bridges"
17 |
18 | "Passwd" = "/sys" / "passwd"
19 | "Logs" = "/sys" / "logs"
20 |
21 | check:
22 | tab[3].label == "Logs"
23 | tab.len == 4
--------------------------------------------------------------------------------
/tests/test_toml.nim:
--------------------------------------------------------------------------------
1 | import std / [ unittest, os ]
2 | import toml_serialization
3 | import ../ src / toml
4 |
5 | suite "TOML":
6 | test "parse":
7 | let t = Toml.loadFile("torci.toml", TorCi, "TorCI")
8 | check t.version == "0.1.3"
9 |
10 | test "compile time":
11 | const
12 | fn = "./" / "torci.toml"
13 | x = Toml.loadFile(fn, TorCi, "TorCI")
14 |
15 | check x.version == "0.1.3"
--------------------------------------------------------------------------------
/tests/test_tor.nim:
--------------------------------------------------------------------------------
1 | import std / [
2 | unittest, importutils,
3 | nativesockets, asyncdispatch
4 | ]
5 | import results, resultsutils
6 | import karax / vdom as kvdom
7 | import ../ src / lib / sys / service
8 | import ../ src / lib / tor / tor {.all.}
9 | import ../ src / lib / tor / bridges {.all.}
10 | import ../ src / lib / tor / vdom
11 |
12 | suite "Tor":
13 | test "TorInfo object":
14 | if waitFor isActiveService("tor"):
15 | var ti = TorInfo.default()
16 | match waitFor getTorInfo("127.0.0.1", 9050.Port):
17 | Ok(ret): ti = ret
18 | Err(msg): fail
19 | check ti.isTor
20 |
21 | # test "Test some methods of Tor":
22 | # privateAccess(TorInfo)
23 | # privateAccess(TorStatus)
24 |
25 |
26 | # var torStatus: TorStatus
27 |
28 | # match waitFor checkTor("127.0.0.1", 9050.Port):
29 | # Ok(status): torStatus = status
30 | # Err(msg): styledEcho(fgRed, "[Error] ", fgWhite, msg); skip()
31 |
32 | # check:
33 | # torStatus.isTor
34 | # withSome torStatus.exitIp:
35 | # some exitIp:
36 | # styledEcho(fgGreen, "[Tor is working]")
37 | # styledEcho(fgGreen, "[Exit node ip address] ", fgWhite, exitIp)
38 | # true
39 | # none: false
40 |
41 | test "Test vdom rendering with TorInfo":
42 | privateAccess(TorInfo)
43 | privateAccess(TorStatus)
44 | privateAccess(Bridge)
45 | let
46 | status = TorStatus(
47 | isTor: true,
48 | exitIp: "1.1.1.1"
49 | )
50 | bridge = Bridge(
51 | kind: BridgeKind.obfs4,
52 | useBridges: true
53 | )
54 |
55 | let dummy = TorInfo(
56 | status: status,
57 | bridge: bridge
58 | )
59 |
60 | let dom = dummy.render()
61 | # styledEcho(fgGreen, "[VDom] ", fgWhite, $dom)
62 | check:
63 | 0 < len($dom)
--------------------------------------------------------------------------------
/tools/gencss.nim:
--------------------------------------------------------------------------------
1 | import sass
2 | import strutils
3 |
4 | const srcPath: string = "index"
5 | let outPath: string = "style"
6 |
7 | compileFile("src/sass/" & $srcPath & ".scss", outputPath = "public/css/" & $outPath & ".css")
8 | echo "\n"
9 | echo "Cpmpiled to public/css/" & $outPath & ".css"
--------------------------------------------------------------------------------
/torci.conf:
--------------------------------------------------------------------------------
1 | [Server]
2 | address = "0.0.0.0"
3 | port = 1984
4 | https = false
5 | staticDir = "./public"
6 | title = "TorBox"
7 | torciVer = "0.1.3"
8 | torboxVer = "0.4.2"
9 | torAddress = "192.168.42.1"
10 | torPort = 9050
11 |
--------------------------------------------------------------------------------
/torci.nimble:
--------------------------------------------------------------------------------
1 | # Package
2 |
3 | version = "0.1.3"
4 | author = "Luca (@nonnil)"
5 | description = "Web-based GUI for TorBox."
6 | license = "GPL-3.0"
7 | srcDir = "src"
8 | bin = @["torci"]
9 |
10 | skipDirs = @["tests", "mockups"]
11 |
12 | # Dependencies
13 | requires "nim >= 1.5.0"
14 | requires "jester >= 0.5.0"
15 | requires "karax >= 1.2.1"
16 | requires "sass"
17 | requires "libcurl >= 1.0.0"
18 | requires "bcrypt >= 0.2.1"
19 | requires "result >= 0.3.0"
20 | requires "validateip >= 0.1.2"
21 | requires "optionsutils >= 1.2.0"
22 | requires "resultsutils >= 0.1.6"
23 | requires "redis >= 0.3.0"
24 | requires "jsony >= 1.1.3"
25 | requires "toml_serialization >= 0.2.3"
26 |
27 | task scss, "Generate css":
28 | exec "nim r tools/gencss"
29 |
30 | task tests, "Run tests":
31 | exec "nimble -d:test test -y"
32 |
33 | task redis, "Run tests in Docker container":
34 | exec "testament p tests/sandbox/tests"
35 |
36 | task fulltest, "":
37 | exec "sudo docker-compose up"
--------------------------------------------------------------------------------
/torci.toml:
--------------------------------------------------------------------------------
1 | [TorCI]
2 | version = "0.1.3"
3 | address = "0.0.0.0"
4 | port = 1984
5 | staticDir = "./public"
--------------------------------------------------------------------------------