├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api ├── auth.go ├── ctx.go ├── interceptor.go ├── pb │ ├── README.md │ ├── auth.pb.go │ ├── auth.pb.gw.go │ ├── auth.proto │ ├── auth.swagger.json │ ├── auth_grpc.pb.go │ ├── google │ │ ├── api │ │ │ ├── annotations.proto │ │ │ ├── field_behavior.proto │ │ │ ├── http.proto │ │ │ └── httpbody.proto │ │ └── rpc │ │ │ ├── code.proto │ │ │ ├── error_details.proto │ │ │ └── status.proto │ ├── network.pb.go │ ├── network.pb.gw.go │ ├── network.proto │ ├── network.swagger.json │ ├── network_grpc.pb.go │ ├── user.pb.go │ ├── user.pb.gw.go │ ├── user.proto │ ├── user.swagger.json │ ├── user_grpc.pb.go │ ├── vpn.pb.go │ ├── vpn.pb.gw.go │ ├── vpn.proto │ ├── vpn.swagger.json │ └── vpn_grpc.pb.go ├── rest.go └── rpc.go ├── build.sh ├── cmd ├── ovpm │ ├── action_net.go │ ├── action_user.go │ ├── action_vpn.go │ ├── cmd_net.go │ ├── cmd_user.go │ ├── cmd_vpn.go │ ├── common_test.go │ ├── global.go │ ├── main.go │ ├── main_test.go │ ├── net_test.go │ ├── user_test.go │ ├── utils.go │ └── vpn_test.go └── ovpmd │ └── main.go ├── const.go ├── contrib ├── deb-repo-config ├── systemd │ ├── ovpmd.service.rhel │ └── ovpmd.service.ubuntu └── yumrepo.repo ├── db.go ├── db_test.go ├── doc.go ├── errors ├── application.go ├── cli.go ├── error.go └── system.go ├── generate.go ├── go.mod ├── go.sum ├── net.go ├── net_test.go ├── nfpm.yaml ├── parselog.go ├── parselog_test.go ├── perms.go ├── permset ├── permset.go └── permset_test.go ├── pki ├── const.go ├── pki.go └── pki_test.go ├── scripts ├── postinstall.sh ├── postremove.sh ├── postupgrade.sh ├── preinstall.sh └── preremove.sh ├── supervisor └── supervisor.go ├── templates.go ├── user.go ├── user_internal_test.go ├── user_test.go ├── vpn.go ├── vpn_test.go └── webui └── ovpm ├── .env ├── .eslintrc.json ├── .gitignore ├── README.md ├── package.json ├── public ├── css │ ├── bootstrap.min.css │ └── mui.min.css ├── fonts │ └── glyphicons-halflings-regular.woff ├── index.html ├── js │ └── mui.min.js ├── logo192.png ├── manifest.json └── robots.txt ├── src ├── App.jsx ├── App.test.js ├── api.js ├── components │ ├── Auth │ │ ├── Login │ │ │ └── index.jsx │ │ ├── LoginRequired │ │ │ └── index.jsx │ │ └── Logout │ │ │ └── index.jsx │ ├── Dashboard │ │ ├── AdminDashboard │ │ │ ├── NetworkEdit │ │ │ │ └── index.jsx │ │ │ ├── UserEdit │ │ │ │ └── index.jsx │ │ │ ├── UserPicker │ │ │ │ └── index.jsx │ │ │ └── index.jsx │ │ ├── UserDashboard │ │ │ ├── PasswordEdit │ │ │ │ └── index.jsx │ │ │ └── index.jsx │ │ └── index.jsx │ └── Master │ │ ├── index.jsx │ │ └── style.css ├── index.css ├── index.js ├── serviceWorker.js ├── setupTests.js └── utils │ ├── auth.js │ └── restClient.js └── yarn.lock /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master, dev ] 6 | pull_request: 7 | branches: [ master, dev ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-18.04 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: '^1.16' 17 | 18 | - name: Get the version 19 | id: get_version 20 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/} 21 | 22 | - name: Deps 23 | run: | 24 | sudo apt-get update -y 25 | sudo apt install reprepro createrepo rsync openvpn -y 26 | mkdir -p /tmp/protoc 27 | pushd /tmp/protoc 28 | wget https://github.com/protocolbuffers/protobuf/releases/download/v3.15.7/protoc-3.15.7-linux-x86_64.zip 29 | unzip protoc-3.15.7-linux-x86_64.zip 30 | popd 31 | sudo chmod +x /tmp/protoc/bin/protoc 32 | sudo cp /tmp/protoc/bin/protoc /usr/bin/protoc 33 | sudo cp -r /tmp/protoc/include/* /usr/local/include/ 34 | sudo chmod -R 777 /usr/local/include/google 35 | sudo apt-get install autoconf automake libtool curl make g++ unzip -y 36 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 37 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 38 | go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest 39 | go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest 40 | go install github.com/kevinburke/go-bindata/go-bindata@latest 41 | go install github.com/goreleaser/nfpm/cmd/nfpm@latest 42 | 43 | - name: Bundle 44 | run: make bundle 45 | 46 | - name: Test 47 | run: make test -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-18.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: '^1.16' 14 | 15 | - name: Get the version 16 | id: get_version 17 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/} 18 | 19 | - name: Deps 20 | run: | 21 | sudo apt-get update -y 22 | sudo apt install reprepro createrepo rsync openvpn -y 23 | mkdir -p /tmp/protoc 24 | pushd /tmp/protoc 25 | wget https://github.com/protocolbuffers/protobuf/releases/download/v3.15.7/protoc-3.15.7-linux-x86_64.zip 26 | unzip protoc-3.15.7-linux-x86_64.zip 27 | popd 28 | sudo chmod +x /tmp/protoc/bin/protoc 29 | sudo cp /tmp/protoc/bin/protoc /usr/bin/protoc 30 | sudo cp -r /tmp/protoc/include/* /usr/local/include/ 31 | sudo chmod -R 777 /usr/local/include/google 32 | sudo apt-get install autoconf automake libtool curl make g++ unzip -y 33 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 34 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 35 | go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest 36 | go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest 37 | go install github.com/kevinburke/go-bindata/go-bindata@latest 38 | go install github.com/goreleaser/nfpm/cmd/nfpm@latest 39 | 40 | - name: Dist 41 | run: VERSION=${{ steps.get_version.outputs.VERSION }} make dist 42 | 43 | - name: Make DEB Repo 44 | run: | 45 | mkdir -p ./repo/deb/conf 46 | cp ./contrib/deb-repo-config ./repo/deb/conf/distributions 47 | cp ./dist/*.deb ./repo/deb 48 | reprepro -b ./repo/deb/ includedeb ovpm ./repo/deb/*.deb 49 | 50 | - name: Make RPM Repo 51 | run: | 52 | mkdir -p ./repo/rpm/ 53 | cp ./contrib/yumrepo.repo ./repo/rpm/ovpm.repo 54 | cp ./dist/*.rpm ./repo/rpm 55 | createrepo --database ./repo/rpm 56 | 57 | - name: Publish to GithubPages 58 | uses: JamesIves/github-pages-deploy-action@4.1.1 59 | with: 60 | token: ${{ secrets.GITHUB_TOKEN }} 61 | branch: gh-pages 62 | folder: repo -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | compress 3 | data_import/ 4 | *.db 5 | *.py[cod] 6 | production.conf 7 | credentialsrc 8 | coverage.txt 9 | bin/ 10 | bundle/ 11 | 12 | # C extensions 13 | .#* 14 | *.c 15 | *.so 16 | 17 | # Packages 18 | *.egg 19 | *.egg-info 20 | dist 21 | build 22 | eggs 23 | parts 24 | var 25 | sdist 26 | develop-eggs 27 | .installed.cfg 28 | 29 | *_build 30 | *_static 31 | *_templates 32 | 33 | 34 | # Installer logs 35 | pip-log.txt 36 | 37 | # Unit test / coverage reports 38 | .coverage 39 | .tox 40 | nosetests.xml 41 | htmlcov 42 | *.dat 43 | *.tfstate 44 | *.tfstate.backup 45 | 46 | # Docs 47 | doc/_build 48 | 49 | # Translations 50 | *.mo 51 | 52 | # Idea 53 | .idea 54 | 55 | # System 56 | .DS_Store 57 | 58 | # VIM swap files 59 | .*.swp 60 | 61 | # VIM temp files 62 | *~ 63 | .venv 64 | .DS_Store 65 | *.log 66 | node_modules 67 | build 68 | *.node 69 | coverage 70 | *.orig 71 | .idea 72 | .cache 73 | sandbox 74 | test/out-fixtures/* 75 | test/watch-*.txt 76 | gulp.1 77 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:latest 2 | LABEL maintainer="Mustafa Arici (mustafa@arici.io)" 3 | 4 | # Deps 5 | RUN dnf install -y git make yarnpkg nodejs protobuf-compiler protobuf-static openvpn golang 6 | RUN go get golang.org/dl/go1.16.3 7 | RUN $(go env GOPATH)/bin/go1.16.3 download 8 | 9 | RUN $(go env GOPATH)/bin/go1.16.3 install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ 10 | $(go env GOPATH)/bin/go1.16.3 install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest && \ 11 | $(go env GOPATH)/bin/go1.16.3 install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest && \ 12 | $(go env GOPATH)/bin/go1.16.3 install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest && \ 13 | $(go env GOPATH)/bin/go1.16.3 install github.com/kevinburke/go-bindata/go-bindata@latest && \ 14 | $(go env GOPATH)/bin/go1.16.3 install github.com/goreleaser/nfpm/cmd/nfpm@latest 15 | 16 | RUN dnf install -y which iptables 17 | RUN echo "alias go=$(go env GOPATH)/bin/go1.16.3" >> /root/.bashrc 18 | RUN echo "export PATH=$PATH:$(go env GOPATH)/bin" >> /root/.bashrc 19 | 20 | VOLUME /app 21 | 22 | WORKDIR /app -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: deps build test bundle-webui clean-bundle bundle-swagger proto bundle build 2 | 3 | # Runs unit tests. 4 | test: 5 | go test -count=1 -race -coverprofile=coverage.txt -covermode=atomic . 6 | 7 | proto: 8 | protoc -I./api/pb/ -I/usr/local/include/ --go_opt=paths=source_relative --go_out=./api/pb user.proto vpn.proto network.proto auth.proto 9 | protoc -I./api/pb/ -I/usr/local/include/ --go-grpc_opt=paths=source_relative --go-grpc_out=./api/pb user.proto vpn.proto network.proto auth.proto 10 | protoc -I./api/pb/ -I/usr/local/include/ --grpc-gateway_out ./api/pb \ 11 | --grpc-gateway_opt logtostderr=true \ 12 | --grpc-gateway_opt paths=source_relative \ 13 | --grpc-gateway_opt generate_unbound_methods=true \ 14 | user.proto vpn.proto network.proto auth.proto 15 | 16 | clean-bundle: 17 | @echo Cleaning up bundle/ 18 | rm -rf bundle/ 19 | mkdir -p bundle/ 20 | 21 | bundle-webui: 22 | @echo Bundling webui 23 | yarn --cwd webui/ovpm/ install 24 | yarn --cwd webui/ovpm/ build 25 | cp -r webui/ovpm/build/* bundle 26 | 27 | bundle-swagger: proto 28 | protoc -I./api/pb -I/usr/local/include/ --openapiv2_out=json_names_for_fields=false:./api/pb --openapiv2_opt logtostderr=true user.proto vpn.proto network.proto auth.proto 29 | 30 | bundle: clean-bundle bundle-webui bundle-swagger 31 | go-bindata -pkg bundle -o bundle/bindata.go bundle/... 32 | 33 | # Builds server and client binaries under ./bin folder. Accetps $VERSION env var. 34 | build: bundle 35 | @echo Building 36 | rm -rf bin/ 37 | mkdir -p bin/ 38 | #CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -X 'github.com/cad/ovpm.Version=$(VERSION)' -extldflags '-static'" -o ./bin/ovpm ./cmd/ovpm 39 | #CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -X 'github.com/cad/ovpm.Version=$(VERSION)' -extldflags '-static'" -o ./bin/ovpmd ./cmd/ovpmd 40 | 41 | # Link dynamically for now 42 | CGO_CFLAGS="-g -O2 -Wno-return-local-addr" go build -ldflags="-X 'github.com/cad/ovpm.Version=$(VERSION)'" -o ./bin/ovpm ./cmd/ovpm 43 | CGO_CFLAGS="-g -O2 -Wno-return-local-addr" go build -ldflags="-X 'github.com/cad/ovpm.Version=$(VERSION)'" -o ./bin/ovpmd ./cmd/ovpmd 44 | 45 | clean-dist: 46 | rm -rf dist/ 47 | mkdir -p dist/ 48 | 49 | # Builds rpm and dep packages under ./dist folder. Accepts $VERSION env var. 50 | dist: clean-dist build 51 | @echo Generating VERSION=$(VERSION) rpm and deb packages under dist/ 52 | nfpm pkg -t ./dist/ovpm.rpm 53 | nfpm pkg -t ./dist/ovpm.deb 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OVPM - OpenVPN Management Server 2 | 3 | ![Build Status](https://github.com/cad/ovpm/workflows/Go/badge.svg) 4 | [![GitHub version](https://badge.fury.io/gh/cad%2Fovpm.svg)](https://badge.fury.io/gh/cad%2Fovpm) 5 | [![codecov](https://codecov.io/gh/cad/ovpm/branch/master/graph/badge.svg)](https://codecov.io/gh/cad/ovpm) 6 | [![GoDoc](https://godoc.org/github.com/cad/ovpm?status.svg)](https://godoc.org/github.com/cad/ovpm) 7 | 8 | *OVPM* allows you to administrate an **OpenVPN** server on linux easily via command line and web interface. 9 | 10 | With OVPM you can create and run an OpenVPN server, add/remove VPN users, generate client .ovpn files for your users etc. 11 | 12 | *This software is not stable yet. We recommend against using it for anything serious until, version 1.0 is released.* 13 | 14 | **NOTICE: Version 0.2.8 comes with `comp-lzo` option disabled by default as it is deprecated by OpenVPN.** 15 | 16 | **Roadmap** 17 | 18 | - [x] OpenVPN management functionality 19 | - [x] User management functionality 20 | - [x] Network management functionality 21 | - [x] Command Line Interface (CLI) 22 | - [x] API (REST and gRPC) 23 | - [x] Web User Interface (WebUI) 24 | - [ ] Import/Export/Backup OVPM config 25 | - [ ] Effortless client profile (.ovpn file) delivery over Web 26 | - [ ] Monitoring and Quota functionality 27 | 28 | **Demo** 29 | Here is a little demo of what it looks on terminal to init the server, create a vpn user and generate **.ovpn** file for the created user. 30 | 31 | [![asciicast](https://asciinema.org/a/136016.png)](https://asciinema.org/a/136016) 32 | 33 | 34 | ## Installation 35 | **from RPM (CentOS/Fedora):** 36 | 37 | ```bash 38 | # Add YUM Repo 39 | $ sudo yum install yum-utils -y 40 | $ sudo yum install epel-release -y 41 | $ sudo yum-config-manager --add-repo https://cad.github.io/ovpm/rpm/ovpm.repo 42 | 43 | # Install OVPM 44 | $ sudo yum install ovpm 45 | 46 | # Enable and start ovpmd service 47 | $ systemctl start ovpmd 48 | $ systemctl enable ovpmd 49 | ``` 50 | 51 | **from DEB (Ubuntu/Debian):** 52 | 53 | This is tested only on Ubuntu >=16.04.3 LTS 54 | 55 | ```bash 56 | # Add APT Repo 57 | $ sudo sh -c 'echo "deb [trusted=yes] https://cad.github.io/ovpm/deb/ ovpm main" >> /etc/apt/sources.list' 58 | $ sudo apt update 59 | 60 | # Install OVPM 61 | $ sudo apt install ovpm 62 | 63 | # Enable and start ovpmd service 64 | $ systemctl start ovpmd 65 | $ systemctl enable ovpmd 66 | ``` 67 | 68 | **from Source (go get):** 69 | 70 | Only dependency for ovpm is **OpenVPN>=2.3.3**. 71 | 72 | ```bash 73 | $ go get -u github.com/cad/ovpm/... 74 | 75 | # Make sure user nobody and group nogroup is available 76 | # on the system 77 | $ sudo useradd nobody 78 | $ sudo groupadd nogroup 79 | 80 | # Start ovpmd on a seperate terminal 81 | $ sudo ovpmd 82 | ``` 83 | 84 | Now ovpmd should be running. 85 | 86 | ## Quickstart 87 | Create a vpn user and export vpn profile for the created user. 88 | 89 | ```bash 90 | # We should init the server after fresh install 91 | $ ovpm vpn init --hostname 92 | INFO[0004] ovpm server initialized 93 | 94 | # Now, lets create a new vpn user 95 | $ ovpm user create -u joe -p verySecretPassword 96 | INFO[0000] user created: joe 97 | 98 | # Finally export the vpn profile for, the created user, joe 99 | $ ovpm user genconfig -u joe 100 | INFO[0000] exported to joe.ovpn 101 | ``` 102 | 103 | OpenVPN profile for user joe is exported to joe.ovpn file. 104 | You can simply use this file with OpenVPN to connect to the vpn server from 105 | another computer. 106 | 107 | 108 | # Next Steps 109 | 110 | * [User Management](https://github.com/cad/ovpm/wiki/User-Management) 111 | * [Network Management](https://github.com/cad/ovpm/wiki/Network-Management) 112 | * [Web Interface](https://github.com/cad/ovpm/wiki/Web-Interface) 113 | 114 | # Troubleshooting 115 | 116 | ## Q: My clients cannot connect to VPN after updating OVPM to v0.2.8 117 | 118 | Since `comp-lzo` is disabled by default in OVPM v0.2.8, existing clients' .ovpn profiles became invalid. 119 | 120 | In order to solve this you have the options below: 121 | 122 | * Generate new .ovpn profile for existing clients 123 | * Or manually remove `comp-lzo` line from clients .ovpn profiles yourself. 124 | * Or you can upgrade to v0.2.9 and enable lzo option back by invoking the following command. 125 | ```bash 126 | $ ovpm vpn update --enable-use-lzo 127 | ``` 128 | But please note that this is not recommended as lzo option is [deprecated](https://community.openvpn.net/openvpn/wiki/DeprecatedOptions?__cf_chl_jschl_tk__=0468cbb180cdf21ca5119b591d260538cf788d30-1595873970-0-AY1Yn79gf57uYv2hrAKPwvzk-xuDvhY79eHrxJqWw1hpbapF-XgOJSsglI70HxmV78LDzJSz7m_A7eDhvzo_hCM-tx4UB7PfccKTtoHATGrOBqq4mHDhggN_EwJ7yee3fIzLgc9kvhL9pOCiISlE3NpbC0SOX21tYwFs1njdpOVGG4dHLMyudNKRGexapsQxiD2i23r30i_dzqS12QobGvPe96CuWS84ARjIRAUlutT6t5SxkccyOBunduDnbgYoB7RN8x7ab8y8Paim9ypizKiEHbxwP0Z2Y3lXByKdzHUUZSJzjzolHyRyQx-nSBuZQQ#Option:--comp-lzo) in OpenVPN. -------------------------------------------------------------------------------- /api/auth.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/cad/ovpm" 8 | "github.com/cad/ovpm/permset" 9 | "github.com/sirupsen/logrus" 10 | gcontext "golang.org/x/net/context" 11 | "google.golang.org/grpc" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/metadata" 14 | ) 15 | 16 | func authRequired(ctx gcontext.Context, req interface{}, handler grpc.UnaryHandler) (resp interface{}, err error) { 17 | logrus.Debugln("rpc: auth applied") 18 | token, err := authzTokenFromContext(ctx) 19 | if err != nil { 20 | logrus.Debugln("rpc: auth denied because token can not be gathered from header contest") 21 | return nil, grpc.Errorf(codes.Unauthenticated, err.Error()) 22 | } 23 | user, err := ovpm.GetUserByToken(token) 24 | if err != nil { 25 | logrus.Debugln("rpc: auth denied because user with this token can not be found") 26 | return nil, grpc.Errorf(codes.Unauthenticated, "access denied") 27 | } 28 | 29 | // Set user's permissions according to it's criteria. 30 | var permissions permset.Permset 31 | if user.IsAdmin() { 32 | permissions = permset.New(ovpm.AdminPerms()...) 33 | } else { 34 | permissions = permset.New(ovpm.UserPerms()...) 35 | } 36 | 37 | newCtx := NewUsernameContext(ctx, user.GetUsername()) 38 | newCtx = permset.NewContext(newCtx, permissions) 39 | return handler(newCtx, req) 40 | } 41 | 42 | func authzTokenFromContext(ctx gcontext.Context) (string, error) { 43 | // retrieve metadata from context 44 | md, ok := metadata.FromIncomingContext(ctx) 45 | if !ok { 46 | return "", fmt.Errorf("authentication required") 47 | } 48 | if len(md["authorization"]) != 1 { 49 | return "", fmt.Errorf("authentication required (length)") 50 | } 51 | 52 | authHeader := md["authorization"][0] 53 | 54 | // split authorization header into two 55 | splitToken := strings.Split(authHeader, "Bearer") 56 | if len(splitToken) != 2 { 57 | return "", fmt.Errorf("invalid Authorization header. it should be in the form of 'Bearer ': %s", authHeader) 58 | } 59 | // get token 60 | token := splitToken[1] 61 | token = strings.TrimSpace(token) 62 | return token, nil 63 | } 64 | -------------------------------------------------------------------------------- /api/ctx.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | gcontext "golang.org/x/net/context" 8 | ) 9 | 10 | type apiKey int 11 | 12 | const ( 13 | originTypeKey apiKey = iota 14 | userKey 15 | ) 16 | 17 | // OriginType indicates where the gRPC request actually came from. 18 | // 19 | // e.g. Is it from REST gateway? Or somewhere else? 20 | type OriginType int 21 | 22 | // Known origins 23 | const ( 24 | OriginTypeUnknown OriginType = iota 25 | OriginTypeREST 26 | ) 27 | 28 | // NewOriginTypeContext creates a new ctx from the OriginType. 29 | func NewOriginTypeContext(ctx gcontext.Context, originType OriginType) context.Context { 30 | return context.WithValue(ctx, originTypeKey, originType) 31 | } 32 | 33 | // GetOriginTypeFromContext returns the OriginType from context. 34 | func GetOriginTypeFromContext(ctx gcontext.Context) OriginType { 35 | originType, ok := ctx.Value(originTypeKey).(OriginType) 36 | if !ok { 37 | return OriginTypeUnknown 38 | } 39 | return originType 40 | } 41 | 42 | // NewUsernameContext creates a new ctx from username and returns it. 43 | func NewUsernameContext(ctx gcontext.Context, username string) context.Context { 44 | return context.WithValue(ctx, userKey, username) 45 | } 46 | 47 | // GetUsernameFromContext returns the Username from context. 48 | func GetUsernameFromContext(ctx gcontext.Context) (string, error) { 49 | username, ok := ctx.Value(userKey).(string) 50 | if !ok { 51 | return "", fmt.Errorf("cannot get context value") 52 | } 53 | return username, nil 54 | } 55 | -------------------------------------------------------------------------------- /api/interceptor.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/asaskevich/govalidator" 9 | "github.com/cad/ovpm" 10 | "github.com/cad/ovpm/permset" 11 | gcontext "golang.org/x/net/context" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/metadata" 14 | ) 15 | 16 | // AuthUnaryInterceptor is a interceptor function. 17 | // 18 | // See https://godoc.org/google.golang.org/grpc#UnaryServerInterceptor. 19 | func AuthUnaryInterceptor(ctx gcontext.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 20 | var enableAuthCheck bool 21 | 22 | md, ok := metadata.FromIncomingContext(ctx) 23 | if !ok { 24 | return nil, fmt.Errorf("Expected 2 metadata items in context; got %v", md) 25 | } 26 | 27 | // We enable auth check if we find a non-loopback 28 | // or invalid IP in the headers coming from the grpc-gateway. 29 | for _, userAgentIP := range md["x-forwarded-for"] { 30 | // Check if the remote user IP addr is a proper IP addr. 31 | if !govalidator.IsIP(userAgentIP) { 32 | enableAuthCheck = true 33 | logrus.Debugf("grpc request user agent ip can not be fetched from x-forwarded-for metadata, enabling auth check module '%s'", userAgentIP) 34 | break 35 | } 36 | 37 | // Check if the remote user IP addr is a loopback IP addr. 38 | if ip := net.ParseIP(userAgentIP); !ip.IsLoopback() { 39 | enableAuthCheck = true 40 | logrus.Debugf("grpc request user agent ips include non-loopback ip, enabling auth check module '%s'", userAgentIP) 41 | break 42 | } 43 | 44 | // TODO(cad): We assume gRPC endpoints are for cli only therefore 45 | // we are listening only on looback IP. 46 | // 47 | // But if we decide use gRPC endpoints publicly, we need to add 48 | // extra checks against gRPC remote peer IP to test if the request 49 | // is coming from a remote peer IP or also from a loopback ip. 50 | } 51 | 52 | if !enableAuthCheck { 53 | logrus.Debugf("rpc: auth-check not enabled: %s", md["x-forwarded-for"]) 54 | ctx = NewUsernameContext(ctx, "root") 55 | permissions := permset.New(ovpm.AdminPerms()...) 56 | ctx = permset.NewContext(ctx, permissions) 57 | } 58 | 59 | if enableAuthCheck { 60 | switch info.FullMethod { 61 | // AuthService methods 62 | case "/pb.AuthService/Status": 63 | return authRequired(ctx, req, handler) 64 | 65 | // UserService methods 66 | case "/pb.UserService/List": 67 | return authRequired(ctx, req, handler) 68 | case "/pb.UserService/Create": 69 | return authRequired(ctx, req, handler) 70 | case "/pb.UserService/Update": 71 | return authRequired(ctx, req, handler) 72 | case "/pb.UserService/Delete": 73 | return authRequired(ctx, req, handler) 74 | case "/pb.UserService/Renew": 75 | return authRequired(ctx, req, handler) 76 | case "/pb.UserService/GenConfig": 77 | return authRequired(ctx, req, handler) 78 | 79 | // VPNService methods 80 | case "/pb.VPNService/Status": 81 | return authRequired(ctx, req, handler) 82 | case "/pb.VPNService/Init": 83 | return authRequired(ctx, req, handler) 84 | case "/pb.VPNService/Update": 85 | return authRequired(ctx, req, handler) 86 | case "/pb.VPNService/Restart": 87 | return authRequired(ctx, req, handler) 88 | 89 | // NetworkService methods 90 | case "/pb.NetworkService/Create": 91 | return authRequired(ctx, req, handler) 92 | case "/pb.NetworkService/List": 93 | return authRequired(ctx, req, handler) 94 | case "/pb.NetworkService/Delete": 95 | return authRequired(ctx, req, handler) 96 | case "/pb.NetworkService/GetAllTypes": 97 | return authRequired(ctx, req, handler) 98 | case "/pb.NetworkService/GetAssociatedUsers": 99 | return authRequired(ctx, req, handler) 100 | case "/pb.NetworkService/Associate": 101 | return authRequired(ctx, req, handler) 102 | case "/pb.NetworkService/Dissociate": 103 | return authRequired(ctx, req, handler) 104 | default: 105 | logrus.Debugf("rpc: auth not required for endpoint: '%s'", info.FullMethod) 106 | } 107 | } 108 | return handler(ctx, req) 109 | } 110 | -------------------------------------------------------------------------------- /api/pb/README.md: -------------------------------------------------------------------------------- 1 | # OVPM API 2 | 3 | This package holds **gRPC** definition of the *OVPM* API. 4 | And there is also a REST API implementation that is generated automatically from gRPC proto files as well. REST APIs are automatically documented with swagger. 5 | 6 | | File | Description | 7 | |----------------------|------------------------------------------------| 8 | | user.proto | gRPC Service Definition for User Service | 9 | | network.proto | gRPC Service Definition for Network Service | 10 | | vpn.proto | gRPC Service Definition for VPN Service | 11 | | network.swagger.json | Swagger Specification for the Network REST API | 12 | | user.swagger.json | Swagger Specification for the User REST API | 13 | | vpn.swagger.json | Swagger Specification for the VPN REST API | 14 | 15 | -------------------------------------------------------------------------------- /api/pb/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | option go_package = "github.com/cad/ovpm/api/pb"; 3 | 4 | package pb; 5 | 6 | import "google/api/annotations.proto"; 7 | import "user.proto"; 8 | 9 | message AuthStatusRequest { 10 | } 11 | 12 | message AuthAuthenticateRequest { 13 | string username = 1; 14 | string password = 2; 15 | } 16 | 17 | service AuthService { 18 | rpc Status (AuthStatusRequest) returns (AuthStatusResponse) { 19 | option (google.api.http) = { 20 | get: "/api/v1/auth/status" 21 | //body: "*" 22 | };} 23 | 24 | rpc Authenticate (AuthAuthenticateRequest) returns (AuthAuthenticateResponse) { 25 | option (google.api.http) = { 26 | post: "/api/v1/auth/authenticate" 27 | body: "*" 28 | };} 29 | } 30 | 31 | message AuthStatusResponse { 32 | UserResponse.User user = 1; 33 | bool is_root = 2; 34 | } 35 | 36 | message AuthAuthenticateResponse { 37 | string token = 1; 38 | } 39 | -------------------------------------------------------------------------------- /api/pb/auth.swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "auth.proto", 5 | "version": "version not set" 6 | }, 7 | "tags": [ 8 | { 9 | "name": "AuthService" 10 | } 11 | ], 12 | "consumes": [ 13 | "application/json" 14 | ], 15 | "produces": [ 16 | "application/json" 17 | ], 18 | "paths": { 19 | "/api/v1/auth/authenticate": { 20 | "post": { 21 | "operationId": "AuthService_Authenticate", 22 | "responses": { 23 | "200": { 24 | "description": "A successful response.", 25 | "schema": { 26 | "$ref": "#/definitions/pbAuthAuthenticateResponse" 27 | } 28 | }, 29 | "default": { 30 | "description": "An unexpected error response.", 31 | "schema": { 32 | "$ref": "#/definitions/rpcStatus" 33 | } 34 | } 35 | }, 36 | "parameters": [ 37 | { 38 | "name": "body", 39 | "in": "body", 40 | "required": true, 41 | "schema": { 42 | "$ref": "#/definitions/pbAuthAuthenticateRequest" 43 | } 44 | } 45 | ], 46 | "tags": [ 47 | "AuthService" 48 | ] 49 | } 50 | }, 51 | "/api/v1/auth/status": { 52 | "get": { 53 | "operationId": "AuthService_Status", 54 | "responses": { 55 | "200": { 56 | "description": "A successful response.", 57 | "schema": { 58 | "$ref": "#/definitions/pbAuthStatusResponse" 59 | } 60 | }, 61 | "default": { 62 | "description": "An unexpected error response.", 63 | "schema": { 64 | "$ref": "#/definitions/rpcStatus" 65 | } 66 | } 67 | }, 68 | "tags": [ 69 | "AuthService" 70 | ] 71 | } 72 | } 73 | }, 74 | "definitions": { 75 | "UserResponseUser": { 76 | "type": "object", 77 | "properties": { 78 | "username": { 79 | "type": "string" 80 | }, 81 | "server_serial_number": { 82 | "type": "string" 83 | }, 84 | "cert": { 85 | "type": "string" 86 | }, 87 | "created_at": { 88 | "type": "string" 89 | }, 90 | "ip_net": { 91 | "type": "string" 92 | }, 93 | "no_gw": { 94 | "type": "boolean" 95 | }, 96 | "host_id": { 97 | "type": "integer", 98 | "format": "int64" 99 | }, 100 | "is_admin": { 101 | "type": "boolean" 102 | }, 103 | "is_connected": { 104 | "type": "boolean" 105 | }, 106 | "connected_since": { 107 | "type": "string" 108 | }, 109 | "bytes_sent": { 110 | "type": "string", 111 | "format": "uint64" 112 | }, 113 | "bytes_received": { 114 | "type": "string", 115 | "format": "uint64" 116 | }, 117 | "expires_at": { 118 | "type": "string" 119 | }, 120 | "description": { 121 | "type": "string" 122 | } 123 | } 124 | }, 125 | "pbAuthAuthenticateRequest": { 126 | "type": "object", 127 | "properties": { 128 | "username": { 129 | "type": "string" 130 | }, 131 | "password": { 132 | "type": "string" 133 | } 134 | } 135 | }, 136 | "pbAuthAuthenticateResponse": { 137 | "type": "object", 138 | "properties": { 139 | "token": { 140 | "type": "string" 141 | } 142 | } 143 | }, 144 | "pbAuthStatusResponse": { 145 | "type": "object", 146 | "properties": { 147 | "user": { 148 | "$ref": "#/definitions/UserResponseUser" 149 | }, 150 | "is_root": { 151 | "type": "boolean" 152 | } 153 | } 154 | }, 155 | "protobufAny": { 156 | "type": "object", 157 | "properties": { 158 | "type_url": { 159 | "type": "string" 160 | }, 161 | "value": { 162 | "type": "string", 163 | "format": "byte" 164 | } 165 | } 166 | }, 167 | "rpcStatus": { 168 | "type": "object", 169 | "properties": { 170 | "code": { 171 | "type": "integer", 172 | "format": "int32" 173 | }, 174 | "message": { 175 | "type": "string" 176 | }, 177 | "details": { 178 | "type": "array", 179 | "items": { 180 | "$ref": "#/definitions/protobufAny" 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /api/pb/auth_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package pb 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // AuthServiceClient is the client API for AuthService service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type AuthServiceClient interface { 21 | Status(ctx context.Context, in *AuthStatusRequest, opts ...grpc.CallOption) (*AuthStatusResponse, error) 22 | Authenticate(ctx context.Context, in *AuthAuthenticateRequest, opts ...grpc.CallOption) (*AuthAuthenticateResponse, error) 23 | } 24 | 25 | type authServiceClient struct { 26 | cc grpc.ClientConnInterface 27 | } 28 | 29 | func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { 30 | return &authServiceClient{cc} 31 | } 32 | 33 | func (c *authServiceClient) Status(ctx context.Context, in *AuthStatusRequest, opts ...grpc.CallOption) (*AuthStatusResponse, error) { 34 | out := new(AuthStatusResponse) 35 | err := c.cc.Invoke(ctx, "/pb.AuthService/Status", in, out, opts...) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return out, nil 40 | } 41 | 42 | func (c *authServiceClient) Authenticate(ctx context.Context, in *AuthAuthenticateRequest, opts ...grpc.CallOption) (*AuthAuthenticateResponse, error) { 43 | out := new(AuthAuthenticateResponse) 44 | err := c.cc.Invoke(ctx, "/pb.AuthService/Authenticate", in, out, opts...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return out, nil 49 | } 50 | 51 | // AuthServiceServer is the server API for AuthService service. 52 | // All implementations must embed UnimplementedAuthServiceServer 53 | // for forward compatibility 54 | type AuthServiceServer interface { 55 | Status(context.Context, *AuthStatusRequest) (*AuthStatusResponse, error) 56 | Authenticate(context.Context, *AuthAuthenticateRequest) (*AuthAuthenticateResponse, error) 57 | mustEmbedUnimplementedAuthServiceServer() 58 | } 59 | 60 | // UnimplementedAuthServiceServer must be embedded to have forward compatible implementations. 61 | type UnimplementedAuthServiceServer struct { 62 | } 63 | 64 | func (UnimplementedAuthServiceServer) Status(context.Context, *AuthStatusRequest) (*AuthStatusResponse, error) { 65 | return nil, status.Errorf(codes.Unimplemented, "method Status not implemented") 66 | } 67 | func (UnimplementedAuthServiceServer) Authenticate(context.Context, *AuthAuthenticateRequest) (*AuthAuthenticateResponse, error) { 68 | return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") 69 | } 70 | func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} 71 | 72 | // UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. 73 | // Use of this interface is not recommended, as added methods to AuthServiceServer will 74 | // result in compilation errors. 75 | type UnsafeAuthServiceServer interface { 76 | mustEmbedUnimplementedAuthServiceServer() 77 | } 78 | 79 | func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { 80 | s.RegisterService(&AuthService_ServiceDesc, srv) 81 | } 82 | 83 | func _AuthService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 84 | in := new(AuthStatusRequest) 85 | if err := dec(in); err != nil { 86 | return nil, err 87 | } 88 | if interceptor == nil { 89 | return srv.(AuthServiceServer).Status(ctx, in) 90 | } 91 | info := &grpc.UnaryServerInfo{ 92 | Server: srv, 93 | FullMethod: "/pb.AuthService/Status", 94 | } 95 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 96 | return srv.(AuthServiceServer).Status(ctx, req.(*AuthStatusRequest)) 97 | } 98 | return interceptor(ctx, in, info, handler) 99 | } 100 | 101 | func _AuthService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 102 | in := new(AuthAuthenticateRequest) 103 | if err := dec(in); err != nil { 104 | return nil, err 105 | } 106 | if interceptor == nil { 107 | return srv.(AuthServiceServer).Authenticate(ctx, in) 108 | } 109 | info := &grpc.UnaryServerInfo{ 110 | Server: srv, 111 | FullMethod: "/pb.AuthService/Authenticate", 112 | } 113 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 114 | return srv.(AuthServiceServer).Authenticate(ctx, req.(*AuthAuthenticateRequest)) 115 | } 116 | return interceptor(ctx, in, info, handler) 117 | } 118 | 119 | // AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. 120 | // It's only intended for direct use with grpc.RegisterService, 121 | // and not to be introspected or modified (even as a copy) 122 | var AuthService_ServiceDesc = grpc.ServiceDesc{ 123 | ServiceName: "pb.AuthService", 124 | HandlerType: (*AuthServiceServer)(nil), 125 | Methods: []grpc.MethodDesc{ 126 | { 127 | MethodName: "Status", 128 | Handler: _AuthService_Status_Handler, 129 | }, 130 | { 131 | MethodName: "Authenticate", 132 | Handler: _AuthService_Authenticate_Handler, 133 | }, 134 | }, 135 | Streams: []grpc.StreamDesc{}, 136 | Metadata: "auth.proto", 137 | } 138 | -------------------------------------------------------------------------------- /api/pb/google/api/annotations.proto: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/api/http.proto"; 20 | import "google/protobuf/descriptor.proto"; 21 | 22 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 23 | option java_multiple_files = true; 24 | option java_outer_classname = "AnnotationsProto"; 25 | option java_package = "com.google.api"; 26 | option objc_class_prefix = "GAPI"; 27 | 28 | extend google.protobuf.MethodOptions { 29 | // See `HttpRule`. 30 | HttpRule http = 72295728; 31 | } 32 | -------------------------------------------------------------------------------- /api/pb/google/api/field_behavior.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.api; 18 | 19 | import "google/protobuf/descriptor.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "FieldBehaviorProto"; 24 | option java_package = "com.google.api"; 25 | option objc_class_prefix = "GAPI"; 26 | 27 | extend google.protobuf.FieldOptions { 28 | // A designation of a specific field behavior (required, output only, etc.) 29 | // in protobuf messages. 30 | // 31 | // Examples: 32 | // 33 | // string name = 1 [(google.api.field_behavior) = REQUIRED]; 34 | // State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY]; 35 | // google.protobuf.Duration ttl = 1 36 | // [(google.api.field_behavior) = INPUT_ONLY]; 37 | // google.protobuf.Timestamp expire_time = 1 38 | // [(google.api.field_behavior) = OUTPUT_ONLY, 39 | // (google.api.field_behavior) = IMMUTABLE]; 40 | repeated google.api.FieldBehavior field_behavior = 1052; 41 | } 42 | 43 | // An indicator of the behavior of a given field (for example, that a field 44 | // is required in requests, or given as output but ignored as input). 45 | // This **does not** change the behavior in protocol buffers itself; it only 46 | // denotes the behavior and may affect how API tooling handles the field. 47 | // 48 | // Note: This enum **may** receive new values in the future. 49 | enum FieldBehavior { 50 | // Conventional default for enums. Do not use this. 51 | FIELD_BEHAVIOR_UNSPECIFIED = 0; 52 | 53 | // Specifically denotes a field as optional. 54 | // While all fields in protocol buffers are optional, this may be specified 55 | // for emphasis if appropriate. 56 | OPTIONAL = 1; 57 | 58 | // Denotes a field as required. 59 | // This indicates that the field **must** be provided as part of the request, 60 | // and failure to do so will cause an error (usually `INVALID_ARGUMENT`). 61 | REQUIRED = 2; 62 | 63 | // Denotes a field as output only. 64 | // This indicates that the field is provided in responses, but including the 65 | // field in a request does nothing (the server *must* ignore it and 66 | // *must not* throw an error as a result of the field's presence). 67 | OUTPUT_ONLY = 3; 68 | 69 | // Denotes a field as input only. 70 | // This indicates that the field is provided in requests, and the 71 | // corresponding field is not included in output. 72 | INPUT_ONLY = 4; 73 | 74 | // Denotes a field as immutable. 75 | // This indicates that the field may be set once in a request to create a 76 | // resource, but may not be changed thereafter. 77 | IMMUTABLE = 5; 78 | } 79 | -------------------------------------------------------------------------------- /api/pb/google/api/httpbody.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Google LLC. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | syntax = "proto3"; 17 | 18 | package google.api; 19 | 20 | import "google/protobuf/any.proto"; 21 | 22 | option cc_enable_arenas = true; 23 | option go_package = "google.golang.org/genproto/googleapis/api/httpbody;httpbody"; 24 | option java_multiple_files = true; 25 | option java_outer_classname = "HttpBodyProto"; 26 | option java_package = "com.google.api"; 27 | option objc_class_prefix = "GAPI"; 28 | 29 | // Message that represents an arbitrary HTTP body. It should only be used for 30 | // payload formats that can't be represented as JSON, such as raw binary or 31 | // an HTML page. 32 | // 33 | // 34 | // This message can be used both in streaming and non-streaming API methods in 35 | // the request as well as the response. 36 | // 37 | // It can be used as a top-level request field, which is convenient if one 38 | // wants to extract parameters from either the URL or HTTP template into the 39 | // request fields and also want access to the raw HTTP body. 40 | // 41 | // Example: 42 | // 43 | // message GetResourceRequest { 44 | // // A unique request id. 45 | // string request_id = 1; 46 | // 47 | // // The raw HTTP body is bound to this field. 48 | // google.api.HttpBody http_body = 2; 49 | // } 50 | // 51 | // service ResourceService { 52 | // rpc GetResource(GetResourceRequest) returns (google.api.HttpBody); 53 | // rpc UpdateResource(google.api.HttpBody) returns 54 | // (google.protobuf.Empty); 55 | // } 56 | // 57 | // Example with streaming methods: 58 | // 59 | // service CaldavService { 60 | // rpc GetCalendar(stream google.api.HttpBody) 61 | // returns (stream google.api.HttpBody); 62 | // rpc UpdateCalendar(stream google.api.HttpBody) 63 | // returns (stream google.api.HttpBody); 64 | // } 65 | // 66 | // Use of this type only changes how the request and response bodies are 67 | // handled, all other features will continue to work unchanged. 68 | message HttpBody { 69 | // The HTTP Content-Type header value specifying the content type of the body. 70 | string content_type = 1; 71 | 72 | // The HTTP request/response body as raw binary. 73 | bytes data = 2; 74 | 75 | // Application specific response metadata. Must be set in the first response 76 | // for streaming APIs. 77 | repeated google.protobuf.Any extensions = 3; 78 | } -------------------------------------------------------------------------------- /api/pb/google/rpc/code.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | option go_package = "google.golang.org/genproto/googleapis/rpc/code;code"; 20 | option java_multiple_files = true; 21 | option java_outer_classname = "CodeProto"; 22 | option java_package = "com.google.rpc"; 23 | option objc_class_prefix = "RPC"; 24 | 25 | 26 | // The canonical error codes for Google APIs. 27 | // 28 | // 29 | // Sometimes multiple error codes may apply. Services should return 30 | // the most specific error code that applies. For example, prefer 31 | // `OUT_OF_RANGE` over `FAILED_PRECONDITION` if both codes apply. 32 | // Similarly prefer `NOT_FOUND` or `ALREADY_EXISTS` over `FAILED_PRECONDITION`. 33 | enum Code { 34 | // Not an error; returned on success 35 | // 36 | // HTTP Mapping: 200 OK 37 | OK = 0; 38 | 39 | // The operation was cancelled, typically by the caller. 40 | // 41 | // HTTP Mapping: 499 Client Closed Request 42 | CANCELLED = 1; 43 | 44 | // Unknown error. For example, this error may be returned when 45 | // a `Status` value received from another address space belongs to 46 | // an error space that is not known in this address space. Also 47 | // errors raised by APIs that do not return enough error information 48 | // may be converted to this error. 49 | // 50 | // HTTP Mapping: 500 Internal Server Error 51 | UNKNOWN = 2; 52 | 53 | // The client specified an invalid argument. Note that this differs 54 | // from `FAILED_PRECONDITION`. `INVALID_ARGUMENT` indicates arguments 55 | // that are problematic regardless of the state of the system 56 | // (e.g., a malformed file name). 57 | // 58 | // HTTP Mapping: 400 Bad Request 59 | INVALID_ARGUMENT = 3; 60 | 61 | // The deadline expired before the operation could complete. For operations 62 | // that change the state of the system, this error may be returned 63 | // even if the operation has completed successfully. For example, a 64 | // successful response from a server could have been delayed long 65 | // enough for the deadline to expire. 66 | // 67 | // HTTP Mapping: 504 Gateway Timeout 68 | DEADLINE_EXCEEDED = 4; 69 | 70 | // Some requested entity (e.g., file or directory) was not found. 71 | // 72 | // Note to server developers: if a request is denied for an entire class 73 | // of users, such as gradual feature rollout or undocumented whitelist, 74 | // `NOT_FOUND` may be used. If a request is denied for some users within 75 | // a class of users, such as user-based access control, `PERMISSION_DENIED` 76 | // must be used. 77 | // 78 | // HTTP Mapping: 404 Not Found 79 | NOT_FOUND = 5; 80 | 81 | // The entity that a client attempted to create (e.g., file or directory) 82 | // already exists. 83 | // 84 | // HTTP Mapping: 409 Conflict 85 | ALREADY_EXISTS = 6; 86 | 87 | // The caller does not have permission to execute the specified 88 | // operation. `PERMISSION_DENIED` must not be used for rejections 89 | // caused by exhausting some resource (use `RESOURCE_EXHAUSTED` 90 | // instead for those errors). `PERMISSION_DENIED` must not be 91 | // used if the caller can not be identified (use `UNAUTHENTICATED` 92 | // instead for those errors). This error code does not imply the 93 | // request is valid or the requested entity exists or satisfies 94 | // other pre-conditions. 95 | // 96 | // HTTP Mapping: 403 Forbidden 97 | PERMISSION_DENIED = 7; 98 | 99 | // The request does not have valid authentication credentials for the 100 | // operation. 101 | // 102 | // HTTP Mapping: 401 Unauthorized 103 | UNAUTHENTICATED = 16; 104 | 105 | // Some resource has been exhausted, perhaps a per-user quota, or 106 | // perhaps the entire file system is out of space. 107 | // 108 | // HTTP Mapping: 429 Too Many Requests 109 | RESOURCE_EXHAUSTED = 8; 110 | 111 | // The operation was rejected because the system is not in a state 112 | // required for the operation's execution. For example, the directory 113 | // to be deleted is non-empty, an rmdir operation is applied to 114 | // a non-directory, etc. 115 | // 116 | // Service implementors can use the following guidelines to decide 117 | // between `FAILED_PRECONDITION`, `ABORTED`, and `UNAVAILABLE`: 118 | // (a) Use `UNAVAILABLE` if the client can retry just the failing call. 119 | // (b) Use `ABORTED` if the client should retry at a higher level 120 | // (e.g., when a client-specified test-and-set fails, indicating the 121 | // client should restart a read-modify-write sequence). 122 | // (c) Use `FAILED_PRECONDITION` if the client should not retry until 123 | // the system state has been explicitly fixed. E.g., if an "rmdir" 124 | // fails because the directory is non-empty, `FAILED_PRECONDITION` 125 | // should be returned since the client should not retry unless 126 | // the files are deleted from the directory. 127 | // 128 | // HTTP Mapping: 400 Bad Request 129 | FAILED_PRECONDITION = 9; 130 | 131 | // The operation was aborted, typically due to a concurrency issue such as 132 | // a sequencer check failure or transaction abort. 133 | // 134 | // See the guidelines above for deciding between `FAILED_PRECONDITION`, 135 | // `ABORTED`, and `UNAVAILABLE`. 136 | // 137 | // HTTP Mapping: 409 Conflict 138 | ABORTED = 10; 139 | 140 | // The operation was attempted past the valid range. E.g., seeking or 141 | // reading past end-of-file. 142 | // 143 | // Unlike `INVALID_ARGUMENT`, this error indicates a problem that may 144 | // be fixed if the system state changes. For example, a 32-bit file 145 | // system will generate `INVALID_ARGUMENT` if asked to read at an 146 | // offset that is not in the range [0,2^32-1], but it will generate 147 | // `OUT_OF_RANGE` if asked to read from an offset past the current 148 | // file size. 149 | // 150 | // There is a fair bit of overlap between `FAILED_PRECONDITION` and 151 | // `OUT_OF_RANGE`. We recommend using `OUT_OF_RANGE` (the more specific 152 | // error) when it applies so that callers who are iterating through 153 | // a space can easily look for an `OUT_OF_RANGE` error to detect when 154 | // they are done. 155 | // 156 | // HTTP Mapping: 400 Bad Request 157 | OUT_OF_RANGE = 11; 158 | 159 | // The operation is not implemented or is not supported/enabled in this 160 | // service. 161 | // 162 | // HTTP Mapping: 501 Not Implemented 163 | UNIMPLEMENTED = 12; 164 | 165 | // Internal errors. This means that some invariants expected by the 166 | // underlying system have been broken. This error code is reserved 167 | // for serious errors. 168 | // 169 | // HTTP Mapping: 500 Internal Server Error 170 | INTERNAL = 13; 171 | 172 | // The service is currently unavailable. This is most likely a 173 | // transient condition, which can be corrected by retrying with 174 | // a backoff. 175 | // 176 | // See the guidelines above for deciding between `FAILED_PRECONDITION`, 177 | // `ABORTED`, and `UNAVAILABLE`. 178 | // 179 | // HTTP Mapping: 503 Service Unavailable 180 | UNAVAILABLE = 14; 181 | 182 | // Unrecoverable data loss or corruption. 183 | // 184 | // HTTP Mapping: 500 Internal Server Error 185 | DATA_LOSS = 15; 186 | } 187 | -------------------------------------------------------------------------------- /api/pb/google/rpc/error_details.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/duration.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/rpc/errdetails;errdetails"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "ErrorDetailsProto"; 24 | option java_package = "com.google.rpc"; 25 | option objc_class_prefix = "RPC"; 26 | 27 | 28 | // Describes when the clients can retry a failed request. Clients could ignore 29 | // the recommendation here or retry when this information is missing from error 30 | // responses. 31 | // 32 | // It's always recommended that clients should use exponential backoff when 33 | // retrying. 34 | // 35 | // Clients should wait until `retry_delay` amount of time has passed since 36 | // receiving the error response before retrying. If retrying requests also 37 | // fail, clients should use an exponential backoff scheme to gradually increase 38 | // the delay between retries based on `retry_delay`, until either a maximum 39 | // number of retires have been reached or a maximum retry delay cap has been 40 | // reached. 41 | message RetryInfo { 42 | // Clients should wait at least this long between retrying the same request. 43 | google.protobuf.Duration retry_delay = 1; 44 | } 45 | 46 | // Describes additional debugging info. 47 | message DebugInfo { 48 | // The stack trace entries indicating where the error occurred. 49 | repeated string stack_entries = 1; 50 | 51 | // Additional debugging information provided by the server. 52 | string detail = 2; 53 | } 54 | 55 | // Describes how a quota check failed. 56 | // 57 | // For example if a daily limit was exceeded for the calling project, 58 | // a service could respond with a QuotaFailure detail containing the project 59 | // id and the description of the quota limit that was exceeded. If the 60 | // calling project hasn't enabled the service in the developer console, then 61 | // a service could respond with the project id and set `service_disabled` 62 | // to true. 63 | // 64 | // Also see RetryDetail and Help types for other details about handling a 65 | // quota failure. 66 | message QuotaFailure { 67 | // A message type used to describe a single quota violation. For example, a 68 | // daily quota or a custom quota that was exceeded. 69 | message Violation { 70 | // The subject on which the quota check failed. 71 | // For example, "clientip:" or "project:". 73 | string subject = 1; 74 | 75 | // A description of how the quota check failed. Clients can use this 76 | // description to find more about the quota configuration in the service's 77 | // public documentation, or find the relevant quota limit to adjust through 78 | // developer console. 79 | // 80 | // For example: "Service disabled" or "Daily Limit for read operations 81 | // exceeded". 82 | string description = 2; 83 | } 84 | 85 | // Describes all quota violations. 86 | repeated Violation violations = 1; 87 | } 88 | 89 | // Describes what preconditions have failed. 90 | // 91 | // For example, if an RPC failed because it required the Terms of Service to be 92 | // acknowledged, it could list the terms of service violation in the 93 | // PreconditionFailure message. 94 | message PreconditionFailure { 95 | // A message type used to describe a single precondition failure. 96 | message Violation { 97 | // The type of PreconditionFailure. We recommend using a service-specific 98 | // enum type to define the supported precondition violation types. For 99 | // example, "TOS" for "Terms of Service violation". 100 | string type = 1; 101 | 102 | // The subject, relative to the type, that failed. 103 | // For example, "google.com/cloud" relative to the "TOS" type would 104 | // indicate which terms of service is being referenced. 105 | string subject = 2; 106 | 107 | // A description of how the precondition failed. Developers can use this 108 | // description to understand how to fix the failure. 109 | // 110 | // For example: "Terms of service not accepted". 111 | string description = 3; 112 | } 113 | 114 | // Describes all precondition violations. 115 | repeated Violation violations = 1; 116 | } 117 | 118 | // Describes violations in a client request. This error type focuses on the 119 | // syntactic aspects of the request. 120 | message BadRequest { 121 | // A message type used to describe a single bad request field. 122 | message FieldViolation { 123 | // A path leading to a field in the request body. The value will be a 124 | // sequence of dot-separated identifiers that identify a protocol buffer 125 | // field. E.g., "field_violations.field" would identify this field. 126 | string field = 1; 127 | 128 | // A description of why the request element is bad. 129 | string description = 2; 130 | } 131 | 132 | // Describes all violations in a client request. 133 | repeated FieldViolation field_violations = 1; 134 | } 135 | 136 | // Contains metadata about the request that clients can attach when filing a bug 137 | // or providing other forms of feedback. 138 | message RequestInfo { 139 | // An opaque string that should only be interpreted by the service generating 140 | // it. For example, it can be used to identify requests in the service's logs. 141 | string request_id = 1; 142 | 143 | // Any data that was used to serve this request. For example, an encrypted 144 | // stack trace that can be sent back to the service provider for debugging. 145 | string serving_data = 2; 146 | } 147 | 148 | // Describes the resource that is being accessed. 149 | message ResourceInfo { 150 | // A name for the type of resource being accessed, e.g. "sql table", 151 | // "cloud storage bucket", "file", "Google calendar"; or the type URL 152 | // of the resource: e.g. "type.googleapis.com/google.pubsub.v1.Topic". 153 | string resource_type = 1; 154 | 155 | // The name of the resource being accessed. For example, a shared calendar 156 | // name: "example.com_4fghdhgsrgh@group.calendar.google.com", if the current 157 | // error is [google.rpc.Code.PERMISSION_DENIED][google.rpc.Code.PERMISSION_DENIED]. 158 | string resource_name = 2; 159 | 160 | // The owner of the resource (optional). 161 | // For example, "user:" or "project:". 163 | string owner = 3; 164 | 165 | // Describes what error is encountered when accessing this resource. 166 | // For example, updating a cloud project may require the `writer` permission 167 | // on the developer console project. 168 | string description = 4; 169 | } 170 | 171 | // Provides links to documentation or for performing an out of band action. 172 | // 173 | // For example, if a quota check failed with an error indicating the calling 174 | // project hasn't enabled the accessed service, this can contain a URL pointing 175 | // directly to the right place in the developer console to flip the bit. 176 | message Help { 177 | // Describes a URL link. 178 | message Link { 179 | // Describes what the link offers. 180 | string description = 1; 181 | 182 | // The URL of the link. 183 | string url = 2; 184 | } 185 | 186 | // URL(s) pointing to additional information on handling the current error. 187 | repeated Link links = 1; 188 | } 189 | 190 | // Provides a localized error message that is safe to return to the user 191 | // which can be attached to an RPC error. 192 | message LocalizedMessage { 193 | // The locale used following the specification defined at 194 | // http://www.rfc-editor.org/rfc/bcp/bcp47.txt. 195 | // Examples are: "en-US", "fr-CH", "es-MX" 196 | string locale = 1; 197 | 198 | // The localized error message in the above locale. 199 | string message = 2; 200 | } 201 | -------------------------------------------------------------------------------- /api/pb/google/rpc/status.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package google.rpc; 18 | 19 | import "google/protobuf/any.proto"; 20 | 21 | option go_package = "google.golang.org/genproto/googleapis/rpc/status;status"; 22 | option java_multiple_files = true; 23 | option java_outer_classname = "StatusProto"; 24 | option java_package = "com.google.rpc"; 25 | option objc_class_prefix = "RPC"; 26 | 27 | 28 | // The `Status` type defines a logical error model that is suitable for different 29 | // programming environments, including REST APIs and RPC APIs. It is used by 30 | // [gRPC](https://github.com/grpc). The error model is designed to be: 31 | // 32 | // - Simple to use and understand for most users 33 | // - Flexible enough to meet unexpected needs 34 | // 35 | // # Overview 36 | // 37 | // The `Status` message contains three pieces of data: error code, error message, 38 | // and error details. The error code should be an enum value of 39 | // [google.rpc.Code][google.rpc.Code], but it may accept additional error codes if needed. The 40 | // error message should be a developer-facing English message that helps 41 | // developers *understand* and *resolve* the error. If a localized user-facing 42 | // error message is needed, put the localized message in the error details or 43 | // localize it in the client. The optional error details may contain arbitrary 44 | // information about the error. There is a predefined set of error detail types 45 | // in the package `google.rpc` that can be used for common error conditions. 46 | // 47 | // # Language mapping 48 | // 49 | // The `Status` message is the logical representation of the error model, but it 50 | // is not necessarily the actual wire format. When the `Status` message is 51 | // exposed in different client libraries and different wire protocols, it can be 52 | // mapped differently. For example, it will likely be mapped to some exceptions 53 | // in Java, but more likely mapped to some error codes in C. 54 | // 55 | // # Other uses 56 | // 57 | // The error model and the `Status` message can be used in a variety of 58 | // environments, either with or without APIs, to provide a 59 | // consistent developer experience across different environments. 60 | // 61 | // Example uses of this error model include: 62 | // 63 | // - Partial errors. If a service needs to return partial errors to the client, 64 | // it may embed the `Status` in the normal response to indicate the partial 65 | // errors. 66 | // 67 | // - Workflow errors. A typical workflow has multiple steps. Each step may 68 | // have a `Status` message for error reporting. 69 | // 70 | // - Batch operations. If a client uses batch request and batch response, the 71 | // `Status` message should be used directly inside batch response, one for 72 | // each error sub-response. 73 | // 74 | // - Asynchronous operations. If an API call embeds asynchronous operation 75 | // results in its response, the status of those operations should be 76 | // represented directly using the `Status` message. 77 | // 78 | // - Logging. If some API errors are stored in logs, the message `Status` could 79 | // be used directly after any stripping needed for security/privacy reasons. 80 | message Status { 81 | // The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. 82 | int32 code = 1; 83 | 84 | // A developer-facing error message, which should be in English. Any 85 | // user-facing error message should be localized and sent in the 86 | // [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. 87 | string message = 2; 88 | 89 | // A list of messages that carry the error details. There is a common set of 90 | // message types for APIs to use. 91 | repeated google.protobuf.Any details = 3; 92 | } 93 | -------------------------------------------------------------------------------- /api/pb/network.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | option go_package = "github.com/cad/ovpm/api/pb"; 5 | 6 | import "google/api/annotations.proto"; 7 | 8 | message NetworkCreateRequest { 9 | string name = 1; 10 | string cidr = 2; 11 | string type = 3; 12 | string via = 4; 13 | } 14 | message NetworkListRequest {} 15 | message NetworkDeleteRequest { 16 | string name = 1; 17 | } 18 | message NetworkGetAllTypesRequest {} 19 | message NetworkAssociateRequest { 20 | string name = 1; 21 | string username = 2; 22 | } 23 | message NetworkDissociateRequest { 24 | string name = 1; 25 | string username = 2; 26 | } 27 | 28 | message NetworkGetAssociatedUsersRequest { 29 | string name = 1; 30 | } 31 | service NetworkService { 32 | rpc Create (NetworkCreateRequest) returns (NetworkCreateResponse) { 33 | option (google.api.http) = { 34 | post: "/api/v1/network/create" 35 | body: "*" 36 | }; 37 | 38 | } 39 | rpc List (NetworkListRequest) returns (NetworkListResponse) { 40 | option (google.api.http) = { 41 | get: "/api/v1/network/list" 42 | //body: "*" 43 | }; 44 | 45 | } 46 | rpc Delete (NetworkDeleteRequest) returns (NetworkDeleteResponse) { 47 | option (google.api.http) = { 48 | post: "/api/v1/network/delete" 49 | body: "*" 50 | }; 51 | 52 | } 53 | rpc GetAllTypes(NetworkGetAllTypesRequest) returns (NetworkGetAllTypesResponse) { 54 | option (google.api.http) = { 55 | get: "/api/v1/network/getalltypes" 56 | //body: "*" 57 | }; 58 | 59 | } 60 | rpc GetAssociatedUsers(NetworkGetAssociatedUsersRequest) returns (NetworkGetAssociatedUsersResponse) { 61 | option (google.api.http) = { 62 | get: "/api/v1/network/getassociatedusers" 63 | //body: "*" 64 | }; 65 | 66 | } 67 | rpc Associate (NetworkAssociateRequest) returns (NetworkAssociateResponse) { 68 | option (google.api.http) = { 69 | post: "/api/v1/network/associate" 70 | body: "*" 71 | }; 72 | 73 | } 74 | rpc Dissociate (NetworkDissociateRequest) returns (NetworkDissociateResponse) { 75 | option (google.api.http) = { 76 | post: "/api/v1/network/dissociate" 77 | body: "*" 78 | }; 79 | 80 | } 81 | } 82 | message Network { 83 | string name = 1; 84 | string cidr = 2; 85 | string type = 3; 86 | string created_at = 4; 87 | repeated string associated_usernames = 5; 88 | string via = 6; 89 | } 90 | 91 | message NetworkType { 92 | string type = 1; 93 | string description = 2; 94 | } 95 | message NetworkCreateResponse { 96 | Network network = 1; 97 | } 98 | message NetworkListResponse { 99 | repeated Network networks = 1; 100 | } 101 | message NetworkDeleteResponse { 102 | Network network = 1; 103 | } 104 | message NetworkGetAllTypesResponse { 105 | repeated NetworkType types = 1; 106 | } 107 | message NetworkAssociateResponse {} 108 | message NetworkDissociateResponse {} 109 | message NetworkGetAssociatedUsersResponse { 110 | repeated string usernames = 1; 111 | } 112 | -------------------------------------------------------------------------------- /api/pb/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | option go_package = "github.com/cad/ovpm/api/pb"; 5 | 6 | import "google/api/annotations.proto"; 7 | 8 | message UserListRequest { 9 | 10 | } 11 | 12 | message UserCreateRequest { 13 | string username = 1; 14 | string password = 2; 15 | bool no_gw = 3; 16 | uint32 host_id = 4; 17 | bool is_admin = 5; 18 | string description = 6; 19 | } 20 | 21 | message UserUpdateRequest { 22 | string username = 1; 23 | string password = 2; 24 | enum GWPref { 25 | NOPREF = 0; 26 | NOGW = 1; 27 | GW = 2; 28 | } 29 | GWPref gwpref = 3; 30 | uint32 host_id = 4; 31 | enum StaticPref { 32 | NOPREFSTATIC = 0; 33 | NOSTATIC = 1; 34 | STATIC = 2; 35 | } 36 | StaticPref static_pref = 5; 37 | enum AdminPref { 38 | NOPREFADMIN = 0; 39 | NOADMIN = 1; 40 | ADMIN = 2; 41 | } 42 | AdminPref admin_pref = 6; 43 | string description = 7; 44 | } 45 | 46 | 47 | message UserDeleteRequest { 48 | string username = 1; 49 | } 50 | 51 | message UserRenewRequest { 52 | string username = 1; 53 | } 54 | 55 | message UserGenConfigRequest { 56 | string username = 1; 57 | } 58 | 59 | service UserService { 60 | rpc List (UserListRequest) returns (UserResponse) { 61 | option (google.api.http) = { 62 | get: "/api/v1/user/list" 63 | //body: "*" 64 | }; 65 | 66 | } 67 | rpc Create (UserCreateRequest) returns (UserResponse) { 68 | option (google.api.http) = { 69 | post: "/api/v1/user/create" 70 | body: "*" 71 | }; 72 | } 73 | rpc Update (UserUpdateRequest) returns (UserResponse) { 74 | option (google.api.http) = { 75 | post: "/api/v1/user/update" 76 | body: "*" 77 | }; 78 | 79 | } 80 | rpc Delete (UserDeleteRequest) returns (UserResponse) { 81 | option (google.api.http) = { 82 | post: "/api/v1/user/delete" 83 | body: "*" 84 | }; 85 | 86 | } 87 | rpc Renew (UserRenewRequest) returns (UserResponse) { 88 | option (google.api.http) = { 89 | post: "/api/v1/user/renew" 90 | body: "*" 91 | }; 92 | 93 | } 94 | rpc GenConfig (UserGenConfigRequest) returns (UserGenConfigResponse) { 95 | option (google.api.http) = { 96 | post: "/api/v1/user/genconfig" 97 | body: "*" 98 | }; 99 | } 100 | } 101 | 102 | message UserResponse { 103 | message User { 104 | string username = 1; 105 | string server_serial_number = 2; 106 | string cert = 3; 107 | string created_at = 4; 108 | string ip_net = 5; 109 | bool no_gw = 6; 110 | uint32 host_id = 7; 111 | bool is_admin = 8; 112 | bool is_connected = 9; 113 | string connected_since = 10; 114 | uint64 bytes_sent = 11; 115 | uint64 bytes_received = 12; 116 | string expires_at = 13; 117 | string description = 14; 118 | } 119 | 120 | repeated User users = 1; 121 | } 122 | 123 | message UserGenConfigResponse { 124 | string client_config = 1; 125 | } 126 | -------------------------------------------------------------------------------- /api/pb/vpn.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pb; 4 | option go_package = "github.com/cad/ovpm/api/pb"; 5 | 6 | import "google/api/annotations.proto"; 7 | 8 | enum VPNProto { 9 | NOPREF = 0; 10 | UDP = 1; 11 | TCP = 2; 12 | } 13 | 14 | enum VPNLZOPref { 15 | USE_LZO_NOPREF = 0; 16 | USE_LZO_ENABLE = 1; 17 | USE_LZO_DISABLE= 3; 18 | } 19 | 20 | message VPNStatusRequest {} 21 | message VPNInitRequest { 22 | string hostname = 1; 23 | string port = 2; 24 | VPNProto proto_pref = 3; 25 | string ip_block = 4; 26 | string dns = 5; 27 | string keepalive_period = 6; 28 | string keepalive_timeout = 7; 29 | bool use_lzo = 8; 30 | } 31 | 32 | message VPNUpdateRequest { 33 | string ip_block = 1; 34 | string dns = 2; 35 | VPNLZOPref lzo_pref = 3; 36 | } 37 | message VPNRestartRequest {} 38 | 39 | 40 | service VPNService { 41 | rpc Status (VPNStatusRequest) returns (VPNStatusResponse) { 42 | option (google.api.http) = { 43 | get: "/api/v1/vpn/status" 44 | //body: "*" 45 | };} 46 | rpc Init (VPNInitRequest) returns (VPNInitResponse) { 47 | option (google.api.http) = { 48 | post: "/api/v1/vpn/init" 49 | body: "*" 50 | };} 51 | rpc Update (VPNUpdateRequest) returns (VPNUpdateResponse) { 52 | option (google.api.http) = { 53 | post: "/api/v1/vpn/update" 54 | body: "*" 55 | };} 56 | rpc Restart (VPNRestartRequest) returns (VPNRestartResponse) { 57 | option (google.api.http) = { 58 | post: "/api/v1/vpn/restart" 59 | //body: "*" 60 | };} 61 | 62 | 63 | } 64 | 65 | message VPNStatusResponse { 66 | string name = 1; 67 | string serial_number = 2; 68 | string hostname = 3; 69 | string port = 4; 70 | string cert = 5; 71 | string ca_cert = 6; 72 | string net = 7; 73 | string mask = 8; 74 | string created_at = 9; 75 | string proto = 10; 76 | string dns = 11; 77 | string expires_at = 12; 78 | string ca_expires_at = 13; 79 | bool use_lzo = 14; 80 | } 81 | message VPNInitResponse {} 82 | message VPNUpdateResponse {} 83 | message VPNRestartResponse {} 84 | -------------------------------------------------------------------------------- /api/pb/vpn.swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "vpn.proto", 5 | "version": "version not set" 6 | }, 7 | "tags": [ 8 | { 9 | "name": "VPNService" 10 | } 11 | ], 12 | "consumes": [ 13 | "application/json" 14 | ], 15 | "produces": [ 16 | "application/json" 17 | ], 18 | "paths": { 19 | "/api/v1/vpn/init": { 20 | "post": { 21 | "operationId": "VPNService_Init", 22 | "responses": { 23 | "200": { 24 | "description": "A successful response.", 25 | "schema": { 26 | "$ref": "#/definitions/pbVPNInitResponse" 27 | } 28 | }, 29 | "default": { 30 | "description": "An unexpected error response.", 31 | "schema": { 32 | "$ref": "#/definitions/rpcStatus" 33 | } 34 | } 35 | }, 36 | "parameters": [ 37 | { 38 | "name": "body", 39 | "in": "body", 40 | "required": true, 41 | "schema": { 42 | "$ref": "#/definitions/pbVPNInitRequest" 43 | } 44 | } 45 | ], 46 | "tags": [ 47 | "VPNService" 48 | ] 49 | } 50 | }, 51 | "/api/v1/vpn/restart": { 52 | "post": { 53 | "operationId": "VPNService_Restart", 54 | "responses": { 55 | "200": { 56 | "description": "A successful response.", 57 | "schema": { 58 | "$ref": "#/definitions/pbVPNRestartResponse" 59 | } 60 | }, 61 | "default": { 62 | "description": "An unexpected error response.", 63 | "schema": { 64 | "$ref": "#/definitions/rpcStatus" 65 | } 66 | } 67 | }, 68 | "tags": [ 69 | "VPNService" 70 | ] 71 | } 72 | }, 73 | "/api/v1/vpn/status": { 74 | "get": { 75 | "operationId": "VPNService_Status", 76 | "responses": { 77 | "200": { 78 | "description": "A successful response.", 79 | "schema": { 80 | "$ref": "#/definitions/pbVPNStatusResponse" 81 | } 82 | }, 83 | "default": { 84 | "description": "An unexpected error response.", 85 | "schema": { 86 | "$ref": "#/definitions/rpcStatus" 87 | } 88 | } 89 | }, 90 | "tags": [ 91 | "VPNService" 92 | ] 93 | } 94 | }, 95 | "/api/v1/vpn/update": { 96 | "post": { 97 | "operationId": "VPNService_Update", 98 | "responses": { 99 | "200": { 100 | "description": "A successful response.", 101 | "schema": { 102 | "$ref": "#/definitions/pbVPNUpdateResponse" 103 | } 104 | }, 105 | "default": { 106 | "description": "An unexpected error response.", 107 | "schema": { 108 | "$ref": "#/definitions/rpcStatus" 109 | } 110 | } 111 | }, 112 | "parameters": [ 113 | { 114 | "name": "body", 115 | "in": "body", 116 | "required": true, 117 | "schema": { 118 | "$ref": "#/definitions/pbVPNUpdateRequest" 119 | } 120 | } 121 | ], 122 | "tags": [ 123 | "VPNService" 124 | ] 125 | } 126 | } 127 | }, 128 | "definitions": { 129 | "pbVPNInitRequest": { 130 | "type": "object", 131 | "properties": { 132 | "hostname": { 133 | "type": "string" 134 | }, 135 | "port": { 136 | "type": "string" 137 | }, 138 | "proto_pref": { 139 | "$ref": "#/definitions/pbVPNProto" 140 | }, 141 | "ip_block": { 142 | "type": "string" 143 | }, 144 | "dns": { 145 | "type": "string" 146 | }, 147 | "keepalive_period": { 148 | "type": "string" 149 | }, 150 | "keepalive_timeout": { 151 | "type": "string" 152 | }, 153 | "use_lzo": { 154 | "type": "boolean" 155 | } 156 | } 157 | }, 158 | "pbVPNInitResponse": { 159 | "type": "object" 160 | }, 161 | "pbVPNLZOPref": { 162 | "type": "string", 163 | "enum": [ 164 | "USE_LZO_NOPREF", 165 | "USE_LZO_ENABLE", 166 | "USE_LZO_DISABLE" 167 | ], 168 | "default": "USE_LZO_NOPREF" 169 | }, 170 | "pbVPNProto": { 171 | "type": "string", 172 | "enum": [ 173 | "NOPREF", 174 | "UDP", 175 | "TCP" 176 | ], 177 | "default": "NOPREF" 178 | }, 179 | "pbVPNRestartResponse": { 180 | "type": "object" 181 | }, 182 | "pbVPNStatusResponse": { 183 | "type": "object", 184 | "properties": { 185 | "name": { 186 | "type": "string" 187 | }, 188 | "serial_number": { 189 | "type": "string" 190 | }, 191 | "hostname": { 192 | "type": "string" 193 | }, 194 | "port": { 195 | "type": "string" 196 | }, 197 | "cert": { 198 | "type": "string" 199 | }, 200 | "ca_cert": { 201 | "type": "string" 202 | }, 203 | "net": { 204 | "type": "string" 205 | }, 206 | "mask": { 207 | "type": "string" 208 | }, 209 | "created_at": { 210 | "type": "string" 211 | }, 212 | "proto": { 213 | "type": "string" 214 | }, 215 | "dns": { 216 | "type": "string" 217 | }, 218 | "expires_at": { 219 | "type": "string" 220 | }, 221 | "ca_expires_at": { 222 | "type": "string" 223 | }, 224 | "use_lzo": { 225 | "type": "boolean" 226 | } 227 | } 228 | }, 229 | "pbVPNUpdateRequest": { 230 | "type": "object", 231 | "properties": { 232 | "ip_block": { 233 | "type": "string" 234 | }, 235 | "dns": { 236 | "type": "string" 237 | }, 238 | "lzo_pref": { 239 | "$ref": "#/definitions/pbVPNLZOPref" 240 | } 241 | } 242 | }, 243 | "pbVPNUpdateResponse": { 244 | "type": "object" 245 | }, 246 | "protobufAny": { 247 | "type": "object", 248 | "properties": { 249 | "type_url": { 250 | "type": "string" 251 | }, 252 | "value": { 253 | "type": "string", 254 | "format": "byte" 255 | } 256 | } 257 | }, 258 | "rpcStatus": { 259 | "type": "object", 260 | "properties": { 261 | "code": { 262 | "type": "integer", 263 | "format": "int32" 264 | }, 265 | "message": { 266 | "type": "string" 267 | }, 268 | "details": { 269 | "type": "array", 270 | "items": { 271 | "$ref": "#/definitions/protobufAny" 272 | } 273 | } 274 | } 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /api/pb/vpn_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | 3 | package pb 4 | 5 | import ( 6 | context "context" 7 | grpc "google.golang.org/grpc" 8 | codes "google.golang.org/grpc/codes" 9 | status "google.golang.org/grpc/status" 10 | ) 11 | 12 | // This is a compile-time assertion to ensure that this generated file 13 | // is compatible with the grpc package it is being compiled against. 14 | // Requires gRPC-Go v1.32.0 or later. 15 | const _ = grpc.SupportPackageIsVersion7 16 | 17 | // VPNServiceClient is the client API for VPNService service. 18 | // 19 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 20 | type VPNServiceClient interface { 21 | Status(ctx context.Context, in *VPNStatusRequest, opts ...grpc.CallOption) (*VPNStatusResponse, error) 22 | Init(ctx context.Context, in *VPNInitRequest, opts ...grpc.CallOption) (*VPNInitResponse, error) 23 | Update(ctx context.Context, in *VPNUpdateRequest, opts ...grpc.CallOption) (*VPNUpdateResponse, error) 24 | Restart(ctx context.Context, in *VPNRestartRequest, opts ...grpc.CallOption) (*VPNRestartResponse, error) 25 | } 26 | 27 | type vPNServiceClient struct { 28 | cc grpc.ClientConnInterface 29 | } 30 | 31 | func NewVPNServiceClient(cc grpc.ClientConnInterface) VPNServiceClient { 32 | return &vPNServiceClient{cc} 33 | } 34 | 35 | func (c *vPNServiceClient) Status(ctx context.Context, in *VPNStatusRequest, opts ...grpc.CallOption) (*VPNStatusResponse, error) { 36 | out := new(VPNStatusResponse) 37 | err := c.cc.Invoke(ctx, "/pb.VPNService/Status", in, out, opts...) 38 | if err != nil { 39 | return nil, err 40 | } 41 | return out, nil 42 | } 43 | 44 | func (c *vPNServiceClient) Init(ctx context.Context, in *VPNInitRequest, opts ...grpc.CallOption) (*VPNInitResponse, error) { 45 | out := new(VPNInitResponse) 46 | err := c.cc.Invoke(ctx, "/pb.VPNService/Init", in, out, opts...) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return out, nil 51 | } 52 | 53 | func (c *vPNServiceClient) Update(ctx context.Context, in *VPNUpdateRequest, opts ...grpc.CallOption) (*VPNUpdateResponse, error) { 54 | out := new(VPNUpdateResponse) 55 | err := c.cc.Invoke(ctx, "/pb.VPNService/Update", in, out, opts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | func (c *vPNServiceClient) Restart(ctx context.Context, in *VPNRestartRequest, opts ...grpc.CallOption) (*VPNRestartResponse, error) { 63 | out := new(VPNRestartResponse) 64 | err := c.cc.Invoke(ctx, "/pb.VPNService/Restart", in, out, opts...) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return out, nil 69 | } 70 | 71 | // VPNServiceServer is the server API for VPNService service. 72 | // All implementations must embed UnimplementedVPNServiceServer 73 | // for forward compatibility 74 | type VPNServiceServer interface { 75 | Status(context.Context, *VPNStatusRequest) (*VPNStatusResponse, error) 76 | Init(context.Context, *VPNInitRequest) (*VPNInitResponse, error) 77 | Update(context.Context, *VPNUpdateRequest) (*VPNUpdateResponse, error) 78 | Restart(context.Context, *VPNRestartRequest) (*VPNRestartResponse, error) 79 | mustEmbedUnimplementedVPNServiceServer() 80 | } 81 | 82 | // UnimplementedVPNServiceServer must be embedded to have forward compatible implementations. 83 | type UnimplementedVPNServiceServer struct { 84 | } 85 | 86 | func (UnimplementedVPNServiceServer) Status(context.Context, *VPNStatusRequest) (*VPNStatusResponse, error) { 87 | return nil, status.Errorf(codes.Unimplemented, "method Status not implemented") 88 | } 89 | func (UnimplementedVPNServiceServer) Init(context.Context, *VPNInitRequest) (*VPNInitResponse, error) { 90 | return nil, status.Errorf(codes.Unimplemented, "method Init not implemented") 91 | } 92 | func (UnimplementedVPNServiceServer) Update(context.Context, *VPNUpdateRequest) (*VPNUpdateResponse, error) { 93 | return nil, status.Errorf(codes.Unimplemented, "method Update not implemented") 94 | } 95 | func (UnimplementedVPNServiceServer) Restart(context.Context, *VPNRestartRequest) (*VPNRestartResponse, error) { 96 | return nil, status.Errorf(codes.Unimplemented, "method Restart not implemented") 97 | } 98 | func (UnimplementedVPNServiceServer) mustEmbedUnimplementedVPNServiceServer() {} 99 | 100 | // UnsafeVPNServiceServer may be embedded to opt out of forward compatibility for this service. 101 | // Use of this interface is not recommended, as added methods to VPNServiceServer will 102 | // result in compilation errors. 103 | type UnsafeVPNServiceServer interface { 104 | mustEmbedUnimplementedVPNServiceServer() 105 | } 106 | 107 | func RegisterVPNServiceServer(s grpc.ServiceRegistrar, srv VPNServiceServer) { 108 | s.RegisterService(&VPNService_ServiceDesc, srv) 109 | } 110 | 111 | func _VPNService_Status_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 112 | in := new(VPNStatusRequest) 113 | if err := dec(in); err != nil { 114 | return nil, err 115 | } 116 | if interceptor == nil { 117 | return srv.(VPNServiceServer).Status(ctx, in) 118 | } 119 | info := &grpc.UnaryServerInfo{ 120 | Server: srv, 121 | FullMethod: "/pb.VPNService/Status", 122 | } 123 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 124 | return srv.(VPNServiceServer).Status(ctx, req.(*VPNStatusRequest)) 125 | } 126 | return interceptor(ctx, in, info, handler) 127 | } 128 | 129 | func _VPNService_Init_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 130 | in := new(VPNInitRequest) 131 | if err := dec(in); err != nil { 132 | return nil, err 133 | } 134 | if interceptor == nil { 135 | return srv.(VPNServiceServer).Init(ctx, in) 136 | } 137 | info := &grpc.UnaryServerInfo{ 138 | Server: srv, 139 | FullMethod: "/pb.VPNService/Init", 140 | } 141 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 142 | return srv.(VPNServiceServer).Init(ctx, req.(*VPNInitRequest)) 143 | } 144 | return interceptor(ctx, in, info, handler) 145 | } 146 | 147 | func _VPNService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 148 | in := new(VPNUpdateRequest) 149 | if err := dec(in); err != nil { 150 | return nil, err 151 | } 152 | if interceptor == nil { 153 | return srv.(VPNServiceServer).Update(ctx, in) 154 | } 155 | info := &grpc.UnaryServerInfo{ 156 | Server: srv, 157 | FullMethod: "/pb.VPNService/Update", 158 | } 159 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 160 | return srv.(VPNServiceServer).Update(ctx, req.(*VPNUpdateRequest)) 161 | } 162 | return interceptor(ctx, in, info, handler) 163 | } 164 | 165 | func _VPNService_Restart_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 166 | in := new(VPNRestartRequest) 167 | if err := dec(in); err != nil { 168 | return nil, err 169 | } 170 | if interceptor == nil { 171 | return srv.(VPNServiceServer).Restart(ctx, in) 172 | } 173 | info := &grpc.UnaryServerInfo{ 174 | Server: srv, 175 | FullMethod: "/pb.VPNService/Restart", 176 | } 177 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 178 | return srv.(VPNServiceServer).Restart(ctx, req.(*VPNRestartRequest)) 179 | } 180 | return interceptor(ctx, in, info, handler) 181 | } 182 | 183 | // VPNService_ServiceDesc is the grpc.ServiceDesc for VPNService service. 184 | // It's only intended for direct use with grpc.RegisterService, 185 | // and not to be introspected or modified (even as a copy) 186 | var VPNService_ServiceDesc = grpc.ServiceDesc{ 187 | ServiceName: "pb.VPNService", 188 | HandlerType: (*VPNServiceServer)(nil), 189 | Methods: []grpc.MethodDesc{ 190 | { 191 | MethodName: "Status", 192 | Handler: _VPNService_Status_Handler, 193 | }, 194 | { 195 | MethodName: "Init", 196 | Handler: _VPNService_Init_Handler, 197 | }, 198 | { 199 | MethodName: "Update", 200 | Handler: _VPNService_Update_Handler, 201 | }, 202 | { 203 | MethodName: "Restart", 204 | Handler: _VPNService_Restart_Handler, 205 | }, 206 | }, 207 | Streams: []grpc.StreamDesc{}, 208 | Metadata: "vpn.proto", 209 | } 210 | -------------------------------------------------------------------------------- /api/rest.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "google.golang.org/protobuf/encoding/protojson" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/asaskevich/govalidator" 10 | "github.com/cad/ovpm/api/pb" 11 | "github.com/cad/ovpm/bundle" 12 | assetfs "github.com/elazarl/go-bindata-assetfs" 13 | "github.com/go-openapi/runtime/middleware" 14 | "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" 15 | "github.com/sirupsen/logrus" 16 | "golang.org/x/net/context" 17 | "google.golang.org/grpc" 18 | ) 19 | 20 | // NewRESTServer returns a new REST server. 21 | func NewRESTServer(grpcPort string) (http.Handler, context.CancelFunc, error) { 22 | mux := http.NewServeMux() 23 | ctx := context.Background() 24 | ctx, cancel := context.WithCancel(ctx) 25 | if !govalidator.IsNumeric(grpcPort) { 26 | return nil, cancel, fmt.Errorf("grpcPort should be numeric") 27 | } 28 | endPoint := fmt.Sprintf("localhost:%s", grpcPort) 29 | ctx = NewOriginTypeContext(ctx, OriginTypeREST) 30 | gmux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{ 31 | MarshalOptions: protojson.MarshalOptions{ 32 | UseProtoNames: true, 33 | EmitUnpopulated: true, 34 | }, 35 | UnmarshalOptions: protojson.UnmarshalOptions{ 36 | DiscardUnknown: true, 37 | }, 38 | })) 39 | opts := []grpc.DialOption{grpc.WithInsecure()} 40 | err := pb.RegisterVPNServiceHandlerFromEndpoint(ctx, gmux, endPoint, opts) 41 | if err != nil { 42 | return nil, cancel, err 43 | } 44 | 45 | err = pb.RegisterUserServiceHandlerFromEndpoint(ctx, gmux, endPoint, opts) 46 | if err != nil { 47 | return nil, cancel, err 48 | } 49 | 50 | err = pb.RegisterNetworkServiceHandlerFromEndpoint(ctx, gmux, endPoint, opts) 51 | if err != nil { 52 | return nil, cancel, err 53 | } 54 | 55 | err = pb.RegisterAuthServiceHandlerFromEndpoint(ctx, gmux, endPoint, opts) 56 | if err != nil { 57 | return nil, cancel, err 58 | } 59 | 60 | mux.HandleFunc("/api/specs/", specsHandler) 61 | mware := middleware.Redoc(middleware.RedocOpts{ 62 | BasePath: "/api/docs/", 63 | SpecURL: "/api/specs/user.swagger.json", 64 | Path: "user", 65 | }, gmux) 66 | mware = middleware.Redoc(middleware.RedocOpts{ 67 | BasePath: "/api/docs/", 68 | SpecURL: "/api/specs/vpn.swagger.json", 69 | Path: "vpn", 70 | }, mware) 71 | mware = middleware.Redoc(middleware.RedocOpts{ 72 | BasePath: "/api/docs/", 73 | SpecURL: "/api/specs/network.swagger.json", 74 | Path: "network", 75 | }, mware) 76 | mware = middleware.Redoc(middleware.RedocOpts{ 77 | BasePath: "/api/docs/", 78 | SpecURL: "/api/specs/auth.swagger.json", 79 | Path: "auth", 80 | }, mware) 81 | mux.Handle("/api/", mware) 82 | mux.Handle("/", http.FileServer( 83 | &assetfs.AssetFS{Asset: bundle.Asset, AssetDir: bundle.AssetDir, Prefix: "bundle"})) 84 | 85 | return allowCORS(mux), cancel, nil 86 | } 87 | 88 | func specsHandler(w http.ResponseWriter, r *http.Request) { 89 | w.Header().Set("Content-Type", "application/json") 90 | switch r.URL.Path { 91 | case "/api/specs/user.swagger.json": 92 | userData, err := bundle.Asset("bundle/user.swagger.json") 93 | if err != nil { 94 | logrus.Warn(err) 95 | } 96 | w.Write(userData) 97 | 98 | case "/api/specs/network.swagger.json": 99 | networkData, err := bundle.Asset("bundle/network.swagger.json") 100 | if err != nil { 101 | logrus.Warn(err) 102 | } 103 | w.Write(networkData) 104 | case "/api/specs/vpn.swagger.json": 105 | vpnData, err := bundle.Asset("bundle/vpn.swagger.json") 106 | if err != nil { 107 | logrus.Warn(err) 108 | } 109 | w.Write(vpnData) 110 | case "/api/specs/auth.swagger.json": 111 | vpnData, err := bundle.Asset("bundle/auth.swagger.json") 112 | if err != nil { 113 | logrus.Warn(err) 114 | } 115 | w.Write(vpnData) 116 | } 117 | } 118 | 119 | func webuiHandler(w http.ResponseWriter, r *http.Request) { 120 | switch r.URL.Path { 121 | case "/bundle.js": 122 | userData, err := bundle.Asset("bundle/bundle.js") 123 | if err != nil { 124 | logrus.Warn(err) 125 | } 126 | w.Header().Set("Content-Type", "application/javascript") 127 | w.Write(userData) 128 | case "/js/mui.min.js": 129 | userData, err := bundle.Asset("bundle/js/mui.min.js") 130 | if err != nil { 131 | logrus.Warn(err) 132 | } 133 | w.Header().Set("Content-Type", "application/javascript") 134 | w.Write(userData) 135 | case "/css/bootstrap.min.css": 136 | userData, err := bundle.Asset("bundle/css/bootstrap.min.css") 137 | if err != nil { 138 | logrus.Warn(err) 139 | } 140 | w.Header().Set("Content-Type", "text/css") 141 | w.Write(userData) 142 | case "/css/mui.min.css": 143 | userData, err := bundle.Asset("bundle/css/mui.min.css") 144 | if err != nil { 145 | logrus.Warn(err) 146 | } 147 | w.Header().Set("Content-Type", "text/css") 148 | w.Write(userData) 149 | case "/fonts/glyphicons-halflings-regular.woff": 150 | userData, err := bundle.Asset("bundle/glyphicons-halflings-regular.woff") 151 | if err != nil { 152 | logrus.Warn(err) 153 | } 154 | w.Header().Set("Content-Type", "application/font-woff") 155 | w.Write(userData) 156 | 157 | default: 158 | networkData, err := bundle.Asset("bundle/index.html") 159 | if err != nil { 160 | logrus.Warn(err) 161 | } 162 | w.Write(networkData) 163 | } 164 | } 165 | 166 | func preflightHandler(w http.ResponseWriter, r *http.Request) { 167 | headers := []string{"Content-Type", "Accept", "Authorization"} 168 | w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) 169 | methods := []string{"GET", "HEAD", "POST", "PUT", "DELETE"} 170 | w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) 171 | w.Header().Set("Access-Control-Expose-Headers", "Access-Control-Allow-Origin") 172 | logrus.Debugf("rest: preflight request for %s", r.URL.Path) 173 | return 174 | } 175 | 176 | func allowCORS(h http.Handler) http.Handler { 177 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 178 | if origin := r.Header.Get("Origin"); origin != "" { 179 | w.Header().Set("Access-Control-Allow-Origin", origin) 180 | if r.Method == "OPTIONS" && r.Header.Get("Access-Control-Request-Method") != "" { 181 | preflightHandler(w, r) 182 | return 183 | } 184 | } 185 | h.ServeHTTP(w, r) 186 | }) 187 | } 188 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | ## After Docker 5 | echo "travis build no: $TRAVIS_BUILD_NUMBER" 6 | echo "travis tag: $TRAVIS_TAG" 7 | echo "travis go version: $TRAVIS_GO_VERSION" 8 | 9 | export RELEASEVER=${TRAVIS_BUILD_NUMBER:-"1"} 10 | echo "releasever: $RELEASEVER" 11 | 12 | export VERSION="0.0" 13 | export LOCAL_GIT_TAG=`git name-rev --tags --name-only $(git rev-parse HEAD) | cut -d 'v' -f 2` 14 | if [ "$LOCAL_GIT_TAG" != "undefined" ]; then 15 | export VERSION=$LOCAL_GIT_TAG 16 | fi 17 | echo "Version is $VERSION" 18 | 19 | mkdir -p $RELEASEDIR/ 20 | mkdir -p $RELEASEDIR/build/ 21 | mkdir -p $RELEASEDIR/rpm/ 22 | mkdir -p $RELEASEDIR/deb/ 23 | rm -rf $RELEASEDIR/build/* 24 | rm -rf $RELEASEDIR/rpm/* 25 | rm -rf $RELEASEDIR/deb/* 26 | mkdir -p $RELEASEDIR/build/usr/sbin/ 27 | mkdir -p $RELEASEDIR/build/usr/bin/ 28 | mkdir -p $RELEASEDIR/build/var/db/ovpm 29 | mkdir -p $RELEASEDIR/build/$UNITDIR 30 | mkdir -p $RELEASEDIR/deb/conf 31 | go get -v -t -d ./... 32 | 33 | #build 34 | #install 35 | GOOS=linux go build -o $RELEASEDIR/build/usr/sbin/ovpmd ./cmd/ovpmd 36 | GOOS=linux go build -o $RELEASEDIR/build/usr/bin/ovpm ./cmd/ovpm 37 | cp $DIR/contrib/systemd/ovpmd.service $RELEASEDIR/build/$UNITDIR 38 | cp $DIR/contrib/yumrepo.repo $RELEASEDIR/rpm/ovpm.repo 39 | cp $DIR/contrib/deb-repo-config $RELEASEDIR/deb/conf/distributions 40 | 41 | #package 42 | fpm -s dir -t rpm -n ovpm --version $VERSION --iteration $RELEASEVER --depends openvpn --description "OVPM makes all aspects of OpenVPN server administration a breeze." --before-install $DIR/contrib/beforeinstall.sh --after-install $DIR/contrib/afterinstall.sh --before-remove $DIR/contrib/beforeremove.sh --after-upgrade $DIR/contrib/afterupgrade.sh -p $RELEASEDIR/rpm -C $RELEASEDIR/build . 43 | 44 | fpm -s dir -t deb -n ovpm --version $VERSION --iteration $RELEASEVER --depends openvpn --description "OVPM makes all aspects of OpenVPN server administration a breeze." --before-install $DIR/contrib/beforeinstall.sh --after-install $DIR/contrib/afterinstall.sh --before-remove $DIR/contrib/beforeremove.sh --after-upgrade $DIR/contrib/afterupgrade.sh -p $RELEASEDIR/deb -C $RELEASEDIR/build . 45 | 46 | #create rpm repo 47 | createrepo --database $RELEASEDIR/rpm 48 | 49 | #create deb repo 50 | reprepro -b $RELEASEDIR/deb/ includedeb ovpm $RELEASEDIR/deb/*.deb 51 | 52 | # clean 53 | rm -rf $RELEASEDIR/build 54 | echo "packages are ready at ./deb/ and ./rpm/" 55 | -------------------------------------------------------------------------------- /cmd/ovpm/action_vpn.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "os" 8 | 9 | "github.com/asaskevich/govalidator" 10 | "github.com/cad/ovpm/api/pb" 11 | "github.com/cad/ovpm/errors" 12 | "github.com/olekukonko/tablewriter" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type vpnInitParams struct { 17 | rpcServURLStr string 18 | hostname string 19 | port string 20 | proto pb.VPNProto 21 | netCIDR string 22 | dnsAddr string 23 | keepalivePeriod string 24 | keepaliveTimeout string 25 | useLZO bool 26 | } 27 | 28 | func vpnStatusAction(rpcServURLStr string) error { 29 | // Parse RPC Server's URL. 30 | rpcSrvURL, err := url.Parse(rpcServURLStr) 31 | if err != nil { 32 | return errors.BadURL(rpcServURLStr, err) 33 | } 34 | 35 | // Create a gRPC connection to the server. 36 | rpcConn, err := grpcConnect(rpcSrvURL) 37 | if err != nil { 38 | exit(1) 39 | return err 40 | } 41 | defer rpcConn.Close() 42 | 43 | // Get services. 44 | var vpnSvc = pb.NewVPNServiceClient(rpcConn) 45 | 46 | // Request vpn status and user list from the services. 47 | vpnStatusResp, err := vpnSvc.Status(context.Background(), &pb.VPNStatusRequest{}) 48 | if err != nil { 49 | err := errors.UnknownGRPCError(err) 50 | exit(1) 51 | return err 52 | } 53 | 54 | // Prepare table data and draw it on the terminal. 55 | table := tablewriter.NewWriter(os.Stdout) 56 | table.SetHeader([]string{"attribute", "value"}) 57 | table.Append([]string{"Name", vpnStatusResp.Name}) 58 | table.Append([]string{"Hostname", vpnStatusResp.Hostname}) 59 | table.Append([]string{"Port", vpnStatusResp.Port}) 60 | table.Append([]string{"Proto", vpnStatusResp.Proto}) 61 | table.Append([]string{"Network", vpnStatusResp.Net}) 62 | table.Append([]string{"Netmask", vpnStatusResp.Mask}) 63 | table.Append([]string{"Created At", vpnStatusResp.CreatedAt}) 64 | table.Append([]string{"DNS", vpnStatusResp.Dns}) 65 | table.Append([]string{"Cert Exp", vpnStatusResp.ExpiresAt}) 66 | table.Append([]string{"CA Cert Exp", vpnStatusResp.CaExpiresAt}) 67 | table.Append([]string{"Use LZO", fmt.Sprintf("%t", vpnStatusResp.UseLzo)}) 68 | 69 | table.Render() 70 | 71 | return nil 72 | } 73 | 74 | func vpnInitAction(params vpnInitParams) error { 75 | // Parse RPC Server's URL. 76 | rpcSrvURL, err := url.Parse(params.rpcServURLStr) 77 | if err != nil { 78 | return errors.BadURL(params.rpcServURLStr, err) 79 | } 80 | 81 | // Create a gRPC connection to the server. 82 | rpcConn, err := grpcConnect(rpcSrvURL) 83 | if err != nil { 84 | exit(1) 85 | return err 86 | } 87 | defer rpcConn.Close() 88 | 89 | // Prepare service caller.. 90 | var vpnSvc = pb.NewVPNServiceClient(rpcConn) 91 | 92 | // Request init request from vpn service. 93 | _, err = vpnSvc.Init(context.Background(), &pb.VPNInitRequest{ 94 | Hostname: params.hostname, 95 | Port: params.port, 96 | ProtoPref: params.proto, 97 | IpBlock: params.netCIDR, 98 | Dns: params.dnsAddr, 99 | KeepalivePeriod: params.keepalivePeriod, 100 | KeepaliveTimeout: params.keepaliveTimeout, 101 | UseLzo: params.useLZO, 102 | }) 103 | if err != nil { 104 | err := errors.UnknownGRPCError(err) 105 | exit(1) 106 | return err 107 | } 108 | 109 | logrus.WithFields(logrus.Fields{ 110 | "SERVER": "OpenVPN", 111 | "CIDR": params.netCIDR, 112 | "PROTO": params.proto, 113 | "HOSTNAME": params.hostname, 114 | "PORT": params.port, 115 | "KEEPALIVE_PERIOD": params.keepalivePeriod, 116 | "KEEPALIVE_TIMEOUT": params.keepaliveTimeout, 117 | "USE_LZO": params.useLZO, 118 | }).Infoln("vpn initialized") 119 | return nil 120 | } 121 | 122 | func vpnUpdateAction(rpcServURLStr string, netCIDR *string, dnsAddr *string, useLzo *bool) error { 123 | // Parse RPC Server's URL. 124 | rpcSrvURL, err := url.Parse(rpcServURLStr) 125 | if err != nil { 126 | return errors.BadURL(rpcServURLStr, err) 127 | } 128 | 129 | // Create a gRPC connection to the server. 130 | rpcConn, err := grpcConnect(rpcSrvURL) 131 | if err != nil { 132 | exit(1) 133 | return err 134 | } 135 | defer rpcConn.Close() 136 | 137 | // Set netCIDR if provided. 138 | var targetNetCIDR string 139 | if netCIDR != nil { 140 | if !govalidator.IsCIDR(*netCIDR) { 141 | return errors.NotCIDR(*netCIDR) 142 | } 143 | 144 | var response string 145 | for { 146 | fmt.Println("If you proceed, you will loose all your static ip definitions.") 147 | fmt.Println("Any user that is defined to have a static ip will be set to be dynamic again.") 148 | fmt.Println() 149 | fmt.Println("Are you sure ? (y/N)") 150 | _, err := fmt.Scanln(&response) 151 | if err != nil { 152 | logrus.Fatal(err) 153 | exit(1) 154 | return err 155 | } 156 | okayResponses := []string{"y", "Y", "yes", "Yes", "YES"} 157 | nokayResponses := []string{"n", "N", "no", "No", "NO"} 158 | if stringInSlice(response, okayResponses) { 159 | break 160 | } else if stringInSlice(response, nokayResponses) { 161 | return fmt.Errorf("user decided to cancel") 162 | } 163 | } 164 | targetNetCIDR = *netCIDR 165 | } 166 | 167 | // Set DNS address if provided. 168 | var targetDNSAddr string 169 | if dnsAddr != nil { 170 | if !govalidator.IsIPv4(*dnsAddr) { 171 | return errors.NotIPv4(*dnsAddr) 172 | } 173 | targetDNSAddr = *dnsAddr 174 | } 175 | 176 | // Set USE-LZO preference if provided. 177 | var targetLZOPref pb.VPNLZOPref 178 | if useLzo == nil { 179 | targetLZOPref = pb.VPNLZOPref_USE_LZO_NOPREF 180 | } else { 181 | if *useLzo == true { 182 | targetLZOPref = pb.VPNLZOPref_USE_LZO_ENABLE 183 | } 184 | if *useLzo == false { 185 | targetLZOPref = pb.VPNLZOPref_USE_LZO_DISABLE 186 | } 187 | } 188 | 189 | // Prepare service caller. 190 | var vpnSvc = pb.NewVPNServiceClient(rpcConn) 191 | 192 | // Request update request from vpn service. 193 | _, err = vpnSvc.Update(context.Background(), &pb.VPNUpdateRequest{ 194 | IpBlock: targetNetCIDR, 195 | Dns: targetDNSAddr, 196 | LzoPref: targetLZOPref, 197 | }) 198 | if err != nil { 199 | err := errors.UnknownGRPCError(err) 200 | exit(1) 201 | return err 202 | } 203 | 204 | logrus.WithFields(logrus.Fields{ 205 | "SERVER": "OpenVPN", 206 | "CIDR": targetNetCIDR, 207 | "DNS": targetDNSAddr, 208 | "USE_LZO": targetLZOPref.String(), 209 | }).Infoln("changes applied") 210 | 211 | return nil 212 | } 213 | 214 | func vpnRestartAction(rpcServURLStr string) error { 215 | // Parse RPC Server's URL. 216 | rpcSrvURL, err := url.Parse(rpcServURLStr) 217 | if err != nil { 218 | return errors.BadURL(rpcServURLStr, err) 219 | } 220 | 221 | // Create a gRPC connection to the server. 222 | rpcConn, err := grpcConnect(rpcSrvURL) 223 | if err != nil { 224 | err := errors.UnknownGRPCError(err) 225 | exit(1) 226 | return err 227 | } 228 | defer rpcConn.Close() 229 | 230 | // Prepare service caller. 231 | var vpnSvc = pb.NewVPNServiceClient(rpcConn) 232 | 233 | _, err = vpnSvc.Restart(context.Background(), &pb.VPNRestartRequest{}) 234 | if err != nil { 235 | err := errors.UnknownGRPCError(err) 236 | exit(1) 237 | return err 238 | } 239 | 240 | logrus.Info("ovpm server restarted") 241 | return nil 242 | } 243 | -------------------------------------------------------------------------------- /cmd/ovpm/cmd_net.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/asaskevich/govalidator" 7 | "github.com/cad/ovpm" 8 | "github.com/cad/ovpm/errors" 9 | "github.com/urfave/cli" 10 | ) 11 | 12 | var netDefineCommand = cli.Command{ 13 | Name: "def", 14 | Aliases: []string{"d"}, 15 | Usage: "Define a network.", 16 | Flags: []cli.Flag{ 17 | cli.StringFlag{ 18 | Name: "cidr, c", 19 | Usage: "CIDR of the network", 20 | }, 21 | cli.StringFlag{ 22 | Name: "name, n", 23 | Usage: "name of the network", 24 | }, 25 | cli.StringFlag{ 26 | Name: "type, t", 27 | Usage: "type of the network (see $ovpm net types)", 28 | }, 29 | cli.StringFlag{ 30 | Name: "via, v", 31 | Usage: "if network type is route, via represents route's gateway", 32 | }, 33 | }, 34 | Action: func(c *cli.Context) error { 35 | action = "net:create" 36 | 37 | // Use default port if no port is specified. 38 | daemonPort := ovpm.DefaultDaemonPort 39 | if port := c.GlobalInt("daemon-port"); port != 0 { 40 | daemonPort = port 41 | } 42 | 43 | // Validate network name. 44 | if netName := c.String("name"); govalidator.IsNull(netName) { 45 | err := errors.EmptyValue("net", netName) 46 | exit(1) 47 | return err 48 | } 49 | 50 | // Validate network types. 51 | if netType := c.String("type"); !ovpm.IsNetworkType(netType) { 52 | err := errors.NotValidNetworkType("type", netType) 53 | exit(1) 54 | return err 55 | } 56 | 57 | // Validate if via can be set. 58 | if netVia := c.String("via"); !govalidator.IsNull(netVia) { 59 | if ovpm.NetworkTypeFromString(c.String("type")) != ovpm.ROUTE { 60 | err := errors.ConflictingDemands("--via flag can only be used with --type ROUTE") 61 | exit(1) 62 | return err 63 | } 64 | } 65 | 66 | // Validate network CIDR. 67 | if netCIDR := c.String("cidr"); !govalidator.IsCIDR(netCIDR) { 68 | err := errors.NotCIDR(netCIDR) 69 | exit(1) 70 | return err 71 | } 72 | 73 | var via *string 74 | if !govalidator.IsNull(c.String("via")) { 75 | tmp := c.String("via") 76 | via = &tmp 77 | } 78 | 79 | // If dry run, then don't call the action, just preprocess. 80 | if c.GlobalBool("dry-run") { 81 | return nil 82 | } 83 | 84 | return netDefAction(fmt.Sprintf("grpc://localhost:%d", daemonPort), c.String("name"), c.String("cidr"), c.String("type"), via) 85 | }, 86 | } 87 | 88 | var netListCommand = cli.Command{ 89 | Name: "list", 90 | Aliases: []string{"l"}, 91 | Usage: "List defined networks.", 92 | Action: func(c *cli.Context) error { 93 | action = "net:list" 94 | // Use default port if no port is specified. 95 | daemonPort := ovpm.DefaultDaemonPort 96 | if port := c.GlobalInt("daemon-port"); port != 0 { 97 | daemonPort = port 98 | } 99 | 100 | // If dry run, then don't call the action, just preprocess. 101 | if c.GlobalBool("dry-run") { 102 | return nil 103 | } 104 | 105 | return netListAction(fmt.Sprintf("grpc://localhost:%d", daemonPort)) 106 | }, 107 | } 108 | 109 | var netTypesCommand = cli.Command{ 110 | Name: "types", 111 | Aliases: []string{"t"}, 112 | Usage: "Show available network types.", 113 | Action: func(c *cli.Context) error { 114 | action = "net:types" 115 | 116 | // Use default port if no port is specified. 117 | daemonPort := ovpm.DefaultDaemonPort 118 | if port := c.GlobalInt("daemon-port"); port != 0 { 119 | daemonPort = port 120 | } 121 | 122 | // If dry run, then don't call the action, just preprocess. 123 | if c.GlobalBool("dry-run") { 124 | return nil 125 | } 126 | 127 | return netTypesAction(fmt.Sprintf("grpc://localhost:%d", daemonPort)) 128 | }, 129 | } 130 | 131 | var netUndefineCommand = cli.Command{ 132 | Name: "undef", 133 | Aliases: []string{"u"}, 134 | Usage: "Undefine an existing network.", 135 | Flags: []cli.Flag{ 136 | cli.StringFlag{ 137 | Name: "net, n", 138 | Usage: "name of the network", 139 | }, 140 | }, 141 | Action: func(c *cli.Context) error { 142 | action = "net:delete" 143 | 144 | // Use default port if no port is specified. 145 | daemonPort := ovpm.DefaultDaemonPort 146 | if port := c.GlobalInt("daemon-port"); port != 0 { 147 | daemonPort = port 148 | } 149 | 150 | // Validate network name. 151 | if networkName := c.String("net"); govalidator.IsNull(networkName) { 152 | err := errors.EmptyValue("net", networkName) 153 | exit(1) 154 | return err 155 | } 156 | 157 | // If dry run, then don't call the action, just preprocess. 158 | if c.GlobalBool("dry-run") { 159 | return nil 160 | } 161 | 162 | return netUndefAction(fmt.Sprintf("grpc://localhost:%d", daemonPort), c.String("net")) 163 | }, 164 | } 165 | 166 | var netAssociateCommand = cli.Command{ 167 | Name: "assoc", 168 | Aliases: []string{"a"}, 169 | Usage: "Associate a user with a network.", 170 | Flags: []cli.Flag{ 171 | cli.StringFlag{ 172 | Name: "net, n", 173 | Usage: "name of the network", 174 | }, 175 | 176 | cli.StringFlag{ 177 | Name: "user, u", 178 | Usage: "name of the user", 179 | }, 180 | }, 181 | Action: func(c *cli.Context) error { 182 | action = "net:associate" 183 | 184 | // Use default port if no port is specified. 185 | daemonPort := ovpm.DefaultDaemonPort 186 | if port := c.GlobalInt("daemon-port"); port != 0 { 187 | daemonPort = port 188 | } 189 | 190 | var inBulk bool 191 | 192 | // Validate username and network name. 193 | if netName := c.String("net"); govalidator.IsNull(netName) { 194 | err := errors.EmptyValue("network", netName) 195 | exit(1) 196 | return err 197 | } 198 | if username := c.String("user"); govalidator.IsNull(username) { 199 | err := errors.EmptyValue("username", username) 200 | exit(1) 201 | return err 202 | } 203 | 204 | // Mark inBulk if username is set to asterisk. 205 | if c.String("user") == "*" { 206 | inBulk = true 207 | } 208 | 209 | // If dry run, then don't call the action, just preprocess. 210 | if c.GlobalBool("dry-run") { 211 | return nil 212 | } 213 | 214 | return netAssocAction(fmt.Sprintf("grpc://localhost:%d", daemonPort), c.String("net"), c.String("user"), inBulk) 215 | }, 216 | } 217 | 218 | var netDissociateCommand = cli.Command{ 219 | Name: "dissoc", 220 | Aliases: []string{"di"}, 221 | Usage: "Dissociate a user from a network.", 222 | Flags: []cli.Flag{ 223 | cli.StringFlag{ 224 | Name: "net, n", 225 | Usage: "name of the network", 226 | }, 227 | 228 | cli.StringFlag{ 229 | Name: "user, u", 230 | Usage: "name of the user", 231 | }, 232 | }, 233 | Action: func(c *cli.Context) error { 234 | action = "net:dissociate" 235 | 236 | // Use default port if no port is specified. 237 | daemonPort := ovpm.DefaultDaemonPort 238 | if port := c.GlobalInt("daemon-port"); port != 0 { 239 | daemonPort = port 240 | } 241 | 242 | var inBulk bool 243 | 244 | // Validate username and network name. 245 | if netName := c.String("net"); govalidator.IsNull(netName) { 246 | err := errors.EmptyValue("network", netName) 247 | exit(1) 248 | return err 249 | } 250 | if username := c.String("user"); govalidator.IsNull(username) { 251 | err := errors.EmptyValue("username", username) 252 | exit(1) 253 | return err 254 | } 255 | 256 | // Mark inBulk if username is set to asterisk. 257 | if c.String("user") == "*" { 258 | inBulk = true 259 | } 260 | 261 | // If dry run, then don't call the action, just preprocess. 262 | if c.GlobalBool("dry-run") { 263 | return nil 264 | } 265 | 266 | return netDissocAction(fmt.Sprintf("grpc://localhost:%d", daemonPort), c.String("net"), c.String("user"), inBulk) 267 | }, 268 | } 269 | 270 | func init() { 271 | app.Commands = append(app.Commands, 272 | cli.Command{ 273 | Name: "net", 274 | Usage: "Network Operations", 275 | Aliases: []string{"n"}, 276 | Subcommands: []cli.Command{ 277 | netListCommand, 278 | netTypesCommand, 279 | netDefineCommand, 280 | netUndefineCommand, 281 | netAssociateCommand, 282 | netDissociateCommand, 283 | }, 284 | }, 285 | ) 286 | } 287 | -------------------------------------------------------------------------------- /cmd/ovpm/common_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/urfave/cli" 7 | ) 8 | 9 | func SetupTest() { 10 | } 11 | 12 | func init() { 13 | prevBeforeFunc := app.Before 14 | 15 | // Override dry-run flag ensuring it's set to true when testing. 16 | app.Before = func(c *cli.Context) error { 17 | if err := c.GlobalSet("dry-run", "true"); err != nil { 18 | fmt.Printf("can not set global flag 'dry-run' to true: %v\n", err) 19 | return err 20 | } 21 | 22 | return prevBeforeFunc(c) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/ovpm/global.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // label of the action that is being currently executed. 4 | // 5 | // TODO(cad): Since can achieve the same functionality with cmd contexts, 6 | // lets remove this in the future. 7 | var action string 8 | -------------------------------------------------------------------------------- /cmd/ovpm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/cad/ovpm" 7 | "github.com/sirupsen/logrus" 8 | "github.com/urfave/cli" 9 | ) 10 | 11 | var app = cli.NewApp() 12 | 13 | func main() { 14 | app.Run(os.Args) 15 | } 16 | 17 | func init() { 18 | app.Name = "ovpm" 19 | app.Usage = "OpenVPN Manager" 20 | app.Version = ovpm.Version 21 | app.Flags = []cli.Flag{ 22 | cli.BoolFlag{ 23 | Name: "verbose", 24 | Usage: "verbose output", 25 | }, 26 | cli.IntFlag{ 27 | Name: "daemon-port", 28 | Usage: "port number for OVPM daemon to call", 29 | }, 30 | cli.BoolFlag{ 31 | Name: "dry-run", 32 | Usage: "just validate command flags; not make any calls to the daemon behind", 33 | }, 34 | } 35 | app.Before = func(c *cli.Context) error { 36 | logrus.SetLevel(logrus.InfoLevel) 37 | if c.GlobalBool("verbose") { 38 | logrus.SetLevel(logrus.DebugLevel) 39 | } 40 | return nil 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /cmd/ovpm/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/cad/ovpm" 9 | ) 10 | 11 | func TestMainCmd(t *testing.T) { 12 | output := new(bytes.Buffer) 13 | app.Writer = output 14 | 15 | err := app.Run([]string{"ovpm"}) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | if !strings.Contains(output.String(), ovpm.Version) { 21 | t.Fatal("version is missing") 22 | } 23 | 24 | if !strings.Contains(output.String(), "user, u") { 25 | t.Fatal("subcommand missing 'user'") 26 | } 27 | 28 | if !strings.Contains(output.String(), "vpn, v") { 29 | t.Fatal("subcommand missing 'vpn'") 30 | } 31 | 32 | if !strings.Contains(output.String(), "net, n") { 33 | t.Fatal("subcommand missing 'net'") 34 | } 35 | 36 | if !strings.Contains(output.String(), "help, h") { 37 | t.Fatal("subcommand missing 'help'") 38 | } 39 | 40 | if !strings.Contains(output.String(), "--daemon-port") { 41 | t.Fatal("flag missing '--daemon-port'") 42 | } 43 | 44 | if !strings.Contains(output.String(), "--verbose") { 45 | t.Fatal("flag missing '--verbose'") 46 | } 47 | 48 | if !strings.Contains(output.String(), "--version") { 49 | t.Fatal("flag missing '--version'") 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /cmd/ovpm/net_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestNetCmd(t *testing.T) { 10 | output := new(bytes.Buffer) 11 | app.Writer = output 12 | 13 | err := app.Run([]string{"ovpm", "net"}) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | if !strings.Contains(output.String(), "list, l") { 19 | t.Fatal("subcommand missing 'list, l'") 20 | } 21 | 22 | if !strings.Contains(output.String(), "types, t") { 23 | t.Fatal("subcommand missing 'types, t'") 24 | } 25 | 26 | if !strings.Contains(output.String(), "def, d") { 27 | t.Fatal("subcommand missing 'undef, u'") 28 | } 29 | 30 | if !strings.Contains(output.String(), "assoc, a") { 31 | t.Fatal("subcommand missing 'assoc, a'") 32 | } 33 | 34 | if !strings.Contains(output.String(), "dissoc, di") { 35 | t.Fatal("subcommand missing 'dissoc, di'") 36 | } 37 | } 38 | 39 | func TestNetDefineCmd(t *testing.T) { 40 | output := new(bytes.Buffer) 41 | app.Writer = output 42 | 43 | var err error 44 | 45 | // Empty call 46 | err = app.Run([]string{"ovpm", "net", "def"}) 47 | if err == nil { 48 | t.Fatal("error is expected about missing fields, but we didn't got error") 49 | } 50 | 51 | // Missing type 52 | err = app.Run([]string{"ovpm", "net", "def", "--cidr", "192.168.1.1/24"}) 53 | if err == nil { 54 | t.Fatal("error is expected about missing network type, but we didn't got error") 55 | } 56 | // Missing name 57 | err = app.Run([]string{"ovpm", "net", "def", "--type", "SERVERNET", "--cidr", "192.168.1.1/24"}) 58 | if err == nil { 59 | t.Fatal("error is expected about missing network name, but we didn't got error") 60 | } 61 | 62 | // Incorrect type 63 | err = app.Run([]string{"ovpm", "net", "def", "--name", "asd", "--type", "SERVERNUT", "--cidr", "192.168.1.1/24"}) 64 | if err == nil { 65 | t.Fatal("error is expected about incorrect server type, but we didn't got error") 66 | } 67 | 68 | // Incorrect use of via 69 | err = app.Run([]string{"ovpm", "net", "def", "--name", "asd", "--type", "SERVERNET", "--cidr", "192.168.1.1/24", "--via", "8.8.8.8"}) 70 | if err == nil { 71 | t.Fatal("error is expected about incorrect use of via, but we didn't got error") 72 | } 73 | 74 | // Incorrect cidr format 75 | err = app.Run([]string{"ovpm", "net", "def", "--name", "asd", "--type", "SERVERNET", "--cidr", "192.168.1.1"}) 76 | if err == nil { 77 | t.Fatal("error is expected about incorrect cidr format, but we didn't got error") 78 | } 79 | 80 | // Ensure ROUTE type use without --via 81 | err = app.Run([]string{"ovpm", "net", "def", "--name", "asd", "--type", "ROUTE", "--cidr", "192.168.1.1/24"}) 82 | if err != nil && !strings.Contains(err.Error(), "grpc") { 83 | t.Fatalf("error is not expected: %v", err) 84 | } 85 | 86 | // Incorrect use of via 87 | err = app.Run([]string{"ovpm", "net", "def", "--name", "asd", "--type", "SERVERNET", "--cidr", "192.168.1.1/24", "--via", "8.8.8.8/24"}) 88 | if err == nil { 89 | t.Fatal("error is expected about incorrect via format, but we didn't got error") 90 | } 91 | 92 | // Ensure network name alphanumeric and dot, underscore chars are allowed 93 | err = app.Run([]string{"ovpm", "net", "def", "--name", "asd.asdd5sa_fasA32", "--type", "ROUTE", "--cidr", "192.168.1.1/24"}) 94 | if err != nil && !strings.Contains(err.Error(), "grpc") { 95 | t.Fatalf("error is not expected: %v", err) 96 | } 97 | 98 | } 99 | 100 | func TestNetUnDefineCmd(t *testing.T) { 101 | output := new(bytes.Buffer) 102 | app.Writer = output 103 | 104 | var err error 105 | 106 | // Empty call 107 | err = app.Run([]string{"ovpm", "net", "undef"}) 108 | if err == nil { 109 | t.Fatal("error is expected about missing fields, but we didn't got error") 110 | } 111 | 112 | } 113 | 114 | func TestAssocCmd(t *testing.T) { 115 | output := new(bytes.Buffer) 116 | app.Writer = output 117 | 118 | var err error 119 | 120 | // Empty call 121 | err = app.Run([]string{"ovpm", "net", "assoc"}) 122 | if err == nil { 123 | t.Fatal("error is expected about missing fields, but we didn't got error") 124 | } 125 | 126 | // Missing network name 127 | err = app.Run([]string{"ovpm", "net", "def", "--user", "asd"}) 128 | if err == nil { 129 | t.Fatal("error is expected about missing network name, but we didn't got error") 130 | } 131 | 132 | // Missing username 133 | err = app.Run([]string{"ovpm", "net", "def", "--network", "asddsa"}) 134 | if err == nil { 135 | t.Fatal("error is expected about missing username, but we didn't got error") 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cmd/ovpm/user_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestUserCmd(t *testing.T) { 10 | output := new(bytes.Buffer) 11 | app.Writer = output 12 | 13 | err := app.Run([]string{"ovpm", "user"}) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | if !strings.Contains(output.String(), "list, l") { 19 | t.Fatal("subcommand missing 'list, l'") 20 | } 21 | 22 | if !strings.Contains(output.String(), "create, c") { 23 | t.Fatal("subcommand missing 'create, c'") 24 | } 25 | 26 | if !strings.Contains(output.String(), "update, u") { 27 | t.Fatal("subcommand missing 'update, u'") 28 | } 29 | 30 | if !strings.Contains(output.String(), "delete, d") { 31 | t.Fatal("subcommand missing 'delete, d'") 32 | } 33 | 34 | if !strings.Contains(output.String(), "renew, r") { 35 | t.Fatal("subcommand missing 'renew, r'") 36 | } 37 | 38 | if !strings.Contains(output.String(), "genconfig, g") { 39 | t.Fatal("subcommand missing 'update, u'") 40 | } 41 | } 42 | 43 | func TestUserCreateCmd(t *testing.T) { 44 | output := new(bytes.Buffer) 45 | app.Writer = output 46 | 47 | var err error 48 | 49 | // Empty call 50 | err = app.Run([]string{"ovpm", "user", "create"}) 51 | if err == nil { 52 | t.Fatal("error is expected about missing fields, but we didn't got error") 53 | } 54 | 55 | // Missing password 56 | err = app.Run([]string{"ovpm", "user", "create", "--username", "sad"}) 57 | if err == nil { 58 | t.Fatal("error is expected about missing password, but we didn't got error") 59 | } 60 | 61 | // Missing username 62 | err = app.Run([]string{"ovpm", "user", "create", "--password", "sad"}) 63 | if err == nil { 64 | t.Fatal("error is expected about missing password, but we didn't got error") 65 | } 66 | 67 | // Malformed static ip 68 | err = app.Run([]string{"ovpm", "user", "create", "--username", "sad", "--password", "asdf", "--static", "asdf"}) 69 | if err == nil { 70 | t.Fatal("error is expected about malformed static ip, but we didn't got error") 71 | } 72 | 73 | // Ensure proper static ip 74 | err = app.Run([]string{"ovpm", "user", "create", "--username", "adsf", "--password", "1234", "--static", "10.9.0.4"}) 75 | if err != nil && !strings.Contains(err.Error(), "grpc") { 76 | t.Fatalf("error is not expected: %v", err) 77 | } 78 | 79 | // Ensure username chars 80 | err = app.Run([]string{"ovpm", "user", "create", "--username", "sdafADSFasdf325235.dsafsaf-asdffdsa_h5223s", "--password", "1234", "--static", "10.9.0.4"}) 81 | if err != nil && !strings.Contains(err.Error(), "grpc") { 82 | t.Fatalf("error is not expected: %v", err) 83 | } 84 | 85 | } 86 | 87 | func TestUserUpdateCmd(t *testing.T) { 88 | output := new(bytes.Buffer) 89 | app.Writer = output 90 | 91 | var err error 92 | 93 | // Empty call 94 | err = app.Run([]string{"ovpm", "user", "update"}) 95 | if err == nil { 96 | t.Fatal("error is expected about missing fields, but we didn't got error") 97 | } 98 | 99 | // Commented out because it makes the implementation easier. 100 | // // Ensure missing fields 101 | // err = app.Run([]string{"ovpm", "user", "update", "--username", "foobar"}) 102 | // if err == nil { 103 | // t.Fatal("error is expected about missing fields, but we didn't got error") 104 | // } 105 | 106 | // Mix gw with no-gw 107 | err = app.Run([]string{"ovpm", "user", "update", "--no-gw", "--gw"}) 108 | if err == nil { 109 | t.Fatal("error is expected about gw mutually exclusivity, but we didn't got error") 110 | } 111 | 112 | // Mix admin with no-admin 113 | err = app.Run([]string{"ovpm", "user", "update", "--admin", "--no-admin"}) 114 | if err == nil { 115 | t.Fatal("error is expected about admin mutually exclusivity, but we didn't got error") 116 | } 117 | 118 | // Malformed static 119 | err = app.Run([]string{"ovpm", "user", "update", "--username", "foo", "--static", "sadfsadf"}) 120 | if err == nil { 121 | t.Fatal("error is expected about static being malformed ip, but we didn't got error") 122 | } 123 | 124 | // Bulk update mutex 125 | err = app.Run([]string{"ovpm", "user", "update", "--username", "*", "--static", "12.12.12.12"}) 126 | if err == nil { 127 | t.Fatal("error is expected about bulk and --static conflict") 128 | } 129 | } 130 | 131 | func TestUserDeleteCmd(t *testing.T) { 132 | output := new(bytes.Buffer) 133 | app.Writer = output 134 | 135 | var err error 136 | 137 | // Empty call 138 | err = app.Run([]string{"ovpm", "user", "delete"}) 139 | if err == nil { 140 | t.Fatal("error is expected about missing fields, but we didn't got error") 141 | } 142 | } 143 | 144 | func TestUserRenewCmd(t *testing.T) { 145 | output := new(bytes.Buffer) 146 | app.Writer = output 147 | 148 | var err error 149 | 150 | // Empty call 151 | err = app.Run([]string{"ovpm", "user", "renew"}) 152 | if err == nil { 153 | t.Fatal("error is expected about missing fields, but we didn't got error") 154 | } 155 | } 156 | 157 | func TestUserGenconfigCmd(t *testing.T) { 158 | output := new(bytes.Buffer) 159 | app.Writer = output 160 | 161 | var err error 162 | 163 | // Empty call 164 | err = app.Run([]string{"ovpm", "user", "delete"}) 165 | if err == nil { 166 | t.Fatal("error is expected about missing fields, but we didn't got error") 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /cmd/ovpm/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/sirupsen/logrus" 11 | "github.com/cad/ovpm/errors" 12 | "github.com/urfave/cli" 13 | 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | func emitToFile(filePath, content string, mode uint) error { 18 | file, err := os.Create(filePath) 19 | if err != nil { 20 | return fmt.Errorf("Cannot create file %s: %v", filePath, err) 21 | 22 | } 23 | if mode != 0 { 24 | file.Chmod(os.FileMode(mode)) 25 | } 26 | defer file.Close() 27 | fmt.Fprintf(file, content) 28 | return nil 29 | } 30 | 31 | func getConn(port string) *grpc.ClientConn { 32 | if port == "" { 33 | port = "9090" 34 | } 35 | 36 | conn, err := grpc.Dial(fmt.Sprintf(":%s", port), grpc.WithInsecure()) 37 | if err != nil { 38 | logrus.Fatalf("fail to dial: %v", err) 39 | } 40 | return conn 41 | } 42 | 43 | // grpcConnect receives a rpc server url and makes a connection to the 44 | // GRPC server. 45 | func grpcConnect(rpcServURL *url.URL) (*grpc.ClientConn, error) { 46 | // Ensure rpcServURL host part contains a localhost addr only. 47 | if !isLoopbackURL(rpcServURL) { 48 | return nil, errors.MustBeLoopbackURL(rpcServURL) 49 | } 50 | 51 | conn, err := grpc.Dial(rpcServURL.Host, grpc.WithInsecure()) 52 | if err != nil { 53 | return nil, errors.UnknownSysError(err) 54 | } 55 | 56 | return conn, nil 57 | } 58 | 59 | // isLoopbackURL is a utility function that determines whether the 60 | // given url.URL's host part resolves to a loopback ip addr or not. 61 | func isLoopbackURL(u *url.URL) bool { 62 | // Resolve url to ip addresses. 63 | ips, err := net.LookupIP(u.Hostname()) 64 | if err != nil { 65 | fmt.Println(err) 66 | return false 67 | } 68 | 69 | // Ensure all resolved ip addrs are loopback addrs. 70 | for _, ip := range ips { 71 | if !ip.IsLoopback() { 72 | return false 73 | } 74 | } 75 | 76 | return true 77 | } 78 | 79 | func stringInSlice(a string, list []string) bool { 80 | for _, b := range list { 81 | if b == a { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | 88 | func exit(status int) { 89 | if flag.Lookup("test.v") == nil { 90 | os.Exit(status) 91 | } else { 92 | 93 | } 94 | } 95 | 96 | // Prints the received message followed by the usage string. 97 | func failureMsg(c *cli.Context, msg string) { 98 | fmt.Printf(msg) 99 | fmt.Println() 100 | fmt.Println(cli.ShowSubcommandHelp(c)) 101 | } 102 | -------------------------------------------------------------------------------- /cmd/ovpm/vpn_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestVPNCmd(t *testing.T) { 10 | output := new(bytes.Buffer) 11 | app.Writer = output 12 | 13 | err := app.Run([]string{"ovpm", "vpn"}) 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | if !strings.Contains(output.String(), "status, s") { 19 | t.Fatal("subcommand missing 'status, s'") 20 | } 21 | 22 | if !strings.Contains(output.String(), "init, i") { 23 | t.Fatal("subcommand missing 'init, i'") 24 | } 25 | 26 | if !strings.Contains(output.String(), "update, u") { 27 | t.Fatal("subcommand missing 'update, u'") 28 | } 29 | 30 | if !strings.Contains(output.String(), "restart, r") { 31 | t.Fatal("subcommand missing 'restart, r'") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cmd/ovpmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strconv" 12 | "syscall" 13 | "time" 14 | 15 | "google.golang.org/grpc" 16 | 17 | "github.com/cad/ovpm" 18 | "github.com/cad/ovpm/api" 19 | "github.com/sirupsen/logrus" 20 | "github.com/urfave/cli" 21 | ) 22 | 23 | var action string 24 | var db *ovpm.DB 25 | 26 | func main() { 27 | app := cli.NewApp() 28 | app.Name = "ovpmd" 29 | app.Usage = "OpenVPN Manager Daemon" 30 | app.Version = ovpm.Version 31 | app.Flags = []cli.Flag{ 32 | cli.BoolFlag{ 33 | Name: "verbose", 34 | Usage: "verbose output", 35 | }, 36 | cli.StringFlag{ 37 | Name: "port", 38 | Usage: "port number for gRPC API daemon", 39 | }, 40 | cli.StringFlag{ 41 | Name: "web-port", 42 | Usage: "port number for the REST API daemon", 43 | }, 44 | } 45 | app.Before = func(c *cli.Context) error { 46 | logrus.SetLevel(logrus.InfoLevel) 47 | if c.GlobalBool("verbose") { 48 | logrus.SetLevel(logrus.DebugLevel) 49 | } 50 | db = ovpm.CreateDB("sqlite3", "") 51 | return nil 52 | } 53 | app.After = func(c *cli.Context) error { 54 | db.Cease() 55 | return nil 56 | } 57 | app.Action = func(c *cli.Context) error { 58 | port := c.String("port") 59 | if port == "" { 60 | port = "9090" 61 | } 62 | 63 | webPort := c.String("web-port") 64 | if webPort == "" { 65 | webPort = "8080" 66 | } 67 | 68 | s := newServer(port, webPort) 69 | s.start() 70 | s.waitForInterrupt() 71 | s.stop() 72 | return nil 73 | } 74 | app.Run(os.Args) 75 | } 76 | 77 | type server struct { 78 | grpcPort string 79 | lis net.Listener 80 | restLis net.Listener 81 | grpcServer *grpc.Server 82 | restServer http.Handler 83 | restCancel context.CancelFunc 84 | restPort string 85 | signal chan os.Signal 86 | done chan bool 87 | } 88 | 89 | func newServer(port, webPort string) *server { 90 | sigs := make(chan os.Signal, 1) 91 | done := make(chan bool, 1) 92 | 93 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 94 | 95 | go func() { 96 | sig := <-sigs 97 | fmt.Println() 98 | fmt.Println(sig) 99 | done <- true 100 | }() 101 | if !ovpm.Testing { 102 | // NOTE(cad): gRPC endpoint listens on localhost. This is important 103 | // because we don't authenticate requests coming from localhost. 104 | // So gRPC endpoint should never listen on something else then 105 | // localhost. 106 | lis, err := net.Listen("tcp4", fmt.Sprintf("127.0.0.1:%s", port)) 107 | if err != nil { 108 | logrus.Fatalf("could not listen to port %s: %v", port, err) 109 | } 110 | 111 | restLis, err := net.Listen("tcp4", fmt.Sprintf("0.0.0.0:%s", webPort)) 112 | if err != nil { 113 | logrus.Fatalf("could not listen to port %s: %v", webPort, err) 114 | } 115 | 116 | rpcServer := api.NewRPCServer() 117 | restServer, restCancel, err := api.NewRESTServer(port) 118 | if err != nil { 119 | logrus.Fatalf("could not get new rest server :%v", err) 120 | } 121 | 122 | return &server{ 123 | lis: lis, 124 | restLis: restLis, 125 | grpcServer: rpcServer, 126 | restServer: restServer, 127 | restCancel: context.CancelFunc(restCancel), 128 | restPort: webPort, 129 | signal: sigs, 130 | done: done, 131 | grpcPort: port, 132 | } 133 | } 134 | return &server{} 135 | 136 | } 137 | 138 | func (s *server) start() { 139 | logrus.Infof("OVPM %s is running gRPC:%s, REST:%s ...", ovpm.Version, s.grpcPort, s.restPort) 140 | go s.grpcServer.Serve(s.lis) 141 | go http.Serve(s.restLis, s.restServer) 142 | ovpm.TheServer().StartVPNProc() 143 | } 144 | 145 | func (s *server) stop() { 146 | logrus.Info("OVPM is shutting down ...") 147 | s.grpcServer.Stop() 148 | s.restCancel() 149 | ovpm.TheServer().StopVPNProc() 150 | 151 | } 152 | 153 | func (s *server) waitForInterrupt() { 154 | <-s.done 155 | go timeout(8 * time.Second) 156 | } 157 | 158 | func timeout(interval time.Duration) { 159 | time.Sleep(interval) 160 | log.Println("Timeout! Killing the main thread...") 161 | os.Exit(-1) 162 | } 163 | 164 | func stringInSlice(a string, list []string) bool { 165 | for _, b := range list { 166 | if b == a { 167 | return true 168 | } 169 | } 170 | return false 171 | } 172 | 173 | func increasePort(p string) string { 174 | i, err := strconv.Atoi(p) 175 | if err != nil { 176 | logrus.Panicf(fmt.Sprintf("can't convert %s to int: %v", p, err)) 177 | 178 | } 179 | i++ 180 | return fmt.Sprintf("%d", i) 181 | } 182 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | // Version defines the version of ovpm. 4 | var Version = "development" 5 | 6 | const ( 7 | // DefaultVPNPort is the default OpenVPN port to listen. 8 | DefaultVPNPort = "1197" 9 | 10 | // DefaultVPNProto is the default OpenVPN protocol to use. 11 | DefaultVPNProto = UDPProto 12 | 13 | // DefaultVPNNetwork is the default OpenVPN network to use. 14 | DefaultVPNNetwork = "10.9.0.0/24" 15 | 16 | // DefaultVPNDNS is the default DNS to push to clients. 17 | DefaultVPNDNS = "8.8.8.8" 18 | 19 | // DefaultDaemonPort is the port OVPMD will listen by default if something else is not specified. 20 | DefaultDaemonPort = 9090 21 | 22 | // DefaultKeepalivePeriod is the default ping period to check if the remote peer is alive. 23 | DefaultKeepalivePeriod = "2" 24 | 25 | // DefaultKeepaliveTimeout is the default ping timeout to assume that remote peer is down. 26 | DefaultKeepaliveTimeout = "4" 27 | 28 | etcBasePath = "/etc/ovpm/" 29 | varBasePath = "/var/db/ovpm/" 30 | 31 | _DefaultConfigPath = etcBasePath + "ovpm.ini" 32 | _DefaultDBPath = varBasePath + "db.sqlite3" 33 | _DefaultVPNConfPath = varBasePath + "server.conf" 34 | _DefaultVPNCCDPath = varBasePath + "ccd" 35 | _DefaultCertPath = varBasePath + "server.crt" 36 | _DefaultKeyPath = varBasePath + "server.key" 37 | _DefaultCACertPath = varBasePath + "ca.crt" 38 | _DefaultCAKeyPath = varBasePath + "ca.key" 39 | _DefaultDHParamsPath = varBasePath + "dh4096.pem" 40 | _DefaultCRLPath = varBasePath + "crl.pem" 41 | _DefaultStatusLogPath = varBasePath + "openvpn-status.log" 42 | ) 43 | 44 | // Testing is used to determine whether we are testing or running normally. 45 | // Set it to true when testing. 46 | var Testing = false 47 | -------------------------------------------------------------------------------- /contrib/deb-repo-config: -------------------------------------------------------------------------------- 1 | Codename: ovpm 2 | Components: main 3 | Architectures: i386 amd64 4 | -------------------------------------------------------------------------------- /contrib/systemd/ovpmd.service.rhel: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenVPn Manager 3 | Before=multi-user.target 4 | Before=shutdown.target 5 | After=local-fs.target 6 | After=remote-fs.target 7 | After=network-online.target 8 | After=systemd-journald-dev-log.socket 9 | Wants=network-online.target 10 | Conflicts=shutdown.target 11 | 12 | [Service] 13 | TimeoutSec=5min 14 | PIDFile=/var/run/ovpmd.pid 15 | ExecStart=/usr/sbin/ovpmd 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /contrib/systemd/ovpmd.service.ubuntu: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=OpenVPn Manager 3 | Before=multi-user.target 4 | Before=shutdown.target 5 | After=local-fs.target 6 | After=remote-fs.target 7 | After=network-online.target 8 | After=systemd-journald-dev-log.socket 9 | Wants=network-online.target 10 | Conflicts=shutdown.target 11 | 12 | [Service] 13 | TimeoutSec=5min 14 | PIDFile=/var/run/ovpmd.pid 15 | ExecStart=/sbin/ovpmd 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /contrib/yumrepo.repo: -------------------------------------------------------------------------------- 1 | [main] 2 | plugins=1 3 | 4 | [ovpm] 5 | name = Official OVPM yum repository 6 | baseurl = https://cad.github.io/ovpm/rpm/ 7 | gpgcheck=0 8 | sslverify=1 9 | enabled=1 10 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | "github.com/jinzhu/gorm" 6 | 7 | // We blank import sqlite here because gorm needs it. 8 | _ "github.com/jinzhu/gorm/dialects/sqlite" 9 | ) 10 | 11 | var db *DB 12 | 13 | // DB represents a persistent storage. 14 | type DB struct { 15 | *gorm.DB 16 | } 17 | 18 | // CreateDB prepares and returns new storage. 19 | // 20 | // It should be run at the start of the program. 21 | func CreateDB(dialect string, args ...interface{}) *DB { 22 | if len(args) > 0 && args[0] == "" { 23 | args[0] = _DefaultDBPath 24 | } 25 | var err error 26 | 27 | dbase, err := gorm.Open(dialect, args...) 28 | if err != nil { 29 | logrus.Fatalf("couldn't open sqlite database %v: %v", args, err) 30 | } 31 | 32 | dbase.AutoMigrate(&dbUserModel{}) 33 | dbase.AutoMigrate(&dbServerModel{}) 34 | dbase.AutoMigrate(&dbRevokedModel{}) 35 | dbase.AutoMigrate(&dbNetworkModel{}) 36 | 37 | dbPTR := &DB{DB: dbase} 38 | db = dbPTR 39 | return dbPTR 40 | } 41 | 42 | // Cease closes the database. 43 | // 44 | // It should be run at the exit of the program. 45 | func (db *DB) Cease() { 46 | db.DB.Close() 47 | } 48 | -------------------------------------------------------------------------------- /db_test.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | import "testing" 4 | 5 | func TestDBSetup(t *testing.T) { 6 | // Initialize: 7 | Testing = true 8 | 9 | // Prepare: 10 | // Test: 11 | 12 | // Create database. 13 | CreateDB("sqlite3", ":memory:") 14 | 15 | // Is database created? 16 | if db == nil { 17 | t.Fatalf("database is expected to be not nil but it's nil") 18 | } 19 | } 20 | 21 | func TestDBCease(t *testing.T) { 22 | // Initialize: 23 | Testing = true 24 | 25 | // Prepare: 26 | CreateDB("sqlite3", ":memory:") 27 | user := dbUserModel{Username: "testUser"} 28 | db.Save(&user) 29 | 30 | // Test: 31 | // Close database. 32 | db.Cease() 33 | 34 | var users []dbUserModel 35 | db.Find(&users) 36 | 37 | // Is length zero? 38 | if len(users) != 0 { 39 | t.Fatalf("length of user should be 0 but it's %d", len(users)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package ovpm provides the implementation of core OVPM API. 2 | // 3 | // ovpm can create and destroy OpenVPN servers, manage vpn users, handle certificates etc... 4 | package ovpm 5 | -------------------------------------------------------------------------------- /errors/application.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // ApplicationError error group indicates application related errors. 11 | // Starting with error code 3xxx. 12 | const ApplicationError = 3000 13 | 14 | // ErrUnknownApplicationError indicates an unknown application error. 15 | const ErrUnknownApplicationError = 3001 16 | 17 | // UnknownApplicationError ... 18 | func UnknownApplicationError(e error) Error { 19 | err := Error{ 20 | Message: fmt.Sprintf("unknown application error: %v", e), 21 | Code: ErrUnknownApplicationError, 22 | } 23 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 24 | return err 25 | } 26 | 27 | // ErrMustBeLoopbackURL indicates that given url does not resolv to a known looback ip addr. 28 | const ErrMustBeLoopbackURL = 3002 29 | 30 | // MustBeLoopbackURL ... 31 | func MustBeLoopbackURL(url *url.URL) Error { 32 | err := Error{ 33 | Message: "url must resolve to a known looback ip addr", 34 | Code: ErrMustBeLoopbackURL, 35 | Args: map[string]interface{}{ 36 | "url": url.String(), 37 | }, 38 | } 39 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 40 | return err 41 | } 42 | 43 | // ErrBadURL indicates that given url string can not be parsed. 44 | const ErrBadURL = 3003 45 | 46 | // BadURL ... 47 | func BadURL(urlStr string, e error) Error { 48 | err := Error{ 49 | Message: fmt.Sprintf("url string can not be parsed: %v", e), 50 | Code: ErrBadURL, 51 | Args: map[string]interface{}{ 52 | "url": urlStr, 53 | }, 54 | } 55 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 56 | return err 57 | } 58 | 59 | // ErrEmptyValue indicates that given value is empty. 60 | const ErrEmptyValue = 3004 61 | 62 | // EmptyValue ... 63 | func EmptyValue(key string, value interface{}) Error { 64 | err := Error{ 65 | Message: fmt.Sprintf("value is empty: %v", value), 66 | Code: ErrEmptyValue, 67 | Args: map[string]interface{}{ 68 | "key": key, 69 | "value": value, 70 | }, 71 | } 72 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 73 | return err 74 | } 75 | 76 | // ErrNotIPv4 indicates that given value is not an IPv4. 77 | const ErrNotIPv4 = 3005 78 | 79 | // NotIPv4 ... 80 | func NotIPv4(str string) Error { 81 | err := Error{ 82 | Message: fmt.Sprintf("'%s' is not an IPv4 address", str), 83 | Code: ErrNotIPv4, 84 | } 85 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 86 | return err 87 | } 88 | 89 | // ErrConflictingDemands indicates that users demands are conflicting with each other. 90 | const ErrConflictingDemands = 3006 91 | 92 | // ConflictingDemands ... 93 | func ConflictingDemands(msg string) Error { 94 | err := Error{ 95 | Message: msg, 96 | Code: ErrConflictingDemands, 97 | } 98 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 99 | return err 100 | } 101 | 102 | // ErrNotHostname indicates that given value is not an hostname. 103 | const ErrNotHostname = 3007 104 | 105 | // NotHostname ... 106 | func NotHostname(str string) Error { 107 | err := Error{ 108 | Message: fmt.Sprintf("'%s' is not a valid host name", str), 109 | Code: ErrNotHostname, 110 | } 111 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 112 | return err 113 | } 114 | 115 | // ErrNotCIDR indicates that given value is not a CIDR. 116 | const ErrNotCIDR = 3008 117 | 118 | // NotCIDR ... 119 | func NotCIDR(str string) Error { 120 | err := Error{ 121 | Message: fmt.Sprintf("'%s' is not a valid CIDR", str), 122 | Code: ErrNotCIDR, 123 | } 124 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 125 | return err 126 | } 127 | 128 | // ErrInvalidPort indicates that given value is not a valid port number. 129 | const ErrInvalidPort = 3009 130 | 131 | // InvalidPort ... 132 | func InvalidPort(str string) Error { 133 | err := Error{ 134 | Message: fmt.Sprintf("'%s' is not a valid port number", str), 135 | Code: ErrInvalidPort, 136 | } 137 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 138 | return err 139 | } 140 | 141 | // ErrUnconfirmed indicates a UI confirmation dialog is cancelled by the user. 142 | const ErrUnconfirmed = 3010 143 | 144 | // Unconfirmed ... 145 | func Unconfirmed(str string) Error { 146 | err := Error{ 147 | Message: fmt.Sprintf("confirmation failed: '%s'", str), 148 | Code: ErrUnconfirmed, 149 | } 150 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 151 | return err 152 | } 153 | 154 | // ErrNotValidNetworkType indicates that supplied network type is invalid. 155 | const ErrNotValidNetworkType = 3011 156 | 157 | // NotValidNetworkType ... 158 | func NotValidNetworkType(key string, value interface{}) Error { 159 | err := Error{ 160 | Message: fmt.Sprintf("invalid network type: %v", value), 161 | Code: ErrNotValidNetworkType, 162 | Args: map[string]interface{}{ 163 | "key": key, 164 | "value": value, 165 | }, 166 | } 167 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 168 | return err 169 | } 170 | 171 | // ErrNotValidKeepalivePeriod indicates that supplied keepalive period is invalid 172 | const ErrNotValidKeepalivePeriod = 3012 173 | 174 | // NotValidKeepalivePeriod ... 175 | func NotValidKeepalivePeriod(str string) Error { 176 | err := Error{ 177 | Message: fmt.Sprintf("'%s' is not a valid keepalive period, must be number", str), 178 | Code: ErrNotValidKeepalivePeriod, 179 | } 180 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 181 | return err 182 | } 183 | 184 | // ErrNotValidKeepaliveTimeout indicates that supplied keepalive timeout is invalid 185 | const ErrNotValidKeepaliveTimeout = 3013 186 | 187 | // NotValidKeepaliveTimeout ... 188 | func NotValidKeepaliveTimeout(str string) Error { 189 | err := Error{ 190 | Message: fmt.Sprintf("'%s' is not a valid keepalive timeout, must be number", str), 191 | Code: ErrNotValidKeepaliveTimeout, 192 | } 193 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 194 | return err 195 | } 196 | -------------------------------------------------------------------------------- /errors/cli.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // CLIError error group indicates command related errors. 10 | // Starting with error code 2xxx. 11 | const CLIError = 2000 12 | 13 | // ErrUnknownCLIError indicates an unknown cli error. 14 | const ErrUnknownCLIError = 2001 15 | 16 | // UnknownCLIError ... 17 | func UnknownCLIError(e error) Error { 18 | err := Error{ 19 | Message: fmt.Sprintf("unknown cli error: %v", e), 20 | Code: ErrUnknownCLIError, 21 | } 22 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 23 | return err 24 | } 25 | -------------------------------------------------------------------------------- /errors/error.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | // Error ... 4 | type Error struct { 5 | Message string `json:"message"` 6 | Args map[string]interface{} `json:"args"` 7 | Code int `json:"code"` 8 | } 9 | 10 | func (e Error) Error() string { 11 | return e.Message 12 | } 13 | -------------------------------------------------------------------------------- /errors/system.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | // SystemError error group indicates system related errors. 10 | // Starting with error code 1xxx. 11 | const SystemError = 1000 12 | 13 | // ErrUnknownSysError indicates an unknown system error. 14 | const ErrUnknownSysError = 1001 15 | 16 | // UnknownSysError ... 17 | func UnknownSysError(e error) Error { 18 | err := Error{ 19 | Message: fmt.Sprintf("unknown system error: %v", e), 20 | Code: ErrUnknownSysError, 21 | } 22 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 23 | return err 24 | } 25 | 26 | // ErrUnknownGRPCError indicates an unknown gRPC error. 27 | const ErrUnknownGRPCError = 1002 28 | 29 | // UnknownGRPCError ... 30 | func UnknownGRPCError(e error) Error { 31 | err := Error{ 32 | Message: fmt.Sprintf("grpc error: %v", e), 33 | Code: ErrUnknownGRPCError, 34 | } 35 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 36 | return err 37 | } 38 | 39 | // ErrUnknownFileIOError indicates an unknown File IO error. 40 | const ErrUnknownFileIOError = 1003 41 | 42 | // UnknownFileIOError ... 43 | func UnknownFileIOError(e error) Error { 44 | err := Error{ 45 | Message: fmt.Sprintf("file io error: %v", e), 46 | Code: ErrUnknownFileIOError, 47 | } 48 | logrus.WithFields(logrus.Fields(err.Args)).Error(err) 49 | return err 50 | } 51 | -------------------------------------------------------------------------------- /generate.go: -------------------------------------------------------------------------------- 1 | //go:generate make bundle 2 | 3 | package ovpm 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cad/ovpm 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef 7 | github.com/coreos/go-iptables v0.5.0 8 | github.com/dustin/go-humanize v1.0.0 9 | github.com/elazarl/go-bindata-assetfs v1.0.1 10 | github.com/go-openapi/loads v0.20.1 // indirect 11 | github.com/go-openapi/runtime v0.19.26 12 | github.com/go-openapi/spec v0.20.2 // indirect 13 | github.com/golang/protobuf v1.5.2 // indirect 14 | github.com/google/uuid v1.2.0 15 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.3.0 16 | github.com/jinzhu/gorm v1.9.16 17 | github.com/mattn/go-runewidth v0.0.10 // indirect 18 | github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect 19 | github.com/olekukonko/tablewriter v0.0.4 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 22 | github.com/sirupsen/logrus v1.7.0 23 | github.com/stretchr/testify v1.7.0 24 | github.com/urfave/cli v1.22.5 25 | go.uber.org/thriftrw v1.25.1 26 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect 27 | golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c 28 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 // indirect 29 | golang.org/x/text v0.3.6 // indirect 30 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1 31 | google.golang.org/grpc v1.36.1 32 | google.golang.org/protobuf v1.26.0 33 | gopkg.in/hlandau/easymetric.v1 v1.0.0 // indirect 34 | gopkg.in/hlandau/measurable.v1 v1.0.1 // indirect 35 | gopkg.in/hlandau/passlib.v1 v1.0.10 36 | ) 37 | -------------------------------------------------------------------------------- /nfpm.yaml: -------------------------------------------------------------------------------- 1 | name: "ovpm" 2 | arch: "amd64" 3 | platform: "linux" 4 | version: "${VERSION}" 5 | section: "default" 6 | priority: "extra" 7 | depends: 8 | - openvpn 9 | maintainer: "Mustafa ARICI " 10 | description: | 11 | OVPM makes all aspects of OpenVPN server administration a breeze. 12 | vendor: "" 13 | homepage: "https://github.com/cad/ovpm" 14 | license: "AGPL3" 15 | overrides: 16 | rpm: 17 | files: 18 | ./bin/ovpm: "/bin/ovpm" 19 | ./bin/ovpmd: "/sbin/ovpmd" 20 | ./contrib/systemd/ovpmd.service.rhel: "/usr/lib/systemd/system/ovpmd.service" 21 | deb: 22 | files: 23 | ./bin/ovpm: "/bin/ovpm" 24 | ./bin/ovpmd: "/sbin/ovpmd" 25 | ./contrib/systemd/ovpmd.service.ubuntu: "/lib/systemd/system/ovpmd.service" 26 | scripts: 27 | preinstall: ./scripts/preinstall.sh 28 | postinstall: ./scripts/postinstall.sh 29 | preremove: ./scripts/preremove.sh 30 | postremove: ./scripts/postremove.sh 31 | empty_folders: 32 | - "/var/db/ovpm" 33 | 34 | -------------------------------------------------------------------------------- /parselog.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // clEntry reprsents a parsed entry that is present on OpenVPN 14 | // log section CLIENT LIST. 15 | type clEntry struct { 16 | CommonName string `json:"common_name"` 17 | RealAddress string `json:"real_address"` 18 | BytesReceived uint64 `json:"bytes_received"` 19 | BytesSent uint64 `json:"bytes_sent"` 20 | ConnectedSince time.Time `json:"connected_since"` 21 | } 22 | 23 | // rtEntry reprsents a parsed entry that is present on OpenVPN 24 | // log section ROUTING TABLE. 25 | type rtEntry struct { 26 | VirtualAddress string `json:"virtual_address"` 27 | CommonName string `json:"common_name"` 28 | RealAddress string `json:"real_address"` 29 | LastRef time.Time `json:"last_ref"` 30 | } 31 | 32 | // parseStatusLog parses the received OpenVPN status log file. 33 | // And then returns the parsed client information. 34 | func parseStatusLog(f io.Reader) ([]clEntry, []rtEntry) { 35 | // Recover any panics 36 | defer func() { 37 | if r := recover(); r != nil { 38 | logrus.WithField("panic", r).Error("OpenVPN log file is corrupt") 39 | } 40 | }() 41 | 42 | // Parsing stages. 43 | const stageCL int = 0 44 | const stageRT int = 1 45 | const stageFin int = 2 46 | 47 | // Parsing variables. 48 | var currStage int 49 | var skipFor int 50 | var cl []clEntry 51 | var rt []rtEntry 52 | 53 | // Scan and parse the file by dividing it into chunks. 54 | scanner, skipFor := bufio.NewScanner(f), 3 55 | for lc := 0; scanner.Scan(); lc++ { 56 | if skipFor > 0 { 57 | skipFor-- 58 | continue 59 | } 60 | txt := scanner.Text() 61 | switch currStage { 62 | case stageCL: 63 | if strings.Contains(txt, "ROUTING TABLE") { 64 | currStage = stageRT 65 | skipFor = 1 66 | continue 67 | } 68 | dat := strings.Split(txt, ",") 69 | cl = append(cl, clEntry{ 70 | CommonName: trim(dat[0]), 71 | RealAddress: trim(dat[1]), 72 | BytesReceived: stoui64(trim(dat[2])), 73 | BytesSent: stoui64(trim(dat[3])), 74 | ConnectedSince: stodt(trim(dat[4])), 75 | }) 76 | case stageRT: 77 | if strings.Contains(txt, "GLOBAL STATS") { 78 | currStage = stageFin 79 | break 80 | } 81 | dat := strings.Split(txt, ",") 82 | rt = append(rt, rtEntry{ 83 | VirtualAddress: trim(dat[0]), 84 | CommonName: trim(dat[1]), 85 | RealAddress: trim(dat[2]), 86 | LastRef: stodt(trim(dat[3])), 87 | }) 88 | } 89 | } 90 | if err := scanner.Err(); err != nil { 91 | panic(err) 92 | } 93 | 94 | return cl, rt 95 | } 96 | 97 | // stoi64 converts string to uint64. 98 | func stoui64(s string) uint64 { 99 | i, err := strconv.ParseInt(s, 0, 64) 100 | if err != nil { 101 | panic(err) 102 | } 103 | return uint64(i) 104 | } 105 | 106 | // stodt converts string to date time. 107 | func stodt(s string) time.Time { 108 | t, err := time.ParseInLocation(time.ANSIC, s, time.Local) 109 | if err != nil { 110 | panic(err) 111 | } 112 | return t 113 | } 114 | 115 | // trim will trim all leading and trailing whitespace from the s. 116 | func trim(s string) string { 117 | return strings.TrimSpace(s) 118 | } 119 | -------------------------------------------------------------------------------- /parselog_test.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func Test_parseStatusLog(t *testing.T) { 13 | const exampleLogFile = `OpenVPN CLIENT LIST 14 | Updated,Mon Mar 26 13:26:10 2018 15 | Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since 16 | google.DNS,8.8.8.8:53246,527914279,3204562859,Sat Mar 17 16:26:38 2018 17 | google1.DNS,8.8.4.4:33974,42727443,291595456,Mon Mar 26 08:24:08 2018 18 | ROUTING TABLE 19 | Virtual Address,Common Name,Real Address,Last Ref 20 | 10.20.30.6,google.DNS,8.8.8.8:33974,Mon Mar 26 13:26:04 2018 21 | 10.20.30.5,google1.DNS,8.8.4.4:53246,Mon Mar 26 13:25:57 2018 22 | GLOBAL STATS 23 | Max bcast/mcast queue length,4 24 | END 25 | ` 26 | 27 | // Mock the status log file. 28 | f := bytes.NewBufferString(exampleLogFile) 29 | 30 | type args struct { 31 | f io.Reader 32 | } 33 | tests := []struct { 34 | name string 35 | args args 36 | want []clEntry 37 | want1 []rtEntry 38 | }{ 39 | { 40 | "google", args{f}, 41 | []clEntry{ 42 | clEntry{ 43 | CommonName: "google.DNS", 44 | RealAddress: "8.8.8.8:53246", 45 | BytesReceived: 527914279, 46 | BytesSent: 3204562859, 47 | ConnectedSince: stodt("Sat Mar 17 16:26:38 2018"), 48 | }, 49 | clEntry{ 50 | CommonName: "google1.DNS", 51 | RealAddress: "8.8.4.4:33974", 52 | BytesReceived: 42727443, 53 | BytesSent: 291595456, 54 | ConnectedSince: stodt("Mon Mar 26 08:24:08 2018"), 55 | }, 56 | }, 57 | []rtEntry{ 58 | rtEntry{ 59 | VirtualAddress: "10.20.30.6", 60 | CommonName: "google.DNS", 61 | RealAddress: "8.8.8.8:33974", 62 | LastRef: stodt("Mon Mar 26 13:26:04 2018"), 63 | }, 64 | rtEntry{ 65 | VirtualAddress: "10.20.30.5", 66 | CommonName: "google1.DNS", 67 | RealAddress: "8.8.4.4:53246", 68 | LastRef: stodt("Mon Mar 26 13:25:57 2018"), 69 | }, 70 | }, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | got, got1 := parseStatusLog(tt.args.f) 76 | if !reflect.DeepEqual(got, tt.want) { 77 | t.Errorf("parseStatusLog() got = %v, want %v", got, tt.want) 78 | } 79 | if !reflect.DeepEqual(got1, tt.want1) { 80 | t.Errorf("parseStatusLog() got1 = %v, want %v", got1, tt.want1) 81 | } 82 | }) 83 | } 84 | } 85 | 86 | func Test_parseStatusLog_CorruptStatusLog(t *testing.T) { 87 | const exampleLogFile = `OpenVPN CLIENT LIST 88 | Updated,Mon Mar 26 13:26:10 2018 89 | Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since 90 | google.DNS,8.8.8.8Name,Real Addressdfs,Last Ref 91 | 10.20.30.6,google.DNS,8.8.8.8:33974,Mon Mar 26 13:26:04 2018 92 | 10.20.30.5,google1.DNS,8..4.4:53246,Mon Mar 26 13:25:57 2018 93 | GLOBAL STATS 94 | Max bcast/mcast queue length,4 95 | END 96 | ` 97 | 98 | // Mock the status log file. 99 | f := bytes.NewBufferString(exampleLogFile) 100 | 101 | cl, rt := parseStatusLog(f) 102 | 103 | assert.Empty(t, cl) 104 | assert.Empty(t, rt) 105 | } 106 | -------------------------------------------------------------------------------- /perms.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | import "github.com/cad/ovpm/permset" 4 | 5 | // OVPM available permissions. 6 | const ( 7 | // User permissions 8 | CreateUserPerm permset.Perm = iota 9 | GetAnyUserPerm 10 | GetSelfPerm 11 | UpdateAnyUserPerm 12 | UpdateSelfPerm 13 | DeleteAnyUserPerm 14 | RenewAnyUserPerm 15 | GenConfigAnyUserPerm 16 | GenConfigSelfPerm 17 | 18 | // VPN permissions 19 | GetVPNStatusPerm 20 | InitVPNPerm 21 | UpdateVPNPerm 22 | RestartVPNPerm 23 | 24 | // Network permissions 25 | ListNetworksPerm 26 | CreateNetworkPerm 27 | DeleteNetworkPerm 28 | GetNetworkTypesPerm 29 | GetNetworkAssociatedUsersPerm 30 | AssociateNetworkUserPerm 31 | DissociateNetworkUserPerm 32 | ) 33 | 34 | // AdminPerms returns the list of permissions that admin type user has. 35 | func AdminPerms() []permset.Perm { 36 | return []permset.Perm{ 37 | CreateUserPerm, 38 | GetAnyUserPerm, 39 | GetSelfPerm, 40 | UpdateAnyUserPerm, 41 | UpdateSelfPerm, 42 | DeleteAnyUserPerm, 43 | RenewAnyUserPerm, 44 | GenConfigAnyUserPerm, 45 | GenConfigSelfPerm, 46 | GetVPNStatusPerm, 47 | InitVPNPerm, 48 | UpdateVPNPerm, 49 | RestartVPNPerm, 50 | ListNetworksPerm, 51 | CreateNetworkPerm, 52 | DeleteNetworkPerm, 53 | GetNetworkTypesPerm, 54 | GetNetworkAssociatedUsersPerm, 55 | AssociateNetworkUserPerm, 56 | DissociateNetworkUserPerm, 57 | } 58 | } 59 | 60 | // UserPerms returns the collection of permissions that the regular users have. 61 | func UserPerms() []permset.Perm { 62 | return []permset.Perm{ 63 | GetSelfPerm, 64 | UpdateSelfPerm, 65 | GenConfigSelfPerm, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /permset/permset.go: -------------------------------------------------------------------------------- 1 | // Package permset provides primitives for permission management. 2 | package permset 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | ) 8 | 9 | // Perm is a permission to do some action. 10 | type Perm int 11 | 12 | // Permset represents a set of permissions. 13 | type Permset struct { 14 | permset map[Perm]bool 15 | } 16 | 17 | // New receives permissions to contain and returns a permset from it. 18 | func New(perms ...Perm) Permset { 19 | permset := Permset{permset: make(map[Perm]bool)} 20 | permset.Add(perms...) 21 | return permset 22 | } 23 | 24 | // Add adds the received perms to the permset. 25 | func (ps *Permset) Add(perms ...Perm) { 26 | for _, perm := range perms { 27 | ps.permset[perm] = true 28 | } 29 | } 30 | 31 | // Remove removes the received perms from the permset. 32 | func (ps *Permset) Remove(perms ...Perm) { 33 | for _, perm := range perms { 34 | if _, ok := ps.permset[perm]; ok { 35 | delete(ps.permset, perm) 36 | } 37 | } 38 | } 39 | 40 | // Perms returns the permissions contained within the permset. 41 | func (ps *Permset) Perms() []Perm { 42 | var perms []Perm 43 | for k := range ps.permset { 44 | perms = append(perms, k) 45 | } 46 | return perms 47 | } 48 | 49 | // Contains receives single Perm and returns true if the permset contains it. 50 | func (ps *Permset) Contains(perm Perm) bool { 51 | if _, ok := ps.permset[perm]; !ok { 52 | return false 53 | } 54 | return true 55 | } 56 | 57 | // ContainsAll returns true if the permset contains all received Perms. 58 | func (ps *Permset) ContainsAll(perms ...Perm) bool { 59 | for _, perm := range perms { 60 | if _, ok := ps.permset[perm]; !ok { 61 | return false 62 | } 63 | } 64 | return true 65 | } 66 | 67 | // ContainsSome returns true if the permset contains any one or more of the received Perms. 68 | func (ps *Permset) ContainsSome(perms ...Perm) bool { 69 | for _, perm := range perms { 70 | if _, ok := ps.permset[perm]; ok { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | 77 | // ContainsNone returns true if the permset contains none one of the received Perms. 78 | func (ps *Permset) ContainsNone(perms ...Perm) bool { 79 | for _, perm := range perms { 80 | if _, ok := ps.permset[perm]; ok { 81 | return false 82 | } 83 | } 84 | return true 85 | } 86 | 87 | type permsetKeyType int 88 | 89 | const permsetKey permsetKeyType = iota 90 | 91 | // NewContext receives perms and returns a context with the received perms are the value of the context. 92 | func NewContext(ctx context.Context, permset Permset) context.Context { 93 | return context.WithValue(ctx, permsetKey, permset) 94 | } 95 | 96 | // FromContext receives a context and returns the permset in it. 97 | func FromContext(ctx context.Context) (Permset, error) { 98 | permset, ok := ctx.Value(permsetKey).(Permset) 99 | if !ok { 100 | return Permset{}, fmt.Errorf("cannot get context value") 101 | } 102 | return permset, nil 103 | } 104 | -------------------------------------------------------------------------------- /permset/permset_test.go: -------------------------------------------------------------------------------- 1 | package permset 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | ) 7 | 8 | const ( 9 | TestPerm1 Perm = iota 10 | TestPerm2 11 | TestPerm3 12 | ) 13 | 14 | func TestNew(t *testing.T) { 15 | var newtests = []struct { 16 | perms []Perm 17 | }{ 18 | {[]Perm{TestPerm1}}, 19 | {[]Perm{TestPerm1, TestPerm2}}, 20 | {[]Perm{TestPerm3, TestPerm2}}, 21 | {[]Perm{TestPerm2, TestPerm2}}, 22 | {[]Perm{TestPerm3, TestPerm2, TestPerm3}}, 23 | } 24 | 25 | for _, tt := range newtests { 26 | permset := New(tt.perms...) 27 | 28 | // See if perms within the permset checks out with the ones provided to the New(). 29 | for _, perm := range tt.perms { 30 | if _, ok := permset.permset[perm]; !ok { 31 | t.Fatalf("perm should exist in the permset: %v", perm) 32 | } 33 | } 34 | 35 | // See if there are any extra perms in the permset that is not provided to the New(). 36 | for perm := range permset.permset { 37 | var found bool 38 | for _, ttPerm := range tt.perms { 39 | if ttPerm == perm { 40 | found = true 41 | } 42 | } 43 | 44 | if !found { 45 | t.Fatalf("perm should not exist in the permset: %v", perm) 46 | } 47 | } 48 | } 49 | } 50 | 51 | func TestAdd(t *testing.T) { 52 | permset := New(TestPerm2) 53 | permset.Add(TestPerm3) 54 | 55 | if _, ok := permset.permset[TestPerm3]; !ok { 56 | t.Fatal("perm TestPerm3 should exist in the permset") 57 | } 58 | if _, ok := permset.permset[TestPerm2]; !ok { 59 | t.Fatal("perm TestPerm2 should exist in the permset") 60 | } 61 | 62 | } 63 | 64 | func TestRemove(t *testing.T) { 65 | // See if remove works OK. 66 | permset := New(TestPerm2, TestPerm3) 67 | permset.Remove(TestPerm3) 68 | 69 | if _, ok := permset.permset[TestPerm3]; ok { 70 | t.Fatal("perm TestPerm3 should not exist in the permset") 71 | } 72 | if _, ok := permset.permset[TestPerm2]; !ok { 73 | t.Fatal("perm TestPerm2 should exist in the permset") 74 | } 75 | 76 | // See if double remove breaks it. 77 | permset = New(TestPerm2, TestPerm3) 78 | permset.Remove(TestPerm3) 79 | permset.Remove(TestPerm3) 80 | 81 | if _, ok := permset.permset[TestPerm3]; ok { 82 | t.Fatal("perm TestPerm3 should not exist in the permset") 83 | } 84 | if _, ok := permset.permset[TestPerm2]; !ok { 85 | t.Fatal("perm TestPerm2 should exist in the permset") 86 | } 87 | 88 | } 89 | 90 | func TestPerms(t *testing.T) { 91 | permset := New(TestPerm2, TestPerm3) 92 | perms := permset.Perms() 93 | 94 | var found bool 95 | for _, perm := range perms { 96 | if perm == TestPerm2 { 97 | found = true 98 | } 99 | } 100 | 101 | if !found { 102 | t.Fatal("Perms() should return all the perms within the permset") 103 | } 104 | } 105 | 106 | func TestContains(t *testing.T) { 107 | permset := New(TestPerm2, TestPerm3) 108 | 109 | if !permset.Contains(TestPerm2) { 110 | t.Fatal("permset should contain TestPerm2") 111 | } 112 | 113 | if !permset.Contains(TestPerm3) { 114 | t.Fatal("permset should contain TestPerm3") 115 | } 116 | 117 | if permset.Contains(TestPerm1) { 118 | t.Fatal("permset should not contain TestPerm1") 119 | } 120 | } 121 | 122 | func TestContainsAll(t *testing.T) { 123 | permset := New(TestPerm2, TestPerm3) 124 | 125 | if !permset.ContainsAll(TestPerm2) { 126 | t.Fatal("permset should contain TestPerm2") 127 | } 128 | 129 | if !permset.ContainsAll(TestPerm3) { 130 | t.Fatal("permset should contain TestPerm3") 131 | } 132 | 133 | if permset.ContainsAll(TestPerm1) { 134 | t.Fatal("permset should not contain TestPerm1") 135 | } 136 | 137 | if !permset.ContainsAll(TestPerm2, TestPerm3) { 138 | t.Fatal("permset should contain TestPerm2 and TestPerm3") 139 | } 140 | 141 | if !permset.ContainsAll(TestPerm3, TestPerm2) { 142 | t.Fatal("permset should contain TestPerm2 and TestPerm3") 143 | } 144 | 145 | if permset.ContainsAll(TestPerm1, TestPerm2) { 146 | t.Fatal("permset should not contain TestPerm1 and TestPerm3") 147 | } 148 | 149 | } 150 | 151 | func TestContainsSome(t *testing.T) { 152 | permset := New(TestPerm2, TestPerm3) 153 | 154 | if !permset.ContainsSome(TestPerm2) { 155 | t.Fatal("permset should contain TestPerm2") 156 | } 157 | 158 | if !permset.ContainsSome(TestPerm1, TestPerm3) { 159 | t.Fatal("permset should contain TestPerm1 and TestPerm3") 160 | } 161 | 162 | if permset.ContainsSome(TestPerm1) { 163 | t.Fatal("permset should contain TestPerm1") 164 | } 165 | 166 | } 167 | 168 | func TestContainsNone(t *testing.T) { 169 | permset := New(TestPerm2, TestPerm3) 170 | 171 | if !permset.ContainsNone(TestPerm1) { 172 | t.Fatal("ContainsNone should return true") 173 | } 174 | 175 | if permset.ContainsNone(TestPerm2) { 176 | t.Fatal("ContainsNone should return false") 177 | } 178 | } 179 | 180 | func TestNewContext(t *testing.T) { 181 | permset := New(TestPerm2, TestPerm3) 182 | ctx := NewContext(context.Background(), permset) 183 | 184 | permsetFromCtx, ok := ctx.Value(permsetKey).(Permset) 185 | if !ok { 186 | t.Fatal("can't extract permset from ctx") 187 | } 188 | 189 | if !permset.ContainsAll(permsetFromCtx.Perms()...) { 190 | t.Fatal("permsets should match") 191 | } 192 | } 193 | 194 | func TestFromContext(t *testing.T) { 195 | permset := New(TestPerm2, TestPerm3) 196 | ctx := NewContext(context.Background(), permset) 197 | 198 | permsetFromCtx, err := FromContext(ctx) 199 | if err != nil { 200 | t.Fatalf("error is not expected here: %v", err) 201 | } 202 | 203 | if !permset.ContainsAll(permsetFromCtx.Perms()...) { 204 | t.Fatal("permsets should match") 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /pki/const.go: -------------------------------------------------------------------------------- 1 | package pki 2 | 3 | // PEM encoding types 4 | const ( 5 | PEMCertificateBlockType string = "CERTIFICATE" 6 | PEMRSAPrivateKeyBlockType = "RSA PRIVATE KEY" 7 | PEMx509CRLBlockType = "X509 CRL" 8 | PEMCSRBlockType = "CERTIFICATE REQUEST" 9 | ) 10 | -------------------------------------------------------------------------------- /pki/pki_test.go: -------------------------------------------------------------------------------- 1 | package pki_test 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "fmt" 7 | "math/big" 8 | "math/rand" 9 | "testing" 10 | "time" 11 | 12 | "github.com/cad/ovpm/pki" 13 | ) 14 | 15 | func TestNewCA(t *testing.T) { 16 | // Initialize: 17 | // Prepare: 18 | ca, err := pki.NewCA() 19 | if err != nil { 20 | t.Fatalf("can not create CA in test: %v", err) 21 | } 22 | 23 | // Test: 24 | // Is CertHolder empty? 25 | if ca.CertHolder == (pki.CertHolder{}) { 26 | t.Errorf("returned ca.CertHolder can't be empty: %+v", ca.CertHolder) 27 | } 28 | 29 | // Is CSR empty length? 30 | if len(ca.CSR) == 0 { 31 | t.Errorf("returned ca.CSR is a zero-length string") 32 | } 33 | 34 | var encodingtests = []struct { 35 | name string // name 36 | block string // pem block string 37 | typ string // expected pem block type 38 | }{ 39 | {"ca.CSR", ca.CSR, pki.PEMCSRBlockType}, 40 | {"ca.CertHolder.Cert", ca.CertHolder.Cert, pki.PEMCertificateBlockType}, 41 | {"ca.CertHolder.Key", ca.CertHolder.Key, pki.PEMRSAPrivateKeyBlockType}, 42 | } 43 | 44 | // Is PEM encoded properly? 45 | for _, tt := range encodingtests { 46 | if !isPEMEncodedProperly(t, tt.block, tt.typ) { 47 | t.Errorf("returned '%s' is not PEM encoded properly: %+v", tt.name, tt.block) 48 | } 49 | } 50 | 51 | } 52 | 53 | // TestNewCertHolders tests pki.NewServerCertHolder and pki.NewClientCertHolder functions. 54 | func TestNewCertHolders(t *testing.T) { 55 | // Initialize: 56 | ca, _ := pki.NewCA() 57 | 58 | // Prepare: 59 | sch, err := pki.NewServerCertHolder(ca) 60 | if err != nil { 61 | t.Fatalf("can not create server cert holder: %v", err) 62 | } 63 | cch, err := pki.NewClientCertHolder(ca, "test-user") 64 | if err != nil { 65 | t.Fatalf("can not create client cert holder: %v", err) 66 | } 67 | 68 | // Test: 69 | var certholdertests = []struct { 70 | name string 71 | certHolder *pki.CertHolder 72 | }{ 73 | {"server", sch}, 74 | {"client", cch}, 75 | } 76 | 77 | for _, tt := range certholdertests { 78 | 79 | // Is CertHolder empty? 80 | if *tt.certHolder == (pki.CertHolder{}) { 81 | t.Errorf("returned '%s' cert holder can't be empty: %+v", tt.name, sch) 82 | } 83 | 84 | var encodingtests = []struct { 85 | name string // name 86 | block string // pem block string 87 | typ string // expected pem block type 88 | }{ 89 | {tt.name + "CertHolder.Cert", tt.certHolder.Cert, pki.PEMCertificateBlockType}, 90 | {tt.name + "CertHolder.Key", tt.certHolder.Key, pki.PEMRSAPrivateKeyBlockType}, 91 | } 92 | 93 | // Is PEM encoded properly? 94 | for _, tt := range encodingtests { 95 | if !isPEMEncodedProperly(t, tt.block, tt.typ) { 96 | t.Errorf("returned '%s' is not PEM encoded properly: %+v", tt.name, tt.block) 97 | } 98 | } 99 | 100 | } 101 | 102 | } 103 | 104 | func TestNewCRL(t *testing.T) { 105 | // Initialize: 106 | max := 5 107 | n := randomBetween(1, max) 108 | ca, _ := pki.NewCA() 109 | 110 | // Prepare: 111 | var certHolders []*pki.CertHolder 112 | for i := 0; i < max; i++ { 113 | username := fmt.Sprintf("user-%d", i) 114 | ch, _ := pki.NewClientCertHolder(ca, username) 115 | certHolders = append(certHolders, ch) 116 | } 117 | 118 | // Test: 119 | // Create CRL that revokes first n certificates. 120 | var serials []*big.Int 121 | for i := 0; i < n; i++ { 122 | serials = append(serials, getSerial(t, certHolders[i].Cert)) 123 | } 124 | 125 | crl, err := pki.NewCRL(ca, serials...) 126 | if err != nil { 127 | t.Fatalf("crl can not be created: %v", err) 128 | } 129 | 130 | // Is CRL empty? 131 | if len(crl) == 0 { 132 | t.Fatalf("CRL length expected to be NOT EMPTY %+v", crl) 133 | } 134 | 135 | // Is CRL PEM encoded properly? 136 | if !isPEMEncodedProperly(t, crl, pki.PEMx509CRLBlockType) { 137 | t.Fatalf("CRL is expected to be properly PEM encoded %+v", crl) 138 | } 139 | 140 | // Parse CRL and get revoked certList. 141 | block, _ := pem.Decode([]byte(crl)) 142 | certList, err := x509.ParseCRL(block.Bytes) 143 | if err != nil { 144 | t.Fatalf("CRL's PEM block is expected to be parsed '%+v' but instead it CAN'T BE PARSED: %v", block, err) 145 | } 146 | 147 | rcl := certList.TBSCertList.RevokedCertificates 148 | 149 | // Is revoked cert list length is n, as correctly? 150 | if len(rcl) != n { 151 | t.Fatalf("revoked cert list lenth is expected to be %d but it is %d", n, len(rcl)) 152 | } 153 | 154 | // Is revoked certificate list is correct? 155 | for _, serial := range serials { 156 | found := false 157 | for _, rc := range rcl { 158 | //t.Logf("%d == %d", rc.SerialNumber, serial) 159 | if rc.SerialNumber.Cmp(serial) == 0 { 160 | found = true 161 | break 162 | } 163 | } 164 | if !found { 165 | t.Errorf("revoked serial '%d' is expected to be found in the generated CRL but it is NOT FOUND instead", serial) 166 | } 167 | } 168 | } 169 | 170 | func TestReadCertFromPEM(t *testing.T) { 171 | // Initialize: 172 | ca, _ := pki.NewCA() 173 | 174 | // Prepare: 175 | 176 | // Test: 177 | crt, err := pki.ReadCertFromPEM(ca.Cert) 178 | if err != nil { 179 | t.Fatalf("can not get cert from pem %+v", ca) 180 | } 181 | 182 | // Is crt nil? 183 | if crt == nil { 184 | t.Fatalf("cert is expected to be 'not nil' but it's 'nil' instead") 185 | } 186 | } 187 | 188 | // isPEMEncodedProperly takes an PEM encoded string s and the expected block type typ (e.g. "RSA PRIVATE KEY") and returns whether it can be decodable. 189 | func isPEMEncodedProperly(t *testing.T, s string, typ string) bool { 190 | block, _ := pem.Decode([]byte(s)) 191 | 192 | if block == nil { 193 | t.Logf("block is nil") 194 | return false 195 | } 196 | 197 | if len(block.Bytes) == 0 { 198 | t.Logf("block bytes length is zero") 199 | return false 200 | } 201 | 202 | if block.Type != typ { 203 | t.Logf("expected block type '%s' but got '%s'", typ, block.Type) 204 | return false 205 | } 206 | 207 | switch block.Type { 208 | case pki.PEMCertificateBlockType: 209 | crt, err := x509.ParseCertificate(block.Bytes) 210 | if err != nil { 211 | t.Logf("certificate parse failed %+v: %v", block, err) 212 | return false 213 | } 214 | 215 | if crt == nil { 216 | t.Logf("couldn't parse certificate %+v", block) 217 | return false 218 | } 219 | case pki.PEMRSAPrivateKeyBlockType: 220 | key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 221 | if err != nil { 222 | t.Logf("private key parse failed %+v: %v", block, err) 223 | return false 224 | } 225 | 226 | if key == nil { 227 | t.Logf("couldn't parse private key %+v", block) 228 | return false 229 | } 230 | case pki.PEMCSRBlockType: 231 | csr, err := x509.ParseCertificateRequest(block.Bytes) 232 | if err != nil { 233 | t.Logf("CSR parse failed %+v: %v", block, err) 234 | return false 235 | } 236 | 237 | if csr == nil { 238 | t.Logf("couldn't parse CSR %+v", block) 239 | return false 240 | } 241 | 242 | case pki.PEMx509CRLBlockType: 243 | crl, err := x509.ParseCRL(block.Bytes) 244 | if err != nil { 245 | t.Logf("CRL parse failed %+v: %v", block, err) 246 | return false 247 | } 248 | 249 | if crl == nil { 250 | t.Logf("couldn't parse crl %+v", block) 251 | return false 252 | } 253 | } 254 | return true 255 | } 256 | 257 | // getSerial returns serial number of a pem encoded certificate 258 | func getSerial(t *testing.T, crt string) *big.Int { 259 | // PEM decode. 260 | block, _ := pem.Decode([]byte(crt)) 261 | if block == nil { 262 | t.Fatalf("block is nil %+v", block) 263 | } 264 | 265 | // Parse certificate. 266 | cert, err := x509.ParseCertificate(block.Bytes) 267 | if err != nil { 268 | t.Fatalf("certificate can not be parsed from block %+v: %v", block, err) 269 | } 270 | return cert.SerialNumber 271 | } 272 | 273 | // randomBetween returns a random int between min and max 274 | func randomBetween(min, max int) int { 275 | rand.Seed(time.Now().Unix()) 276 | return rand.Intn(max-min) + min 277 | } 278 | -------------------------------------------------------------------------------- /scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export USER="nobody" 3 | export GROUP="nogroup" 4 | id -u $USER &>/dev/null || useradd $USER 5 | id -g $GROUP &>/dev/null || groupadd $GROUP 6 | 7 | systemctl daemon-reload 8 | -------------------------------------------------------------------------------- /scripts/postremove.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cad/ovpm/2197e9a44c73a9529e1e2a706149f35b1d353607/scripts/postremove.sh -------------------------------------------------------------------------------- /scripts/postupgrade.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | systemctl daemon-reload 3 | if [ "`systemctl is-active ovpmd`" != "active" ] 4 | then 5 | systemctl restart ovpmd 6 | fi 7 | -------------------------------------------------------------------------------- /scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export USER="nobody" 3 | export GROUP="nogroup" 4 | getent passwd $USER || useradd $USER 5 | getent group $GROUP || groupadd $GROUP 6 | -------------------------------------------------------------------------------- /scripts/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | systemctl stop ovpmd 3 | systemctl disable ovpmd 4 | -------------------------------------------------------------------------------- /user_internal_test.go: -------------------------------------------------------------------------------- 1 | package ovpm 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestUser_ConnectionStatus(t *testing.T) { 11 | // Init: 12 | db := CreateDB("sqlite3", ":memory:") 13 | defer db.Cease() 14 | svr := TheServer() 15 | svr.Init("localhost", "", UDPProto, "", "", "", "", false) 16 | 17 | origOpenFunc := svr.openFunc 18 | defer func() { svr.openFunc = origOpenFunc }() 19 | svr.openFunc = func(path string) (io.Reader, error) { 20 | return nil, nil 21 | } 22 | usr1, err := CreateNewUser("usr1", "1234", true, 0, false, "description") 23 | if err != nil { 24 | t.Fatalf("user creation failed: %v", err) 25 | } 26 | now := time.Now() 27 | svr.parseStatusLogFunc = func(f io.Reader) ([]clEntry, []rtEntry) { 28 | clt := []clEntry{ 29 | clEntry{ 30 | CommonName: usr1.GetUsername(), 31 | RealAddress: "1.1.1.1", 32 | ConnectedSince: now, 33 | BytesReceived: 1, 34 | BytesSent: 5, 35 | }, 36 | } 37 | rtt := []rtEntry{ 38 | rtEntry{ 39 | CommonName: usr1.GetUsername(), 40 | RealAddress: "1.1.1.1", 41 | LastRef: now, 42 | VirtualAddress: "10.10.10.1", 43 | }, 44 | } 45 | return clt, rtt 46 | } 47 | 48 | // Test: 49 | type fields struct { 50 | dbUserModel dbUserModel 51 | isConnected bool 52 | connectedSince time.Time 53 | bytesReceived uint64 54 | bytesSent uint64 55 | } 56 | tests := []struct { 57 | name string 58 | fields fields 59 | wantIsConnected bool 60 | wantConnectedSince time.Time 61 | wantBytesSent uint64 62 | wantBytesReceived uint64 63 | }{ 64 | {"default", fields{dbUserModel: dbUserModel{Username: "usr1"}}, true, now, 5, 1}, 65 | } 66 | for _, tt := range tests { 67 | t.Run(tt.name, func(t *testing.T) { 68 | u := &User{ 69 | dbUserModel: tt.fields.dbUserModel, 70 | isConnected: tt.fields.isConnected, 71 | connectedSince: tt.fields.connectedSince, 72 | bytesReceived: tt.fields.bytesReceived, 73 | bytesSent: tt.fields.bytesSent, 74 | } 75 | gotIsConnected, gotConnectedSince, gotBytesSent, gotBytesReceived := u.ConnectionStatus() 76 | if gotIsConnected != tt.wantIsConnected { 77 | t.Errorf("User.ConnectionStatus() gotIsConnected = %v, want %v", gotIsConnected, tt.wantIsConnected) 78 | } 79 | if !reflect.DeepEqual(gotConnectedSince, tt.wantConnectedSince) { 80 | t.Errorf("User.ConnectionStatus() gotConnectedSince = %v, want %v", gotConnectedSince, tt.wantConnectedSince) 81 | } 82 | if gotBytesSent != tt.wantBytesSent { 83 | t.Errorf("User.ConnectionStatus() gotBytesSent = %v, want %v", gotBytesSent, tt.wantBytesSent) 84 | } 85 | if gotBytesReceived != tt.wantBytesReceived { 86 | t.Errorf("User.ConnectionStatus() gotBytesReceived = %v, want %v", gotBytesReceived, tt.wantBytesReceived) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func init() { 93 | Testing = true 94 | } 95 | -------------------------------------------------------------------------------- /webui/ovpm/.env: -------------------------------------------------------------------------------- 1 | EXTEND_ESLINT=true -------------------------------------------------------------------------------- /webui/ovpm/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "rules": { 4 | "import/no-extraneous-dependencies": "off", 5 | "prefer-destructuring": "off", 6 | "consistent-return": "off", 7 | "no-shadow": "off", 8 | "react/require-default-props": "off", 9 | "react/destructuring-assignment": "off", 10 | "react/prop-types": "off", 11 | "react/state-in-constructor": "off", 12 | "react/static-property-placement": "off", 13 | "react/jsx-props-no-spreadin": "off", 14 | "react/jsx-one-expression-per-line": "off", 15 | "react-hooks/rules-of-hooks": "off", 16 | "react-hooks/exhaustive-deps": "off", 17 | "no-undef": "off", 18 | "jsx-a11y/anchor-is-valid": "off" 19 | } 20 | } 21 | 22 | // { 23 | // "parser": "babel-eslint", 24 | // "extends": ["react-app", "airbnb", "prettier", "plugin:jest/recommended"], 25 | // "plugins": ["prettier", "jest"], 26 | // "globals": { 27 | // "window": true, 28 | // "localStorage": true, 29 | // "fetch": true, 30 | // "React": true, 31 | // "shallow": true, 32 | // "render": true, 33 | // "mount": true 34 | // }, 35 | // "rules": { 36 | // "import/no-extraneous-dependencies": ["error", { "devDependencies": true }], 37 | // "prettier/prettier": ["error"], 38 | // "react/jsx-filename-extension": [ 39 | // 1, 40 | // { 41 | // "extensions": [".js", ".jsx"] 42 | // } 43 | // ], 44 | // "react/forbid-prop-types": [ 45 | // 1, 46 | // { 47 | // "forbid": ["any"] 48 | // } 49 | // ], 50 | // "react/react/jsx-props-no-spreading": "off", 51 | // "react/jsx-one-expression-per-line": "off", 52 | // "react-hooks/rules-of-hooks": "error", 53 | // "react-hooks/exhaustive-deps": "warn", 54 | // "no-console": "off" 55 | // } 56 | // } 57 | -------------------------------------------------------------------------------- /webui/ovpm/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /webui/ovpm/README.md: -------------------------------------------------------------------------------- 1 | # OVPM-WebUI 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode. 10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits. 13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode. 18 | 19 | ### `yarn build` 20 | 21 | Builds the app for production to the `build` folder. 22 | It correctly bundles React in production mode and optimizes the build for the best performance. 23 | 24 | The build is minified and the filenames include the hashes. 25 | -------------------------------------------------------------------------------- /webui/ovpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ovpm-webui", 3 | "version": "1.0.0", 4 | "description": "OVPM Web UI", 5 | "private": true, 6 | "author": "mustafa@arici.io", 7 | "license": "MIT", 8 | "dependencies": { 9 | "@testing-library/jest-dom": "^4.2.4", 10 | "@testing-library/react": "^9.3.2", 11 | "@testing-library/user-event": "^7.1.2", 12 | "axios": "^0.21.1", 13 | "hoek": "^6.1.3", 14 | "moment": "^2.25.3", 15 | "muicss": "^0.10.2", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-modal": "^3.11.2", 19 | "react-moment": "^0.9.7", 20 | "react-router-dom": "^5.2.0", 21 | "react-scripts": "3.4.1", 22 | "string-template": "^1.0.0" 23 | }, 24 | "resolutions": { 25 | "minimist": "^1.2.3", 26 | "http-proxy": "^1.18.0", 27 | "yargs-parser": "^18.1.3", 28 | "serialize-javascript": "^3.1.0", 29 | "node-fetch": "^2.6.1", 30 | "node-forge": "^0.10.0", 31 | "object-path": "^0.11.5" 32 | }, 33 | "scripts": { 34 | "start": "react-scripts start", 35 | "build": "react-scripts build", 36 | "test": "react-scripts test", 37 | "eject": "react-scripts eject" 38 | }, 39 | "eslintConfig": { 40 | "extends": "react-app" 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /webui/ovpm/public/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cad/ovpm/2197e9a44c73a9529e1e2a706149f35b1d353607/webui/ovpm/public/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /webui/ovpm/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | 18 | OVPM 19 | 20 | 21 | 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /webui/ovpm/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cad/ovpm/2197e9a44c73a9529e1e2a706149f35b1d353607/webui/ovpm/public/logo192.png -------------------------------------------------------------------------------- /webui/ovpm/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "OVPM", 3 | "name": "OVPM", 4 | "icons": [ 5 | { 6 | "src": "logo192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /webui/ovpm/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /webui/ovpm/src/App.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Master from "./components/Master"; 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /webui/ovpm/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | const { getByText } = render(); 7 | const linkElement = getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /webui/ovpm/src/api.js: -------------------------------------------------------------------------------- 1 | export var baseURL = 2 | window.location.protocol + "//" + window.location.host + "/api/v1"; 3 | 4 | if (process.env.NODE_ENV !== "production") { 5 | // baseURL = "http://172.16.16.53:8080/api/v1" // local pc external ip 6 | baseURL = "http://127.0.0.1:8080/api/v1"; 7 | } 8 | 9 | export const endpoints = { 10 | authenticate: { 11 | path: "/auth/authenticate", 12 | 13 | method: "POST" 14 | }, 15 | authStatus: { 16 | path: "/auth/status", 17 | method: "GET" 18 | }, 19 | genConfig: { 20 | path: "/user/genconfig", 21 | method: "POST" 22 | }, 23 | userList: { 24 | path: "/user/list", 25 | method: "GET" 26 | }, 27 | userCreate: { 28 | path: "/user/create", 29 | method: "POST" 30 | }, 31 | userDelete: { 32 | path: "/user/delete", 33 | method: "POST" 34 | }, 35 | userUpdate: { 36 | path: "/user/update", 37 | method: "POST" 38 | }, 39 | networkList: { 40 | path: "/network/list", 41 | method: "GET" 42 | }, 43 | vpnStatus: { 44 | path: "/vpn/status", 45 | method: "GET" 46 | }, 47 | vpnRestart: { 48 | path: "/vpn/restart", 49 | method: "POST" 50 | }, 51 | netDefine: { 52 | path: "/network/create", 53 | method: "POST" 54 | }, 55 | netUndefine: { 56 | path: "/network/delete", 57 | method: "POST" 58 | }, 59 | netAssociate: { 60 | path: "/network/associate", 61 | method: "POST" 62 | }, 63 | netDissociate: { 64 | path: "/network/dissociate", 65 | method: "POST" 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /webui/ovpm/src/components/Auth/Login/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Button from "muicss/lib/react/button"; 3 | import Input from "muicss/lib/react/input"; 4 | import Panel from "muicss/lib/react/panel"; 5 | import Container from "muicss/lib/react/container"; 6 | 7 | import { Redirect } from "react-router"; 8 | 9 | import { 10 | IsAuthenticated, 11 | SetAuthToken, 12 | ClearAuthToken, 13 | SetItem, 14 | GetItem 15 | } from "../../../utils/auth.js"; 16 | import { API } from "../../../utils/restClient.js"; 17 | import { baseURL, endpoints } from "../../../api.js"; 18 | 19 | export default class Login extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | 23 | this.state = { 24 | username: "", 25 | password: "", 26 | isAuthenticated: false, 27 | isAdmin: false, 28 | error: null 29 | }; 30 | this.api = new API(baseURL, endpoints); 31 | } 32 | 33 | componentWillMount() { 34 | let isAdmin = false; 35 | if (GetItem("isAdmin")) { 36 | isAdmin = true; 37 | } 38 | this.setState({ 39 | isAuthenticated: IsAuthenticated(), 40 | isAdmin: isAdmin 41 | }); 42 | this.api.call( 43 | "authStatus", 44 | {}, 45 | false, 46 | this.handleGetUserInfoSuccess.bind(this), 47 | this.handleGetUserInfoFailure.bind(this) 48 | ); 49 | } 50 | 51 | handleUsernameChange(e) { 52 | this.setState({ username: e.target.value }); 53 | } 54 | 55 | handlePasswordChange(e) { 56 | this.setState({ password: e.target.value }); 57 | } 58 | 59 | handleGetUserInfoSuccess(res) { 60 | if (res.data.user.username === "root") { 61 | SetAuthToken("root"); 62 | this.setState({ isAuthenticated: true }); 63 | this.api.setAuthToken("root"); 64 | SetItem("isAdmin", true); 65 | SetItem("username", "root"); 66 | } else { 67 | SetItem("isAdmin", res.data.user.is_admin); 68 | SetItem("username", this.state.username); 69 | } 70 | } 71 | 72 | handleGetUserInfoFailure(error) { 73 | console.log(error); 74 | } 75 | 76 | handleAuthenticateSuccess(res) { 77 | SetAuthToken(res.data.token); 78 | this.setState({ isAuthenticated: true }); 79 | console.log("authenticated"); 80 | this.api.setAuthToken(res.data.token); 81 | this.api.call( 82 | "authStatus", 83 | {}, 84 | true, 85 | this.handleGetUserInfoSuccess.bind(this), 86 | this.handleGetUserInfoFailure.bind(this) 87 | ); 88 | } 89 | 90 | handleAuthenticateFailure(error) { 91 | ClearAuthToken(); 92 | this.setState({ isAuthenticated: false }); 93 | console.log("authentication error", error); 94 | if (error.response.status >= 400) { 95 | this.setState({ error: "Your credentials are incorrect." }); 96 | } 97 | } 98 | 99 | handleFormSubmit(e) { 100 | this.setState({ error: null }); 101 | if (!this.state.username) { 102 | return; 103 | } 104 | if (!this.state.password) { 105 | return; 106 | } 107 | 108 | let data = { 109 | username: this.state.username, 110 | password: this.state.password 111 | }; 112 | 113 | this.api.call( 114 | "authenticate", 115 | data, 116 | false, 117 | this.handleAuthenticateSuccess.bind(this), 118 | this.handleAuthenticateFailure.bind(this) 119 | ); 120 | e.preventDefault(); 121 | } 122 | render() { 123 | let error; 124 | if (this.state.isAuthenticated) { 125 | return ; 126 | } 127 | 128 | if (this.state.error) { 129 | error = ( 130 | 139 | Authentication Error 140 |

