├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── go.yml ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── demo ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── fonts-bold.ttf │ ├── fonts.ttf │ ├── img │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 13.jpg │ │ ├── 14.jpg │ │ ├── 15.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ └── index.html └── src │ ├── App.vue │ └── main.js ├── errorhandler.go ├── filemodel.go ├── go.mod ├── go.sum ├── handler.go ├── help.go ├── load.go ├── load_test.go ├── main.go ├── test-cert.pem ├── test-secret.key └── test ├── index.css ├── index.html ├── index.js ├── roboto.ttf ├── test.jpg ├── test.png ├── test.svg ├── with_index ├── index.html └── wkwkwk@&$!-_#=+.jpg └── without-index └── never gonna give you up.jpg /.dockerignore: -------------------------------------------------------------------------------- 1 | # Unecessary directories inside docker container 2 | demo/ 3 | test/ 4 | 5 | # Unecessary files inside docker container 6 | README.md 7 | load_test.go 8 | Makefile 9 | test-cert.pem 10 | test-key.key 11 | 12 | # Compiled files 13 | build/ 14 | kuda 15 | 16 | # Hidden files 17 | .editorconfig 18 | .gitignore 19 | .git 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{Makefile,Dockerfile}] 11 | indent_style = tab 12 | indent_size = 4 13 | insert_final_newline = false 14 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.13 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Get dependencies 25 | run: | 26 | go get -v -t -d ./... 27 | if [ -f Gopkg.toml ]; then 28 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 29 | dep ensure 30 | fi 31 | 32 | - name: Build 33 | run: go build -v . 34 | 35 | - name: Test 36 | run: go test -v . 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated file at WSL 2 | *:Zone.Identifier 3 | **/*:Zone.Identifier 4 | 5 | # Generated file at Mac 6 | .DS_STORE 7 | **/.DS_STORE 8 | 9 | # Generated file at Windows XP 10 | Thumbs.db 11 | **/Thumbs.db 12 | 13 | # Compiled 14 | build/ 15 | kuda 16 | kuda.exe 17 | 18 | # Front-end modules and compiled directory 19 | **/node_modules/ 20 | **/dist/ 21 | **/build/ 22 | 23 | # Logs 24 | *.log 25 | **/*.log 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | - 1.15.x 6 | - master 7 | 8 | script: 9 | - go get github.com/valyala/fasthttp 10 | - make test 11 | 12 | branches: 13 | only: 14 | - master 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1-alpine 2 | 3 | COPY . /usr/src 4 | WORKDIR /usr/src 5 | RUN go build -o ../bin 6 | 7 | ENV KUDA_PUBLIC_DIR "/srv" 8 | ENV KUDA_DOMAIN "localhost" 9 | ENV KUDA_PORT "8080" 10 | ENV KUDA_ORIGINS "" 11 | ENV KUDA_PORT_TLS "" 12 | ENV KUDA_CERT "" 13 | ENV KUDA_KEY "" 14 | 15 | CMD kuda ${KUDA_PUBLIC_DIR} --domain=${KUDA_DOMAIN} --port=${KUDA_PORT} --origins=${KUDA_ORIGINS} --portTLS=${KUDA_PORT_TLS} --cert=${KUDA_CERT} --key=${KUDA_KEY} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # Released under MIT License 2 | 3 | Copyright (c) 2020 Athaariq Ardhiansyah. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: clean win_x64 win_x32 linux_x64 linux_x32 linux_arm64 linux_arm32 mac_intel mac_arm 2 | 3 | .PHONY: test demo 4 | 5 | demo: 6 | rm -rf demo/dist 7 | cd demo && npm install && npm run build 8 | go build 9 | ./kuda demo/dist 10 | 11 | test: 12 | go test 13 | 14 | clean: 15 | rm -f kuda 16 | rm -rf build/* 17 | rm -rf demo/dist 18 | 19 | win_x64: 20 | GOOS=windows GOARCH=amd64 go build -o build/kuda-win_x64.exe 21 | 22 | win_x32: 23 | GOOS=windows GOARCH=386 go build -o build/kuda-win_x32.exe 24 | 25 | linux_x64: 26 | GOOS=linux GOARCH=amd64 go build -o build/kuda-linux_x64 27 | 28 | linux_x32: 29 | GOOS=linux GOARCH=386 go build -o build/kuda-linux_x32 30 | 31 | linux_arm64: 32 | GOOS=linux GOARCH=arm64 go build -o build/kuda-linux_arm64 33 | 34 | linux_arm32: 35 | GOOS=linux GOARCH=arm go build -o build/kuda-linux_arm32 36 | 37 | mac_intel: 38 | GOOS=darwin GOARCH=amd64 go build -o build/kuda-mac_intel 39 | 40 | mac_arm: 41 | GOOS=darwin GOARCH=amd64 go build -o build/kuda-mac_arm -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kuda Web Server 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![Open Issues](https://img.shields.io/github/issues-raw/Thor-x86/kuda)](https://github.com/Thor-x86/kuda/issues) 5 | [![Open Pull Request](https://img.shields.io/github/issues-pr-raw/Thor-x86/kuda)](https://github.com/Thor-x86/kuda/pulls) 6 | [![Unit Test Result](https://img.shields.io/travis/Thor-x86/kuda)](https://travis-ci.org/Thor-x86/kuda) 7 | 8 | Fast and concurrent in-memory web server. It compress static files with [gzip](https://en.wikipedia.org/wiki/Gzip), put them into RAM, and serve them as a normal web server. So Dev/Ops don't have to worry about storage speed, just focus on networking matter. 9 | 10 | The best use case is serving Single Page Application (SPA) like [React](https://reactjs.org/), [Vue](https://vuejs.org/), and [Angular](https://angular.io/). 11 | 12 | Special thanks to [fasthttp](https://github.com/valyala/fasthttp) and contributors for making kuda possible 🤘 13 | 14 | ## Download 15 | 16 | See [release timeline](https://github.com/Thor-x86/kuda/releases) 17 | 18 | ## How to use 19 | 20 | ``` 21 | USAGE: 22 | kuda [arguments] 23 | 24 | ARGUMENTS: 25 | --port=... : TCP Port to be listened (default: "8080") 26 | --origins=... : Which domains to be allowed by CORS policy (default: "") 27 | --port-tls=... : Use this to listen for HTTPS requests (default: "") 28 | --domain=... : Required to redirect from http to https (default: "localhost") 29 | --cert=... : SSL certificate file, required if "--port-tls" specified 30 | --key=... : SSL secret key file, required if "--port-tls" specified 31 | ``` 32 | 33 | ## Usage Example 34 | 35 | Let's say you're now in a directory with `kuda` executable and `my-app` as your react app project. 36 | 37 | ``` 38 | cd my-app 39 | npm run build 40 | cd .. 41 | 42 | ./kuda my-app/build --port=8000 --origins=localhost:9000 43 | ``` 44 | 45 | After executed, it will put everything inside `my-app/build` into RAM and serve at port 8000. Beside of that, we're assuming the API backend runs on port 9000. Thus, we have to add it as allowed origins with `--origins=...` flag to prevent [CORS Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) problem. 46 | 47 | ### How about SSL? 48 | 49 | Now we have same directory as above with `cert.pem` and `secret.key` inside of it. 50 | 51 | ``` 52 | ./kuda my-app/build --port=8000 --origins=localhost:9000 --port-tls=8090 --domain=localhost --cert=cert.pem --key=secret.key 53 | ``` 54 | 55 | The `localhost:8000` is considered using HTTP protocol. So every connection coming to that port will be redirected to `https://localhost:8090`. 56 | 57 | ### Serving Production App 58 | 59 | In production environment, assuming you have `www.my-domain.com` web URL and `api.my-domain.com` for API URL: 60 | 61 | ``` 62 | sudo ./kuda my-app/build --port=80 --origins=api.my-domain.com --port-tls=443 --domain=www.my-domain.com --cert=/etc/ssl/certs/my-domain.pem --key=/etc/ssl/private/my-domain.key 63 | ``` 64 | 65 | ## With Docker 66 | 67 | Mostly, we deploy web app with docker and kubernetes (for orchestration). In order to do that, use this commands. 68 | 69 | ``` 70 | docker pull kuda:latest 71 | docker run --name kuda-demo --volume my-compiled-webapp:/srv -p 8080:8080 --rm kuda:latest 72 | ``` 73 | 74 | Where `my-compiled-webapp` is your directory with compiled app inside. **NEVER EVER** point project root directory as public directory, otherwise your machine will run out of memory very shortly! 75 | 76 | ## With Docker Compose 77 | 78 | I would recommend you to use Docker Compose instead of plain Docker for sake of maintainability. This is an example of `docker-compose.yml`: 79 | 80 | ```yml 81 | version: "3" 82 | 83 | services: 84 | kuda: 85 | image: kuda:latest 86 | volumes: 87 | - ./my-compiled-webapp:/srv 88 | environment: 89 | KUDA_PUBLIC_DIR: "/srv" 90 | KUDA_DOMAIN: "localhost" 91 | KUDA_PORT: "8080" 92 | KUDA_ORIGINS: "" 93 | KUDA_PORT_TLS: "" 94 | KUDA_CERT: "" 95 | KUDA_KEY: "" 96 | ports: 97 | - 8080:8080 98 | ``` 99 | 100 | ## Benchmark 101 | 102 | I have no enough resource to benchmark Kuda Web Server myself. If you did benchmark and comparation with kuda, please let us know via **issue** section. 103 | 104 | ## Contribution 105 | 106 | Anyone can contribute on this project. We welcome you to Pull Request or Open an Issue. To working on source code, you will require: 107 | 108 | - Linux or Mac preferred (Use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10) if you are Windows 10 user or [Cygwin](https://www.cygwin.com/) for older Windows) 109 | - Golang 110 | - Makefile 111 | - NPM (for demo) 112 | - Docker + Docker Compose (optional) 113 | 114 | ### Makefile Commands 115 | 116 | There are things you can do with makefile on this project: 117 | 118 | - `make` -- Compile for all platforms and store them inside "build" directory 119 | - `make test` -- Run this everytime you want to pull request 120 | - `make clean` -- Removes all compiled files to reduce storage usage 121 | - `make demo` -- Run demonstration, go to `http://localhost:8080` on browser while running this command 122 | 123 | ### Security Report 124 | 125 | To keep other users' security, please send email to [athaariqa@gmail.com](mailto://athaariqa@gmail.com) instead. **Do not** open issue for security report. 126 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # kuda-demo 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | ``` 17 | 18 | ### Lints and fixes files 19 | ``` 20 | npm run lint 21 | ``` 22 | 23 | ### Customize configuration 24 | See [Configuration Reference](https://cli.vuejs.org/config/). 25 | -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kuda-demo", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^3.6.5", 12 | "vue": "^2.6.11" 13 | }, 14 | "devDependencies": { 15 | "@vue/cli-plugin-babel": "~4.5.0", 16 | "@vue/cli-plugin-eslint": "~4.5.0", 17 | "@vue/cli-service": "~4.5.0", 18 | "babel-eslint": "^10.1.0", 19 | "eslint": "^6.7.2", 20 | "eslint-plugin-vue": "^6.2.2", 21 | "sass": "^1.28.0", 22 | "sass-loader": "^10.0.4", 23 | "vue-template-compiler": "^2.6.11" 24 | }, 25 | "eslintConfig": { 26 | "root": true, 27 | "env": { 28 | "node": true 29 | }, 30 | "extends": [ 31 | "plugin:vue/essential", 32 | "eslint:recommended" 33 | ], 34 | "parserOptions": { 35 | "parser": "babel-eslint" 36 | }, 37 | "rules": {} 38 | }, 39 | "browserslist": [ 40 | "> 1%", 41 | "last 2 versions", 42 | "not dead" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /demo/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/favicon.ico -------------------------------------------------------------------------------- /demo/public/fonts-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/fonts-bold.ttf -------------------------------------------------------------------------------- /demo/public/fonts.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/fonts.ttf -------------------------------------------------------------------------------- /demo/public/img/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/1.jpg -------------------------------------------------------------------------------- /demo/public/img/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/10.jpg -------------------------------------------------------------------------------- /demo/public/img/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/11.jpg -------------------------------------------------------------------------------- /demo/public/img/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/12.jpg -------------------------------------------------------------------------------- /demo/public/img/13.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/13.jpg -------------------------------------------------------------------------------- /demo/public/img/14.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/14.jpg -------------------------------------------------------------------------------- /demo/public/img/15.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/15.jpg -------------------------------------------------------------------------------- /demo/public/img/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/2.jpg -------------------------------------------------------------------------------- /demo/public/img/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/3.jpg -------------------------------------------------------------------------------- /demo/public/img/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/4.jpg -------------------------------------------------------------------------------- /demo/public/img/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/5.jpg -------------------------------------------------------------------------------- /demo/public/img/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/6.jpg -------------------------------------------------------------------------------- /demo/public/img/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/7.jpg -------------------------------------------------------------------------------- /demo/public/img/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/8.jpg -------------------------------------------------------------------------------- /demo/public/img/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/demo/public/img/9.jpg -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Kuda Demo 9 | 19 | 20 | 21 | 24 |
25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 72 | 73 | 101 | -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | Vue.config.productionTip = false 5 | 6 | new Vue({ 7 | render: h => h(App), 8 | }).$mount('#app') 9 | -------------------------------------------------------------------------------- /errorhandler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/valyala/fasthttp" 4 | 5 | // Handles every request other than GET method also bad requests 6 | func errorHandler(ctx *fasthttp.RequestCtx, err error) { 7 | // Clear all existing header 8 | ctx.Response.Reset() 9 | 10 | if ctx.IsOptions() { 11 | ctx.Response.Header.Set("Server", "kuda") 12 | ctx.Response.Header.Set("Access-Control-Allow-Origin", origins) 13 | ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, OPTIONS") 14 | ctx.Response.Header.Set("Access-Control-Allow-Headers", "Host,Accept,Accept-Encoding,Connection,User-Agent") 15 | ctx.Response.SetStatusCode(204) 16 | } else if !ctx.IsGet() { 17 | ctx.Response.SetStatusCode(405) 18 | } else { 19 | ctx.Response.SetStatusCode(400) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /filemodel.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Structure of each file inside RAM 4 | type fileModel struct { 5 | data []byte 6 | mime string 7 | isCompressed bool 8 | } 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Thor-x86/kuda 2 | 3 | go 1.15 4 | 5 | require github.com/valyala/fasthttp v1.16.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= 2 | github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= 3 | github.com/klauspost/compress v1.10.7 h1:7rix8v8GpI3ZBb0nSozFRgbtXKv+hOe+qfEpZqybrAg= 4 | github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 5 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 6 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 7 | github.com/valyala/fasthttp v1.16.0 h1:9zAqOYLl8Tuy3E5R6ckzGDJ1g8+pw15oQp2iL9Jl6gQ= 8 | github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= 9 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 13 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= 15 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/valyala/fasthttp" 9 | ) 10 | 11 | // Handle each request 12 | func handler(ctx *fasthttp.RequestCtx) { 13 | // Making sure all files already loaded 14 | if pathMap == nil { 15 | panic("Not loaded yet!") 16 | } 17 | 18 | // For CORS acknowledge 19 | if ctx.IsOptions() { 20 | ctx.SetStatusCode(204) 21 | return 22 | } 23 | 24 | // Start incoming request timestamp 25 | requestTime := ctx.Time() 26 | 27 | path := string(ctx.Path()) 28 | responseCode := 200 29 | isCompressed := false 30 | 31 | // Remove trailing slash 32 | if strings.HasSuffix(path, "/") && len(path) > 1 { 33 | ctx.Redirect(strings.TrimSuffix(path, "/"), 308) 34 | return 35 | } 36 | 37 | // Process response based on specific condition 38 | path = strings.TrimPrefix(path, "/") 39 | if file, isExist := (*pathMap)[path]; isExist { 40 | // Condition: found static file, e.g. www.mydomain.com/img/something.png 41 | ctx.Response.SetBodyRaw(file.data) 42 | isCompressed = file.isCompressed 43 | ctx.SetContentType(file.mime) 44 | } else if file, isExist := (*pathMap)[path+"/index.html"]; isExist { 45 | // Condition: has index.html in the subpath 46 | // e.g. www.mydomain.com/path/subpath => www.mydomain.com/path/subpath/index.html 47 | ctx.Response.SetBodyRaw(file.data) 48 | isCompressed = file.isCompressed 49 | ctx.SetContentType(file.mime) 50 | } else if file, isExist := (*pathMap)["index.html"]; isExist { 51 | // Condition: found nothing in path map, so index.html will handle the route 52 | ctx.Response.SetBodyRaw(file.data) 53 | isCompressed = file.isCompressed 54 | ctx.SetContentType(file.mime) 55 | } else { 56 | // Condition: developer forgot to add index.html into public root directory 57 | responseCode = 404 58 | ctx.Response.SetBodyString("

There is no root index.html

") 59 | ctx.SetContentType("text/html") 60 | } 61 | 62 | // Tell the browser whether it's compressed or not 63 | if isCompressed { 64 | ctx.Response.Header.Set("Content-Encoding", "gzip") 65 | } else { 66 | ctx.Response.Header.Set("Content-Encoding", "") 67 | } 68 | 69 | // Set headers 70 | ctx.Response.SetStatusCode(responseCode) 71 | ctx.Response.Header.Set("Server", "kuda") 72 | ctx.Response.Header.Set("Access-Control-Allow-Origin", origins) 73 | ctx.Response.Header.Set("Access-Control-Allow-Methods", "GET, OPTIONS") 74 | ctx.Response.Header.Set("Access-Control-Allow-Headers", "Host,Accept,Accept-Encoding,Connection,User-Agent") 75 | 76 | // Get elapsed request-response time and client IPv4 77 | elapsed := time.Now().Sub(requestTime) 78 | clientIP := ctx.RemoteIP().String() 79 | 80 | // Format the elapsed time 81 | elapsedValue := float32(elapsed.Nanoseconds()) 82 | elapsedUnit := "ns" 83 | if elapsedValue >= 1000 { 84 | elapsedValue /= 1000 85 | elapsedUnit = "μs" 86 | } 87 | if elapsedValue >= 1000 { 88 | elapsedValue /= 1000 89 | elapsedUnit = "ms" 90 | } 91 | if elapsedValue >= 1000 { 92 | elapsedValue /= 1000 93 | elapsedUnit = "s" 94 | } 95 | 96 | // Print report to log 97 | fmt.Printf("[KUDA] %d | %.3f%s\t| %s\t| /%s\n", responseCode, elapsedValue, elapsedUnit, clientIP, path) 98 | } 99 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Description for each arguments 9 | var helpArgs map[string]string = map[string]string{ 10 | "domain": "Required to redirect from http to https", 11 | "port": "TCP Port to be listened", 12 | "origins": "Which domains to be allowed by CORS policy", 13 | "port-tls": "Use this to listen for HTTPS requests", 14 | "cert": "SSL certificate file, required if \"--port-tls\" specified", 15 | "key": "SSL secret key file, required if \"--port-tls\" specified", 16 | } 17 | 18 | // Displays usage help 19 | func help() { 20 | fmt.Println("") 21 | fmt.Println("USAGE:") 22 | fmt.Printf(" %s [arguments] \n", os.Args[0]) 23 | fmt.Println("") 24 | fmt.Println("ARGUMENTS:") 25 | for argName, argDesc := range helpArgs { 26 | fmt.Printf(" --%s=... : %s\n", argName, argDesc) 27 | } 28 | fmt.Println("") 29 | } 30 | -------------------------------------------------------------------------------- /load.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // Load all files inside public root directory into RAM, only executed once. 16 | func load(publicDir string) { 17 | // Add temporary map of file 18 | loaded := map[string]fileModel{} 19 | 20 | // Get absolute path of public directory 21 | absolutePublicDir, err1 := filepath.Abs(publicDir) 22 | if err1 != nil { 23 | log.Fatalln(err1.Error()) 24 | return 25 | } 26 | 27 | // Load files one by one 28 | fmt.Printf("Loading from \"%s\":\n", publicDir) 29 | filepath.Walk(publicDir, func(path string, info os.FileInfo, err2 error) error { 30 | // Check for premature error 31 | if err2 != nil { 32 | log.Fatalln(err2.Error()) 33 | return err2 34 | } 35 | 36 | // We just need files, so skip to directory's content 37 | if info.IsDir() { 38 | return nil 39 | } 40 | 41 | // Get absolute path of file path 42 | absolutePath, err3 := filepath.Abs(path) 43 | if err3 != nil { 44 | log.Fatalln(err3.Error()) 45 | return err3 46 | } 47 | 48 | // Get URI path from absolute path 49 | uriPath := strings.TrimPrefix(absolutePath, absolutePublicDir) 50 | uriPath = strings.ReplaceAll(uriPath, "\\", "/") 51 | uriPath = strings.TrimPrefix(uriPath, "/") 52 | fileData, err4 := ioutil.ReadFile(path) 53 | if err4 != nil { 54 | log.Fatalln(err4.Error()) 55 | return err4 56 | } 57 | 58 | isCompressed := false 59 | 60 | // Get MIME of correspond file 61 | var fileMime string 62 | if strings.HasSuffix(uriPath, ".js") { 63 | fileMime = "application/javascript" 64 | isCompressed = true 65 | } else if strings.HasSuffix(uriPath, ".css") { 66 | fileMime = "text/css" 67 | isCompressed = true 68 | } else if strings.HasSuffix(uriPath, ".svg") { 69 | fileMime = "image/svg+xml" 70 | isCompressed = true 71 | } else { 72 | fileMime = http.DetectContentType(fileData) 73 | isCompressed = (fileMime == "image/x-icon") || strings.HasPrefix(fileMime, "text/") || strings.HasPrefix(fileMime, "font/") 74 | } 75 | fileMime = strings.TrimSuffix(fileMime, "; charset=utf-8") 76 | fmt.Printf("\t- %s (%s)\n", uriPath, fileMime) 77 | 78 | // If supposed to not compressed, then just add into map and skip compression process 79 | if !isCompressed { 80 | loaded[uriPath] = fileModel{ 81 | data: fileData, 82 | mime: fileMime, 83 | isCompressed: false, 84 | } 85 | return nil 86 | } 87 | 88 | // Do GZIP compression 89 | var compressedData bytes.Buffer 90 | compressor, err5 := gzip.NewWriterLevel(&compressedData, gzip.BestCompression) 91 | if err5 != nil { 92 | log.Fatalln(err5.Error()) 93 | return err5 94 | } 95 | _, err6 := compressor.Write(fileData) 96 | if err6 != nil { 97 | log.Fatalln(err6.Error()) 98 | return err6 99 | } 100 | compressor.Close() 101 | 102 | // Add compressed file to map 103 | loaded[uriPath] = fileModel{ 104 | data: compressedData.Bytes(), 105 | mime: fileMime, 106 | isCompressed: true, 107 | } 108 | 109 | return nil 110 | }) 111 | 112 | // Assign the created file map to a global pointer, then report via CLI 113 | pathMap = &loaded 114 | fmt.Println("") 115 | fmt.Println("All Files Loaded!") 116 | fmt.Println("") 117 | } 118 | -------------------------------------------------------------------------------- /load_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io/ioutil" 7 | "path/filepath" 8 | "testing" 9 | ) 10 | 11 | func TestLoad(t *testing.T) { 12 | publicPath := filepath.Join("./", "test") 13 | load(publicPath) 14 | 15 | expectations := map[string]fileModel{ 16 | "index.html": { 17 | data: loadFileFromDisk(t, "index.html", true), 18 | mime: "text/html", 19 | isCompressed: true, 20 | }, 21 | "index.css": { 22 | data: loadFileFromDisk(t, "index.css", true), 23 | mime: "text/css", 24 | isCompressed: true, 25 | }, 26 | "index.js": { 27 | data: loadFileFromDisk(t, "index.js", true), 28 | mime: "application/javascript", 29 | isCompressed: true, 30 | }, 31 | "roboto.ttf": { 32 | data: loadFileFromDisk(t, "roboto.ttf", true), 33 | mime: "font/ttf", 34 | isCompressed: true, 35 | }, 36 | "test.jpg": { 37 | data: loadFileFromDisk(t, "test.jpg", false), 38 | mime: "image/jpeg", 39 | isCompressed: false, 40 | }, 41 | "test.png": { 42 | data: loadFileFromDisk(t, "test.png", false), 43 | mime: "image/png", 44 | isCompressed: false, 45 | }, 46 | "test.svg": { 47 | data: loadFileFromDisk(t, "test.svg", true), 48 | mime: "image/svg+xml", 49 | isCompressed: true, 50 | }, 51 | "with_index/index.html": { 52 | data: loadFileFromDisk(t, "with_index/index.html", true), 53 | mime: "text/html", 54 | isCompressed: true, 55 | }, 56 | "with_index/wkwkwk@&$!-_#=+.jpg": { 57 | data: loadFileFromDisk(t, "with_index/wkwkwk@&$!-_#=+.jpg", false), 58 | mime: "image/jpeg", 59 | isCompressed: false, 60 | }, 61 | "without-index/never gonna give you up.jpg": { 62 | data: loadFileFromDisk(t, "without-index/never gonna give you up.jpg", false), 63 | mime: "image/jpeg", 64 | isCompressed: false, 65 | }, 66 | } 67 | 68 | for eachKey, eachFile := range *pathMap { 69 | eachExpect, ok := expectations[eachKey] 70 | if !ok { 71 | t.Errorf("\"%s\" is exist in pathMap but not exist on disk", eachKey) 72 | continue 73 | } 74 | if eachFile.mime != eachExpect.mime { 75 | t.Errorf("\"%s\" has \"%s\" MIME, but expected \"%s\"", eachKey, eachFile.mime, eachExpect.mime) 76 | continue 77 | } 78 | if eachFile.isCompressed != eachExpect.isCompressed { 79 | t.Errorf("\"%s\" has \"%t\" compression, but expected \"%t\"", eachKey, eachFile.isCompressed, eachExpect.isCompressed) 80 | continue 81 | } 82 | if string(eachFile.data) != string(eachExpect.data) { 83 | t.Errorf("\"%s\" has different data from original file", eachKey) 84 | } 85 | } 86 | } 87 | 88 | // Test helper, it helps load manually from disk as comparation 89 | func loadFileFromDisk(t *testing.T, relativePath string, isCompress bool) []byte { 90 | // Get public path 91 | publicPath := filepath.Join("./", "test") 92 | 93 | // Absolute path of file 94 | path := filepath.Join(publicPath, relativePath) 95 | 96 | // Read file from disk, directly return the result if isCompress=false 97 | result, err2 := ioutil.ReadFile(path) 98 | if !isCompress { 99 | return result 100 | } 101 | if err2 != nil { 102 | t.Fatal(err2) 103 | return nil 104 | } 105 | 106 | // Compress if isCompress=true 107 | var compressedResult bytes.Buffer 108 | compressor, err3 := gzip.NewWriterLevel(&compressedResult, gzip.BestCompression) 109 | if err3 != nil { 110 | t.Fatal(err3) 111 | return nil 112 | } 113 | _, err4 := compressor.Write(result) 114 | if err4 != nil { 115 | t.Fatal(err4) 116 | return nil 117 | } 118 | compressor.Close() 119 | 120 | return compressedResult.Bytes() 121 | } 122 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | // Global variables 13 | var pathMap *map[string]fileModel = nil 14 | var origins string = "" 15 | 16 | // Main entry 17 | func main() { 18 | // Initialize required parameters 19 | domain := "localhost" 20 | port := "8080" 21 | portTLS := "" 22 | certFile := "" 23 | keyFile := "" 24 | 25 | // Get parameters from flags 26 | flag.StringVar(&domain, "domain", domain, helpArgs["domain"]) 27 | flag.StringVar(&port, "port", port, helpArgs["port"]) 28 | flag.StringVar(&origins, "origins", origins, helpArgs["origins"]) 29 | flag.StringVar(&portTLS, "port-tls", portTLS, helpArgs["port-tls"]) 30 | flag.StringVar(&certFile, "cert", certFile, helpArgs["cert"]) 31 | flag.StringVar(&keyFile, "key", keyFile, helpArgs["key"]) 32 | flag.Parse() 33 | 34 | // If user doesn't put required arguments, then show help info 35 | if flag.NArg() == 0 { 36 | help() 37 | return 38 | } 39 | 40 | // Public root directory 41 | dir := flag.Args()[0] 42 | 43 | // If using TLS, check for certificate and secret key availability 44 | if len(portTLS) > 0 && len(keyFile) == 0 { 45 | fmt.Println("You forgot \"--key=...\" flag!") 46 | return 47 | } 48 | if len(portTLS) > 0 && len(certFile) == 0 { 49 | fmt.Println("You forgot \"--cert=...\" flag!") 50 | return 51 | } 52 | 53 | // Load all files at public directory to RAM 54 | load(dir) 55 | 56 | // Check if currently working on local or production environtment 57 | domainAsIP, _ := fasthttp.ParseIPv4(net.IP{}, []byte(domain)) 58 | isLocal := (domain == "localhost") || (domainAsIP != nil) 59 | 60 | // Pre-process origins 61 | if isLocal { 62 | if len(portTLS) > 0 { 63 | origins = domain + ":" + portTLS + "," + origins 64 | } else { 65 | origins = domain + ":" + port + "," + origins 66 | } 67 | } else { 68 | origins = domain + "," + origins 69 | } 70 | 71 | fmt.Printf("Kuda is listening at %s port...\n", port) 72 | 73 | // Configure server connection and only allow GET method 74 | server := fasthttp.Server{ 75 | Handler: handler, 76 | ErrorHandler: errorHandler, 77 | GetOnly: true, 78 | DisablePreParseMultipartForm: true, 79 | DisableHeaderNamesNormalizing: true, 80 | } 81 | 82 | // Remember, emptied port-tls means disabled TLS 83 | var err error = nil 84 | if len(portTLS) > 0 { 85 | fmt.Printf("Kuda also securely listening at %s port...\n", portTLS) 86 | 87 | // Listen for incoming HTTP request and redirect them to HTTPS 88 | go fasthttp.ListenAndServe(":"+port, func(ctx *fasthttp.RequestCtx) { 89 | path := string(ctx.URI().Path()) 90 | if isLocal { 91 | ctx.Redirect("https://"+domain+":"+portTLS+"/"+path, 302) 92 | } else { 93 | ctx.Redirect("https://"+domain+"/"+path, 302) 94 | } 95 | }) 96 | 97 | // Listen for incoming HTTPS request: 98 | err = server.ListenAndServeTLS(":"+portTLS, certFile, keyFile) 99 | } else { 100 | // Listen for incoming HTTP request: 101 | err = server.ListenAndServe(":" + port) 102 | } 103 | 104 | log.Fatalln(err) 105 | } 106 | -------------------------------------------------------------------------------- /test-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC+jCCAeKgAwIBAgIUdkGpRu5gjCrqdPEpwDekuQBWjYgwDQYJKoZIhvcNAQEL 3 | BQAwHzEdMBsGA1UEAwwUdGhvcng4Ni11YnVudHUubG9jYWwwHhcNMjAxMDI3MTAy 4 | NzA1WhcNMzAxMDI1MTAyNzA1WjAfMR0wGwYDVQQDDBR0aG9yeDg2LXVidW50dS5s 5 | b2NhbDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANWm7EbGAAAXyGdm 6 | W5/E19dT2SDffk03UIO1mGLBrjzYi6ZnAJpxfQ77KdBgult6/sFx6osRzgFG2JIl 7 | BQ7i+5tOs+MB6EnOBy2FJ2z84OVkb5KdoKJ/fVw0oI6SAqVTZdfLYPYAKdIy49nP 8 | SJTpRya5cVTN/QJoVtnIYUZxmJ04ifS8Y3R2vgS93yMS41O+iAJxoeJgw0Fa/1vX 9 | 7Zsdk265fJ/rL5clV4/QoNwd0E7f91dgA4cPhSNe7m1qw1aVRVRjJZshNGv0IP6g 10 | L6J8MDwHQMmQ86/yvGkFJ2eAacSPKT5KacwRRLNR+Bmh3rjD/5p6xowWJRypXWKz 11 | 0npEbuUCAwEAAaMuMCwwCQYDVR0TBAIwADAfBgNVHREEGDAWghR0aG9yeDg2LXVi 12 | dW50dS5sb2NhbDANBgkqhkiG9w0BAQsFAAOCAQEAKkScmJT3HnLhbow01TJNFQIt 13 | gQmihReMHiUDec6wlhyOMvrfBJlNLI96d+kPgTQ9cTlVGWMqe8ZDvN2N3hf/fFQP 14 | WzC2J7HlB+7VdIRwcBC38EzFFE0Agp3ePItp4oDlKlwq12og6MZbo+zDGu4U6FFQ 15 | uMN9wtgWnPONlJElBlaf7pde4gGOOL54rfDdxKek8K+PusltgOVL2PbXXT3AEz77 16 | 45uphR9ihsv8QNSrVAkFavKHWCNhszaAiWKAdMXVtw70WxSeyvTWtr6nLKLVwhzy 17 | 9iJqiLIf0kfMLVFh+rJejENebda5VToQwpkpDShbEIxaIIJ+3NWDVf0Vpko7IA== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /test-secret.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVpuxGxgAAF8hn 3 | ZlufxNfXU9kg335NN1CDtZhiwa482IumZwCacX0O+ynQYLpbev7BceqLEc4BRtiS 4 | JQUO4vubTrPjAehJzgcthSds/ODlZG+SnaCif31cNKCOkgKlU2XXy2D2ACnSMuPZ 5 | z0iU6UcmuXFUzf0CaFbZyGFGcZidOIn0vGN0dr4Evd8jEuNTvogCcaHiYMNBWv9b 6 | 1+2bHZNuuXyf6y+XJVeP0KDcHdBO3/dXYAOHD4UjXu5tasNWlUVUYyWbITRr9CD+ 7 | oC+ifDA8B0DJkPOv8rxpBSdngGnEjyk+SmnMEUSzUfgZod64w/+aesaMFiUcqV1i 8 | s9J6RG7lAgMBAAECggEAB0G+MME06svkAK8XWZvkAOpWK6mrro+8alzWqjQN6P8p 9 | 7yL8gEJujv208+D1M3pAAO4Pm12lc6GmZYBgZZsMFMBdl85Ox4L592/YYPlN4jzB 10 | FWfJNvvBlEotUepfsKHeia/cwT5MLVmRJ6rEuloaEpSl5s2AVH6axJbrxurA1kYv 11 | djfgwUmsQpryfX4VTRMoQTVocStSKhiKIANDbqDfsV1LrhfWIY+lzHigcdAPqXtO 12 | abrogRxbQ5EQ4Pu24CNUh9ggMkLDBIt+K/DWszg94IJyucoDc6HSRqSGseKDH9v3 13 | Qu58bcihxp01gnTNlJGShRu1KW6j1kXkmLc21u3/mQKBgQDqyCZIzawHsZI3LwMF 14 | oJb5pEmGCumxxTfHFigv5fLJbZYHh+O4NH0Ei8IbE7PuhVXmFlkeAaryOHJ7tcNA 15 | Hgs4EUeEuMi18rtBKYzKWWrhlD4tgia8dALr3+9r00kk1zXhTB3LCLxgUAhccsBb 16 | b6iWLO7u/j1HcW7pfLb3mi/xkwKBgQDo9euONkSrQKGGUky6JS+u/KvxrwLCqK2o 17 | 6cDTV0nNqD3JzrY/VRzITofcrPv/gtCxOJg8O3YC8N2tIk9vdQIKIw+pz1/v8puy 18 | ICwptNVg+EBO0vICJI84rRqjnFt+nd7JkV6bDUXDH403aA1wuiORJCWv3IhyYTJN 19 | armDdJHIpwKBgACKe4QZbTcLUrHr2s7tgsuKdFzps4YkVS+RPoB1wCx0oy7bQkwP 20 | WuZiyAy16+k1x2/jR0tJZ7JtLN5aGdwQ9aeoaojEwDmjGIla5iclM5jXdZk3Po2a 21 | mtMeTffqobWDBW3CdiMcnwp9xLcp0IWlaTcHXXmRfmSmv0GsduN1KH0fAoGBANTa 22 | HWfK3eM7IlqAR+qsn2zbIJ+qsHL7e/Ch47U2RBDMb+g8Hviu46WBW1GeHIHRHK1Q 23 | cIhYK/Nz8JeIidvkpQBuGJmnCJlMqkWOb8uLlosLSHa89rJOhS3bvENRUafWxwCU 24 | +kPhVKR756OARweAi47J4EVW0rTvae6iXO5nS+xxAoGAP4M2GB4BvSaaddDDyt+F 25 | hctGgoieiRZ98VLFHOTG14EmRK6514UYmIpZsarwlIPquswSmPB5/5ElCHDCPuI6 26 | 55gDFPyfCJZVZAkIFfMuePfjjMbXzoqhby9/PkMxgecu5StwzNxFGtVllB+QMfxs 27 | ujriEk8M2fqcSl9avP7Paxo= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/index.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: roboto; 3 | src: url(roboto.ttf) 4 | } 5 | 6 | body { 7 | font-family: roboto; 8 | } 9 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kuda Unit Testing 5 | 6 | 7 | 8 |

This is Kuda Unit Testing

9 |

10 | It's making sure every changes that contributors made 11 | are not breaking the next release. 12 |

13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | alert("The script is working") 2 | -------------------------------------------------------------------------------- /test/roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/test/roboto.ttf -------------------------------------------------------------------------------- /test/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/test/test.jpg -------------------------------------------------------------------------------- /test/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/test/test.png -------------------------------------------------------------------------------- /test/test.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 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 | 42 | -------------------------------------------------------------------------------- /test/with_index/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Kuda Unit Testing 5 | 6 | 7 |

Successfully Indexed!

8 |

9 | The /with_index represents this page 10 | because it has its own index.html 11 |

12 | 13 | -------------------------------------------------------------------------------- /test/with_index/wkwkwk@&$!-_#=+.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/test/with_index/wkwkwk@&$!-_#=+.jpg -------------------------------------------------------------------------------- /test/without-index/never gonna give you up.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thor-x86/kuda/c251b30d2b44970b67b80fd9d7a2c542d212f66d/test/without-index/never gonna give you up.jpg --------------------------------------------------------------------------------