├── .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 | ![Build Status](https://github.com/nonnil/TorCI/workflows/Test%20TorCI/badge.svg) 2 | ![Lines of code](https://tokei.rs/b1/github/nonnil/torci?category=code) 3 | ![Licence: GPL-3.0](https://img.shields.io/github/license/nonnil/TorCI) 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 | ![Login](https://user-images.githubusercontent.com/85566220/168503740-bef02761-800c-4f16-b792-17c85de6079c.png) 28 | ![Status](https://user-images.githubusercontent.com/85566220/168497464-1be40b4f-36a1-4ab6-b249-653930855495.png) 29 | ![AP](https://user-images.githubusercontent.com/85566220/168503738-fb434bae-ec40-4c90-b0f0-4541c6c3a6a4.png) 30 | ![Bridges](bridges.png) 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 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 |
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
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 | 4 | Copyright (C) 2022 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 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" --------------------------------------------------------------------------------