{this.state.error}

141 |
142 | ); 143 | } 144 | return ( 145 |
152 | 153 | {error} 154 | 155 |
156 | 163 | 171 | 174 |
175 |
176 |
177 |
178 | ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /webui/ovpm/src/components/Auth/LoginRequired/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Redirect } from "react-router"; 4 | 5 | import { IsAuthenticated } from "../../../utils/auth.js"; 6 | 7 | export default function loginRequired(WrappedComponent) { 8 | return class extends React.Component { 9 | componentWillReceiveProps(nextProps) { 10 | console.log("Current props: ", this.props); 11 | console.log("Next props: ", nextProps); 12 | this.setState({ isLoggedIn: false }); 13 | } 14 | componentWillMount() { 15 | this.setState({ isLoggedIn: IsAuthenticated() }); 16 | } 17 | render() { 18 | if (!this.state.isLoggedIn) { 19 | return ; 20 | } 21 | return ; 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /webui/ovpm/src/components/Auth/Logout/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "react-router"; 3 | import { ClearAuthToken } from "../../../utils/auth.js"; 4 | export default class Logout extends React.Component { 5 | componentWillMount() { 6 | ClearAuthToken(); // Logout 7 | } 8 | 9 | render() { 10 | return ; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /webui/ovpm/src/components/Dashboard/AdminDashboard/NetworkEdit/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Button from "muicss/lib/react/button"; 4 | import Container from "muicss/lib/react/container"; 5 | import Input from "muicss/lib/react/input"; 6 | import Option from "muicss/lib/react/option"; 7 | import Select from "muicss/lib/react/select"; 8 | 9 | export default class NetworkEdit extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = { 14 | name: this.props.name ? this.props.name : "", 15 | type: this.props.type ? this.props.type : "SERVERNET", 16 | cidr: this.props.cidr ? this.props.cidr : "", 17 | via: this.props.via ? this.props.via : "" 18 | }; 19 | } 20 | 21 | componentWillMount() {} 22 | 23 | handleNameChange(e) { 24 | this.setState({ name: e.target.value }); 25 | } 26 | 27 | handleTypeChange(e) { 28 | this.setState({ type: e.target.value }); 29 | } 30 | 31 | handleCidrChange(e) { 32 | this.setState({ cidr: e.target.value }); 33 | } 34 | 35 | handleViaChange(e) { 36 | this.setState({ via: e.target.value }); 37 | } 38 | 39 | handleFormSubmit() { 40 | console.log(this.state.type); 41 | let network = { 42 | name: this.state.name, 43 | cidr: this.state.cidr, 44 | type: this.state.type, 45 | via: this.state.via 46 | }; 47 | 48 | this.props.onSave(network); 49 | } 50 | 51 | handleFormCancel() { 52 | this.setState({ error: null }); 53 | this.props.onCancel(); 54 | } 55 | 56 | render() { 57 | var via; 58 | if (this.state.type === "ROUTE") { 59 | via = ( 60 | 67 | ); 68 | } 69 | 70 | return ( 71 | 72 |

{this.props.title}

73 | 74 | 82 | 89 | 98 | {via} 99 | 100 |
101 | 108 | 115 |
116 |
117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /webui/ovpm/src/components/Dashboard/AdminDashboard/UserEdit/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Button from "muicss/lib/react/button"; 4 | import Container from "muicss/lib/react/container"; 5 | import Input from "muicss/lib/react/input"; 6 | import Option from "muicss/lib/react/option"; 7 | import Select from "muicss/lib/react/select"; 8 | import Checkbox from "muicss/lib/react/checkbox"; 9 | 10 | export default class UserEdit extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | username: this.props.username ? this.props.username : "", 16 | password: "", 17 | staticIP: this.props.staticIP ? this.props.staticIP : "", 18 | ipAllocationMethod: this.props.ipAllocationMethod 19 | ? this.props.ipAllocationMethod 20 | : "dynamic", 21 | pushGW: this.props.pushGW !== undefined ? this.props.pushGW : true, 22 | isAdmin: this.props.isAdmin ? this.props.isAdmin : false 23 | }; 24 | } 25 | 26 | componentWillMount() {} 27 | 28 | handleUsernameChange(e) { 29 | this.setState({ username: e.target.value }); 30 | } 31 | 32 | handlePasswordChange(e) { 33 | this.setState({ password: e.target.value }); 34 | } 35 | 36 | handleStaticIPChange(e) { 37 | this.setState({ staticIP: e.target.value }); 38 | } 39 | 40 | handleIPAllocationChange(e) { 41 | this.setState({ ipAllocationMethod: e.target.value }); 42 | } 43 | 44 | handlePushGWChange(e) { 45 | this.setState({ pushGW: e.target.checked }); 46 | } 47 | 48 | handleIsAdminChange(e) { 49 | this.setState({ isAdmin: e.target.checked }); 50 | } 51 | 52 | handleFormSubmit() { 53 | this.props.onSave(this.state); 54 | } 55 | 56 | handleFormCancel() { 57 | this.setState({ error: null }); 58 | this.props.onCancel(); 59 | } 60 | 61 | render() { 62 | var staticIPInput; 63 | if (this.state.ipAllocationMethod === "static") { 64 | staticIPInput = ( 65 | 72 | ); 73 | } 74 | return ( 75 | 76 |

{this.props.title}

77 | 78 | 86 | 94 | 103 | {staticIPInput} 104 | 109 | 114 |
115 | 122 | 129 |
130 |
131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /webui/ovpm/src/components/Dashboard/AdminDashboard/UserPicker/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Button from "muicss/lib/react/button"; 4 | import Container from "muicss/lib/react/container"; 5 | import Option from "muicss/lib/react/option"; 6 | import Select from "muicss/lib/react/select"; 7 | 8 | export default class UserPicker extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | username: this.props.userNames.length > 0 ? this.props.userNames[0] : "" 14 | }; 15 | } 16 | 17 | componentWillMount() {} 18 | 19 | handleUserChange(e) { 20 | this.setState({ username: e.target.value }); 21 | } 22 | 23 | handleFormSubmit() { 24 | this.props.onSave(this.state.username); 25 | console.log(this.state.username); 26 | } 27 | 28 | handleFormCancel() { 29 | this.setState({ error: null }); 30 | this.props.onCancel(); 31 | } 32 | 33 | render() { 34 | let users = []; 35 | for (let i in this.props.userNames) { 36 | users.push( 37 |