├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── main.go ├── db ├── db.go └── fileType.go ├── firewallcmd └── util.go ├── firewalld-rest.service ├── go.mod ├── go.sum ├── ip ├── handler.go ├── handler_test.go └── ip.go ├── k8s ├── ingress.yaml ├── svc-nodeport.yaml └── svc.yaml └── route ├── handler.go ├── handler_test.go ├── middleware.go ├── publicCert.go ├── response.go └── route.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.db 3 | build/ 4 | 5 | # coverage 6 | *.html 7 | *.out -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:alpine as builder 2 | RUN mkdir /build 3 | ADD . /build/ 4 | WORKDIR /build 5 | RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' ./cmd/main.go 6 | FROM scratch 7 | COPY --from=builder /build/main /app/ 8 | WORKDIR /app 9 | CMD ["./main"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Prashant Gupta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COVER_PROFILE=cover.out 2 | COVER_PROFILE_TEMP=cover.tmp.out 3 | COVER_HTML=cover.html 4 | 5 | .PHONY: build $(COVER_PROFILE) $(COVER_HTML) 6 | 7 | all: coverage vet 8 | 9 | coverage: $(COVER_HTML) 10 | 11 | $(COVER_HTML): $(COVER_PROFILE) ignoreFiles 12 | go tool cover -html=$(COVER_PROFILE) -o $(COVER_HTML) 13 | 14 | ignoreFiles: 15 | cat $(COVER_PROFILE_TEMP) | grep -v "middleware.go" | grep -v "route.go" > $(COVER_PROFILE) 16 | 17 | $(COVER_PROFILE): 18 | env=local go test -v -failfast -race -coverprofile=$(COVER_PROFILE_TEMP) ./... 19 | 20 | vet: 21 | go vet ./... 22 | start-local: clean-db #for testing on your local system without firewalld 23 | env=local go run cmd/main.go 24 | start-server: 25 | go run cmd/main.go 26 | build-linux: # example: make build-linux DB_PATH=/dir/to/db 27 | env GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/prashantgupta24/firewalld-rest/db.pathFromEnv=$(DB_PATH)" -o build/firewalld-rest cmd/main.go 28 | local-build: 29 | go build -ldflags "-X github.com/prashantgupta24/firewalld-rest/db.pathFromEnv=$(DB_PATH)" -o build/firewalld-rest cmd/main.go 30 | copy: build-linux 31 | scp build/firewalld-rest root@:/root/rest 32 | clean-db: 33 | rm -f *.db 34 | test: 35 | env=local go test -v -failfast -race ./... 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) [![Go Report Card](https://goreportcard.com/badge/github.com/prashantgupta24/firewalld-rest)](https://goreportcard.com/report/github.com/prashantgupta24/firewalld-rest) [![codecov](https://codecov.io/gh/prashantgupta24/firewalld-rest/branch/master/graph/badge.svg)](https://codecov.io/gh/prashantgupta24/firewalld-rest) [![version][version-badge]][releases] 2 | 3 | # Firewalld-rest 4 | 5 | A REST application to dynamically update firewalld rules on a linux server. 6 | 7 | _Firewalld is a firewall management tool for Linux operating systems._ 8 | 9 | ## Purpose 10 | 11 | If you have seen this message when you login to your linux server: 12 | 13 | ``` 14 | There were 534 failed login attempts since the last successful login. 15 | ``` 16 | 17 | Then this idea is for **you**. 18 | 19 | The simple idea behind this is to have a completely isolated system, a system running Firewalld that does not permit SSH access to any IP address by default so there are no brute-force attacks. The only way to access the system is by communicating with a REST application running on the server through a valid request containing your public IP address. 20 | 21 | The REST application validates your request (it checks for a valid JWT, covered later), and if the request is valid, it will add your IP to the firewalld rule for the public zone for SSH, which gives **only your IP** SSH access to the machine. 22 | 23 | Once you are done using the machine, you can remove your IP interacting with the same REST application, and it changes rules in firewalld, shutting off SSH access and isolating the system again. 24 | 25 | ## Comparison with fail2ban 26 | 27 | This repo takes a proactive approach rather than a reactive approach taken by `fail2ban`. `fail2ban` dynamically alters the firewall rules to ban addresses that have unsuccessfully attempted to login a certain number of times. It is reactive - it allows people to try and login to the server, but bans those who are unsuccessful in doing so after a certain number of times. It is like appointing a guard (aka firewall) outside a locked building who checks for suspicious activity and the guard is told by `fail2ban` to ban anyone who tries to open the lock unsuccessfully many times. 28 | 29 | Firewalld-rest is more of a proactive approach. Let me explain. 30 | 31 | ### Proactive approach 32 | 33 | By using the approach presented in this repo, you still add a guard (aka firewall) like you did for fail2ban in front of your locked building (aka the server). But there are 2 main differences here: 34 | 35 | 1. This guard is told to not let **anyone** come near the building by default, so that no one is ever close enough to the lock to try their keys. (This means that the default firewall rules are set up by default in such a way so that no one can even try to SSH to the server). 36 | 2. You can talk to the guard (aka firewall) using this repo, and convince the guard to allow you near the building, provided you possess a certain key (an **RS256** type, covered later). (This means that using the REST interface provided by this repo, you proactively alter firewall rules to allow ONLY your IP to try and login to the server) 37 | 38 | `Note`: Once you are allowed through by the firewall, you still need to have the key to login to the server. 39 | 40 | > It is proactive - You proactively talk to the REST interface and alter the firewall rules to allow your IP to try and login. 41 | 42 | **TL;DR** 43 | 44 | | `fail2ban` | `firewalld-rest ` | 45 | | ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 46 | | `dynamically` alters firewall rules to ban IP addresses that have unsuccessfully attempted to login to server a certain number of times. | provides REST interface to `manually` alter firewall rules to allow ONLY your IP to try and login to server. No IP apart from yours can even try to login to server. | 47 | | `Reactive` - it alters firewall rules _after_ unsuccessfully attempts | `Proactive` - you alter firewall rules _before_ trying to login to server | 48 | 49 | > Note: I am not saying one approach is better than the other. They are just different approaches to the same problem. 50 | 51 | ## Table of Contents 52 | 53 | 54 | 55 | 56 | 57 | - [Purpose](#purpose) 58 | - [Comparison with fail2ban](#comparison-with-fail2ban) 59 | - [Proactive approach](#proactive-approach) 60 | - [Table of Contents](#table-of-contents) 61 | - [1. Pre-requisites](#1-pre-requisites) 62 | - [2. About the application](#2-about-the-application) 63 | - [2.1 Firewall-cmd](#21-firewall-cmd) 64 | - [2.2 Database](#22-database) 65 | - [2.3 Authorization](#23-authorization) 66 | - [2.4 Tests](#24-tests) 67 | - [3. How to install and use on server](#3-how-to-install-and-use-on-server) 68 | - [3.1 Generate JWT](#31-generate-jwt) 69 | - [3.2 Build the application](#32-build-the-application) 70 | - [3.3 Copy binary file over to server](#33-copy-binary-file-over-to-server) 71 | - [3.4 Remove SSH from public firewalld zone](#34-remove-ssh-from-public-firewalld-zone) 72 | - [3.5 Expose the REST application](#35-expose-the-rest-application) 73 | - [3.5.1 Single node cluster](#351-single-node-cluster) 74 | - [3.5.2 Multi-node cluster](#352-multi-node-cluster) 75 | - [3.6 Configure linux systemd service](#36-configure-linux-systemd-service) 76 | - [3.7 Start and enable systemd service.](#37-start-and-enable-systemd-service) 77 | - [3.8 IP JSON](#38-ip-json) 78 | - [3.9 Interacting with the REST application](#39-interacting-with-the-rest-application) 79 | - [3.9.1 Index page](#391-index-page) 80 | - [Sample query](#sample-query) 81 | - [3.9.2 Show all IPs](#392-show-all-ips) 82 | - [Sample query](#sample-query-1) 83 | - [3.9.3 Add new IP](#393-add-new-ip) 84 | - [Sample query](#sample-query-2) 85 | - [3.9.4 Show if IP is present](#394-show-if-ip-is-present) 86 | - [Sample query](#sample-query-3) 87 | - [3.9.5 Delete IP](#395-delete-ip) 88 | - [Sample query](#sample-query-4) 89 | - [4. Helpful tips/links](#4-helpful-tipslinks) 90 | - [5. Commands for generating public/private key](#5-commands-for-generating-publicprivate-key) 91 | - [6. Possible enhancements](#6-possible-enhancements) 92 | 93 | 94 | 95 | ## 1. Pre-requisites 96 | 97 | This repo assumes you have: 98 | 99 | 1. A linux server with `firewalld` installed. 100 | 1. `root` access to the server. (without `root` access, the application will not be able to run the `firewall-cmd` commands needed to add the rule for SSH access) 101 | 1. Some way of exposing the application externally (there are examples in this repo on how to use Kubernetes to expose the service) 102 | 103 | ## 2. About the application 104 | 105 | ### 2.1 Firewall-cmd 106 | 107 | Firewall-cmd is the command line client of the firewalld daemon. Through this, the REST application adds the rule specific to the IP address sent in the request. 108 | 109 | The syntax of adding a rule for an IP address is: 110 | `firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="10.xx.xx.xx/32" port protocol="tcp" port="22" accept'` 111 | 112 | Once the rule for the IP address has been added, the IP address is stored in a database (covered next). The database is just to keep track of all IPs that have rules created for them. 113 | 114 | ### 2.2 Database 115 | 116 | The database for the application stores the list of IP addresses that have rules created for them which allow SSH access for those IPs. Once you interact with the REST application and the application creates a firewalld rule specific to your IP address, then your IP address is stored in the database. It is important that the database is maintained during server restarts, otherwise there may be discrepancy between the IP addresses having firewalld rules and IP addresses stored in the database. 117 | 118 | > Note: Having an IP in the database does not mean that IP address will be given SSH access. The database is just a way to reference all the IPs with rules created in firewalld. 119 | 120 | The application uses a file type database for now. The architecture of the code allows easy integration of any other type of databases. The interface in db.go is what is required to be fulfilled to introduce a new type of database. 121 | 122 | ### 2.3 Authorization 123 | 124 | The application uses `RS256` type algorithm to verify the incoming requests. 125 | 126 | > RS256 (RSA Signature with SHA-256) is an asymmetric algorithm, and it uses a public/private key pair: the identity provider has a private (secret) key used to generate the signature, and the consumer of the JWT gets a public key to validate the signature. 127 | 128 | The public certificate is in this file [publicCert.go](https://github.com/prashantgupta24/firewalld-rest/blob/master/route/publicCert.go), which is something that will have to be changed before you can use it. (more information on how to create a new one later). 129 | 130 | ### 2.4 Tests 131 | 132 | The tests can be run using `make test`. The emphasis has been given to testing the handler functions and making sure that IPs get added and removed successfully from the database. I still have to figure out how to actually automate the tests for the firewalld rules (contributions are welcome!) 133 | 134 | ## 3. How to install and use on server 135 | 136 | ### 3.1 Generate JWT 137 | 138 | Update the file [publicCert.go](https://github.com/prashantgupta24/firewalld-rest/blob/master/route/publicCert.go) with your own `public cert` for which you have the private key. 139 | 140 | If you want to create a new set, see the section on [generating your own public/private key](#5-commands-for-generating-publicprivate-key). Once you have your own public and private key pair, then after updating the file above, you can go to `jwt.io` and generate a valid JWT using `RS256 algorithm` (the payload doesn't matter). You will be using that JWT to make calls to the REST application, so keep the JWT safe. 141 | 142 | ### 3.2 Build the application 143 | 144 | Run the command: 145 | 146 | ``` 147 | make build-linux DB_PATH=/dir/to/db/ 148 | ``` 149 | 150 | It will create a binary under the build directory, called `firewalld-rest`. The `DB_PATH=/dir/to/keep/db` statement sets the path where the `.db` file will be saved **on the server**. It should be saved in a protected location such that it is not accidentally deleted on server restart or by any other user. A good place for it could be the same directory where you will copy the binary over to (in the next step). That way you will not forget where it is. 151 | 152 | If `DB_PATH` variable is not set, the db file will be created by default under `/`. (_This happens because the binary is run by systemd. If we manually ran the binary file on the server, the db file would be created in the same directory._) 153 | 154 | Once the binary is built, it should contain everything required to run the application on a linux based server. 155 | 156 | ### 3.3 Copy binary file over to server 157 | 158 | ``` 159 | scp build/firewalld-rest root@:/root/rest 160 | ``` 161 | 162 | _Note_: if you want to change the directory where you want to keep the binary, then make sure you edit the `firewalld-rest.service` file, as the `linux systemd service` definition example in this repo expects the location of the binary to be `/root/rest`. 163 | 164 | ### 3.4 Remove SSH from public firewalld zone 165 | 166 | This is to remove SSH access from the public zone, which will cease SSH access from everywhere. 167 | 168 | SSH into the server, and run the following command: 169 | 170 | ``` 171 | firewall-cmd --zone=public --remove-service=ssh --permanent 172 | ``` 173 | 174 | then reload (since we are using `--permanent`): 175 | 176 | ``` 177 | firewall-cmd --reload 178 | ``` 179 | 180 | This removes ssh access for everyone. This is where the application will come into play, and we enable access based on IP. 181 | 182 | **Confirmation for the step**: 183 | 184 | ``` 185 | firewall-cmd --zone=public --list-all 186 | ``` 187 | 188 | _Notice the `ssh` service will not be listed in public zone anymore._ 189 | 190 | Also try SSH access into the server from another terminal. It should reject the attempt. 191 | 192 | ### 3.5 Expose the REST application 193 | 194 | The REST application can be exposed in a number of different ways, I have 2 examples on how it can be exposed: 195 | 196 | 1. Using a `NodePort` kubernetes service ([link](https://github.com/prashantgupta24/firewalld-rest/blob/master/k8s/svc-nodeport.yaml)) 197 | 2. Using `ingress` along with a kubernetes service ([link](https://github.com/prashantgupta24/firewalld-rest/blob/master/k8s/ingress.yaml)) 198 | 199 | #### 3.5.1 Single node cluster 200 | 201 | For a single-node cluster, see the kubernetes service example [here](https://github.com/prashantgupta24/firewalld-rest/blob/master/k8s/svc-nodeport.yaml). The important thing to note is that we manually add the `Endpoints` resource for the service, which points to our node's private IP address and port `8080`. 202 | 203 | Once deployed, your service might look like this: 204 | 205 | ``` 206 | kubernetes get svc 207 | 208 | external-rest | NodePort | 10.xx.xx.xx | 169.xx.xx.xx | 8080:31519/TCP 209 | ``` 210 | 211 | Now, you can interact with the application on: 212 | 213 | > 169.xx.xx.xx:31519/m1/ 214 | 215 | _Note: Since there's only 1 node in the cluster, you will only ever use `/m1`. For more than 1 node, see the next section._ 216 | 217 | #### 3.5.2 Multi-node cluster 218 | 219 | For a multi-node cluster, an ingress resource would be highly beneficial. 220 | 221 | The **first** step would be to create the kubernetes service in each individual node, using the example [here](https://github.com/prashantgupta24/firewalld-rest/blob/master/k8s/svc.yaml). The important thing to note is that we manually add the `Endpoints` resource for the service, which points to our node's private IP address and port `8080`. 222 | 223 | The **second** step is the [ingress](https://github.com/prashantgupta24/firewalld-rest/blob/master/k8s/ingress.yaml) resource. It redirects different routes to different nodes in the cluster. For example, in the ingress file above, 224 | 225 | A request to `/m1` will be redirected to the `first` node, a request to `/m2` will be redirected to the `second` node, and so on. This will let you control each node's individual SSH access through a single endpoint. 226 | 227 | ### 3.6 Configure linux systemd service 228 | 229 | See [this](https://github.com/prashantgupta24/firewalld-rest/blob/master/firewalld-rest.service) for an example of a linux systemd service. 230 | 231 | The `.service` file should be placed under `etc/systemd/system` directory. 232 | 233 | **Note**: This service assumes your binary is at `/root/rest/firewalld-rest`. You can change that in the file above. 234 | 235 | ### 3.7 Start and enable systemd service. 236 | 237 | **Start** 238 | 239 | ``` 240 | systemctl start firewalld-rest 241 | ``` 242 | 243 | **Logs** 244 | 245 | You can see the logs for the service using: 246 | 247 | ``` 248 | journalctl -r 249 | ``` 250 | 251 | **Enable** 252 | 253 | ``` 254 | systemctl enable firewalld-rest 255 | ``` 256 | 257 | ### 3.8 IP JSON 258 | 259 | This is how the IP JSON looks like, so that you know how you have to pass your IP and domain to the application: 260 | 261 | ``` 262 | type IP struct { 263 | IP string `json:"ip"` 264 | Domain string `json:"domain"` 265 | } 266 | ``` 267 | 268 | ### 3.9 Interacting with the REST application 269 | 270 | #### 3.9.1 Index page 271 | 272 | ``` 273 | route{ 274 | "Index Page", 275 | "GET", 276 | "/", 277 | } 278 | ``` 279 | 280 | ##### Sample query 281 | 282 | ``` 283 | curl --location --request GET ':8080/m1' \ 284 | --header 'Authorization: Bearer ' 285 | ``` 286 | 287 | #### 3.9.2 Show all IPs 288 | 289 | ``` 290 | route{ 291 | "Show all IPs present", 292 | "GET", 293 | "/ip", 294 | } 295 | ``` 296 | 297 | ##### Sample query 298 | 299 | ``` 300 | curl --location --request GET ':8080/m1/ip' \ 301 | --header 'Authorization: Bearer ' 302 | ``` 303 | 304 | #### 3.9.3 Add new IP 305 | 306 | ``` 307 | route{ 308 | "Add New IP", 309 | "POST", 310 | "/ip", 311 | } 312 | ``` 313 | 314 | ##### Sample query 315 | 316 | ``` 317 | curl --location --request POST ':8080/m1/ip' \ 318 | --header 'Authorization: Bearer ' \ 319 | --header 'Content-Type: application/json' \ 320 | --data-raw '{"ip":"10.xx.xx.xx","domain":"example.com"}' 321 | ``` 322 | 323 | #### 3.9.4 Show if IP is present 324 | 325 | ``` 326 | route{ 327 | "Show if particular IP is present", 328 | "GET", 329 | "/ip/{ip}", 330 | } 331 | ``` 332 | 333 | ##### Sample query 334 | 335 | ``` 336 | curl --location --request GET ':8080/m1/ip/10.xx.xx.xx' \ 337 | --header 'Authorization: Bearer ' 338 | ``` 339 | 340 | #### 3.9.5 Delete IP 341 | 342 | ``` 343 | route{ 344 | "Delete IP", 345 | "DELETE", 346 | "/ip/{ip}", 347 | } 348 | ``` 349 | 350 | ##### Sample query 351 | 352 | ``` 353 | curl --location --request DELETE ':8080/m1/ip/10.xx.xx.xx' \ 354 | --header 'Authorization: Bearer ' 355 | ``` 356 | 357 | ## 4. Helpful tips/links 358 | 359 | - ### 4.1 Creating custom kubernetes endpoint 360 | 361 | - https://theithollow.com/2019/02/04/kubernetes-endpoints/ 362 | 363 | - ### 4.2 Firewalld rules 364 | 365 | - https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-using-firewalld-on-centos-7 366 | 367 | #### 4.2.1 Useful commands 368 | 369 | ``` 370 | firewall-cmd --get-default-zone 371 | firewall-cmd --get-active-zones 372 | 373 | firewall-cmd --list-all-zones | less 374 | 375 | firewall-cmd --zone=public --list-sources 376 | firewall-cmd --zone=public --list-services 377 | firewall-cmd --zone=public --list-all 378 | 379 | firewall-cmd --zone=public --add-service=ssh --permanent 380 | 381 | firewall-cmd --zone=internal --add-source=70.xx.xx.xxx/32 --permanent 382 | 383 | firewall-cmd --reload 384 | ``` 385 | 386 | #### 4.2.2 Rich rules 387 | 388 | `firewall-cmd --permanent --zone=public --list-rich-rules` 389 | 390 | `firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="10.10.99.10/32" port protocol="tcp" port="22" accept'` 391 | 392 | `firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.100.0/24" invert="True" drop'` 393 | 394 | > Reject will reply back with an ICMP packet noting the rejection, while a drop will just silently drop the traffic and do nothing else, so a drop may be preferable in terms of security as a reject response confirms the existence of the system as it is rejecting the request. 395 | 396 | #### 4.2.3 Misc tips 397 | 398 | > --add-source=IP can be used to add an IP address or range of addresses to a zone. This will mean that if any source traffic enters the systems that matches this, the zone that we have set will be applied to that traffic. In this case we set the ‘testing’ zone to be associated with traffic from the 10.10.10.0/24 range. 399 | 400 | ``` 401 | [root@centos7 ~]# firewall-cmd --permanent --zone=testing --add-source=10.10.10.0/24 402 | success 403 | ``` 404 | 405 | - ### 4.3 Using JWT in Go 406 | 407 | - https://www.thepolyglotdeveloper.com/2017/03/authenticate-a-golang-api-with-json-web-tokens/ 408 | 409 | - ### 4.4 Using golang Exec 410 | 411 | - https://stackoverflow.com/questions/39151420/golang-executing-command-with-spaces-in-one-of-the-parts 412 | 413 | - ### 4.5 Systemd services 414 | 415 | - https://medium.com/@benmorel/creating-a-linux-service-with-systemd-611b5c8b91d6 416 | - https://www.digitalocean.com/community/tutorials/understanding-systemd-units-and-unit-files 417 | - [Logs using journalctl](https://www.linode.com/docs/quick-answers/linux/how-to-use-journalctl/) 418 | 419 | - ### 4.6 Using LDFlags in golang 420 | 421 | - https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications 422 | 423 | ## 5. Commands for generating public/private key 424 | 425 | ``` 426 | openssl genrsa -key private-key-sc.pem 427 | openssl req -new -x509 -key private-key-sc.pem -out public.cert 428 | ``` 429 | 430 | [version-badge]: https://img.shields.io/github/v/release/prashantgupta24/firewalld-rest 431 | [releases]: https://github.com/prashantgupta24/firewalld-rest/releases 432 | 433 | ## 6. Possible enhancements 434 | 435 | - Rate limiting the number of requests that can be made to the application 436 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/prashantgupta24/firewalld-rest/route" 9 | ) 10 | 11 | func main() { 12 | fmt.Println("starting application") 13 | 14 | router := route.NewRouter() 15 | log.Fatal(http.ListenAndServe(":8080", router)) 16 | } 17 | -------------------------------------------------------------------------------- /db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | //Instance DB interface for application 4 | type Instance interface { 5 | Type() string 6 | Register(v interface{}) //needed for fileType. Can be left blank for other types of db 7 | Save(v interface{}) error 8 | Load(v interface{}) error 9 | } 10 | -------------------------------------------------------------------------------- /db/fileType.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "io" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | ) 13 | 14 | var lock sync.Mutex 15 | var once sync.Once 16 | var pathFromEnv string //This will be set through the build command, see Makefile 17 | 18 | const ( 19 | fileName = "firewalld-rest.db" 20 | defaultPath = "./" 21 | ) 22 | 23 | //singleton reference 24 | var fileTypeInstance *fileType 25 | 26 | //fileType is the main struct for file database 27 | type fileType struct { 28 | path string 29 | } 30 | 31 | //GetFileTypeInstance returns the singleton instance of the filedb object 32 | func GetFileTypeInstance() Instance { 33 | once.Do(func() { 34 | path := defaultPath + fileName 35 | if pathFromEnv != "" { 36 | pathFromEnv = parsePath(pathFromEnv) 37 | pathFromEnv += fileName 38 | path = pathFromEnv 39 | } 40 | fileTypeInstance = &fileType{path: path} 41 | }) 42 | return fileTypeInstance 43 | } 44 | 45 | //Type of the db 46 | func (fileType *fileType) Type() string { 47 | return "fileType" 48 | } 49 | 50 | // Register interface with gob 51 | func (fileType *fileType) Register(v interface{}) { 52 | gob.Register(v) 53 | } 54 | 55 | // Save saves a representation of v to the file at path. 56 | func (fileType *fileType) Save(v interface{}) error { 57 | lock.Lock() 58 | defer lock.Unlock() 59 | f, err := os.Create(fileType.path) 60 | if err != nil { 61 | return err 62 | } 63 | defer f.Close() 64 | r, err := marshal(v) 65 | if err != nil { 66 | return err 67 | } 68 | _, err = io.Copy(f, r) 69 | return err 70 | } 71 | 72 | // Load loads the file at path into v. 73 | func (fileType *fileType) Load(v interface{}) error { 74 | fullPath, err := filepath.Abs(fileType.path) 75 | if err != nil { 76 | return fmt.Errorf("could not locate absolute path : %v", err) 77 | } 78 | if fileExists(fileType.path) { 79 | lock.Lock() 80 | defer lock.Unlock() 81 | f, err := os.Open(fileType.path) 82 | if err != nil { 83 | return err 84 | } 85 | defer f.Close() 86 | return unmarshal(f, v) 87 | } 88 | log.Printf("Db file not found, will be created here: %v\n", fullPath) 89 | return nil 90 | } 91 | 92 | // marshal is a function that marshals the object into an 93 | // io.Reader. 94 | var marshal = func(v interface{}) (io.Reader, error) { 95 | var buf bytes.Buffer 96 | e := gob.NewEncoder(&buf) 97 | err := e.Encode(v) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return bytes.NewReader(buf.Bytes()), nil 102 | } 103 | 104 | // unmarshal is a function that unmarshals the data from the 105 | // reader into the specified value. 106 | var unmarshal = func(r io.Reader, v interface{}) error { 107 | d := gob.NewDecoder(r) 108 | err := d.Decode(v) 109 | if err != nil { 110 | return err 111 | } 112 | return nil 113 | } 114 | 115 | // fileExists checks if a file exists and is not a directory before we 116 | // try using it to prevent further errors. 117 | func fileExists(filename string) bool { 118 | info, err := os.Stat(filename) 119 | if os.IsNotExist(err) { 120 | return false 121 | } 122 | return !info.IsDir() 123 | } 124 | 125 | func parsePath(path string) string { 126 | lastChar := path[len(path)-1:] 127 | 128 | if lastChar != "/" { 129 | path += "/" 130 | } 131 | return path 132 | } 133 | -------------------------------------------------------------------------------- /firewallcmd/util.go: -------------------------------------------------------------------------------- 1 | package firewallcmd 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | ) 7 | 8 | //EnableRichRuleForIP enables rich rule for IP access + reloads 9 | //example: 10 | //firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="10.10.99.10/32" port protocol="tcp" port="22" accept' 11 | func EnableRichRuleForIP(ipAddr string) (string, error) { 12 | cmd1 := exec.Command(`firewall-cmd`, `--permanent`, "--zone=public", `--add-rich-rule=rule family="ipv4" source address="`+ipAddr+`/32" port protocol="tcp" port="22" accept`) 13 | //uncomment for debugging 14 | // for _, v := range cmd1.Args { 15 | // fmt.Println(v) 16 | // } 17 | output1, err1 := cmd1.CombinedOutput() 18 | if err1 != nil { 19 | return cmd1.String(), err1 20 | } 21 | fmt.Printf("rich rule added successfully for ip %v : %v", ipAddr, string(output1)) 22 | 23 | cmd2, output2, err2 := reload() 24 | if err2 != nil { 25 | return cmd2.String(), err2 26 | } 27 | fmt.Printf("firewalld reloaded successfully : %v", string(output2)) 28 | return "", nil 29 | } 30 | 31 | //DisableRichRuleForIP disables rich rule for IP access + reloads 32 | func DisableRichRuleForIP(ipAddr string) (string, error) { 33 | cmd1 := exec.Command(`firewall-cmd`, `--permanent`, "--zone=public", `--remove-rich-rule=rule family="ipv4" source address="`+ipAddr+`/32" port protocol="tcp" port="22" accept`) 34 | output1, err1 := cmd1.CombinedOutput() 35 | if err1 != nil { 36 | return cmd1.String(), err1 37 | } 38 | fmt.Printf("rich rule deleted successfully for ip %v : %v", ipAddr, string(output1)) 39 | 40 | cmd2, output2, err2 := reload() 41 | if err2 != nil { 42 | return cmd2.String(), err2 43 | } 44 | fmt.Printf("firewalld reloaded successfully : %v", string(output2)) 45 | return "", nil 46 | } 47 | 48 | //reload reloads firewall for setting to take effect 49 | func reload() (*exec.Cmd, []byte, error) { 50 | cmd := exec.Command("firewall-cmd", "--reload") 51 | output, err := cmd.CombinedOutput() 52 | return cmd, output, err 53 | } 54 | -------------------------------------------------------------------------------- /firewalld-rest.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Firewalld rest service 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | Restart=always 8 | RestartSec=1 9 | User=root 10 | ExecStart=/root/rest/firewalld-rest 11 | 12 | [Install] 13 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/prashantgupta24/firewalld-rest 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/gorilla/mux v1.7.4 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 2 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 3 | github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= 4 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 5 | -------------------------------------------------------------------------------- /ip/handler.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sync" 7 | 8 | "github.com/prashantgupta24/firewalld-rest/db" 9 | ) 10 | 11 | var once sync.Once 12 | 13 | //singleton reference 14 | var handlerInstance *handlerStruct 15 | 16 | //handlerStruct for managing IP related tasks 17 | type handlerStruct struct { 18 | db db.Instance 19 | } 20 | 21 | //GetHandler gets singleton handler for IP management 22 | func GetHandler() Handler { 23 | once.Do(func() { 24 | dbInstance := db.GetFileTypeInstance() 25 | handlerInstance = &handlerStruct{ 26 | db: dbInstance, 27 | } 28 | ipStore, err := handlerInstance.loadIPStore() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | if len(ipStore) == 0 { 33 | //in case you want to store some IPs before hand 34 | // ipStore["1.2.3.4"] = &Instance{ 35 | // IP: "1.2.3.4", 36 | // Domain: "first.com", 37 | // } 38 | // ipStore["5.6.7.8"] = &Instance{ 39 | // IP: "5.6.7.8", 40 | // Domain: "second", 41 | // } 42 | handlerInstance.db.Register(ipStore) 43 | if err := handlerInstance.saveIPStore(ipStore); err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | }) 48 | return handlerInstance 49 | } 50 | 51 | //GetIP from the db 52 | func (handler *handlerStruct) GetIP(ipAddr string) (*Instance, error) { 53 | ipStore, err := handler.loadIPStore() 54 | if err != nil { 55 | return nil, err 56 | } 57 | ip, ok := ipStore[ipAddr] 58 | if !ok { 59 | return nil, fmt.Errorf("record not found") 60 | } 61 | return ip, nil 62 | } 63 | 64 | //GetAllIPs from the db 65 | func (handler *handlerStruct) GetAllIPs() ([]*Instance, error) { 66 | ips := []*Instance{} 67 | ipStore, err := handler.loadIPStore() 68 | if err != nil { 69 | return nil, err 70 | } 71 | for _, ip := range ipStore { 72 | ips = append(ips, ip) 73 | } 74 | return ips, nil 75 | } 76 | 77 | //CheckIPExists checks if IP is in db 78 | func (handler *handlerStruct) CheckIPExists(ipAddr string) (bool, error) { 79 | ipStore, err := handler.loadIPStore() 80 | if err != nil { 81 | return false, err 82 | } 83 | _, ok := ipStore[ipAddr] 84 | if ok { 85 | return true, nil 86 | } 87 | return false, nil 88 | } 89 | 90 | //AddIP to the db 91 | func (handler *handlerStruct) AddIP(ip *Instance) error { 92 | ipStore, err := handler.loadIPStore() 93 | if err != nil { 94 | return err 95 | } 96 | _, ok := ipStore[ip.IP] 97 | if ok { 98 | return fmt.Errorf("ip already exists") 99 | } 100 | ipStore[ip.IP] = ip 101 | if err := handler.saveIPStore(ipStore); err != nil { 102 | return fmt.Errorf("error while saving to file : %v", err) 103 | } 104 | return nil 105 | } 106 | 107 | //DeleteIP from the db 108 | func (handler *handlerStruct) DeleteIP(ipAddr string) (*Instance, error) { 109 | ipStore, err := handler.loadIPStore() 110 | if err != nil { 111 | return nil, err 112 | } 113 | ip, ok := ipStore[ipAddr] 114 | if !ok { 115 | return nil, fmt.Errorf("record not found") 116 | } 117 | delete(ipStore, ipAddr) 118 | if err := handler.saveIPStore(ipStore); err != nil { 119 | return nil, fmt.Errorf("error while saving to file : %v", err) 120 | } 121 | return ip, nil 122 | } 123 | 124 | func (handler *handlerStruct) loadIPStore() (map[string]*Instance, error) { 125 | var ipStore = make(map[string]*Instance) 126 | if err := handler.db.Load(&ipStore); err != nil { 127 | return nil, fmt.Errorf("error while loading from file : %v", err) 128 | } 129 | return ipStore, nil 130 | } 131 | 132 | func (handler *handlerStruct) saveIPStore(ipStore map[string]*Instance) error { 133 | if err := handler.db.Save(ipStore); err != nil { 134 | return fmt.Errorf("error while saving to file : %v", err) 135 | } 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /ip/handler_test.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "os/exec" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var handler Handler 12 | var ipAddr string 13 | var ipInstance *Instance 14 | 15 | func setup() { 16 | handler = GetHandler() 17 | ipAddr = "10.20.30.40" 18 | ipInstance = &Instance{ 19 | IP: ipAddr, 20 | Domain: "test", 21 | } 22 | } 23 | 24 | func shutdown() { 25 | os.Remove("firewalld-rest.db") 26 | } 27 | 28 | func TestMain(m *testing.M) { 29 | setup() 30 | code := m.Run() 31 | shutdown() 32 | os.Exit(code) 33 | } 34 | 35 | func TestGetHandler(t *testing.T) { 36 | if handler == nil { 37 | t.Errorf("handler should not be nil") 38 | } 39 | } 40 | 41 | func TestGetAllIPsFileError(t *testing.T) { 42 | changeFilePermission("100") 43 | _, err := handler.GetAllIPs() 44 | if err == nil { 45 | t.Errorf("should have errored") 46 | } 47 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 48 | t.Errorf("should have received permission error, instead got : %v", err) 49 | } 50 | changeFilePermission("644") 51 | } 52 | 53 | func TestGetAllIPs(t *testing.T) { 54 | ips, err := handler.GetAllIPs() 55 | if err != nil { 56 | t.Errorf("should not have errored, err : %v", err) 57 | } 58 | if len(ips) != 0 { 59 | t.Errorf("should have been an empty list, instead got : %v", len(ips)) 60 | } 61 | } 62 | 63 | func TestAddIPFileError(t *testing.T) { 64 | changeFilePermission("100") 65 | err := handler.AddIP(ipInstance) 66 | if err == nil { 67 | t.Errorf("should have errored") 68 | } 69 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 70 | t.Errorf("should have received permission error, instead got : %v", err) 71 | } 72 | 73 | changeFilePermission("500") 74 | err = handler.AddIP(ipInstance) 75 | if err == nil { 76 | t.Errorf("should have errored") 77 | } 78 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 79 | t.Errorf("should have received permission error, instead got : %v", err) 80 | } 81 | changeFilePermission("644") 82 | } 83 | func TestAddIP(t *testing.T) { 84 | err := handler.AddIP(ipInstance) 85 | if err != nil { 86 | t.Errorf("should not have errored, err : %v", err) 87 | } 88 | } 89 | 90 | func TestAddIPDup(t *testing.T) { 91 | err := handler.AddIP(ipInstance) 92 | if err == nil { 93 | t.Errorf("should have errored for duplicate IP") 94 | } 95 | } 96 | 97 | func TestGetAllIPsAfterAdd(t *testing.T) { 98 | ips, err := handler.GetAllIPs() 99 | if err != nil { 100 | t.Errorf("should not have errored, err : %v", err) 101 | } 102 | if len(ips) == 0 { 103 | t.Errorf("should have included %v , instead got : %v", ipAddr, ips) 104 | } 105 | } 106 | 107 | func TestCheckIPExistsFileError(t *testing.T) { 108 | changeFilePermission("100") 109 | _, err := handler.CheckIPExists(ipAddr) 110 | if err == nil { 111 | t.Errorf("should have errored") 112 | } 113 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 114 | t.Errorf("should have received permission error, instead got : %v", err) 115 | } 116 | changeFilePermission("644") 117 | } 118 | 119 | func TestCheckIPExists(t *testing.T) { 120 | ipExists, err := handler.CheckIPExists(ipAddr) 121 | if err != nil { 122 | t.Errorf("should not have errored, err : %v", err) 123 | } 124 | if !ipExists { 125 | t.Errorf("ip %v should exist", ipAddr) 126 | } 127 | } 128 | 129 | func TestGetIPFileError(t *testing.T) { 130 | changeFilePermission("100") 131 | _, err := handler.GetIP(ipAddr) 132 | if err == nil { 133 | t.Errorf("should have errored") 134 | } 135 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 136 | t.Errorf("should have received permission error, instead got : %v", err) 137 | } 138 | changeFilePermission("644") 139 | } 140 | 141 | func TestGetInvalidIP(t *testing.T) { 142 | _, err := handler.GetIP("invalid_ip") 143 | if err == nil { 144 | t.Errorf("should have errored") 145 | } 146 | if err != nil && err.Error() != "record not found" { 147 | t.Errorf("record should not have been found, instead got : %v", err) 148 | } 149 | } 150 | 151 | func TestGetIP(t *testing.T) { 152 | ipRecd, err := handler.GetIP(ipAddr) 153 | if err != nil { 154 | t.Errorf("should not have errored, err : %v", err) 155 | } 156 | if ipRecd.IP != ipInstance.IP { 157 | t.Errorf("ip should be same, got %v want %v", ipRecd.IP, ipInstance.IP) 158 | } 159 | } 160 | 161 | func TestDeleteIPFileError(t *testing.T) { 162 | changeFilePermission("100") 163 | _, err := handler.DeleteIP(ipAddr) 164 | if err == nil { 165 | t.Errorf("should have errored") 166 | } 167 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 168 | t.Errorf("should have received permission error, instead got : %v", err) 169 | } 170 | 171 | changeFilePermission("500") 172 | _, err = handler.DeleteIP(ipAddr) 173 | if err == nil { 174 | t.Errorf("should have errored") 175 | } 176 | if err != nil && strings.Index(err.Error(), "permission denied") == -1 { 177 | t.Errorf("should have received permission error, instead got : %v", err) 178 | } 179 | changeFilePermission("644") 180 | } 181 | 182 | func TestDeleteInvalidIP(t *testing.T) { 183 | _, err := handler.DeleteIP("invalid_ip") 184 | if err == nil { 185 | t.Errorf("should have errored") 186 | } 187 | if err != nil && err.Error() != "record not found" { 188 | t.Errorf("record should not have been found, instead got : %v", err) 189 | } 190 | } 191 | 192 | func TestDeleteIP(t *testing.T) { 193 | ipDeleted, err := handler.DeleteIP(ipAddr) 194 | if err != nil { 195 | t.Errorf("should not have errored, err : %v", err) 196 | } 197 | if ipAddr != ipDeleted.IP { 198 | t.Errorf("ip %v should be the same", ipAddr) 199 | } 200 | ipExists, err := handler.CheckIPExists(ipAddr) 201 | if err != nil { 202 | t.Errorf("should not have errored, err : %v", err) 203 | } 204 | if ipExists { 205 | t.Errorf("ip %v should be deleted", ipAddr) 206 | } 207 | } 208 | 209 | func changeFilePermission(permission string) { 210 | cmd := exec.Command("chmod", permission, "firewalld-rest.db") 211 | err := cmd.Run() 212 | if err != nil { 213 | log.Fatalf("could not change permission of file, err : %v", err) 214 | } 215 | // cmd1 := exec.Command("ls", "-la") 216 | // o1, _ := cmd1.CombinedOutput() 217 | // fmt.Println("cmd1 : ", string(o1)) 218 | } 219 | -------------------------------------------------------------------------------- /ip/ip.go: -------------------------------------------------------------------------------- 1 | package ip 2 | 3 | // Instance holds json for ip and domain 4 | type Instance struct { 5 | IP string `json:"ip"` 6 | Domain string `json:"domain"` 7 | } 8 | 9 | //Handler interface for handling IP related tasks 10 | type Handler interface { 11 | GetIP(string) (*Instance, error) 12 | GetAllIPs() ([]*Instance, error) 13 | CheckIPExists(ipAddr string) (bool, error) 14 | AddIP(ip *Instance) error 15 | DeleteIP(ipAddr string) (*Instance, error) 16 | } 17 | -------------------------------------------------------------------------------- /k8s/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: firewalld-ingress 5 | spec: 6 | rules: 7 | - host: 8 | http: 9 | #different paths for different hosts under the same k8s config 10 | paths: 11 | - path: /m1 12 | backend: 13 | serviceName: external-rest 14 | servicePort: 80 15 | - path: /m2 16 | backend: 17 | serviceName: external-rest-2 18 | servicePort: 80 19 | - path: /m3 20 | backend: 21 | serviceName: external-rest-3 22 | servicePort: 80 -------------------------------------------------------------------------------- /k8s/svc-nodeport.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: external-rest 5 | spec: 6 | externalIPs: 7 | - 169.xx.xx.xxx # public IP of the server 8 | type: NodePort 9 | ports: 10 | - name: firewalld 11 | protocol: TCP 12 | port: 8080 13 | targetPort: 8080 14 | --- 15 | apiVersion: v1 16 | kind: Endpoints 17 | metadata: 18 | name: external-rest 19 | subsets: 20 | - addresses: 21 | - ip: 10.xx.xx.xx #private IP of server 22 | ports: 23 | - port: 8080 24 | name: firewalld -------------------------------------------------------------------------------- /k8s/svc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: external-rest #external-rest-2 for 2nd machine and so on 5 | spec: 6 | ports: 7 | - name: firewalld 8 | protocol: TCP 9 | port: 80 10 | targetPort: 8080 11 | --- 12 | apiVersion: v1 13 | kind: Endpoints 14 | metadata: 15 | name: external-rest #external-rest-2 for 2nd machine and so on 16 | subsets: 17 | - addresses: 18 | - ip: 10.xx.xx.xx #private IP of node 19 | ports: 20 | - port: 8080 21 | name: firewalld -------------------------------------------------------------------------------- /route/handler.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/prashantgupta24/firewalld-rest/firewallcmd" 14 | "github.com/prashantgupta24/firewalld-rest/ip" 15 | ) 16 | 17 | //Index page 18 | // GET / 19 | func Index(w http.ResponseWriter, r *http.Request) { 20 | fmt.Fprint(w, "Welcome!\n") 21 | } 22 | 23 | // IPAdd for the Create action 24 | // POST /ip 25 | func IPAdd(w http.ResponseWriter, r *http.Request) { 26 | ipInstance := &ip.Instance{} 27 | if err := populateModelFromHandler(w, r, ipInstance); err != nil { 28 | writeErrorResponse(w, http.StatusUnprocessableEntity, "Unprocessible Entity") 29 | return 30 | } 31 | ipExists, err := ip.GetHandler().CheckIPExists(ipInstance.IP) 32 | if err != nil { 33 | writeErrorResponse(w, http.StatusInternalServerError, err.Error()) 34 | return 35 | } 36 | if ipExists { 37 | writeErrorResponse(w, http.StatusBadRequest, "ip already exists") 38 | return 39 | } 40 | 41 | env := os.Getenv("env") 42 | if env != "local" { 43 | command, err := firewallcmd.EnableRichRuleForIP(ipInstance.IP) 44 | if err != nil { 45 | writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("cannot exec command %v, err : %v", command, err.Error())) 46 | return 47 | } 48 | } 49 | 50 | err = ip.GetHandler().AddIP(ipInstance) 51 | if err != nil { 52 | writeErrorResponse(w, http.StatusInternalServerError, err.Error()) 53 | return 54 | } 55 | 56 | writeOKResponse(w, ipInstance) 57 | } 58 | 59 | // IPShow for the ip Show action 60 | // GET /ip/{ip} 61 | func IPShow(w http.ResponseWriter, r *http.Request) { 62 | vars := mux.Vars(r) 63 | ipAddr := vars["ip"] 64 | ip, err := ip.GetHandler().GetIP(ipAddr) 65 | if err != nil { 66 | // No IP found 67 | writeErrorResponse(w, http.StatusNotFound, err.Error()) 68 | return 69 | } 70 | writeOKResponse(w, ip) 71 | } 72 | 73 | // ShowAllIPs shows all IPs 74 | // GET /ip 75 | func ShowAllIPs(w http.ResponseWriter, r *http.Request) { 76 | ips, err := ip.GetHandler().GetAllIPs() 77 | if err != nil { 78 | writeErrorResponse(w, http.StatusInternalServerError, err.Error()) 79 | return 80 | } 81 | writeOKResponse(w, ips) 82 | } 83 | 84 | // IPDelete for the ip Delete action 85 | // DELETE /ip/{ip} 86 | func IPDelete(w http.ResponseWriter, r *http.Request) { 87 | vars := mux.Vars(r) 88 | ipAddr := vars["ip"] 89 | log.Printf("IP to delete %s\n", ipAddr) 90 | 91 | ipExists, err := ip.GetHandler().CheckIPExists(ipAddr) 92 | if err != nil { 93 | writeErrorResponse(w, http.StatusInternalServerError, err.Error()) 94 | return 95 | } 96 | if !ipExists { 97 | writeErrorResponse(w, http.StatusNotFound, "ip does not exist") 98 | return 99 | } 100 | 101 | env := os.Getenv("env") 102 | if env != "local" { 103 | command, err := firewallcmd.DisableRichRuleForIP(ipAddr) 104 | if err != nil { 105 | writeErrorResponse(w, http.StatusInternalServerError, fmt.Sprintf("cannot exec command %v, err : %v", command, err.Error())) 106 | return 107 | } 108 | } 109 | 110 | ip, err := ip.GetHandler().DeleteIP(ipAddr) 111 | if err != nil { 112 | // IP could not be deleted 113 | writeErrorResponse(w, http.StatusNotFound, err.Error()) 114 | return 115 | } 116 | writeOKResponse(w, ip) 117 | } 118 | 119 | // Writes the response as a standard JSON response with StatusOK 120 | func writeOKResponse(w http.ResponseWriter, m interface{}) { 121 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 122 | w.WriteHeader(http.StatusOK) 123 | if err := json.NewEncoder(w).Encode(&JSONResponse{Data: m}); err != nil { 124 | writeErrorResponse(w, http.StatusInternalServerError, "Internal Server Error") 125 | } 126 | } 127 | 128 | // Writes the error response as a Standard API JSON response with a response code 129 | func writeErrorResponse(w http.ResponseWriter, errorCode int, errorMsg string) { 130 | w.Header().Set("Content-Type", "application/json; charset=UTF-8") 131 | w.WriteHeader(errorCode) 132 | json. 133 | NewEncoder(w). 134 | Encode(&JSONErrorResponse{Error: &APIError{Status: errorCode, Title: errorMsg}}) 135 | } 136 | 137 | //Populates a ip from the params in the Handler 138 | func populateModelFromHandler(w http.ResponseWriter, r *http.Request, ip interface{}) error { 139 | body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1048576)) 140 | if err != nil { 141 | return err 142 | } 143 | if err := r.Body.Close(); err != nil { 144 | return err 145 | } 146 | if err := json.Unmarshal(body, ip); err != nil { 147 | return err 148 | } 149 | return nil 150 | } 151 | -------------------------------------------------------------------------------- /route/handler_test.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/prashantgupta24/firewalld-rest/ip" 12 | ) 13 | 14 | var data1 string 15 | var data2 string 16 | var dataInvalid string 17 | var ipAddr1 string 18 | var ipAddr2 string 19 | var ipAddr3 string 20 | 21 | func setup() { 22 | ipAddr1 = "10.20.30.40" 23 | ipAddr2 = "20.40.60.80" 24 | ipAddr3 = "10.50.100.150" 25 | data1 = `{"ip":"` + ipAddr1 + `","domain":"test.com"}` 26 | data2 = `{"ip":"` + ipAddr2 + `","domain":"test.com"}` 27 | dataInvalid = `{"ip":"` + ipAddr2 //missing domain 28 | } 29 | 30 | func shutdown() { 31 | os.Remove("firewalld-rest.db") 32 | } 33 | 34 | func TestMain(m *testing.M) { 35 | setup() 36 | code := m.Run() 37 | shutdown() 38 | os.Exit(code) 39 | } 40 | 41 | func TestIndex(t *testing.T) { 42 | req, err := http.NewRequest("GET", "/", nil) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | rr := newRequestRecorder(req, Index) 47 | 48 | if status := rr.Code; status != http.StatusOK { 49 | t.Errorf("handler returned wrong status code: got %v want %v", 50 | status, http.StatusOK) 51 | } 52 | 53 | // Check the response body is what we expect. 54 | expected := `Welcome!` 55 | if strings.TrimSpace(rr.Body.String()) != expected { 56 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 57 | rr.Body.String(), expected) 58 | } 59 | } 60 | 61 | func TestShowAllIPs(t *testing.T) { 62 | req, err := http.NewRequest("GET", "/ip", nil) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | rr := newRequestRecorder(req, ShowAllIPs) 67 | 68 | if status := rr.Code; status != http.StatusOK { 69 | t.Errorf("handler returned wrong status code: got %v want %v", 70 | status, http.StatusOK) 71 | } 72 | 73 | // Check the response body is what we expect. 74 | expected := `{"meta":null,"data":[]}` 75 | if strings.TrimSpace(rr.Body.String()) != expected { 76 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 77 | rr.Body.String(), expected) 78 | } 79 | } 80 | 81 | func TestAddBadIP(t *testing.T) { 82 | req, err := http.NewRequest("POST", "/ip", strings.NewReader(dataInvalid)) 83 | if err != nil { 84 | t.Fatal(err) 85 | } 86 | rr := newRequestRecorder(req, IPAdd) 87 | 88 | if status := rr.Code; status != http.StatusUnprocessableEntity { 89 | t.Errorf("handler returned wrong status code: got %v want %v", 90 | status, http.StatusOK) 91 | } 92 | 93 | // Check the response body is what we expect. 94 | expected := `{"error":{"status":422,"title":"Unprocessible Entity"}}` 95 | if strings.TrimSpace(rr.Body.String()) != expected { 96 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 97 | rr.Body.String(), expected) 98 | } 99 | } 100 | 101 | func TestAddIPNonLocal(t *testing.T) { 102 | oldEnv := os.Getenv("env") 103 | os.Setenv("env", "staging") 104 | req, err := http.NewRequest("POST", "/ip", strings.NewReader(data1)) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | rr := newRequestRecorder(req, IPAdd) 109 | 110 | if status := rr.Code; status != http.StatusInternalServerError { 111 | t.Errorf("handler returned wrong status code: got %v want %v", 112 | status, http.StatusOK) 113 | } 114 | 115 | // Check the response body is what we expect. 116 | expected := `cannot exec command ` 117 | if strings.Index(rr.Body.String(), expected) == -1 { 118 | t.Errorf("handler returned unexpected body: \ngot %v should have included : %v", 119 | rr.Body.String(), expected) 120 | } 121 | os.Setenv("env", oldEnv) 122 | } 123 | 124 | func TestAddIP(t *testing.T) { 125 | req, err := http.NewRequest("POST", "/ip", strings.NewReader(data1)) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | rr := newRequestRecorder(req, IPAdd) 130 | 131 | if status := rr.Code; status != http.StatusOK { 132 | t.Errorf("handler returned wrong status code: got %v want %v", 133 | status, http.StatusOK) 134 | } 135 | 136 | // Check the response body is what we expect. 137 | expected := `{"meta":null,"data":` + data1 + `}` 138 | if strings.TrimSpace(rr.Body.String()) != expected { 139 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 140 | rr.Body.String(), expected) 141 | } 142 | } 143 | 144 | func TestAddIPDup(t *testing.T) { 145 | req, err := http.NewRequest("POST", "/ip", strings.NewReader(data1)) 146 | if err != nil { 147 | t.Fatal(err) 148 | } 149 | rr := newRequestRecorder(req, IPAdd) 150 | 151 | if status := rr.Code; status != http.StatusBadRequest { 152 | t.Errorf("handler returned wrong status code: got %v want %v", 153 | status, http.StatusOK) 154 | } 155 | 156 | // Check the response body is what we expect. 157 | expected := `{"error":{"status":400,"title":"ip already exists"}}` 158 | if strings.TrimSpace(rr.Body.String()) != expected { 159 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 160 | rr.Body.String(), expected) 161 | } 162 | } 163 | 164 | func TestShowIP(t *testing.T) { 165 | req, err := http.NewRequest("GET", "/ip/"+ipAddr1, nil) 166 | if err != nil { 167 | t.Fatal(err) 168 | } 169 | rr := httptest.NewRecorder() 170 | router := mux.NewRouter() 171 | router.HandleFunc("/ip/{ip}", IPShow) 172 | router.ServeHTTP(rr, req) 173 | 174 | if status := rr.Code; status != http.StatusOK { 175 | t.Errorf("handler returned wrong status code: got %v want %v", 176 | status, http.StatusOK) 177 | } 178 | 179 | // Check the response body is what we expect. 180 | expected := `{"meta":null,"data":` + data1 + `}` 181 | if strings.TrimSpace(rr.Body.String()) != expected { 182 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 183 | rr.Body.String(), expected) 184 | } 185 | } 186 | 187 | func TestShowIPNotFound(t *testing.T) { 188 | req, err := http.NewRequest("GET", "/ip/"+ipAddr3, nil) 189 | if err != nil { 190 | t.Fatal(err) 191 | } 192 | rr := httptest.NewRecorder() 193 | router := mux.NewRouter() 194 | router.HandleFunc("/ip/{ip}", IPShow) 195 | router.ServeHTTP(rr, req) 196 | 197 | if status := rr.Code; status != http.StatusNotFound { 198 | t.Errorf("handler returned wrong status code: got %v want %v", 199 | status, http.StatusOK) 200 | } 201 | 202 | // Check the response body is what we expect. 203 | expected := `{"error":{"status":404,"title":"record not found"}}` 204 | if strings.TrimSpace(rr.Body.String()) != expected { 205 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 206 | rr.Body.String(), expected) 207 | } 208 | } 209 | 210 | func TestAddIP2(t *testing.T) { 211 | req, err := http.NewRequest("POST", "/ip", strings.NewReader(data2)) 212 | if err != nil { 213 | t.Fatal(err) 214 | } 215 | rr := newRequestRecorder(req, IPAdd) 216 | 217 | if status := rr.Code; status != http.StatusOK { 218 | t.Errorf("handler returned wrong status code: got %v want %v", 219 | status, http.StatusOK) 220 | } 221 | 222 | // Check the response body is what we expect. 223 | expected := `{"meta":null,"data":` + data2 + `}` 224 | if strings.TrimSpace(rr.Body.String()) != expected { 225 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 226 | rr.Body.String(), expected) 227 | } 228 | } 229 | 230 | func TestShowAllIPsAfterAdding(t *testing.T) { 231 | req, err := http.NewRequest("GET", "/ip", nil) 232 | if err != nil { 233 | t.Fatal(err) 234 | } 235 | rr := newRequestRecorder(req, ShowAllIPs) 236 | 237 | if status := rr.Code; status != http.StatusOK { 238 | t.Errorf("handler returned wrong status code: got %v want %v", 239 | status, http.StatusOK) 240 | } 241 | //make sure both IPs exist 242 | if strings.Index(rr.Body.String(), ipAddr1) == -1 || strings.Index(rr.Body.String(), ipAddr2) == -1 { 243 | t.Errorf("handler returned without required body: \ngot %v want %v", 244 | rr.Body.String(), ipAddr1) 245 | } 246 | } 247 | 248 | func TestDeleteIPNonLocal(t *testing.T) { 249 | oldEnv := os.Getenv("env") 250 | os.Setenv("env", "staging") 251 | req, err := http.NewRequest("DELETE", "/ip/"+ipAddr1, nil) 252 | if err != nil { 253 | t.Fatal(err) 254 | } 255 | rr := httptest.NewRecorder() 256 | router := mux.NewRouter() 257 | router.HandleFunc("/ip/{ip}", IPDelete) 258 | router.ServeHTTP(rr, req) 259 | 260 | if status := rr.Code; status != http.StatusInternalServerError { 261 | t.Errorf("handler returned wrong status code: got %v want %v", 262 | status, http.StatusOK) 263 | } 264 | 265 | // Check the response body is what we expect. 266 | expected := `cannot exec command ` 267 | if strings.Index(rr.Body.String(), expected) == -1 { 268 | t.Errorf("handler returned unexpected body: \ngot %v should have included : %v", 269 | rr.Body.String(), expected) 270 | } 271 | os.Setenv("env", oldEnv) 272 | } 273 | 274 | func TestDeleteIP(t *testing.T) { 275 | req, err := http.NewRequest("DELETE", "/ip/"+ipAddr1, nil) 276 | if err != nil { 277 | t.Fatal(err) 278 | } 279 | rr := httptest.NewRecorder() 280 | router := mux.NewRouter() 281 | router.HandleFunc("/ip/{ip}", IPDelete) 282 | router.ServeHTTP(rr, req) 283 | 284 | if status := rr.Code; status != http.StatusOK { 285 | t.Errorf("handler returned wrong status code: got %v want %v", 286 | status, http.StatusOK) 287 | } 288 | 289 | // Check the response body is what we expect. 290 | expected := `{"meta":null,"data":` + data1 + `}` 291 | if strings.TrimSpace(rr.Body.String()) != expected { 292 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 293 | rr.Body.String(), expected) 294 | } 295 | } 296 | 297 | func TestDeleteIPNotFound(t *testing.T) { 298 | req, err := http.NewRequest("DELETE", "/ip/"+ipAddr1, nil) 299 | if err != nil { 300 | t.Fatal(err) 301 | } 302 | rr := httptest.NewRecorder() 303 | router := mux.NewRouter() 304 | router.HandleFunc("/ip/{ip}", IPDelete) 305 | router.ServeHTTP(rr, req) 306 | 307 | if status := rr.Code; status != http.StatusNotFound { 308 | t.Errorf("handler returned wrong status code: got %v want %v", 309 | status, http.StatusNotFound) 310 | } 311 | 312 | // Check the response body is what we expect. 313 | expected := `{"error":{"status":404,"title":"ip does not exist"}}` 314 | if strings.TrimSpace(rr.Body.String()) != expected { 315 | t.Errorf("handler returned unexpected body: \ngot %v want %v", 316 | rr.Body.String(), expected) 317 | } 318 | } 319 | 320 | func TestShowAllIPsAfter(t *testing.T) { 321 | req, err := http.NewRequest("GET", "/ip", nil) 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | rr := newRequestRecorder(req, ShowAllIPs) 326 | 327 | if status := rr.Code; status != http.StatusOK { 328 | t.Errorf("handler returned wrong status code: got %v want %v", 329 | status, http.StatusOK) 330 | } 331 | //make sure ip doesn't exist 332 | if strings.Index(rr.Body.String(), ipAddr1) != -1 { 333 | t.Errorf("handler contained deleted entry: \ngot %vdeleted %v", 334 | rr.Body.String(), ipAddr1) 335 | } 336 | } 337 | 338 | func TestPopulateModelFromHandler(t *testing.T) { 339 | 340 | req, err := http.NewRequest("POST", "/ip", strings.NewReader("garbage")) 341 | if err != nil { 342 | t.Fatal(err) 343 | } 344 | ipInstance := &ip.Instance{} 345 | if err := populateModelFromHandler(nil, req, ipInstance); err == nil { 346 | t.Errorf("should have errored") 347 | } 348 | 349 | req, err = http.NewRequest("POST", "/ip", strings.NewReader(dataInvalid)) 350 | if err != nil { 351 | t.Fatal(err) 352 | } 353 | ipInstance = &ip.Instance{} 354 | if err := populateModelFromHandler(nil, req, ipInstance); err == nil { 355 | t.Errorf("should have errored") 356 | } 357 | 358 | req, err = http.NewRequest("POST", "/ip", strings.NewReader(data1)) 359 | if err != nil { 360 | t.Fatal(err) 361 | } 362 | ipInstance = &ip.Instance{} 363 | if err := populateModelFromHandler(nil, req, ipInstance); err != nil { 364 | t.Errorf("should not have errored : %v", err) 365 | } 366 | } 367 | 368 | // Mocks a handler and returns a httptest.ResponseRecorder 369 | func newRequestRecorder(req *http.Request, fnHandler func(w http.ResponseWriter, r *http.Request)) *httptest.ResponseRecorder { 370 | rr := httptest.NewRecorder() 371 | handler := http.HandlerFunc(fnHandler) 372 | handler.ServeHTTP(rr, req) 373 | return rr 374 | } 375 | -------------------------------------------------------------------------------- /route/middleware.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/dgrijalva/jwt-go" 11 | ) 12 | 13 | type exception struct { 14 | Message string `json:"message"` 15 | } 16 | 17 | //validateMiddleware validates the JWT 18 | func validateMiddleware(next http.Handler) http.Handler { 19 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 20 | authorizationHeader := req.Header.Get("authorization") 21 | if authorizationHeader != "" { 22 | //fmt.Println("authorizationHeader : ", authorizationHeader) 23 | bearerToken := strings.Split(authorizationHeader, " ") 24 | if len(bearerToken) == 2 { 25 | key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(publicCertContent)) 26 | if err != nil { 27 | json.NewEncoder(w).Encode(exception{Message: err.Error()}) 28 | return 29 | } 30 | token, error := jwt.Parse(bearerToken[1], func(token *jwt.Token) (interface{}, error) { 31 | if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { 32 | return nil, fmt.Errorf("There was an error") 33 | } 34 | return key, nil 35 | }) 36 | if error != nil { 37 | json.NewEncoder(w).Encode(exception{Message: error.Error()}) 38 | return 39 | } 40 | if token.Valid { 41 | next.ServeHTTP(w, req) 42 | } else { 43 | json.NewEncoder(w).Encode(exception{Message: "Invalid authorization token"}) 44 | } 45 | } 46 | } else { 47 | json.NewEncoder(w).Encode(exception{Message: "An authorization header is required"}) 48 | } 49 | }) 50 | } 51 | 52 | func loggingMiddleware(next http.Handler) http.Handler { 53 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | // Do stuff here 55 | log.Println(r.RequestURI) 56 | // Call the next handler, which can be another middleware in the chain, or the final handler. 57 | next.ServeHTTP(w, r) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /route/publicCert.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | //publicCertContent required for authentication 4 | const publicCertContent = ` 5 | -----BEGIN CERTIFICATE----- 6 | MIIDgjCCAmoCCQDybHZ/ZguMATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMC 7 | VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExETAPBgNVBAcMCFNhbiBKb3NlMQwwCgYD 8 | VQQKDANJQk0xFDASBgNVBAsMC0RhdGEgYW5kIEFJMScwJQYJKoZIhvcNAQkBFhhw 9 | cmFzaGFudGd1cHRhQHVzLmlibS5jb20wHhcNMjAwNjExMjA1MDU1WhcNMjAwNzEx 10 | MjA1MDU1WjCBgjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExETAP 11 | BgNVBAcMCFNhbiBKb3NlMQwwCgYDVQQKDANJQk0xFDASBgNVBAsMC0RhdGEgYW5k 12 | IEFJMScwJQYJKoZIhvcNAQkBFhhwcmFzaGFudGd1cHRhQHVzLmlibS5jb20wggEi 13 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDnQKV05br1v8+lyt+js9Lhhpej 14 | QEjLQvWKkeWIC+VwXekXIrraM8Z1MImI9hAGDgyK7b18uk3paZ7KxTxtaWxVodUU 15 | 75qDEsPRy8byQjAdDKnccMvDI1LMQ1HHw6Hfv8GEOcBebKwdsbtpBYtqKsaZn95N 16 | Iw8k2alDAUVe8y840lTzOhowsaN9I4bQV2nNosfWWYGOCacV8vIquJ0s71BdFk7A 17 | gXAO4vONEwrpJICiBRQRWTcKFNeCBoNn5zNFs0LsncrhKajENeXT+NfLJUVLEwTc 18 | 0X0yqz587rlQrKY7/xDQukVhGq1HCRMeWbeK87/jcTf8QyQu4nbqrgAJPzQBAgMB 19 | AAEwDQYJKoZIhvcNAQELBQADggEBALhCTiALoDxLpa3KK3utgBG7VUcz3pbhnCdp 20 | Qj1y/U3lQ5Am+Lztc9V0CpbVRlX/B2Kmj3Eln+JOPnucHFOv4VbY6cE7oOIzgcAY 21 | Tp6/rn1KFXlb72cRahpFgnP7m2WWKXDibHNx7H4nGpmDNjMgmCgLP4KisauMrork 22 | CTEqVJk5abC/JCO7HBUBZkCsY0GgxmZDivWcpzxHjl7/U5dSlUEIlbcrmBaVN3fy 23 | aATm38m1e1h/ZE2gQRmfumA9DMEs3HJ4fLbEnpxJ+dJJM6uu7/jJaz1aPvNaNYby 24 | sAFLryXHIMWAKDO6o02IhcFpbw0T4GxpTYqsEcj7BrdbFdsUeN0= 25 | -----END CERTIFICATE----- 26 | ` 27 | -------------------------------------------------------------------------------- /route/response.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | // JSONResponse defines meta and interface struct 4 | type JSONResponse struct { 5 | // Reserved field to add some meta information to the API response 6 | Meta interface{} `json:"meta"` 7 | Data interface{} `json:"data"` 8 | } 9 | 10 | // JSONErrorResponse defines error struct 11 | type JSONErrorResponse struct { 12 | Error *APIError `json:"error"` 13 | } 14 | 15 | // APIError defines api error struct 16 | type APIError struct { 17 | Status int `json:"status"` 18 | Title string `json:"title"` 19 | } 20 | -------------------------------------------------------------------------------- /route/route.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gorilla/mux" 7 | ) 8 | 9 | type route struct { 10 | Name string 11 | Method string 12 | Pattern string 13 | HandlerFunc http.HandlerFunc 14 | } 15 | 16 | type routes []route 17 | 18 | //NewRouter creates a new mux router for application 19 | func NewRouter() *mux.Router { 20 | 21 | router := mux.NewRouter() 22 | subrouter := router.PathPrefix("/m{[0-9]+}").Subrouter().StrictSlash(true) 23 | 24 | subrouter.Use(loggingMiddleware, validateMiddleware) 25 | for _, route := range routesForApp { 26 | subrouter. 27 | Methods(route.Method). 28 | Path(route.Pattern). 29 | Name(route.Name). 30 | Handler(route.HandlerFunc) 31 | } 32 | 33 | return subrouter 34 | } 35 | 36 | var routesForApp = routes{ 37 | route{ 38 | "Index Page", 39 | "GET", 40 | "/", 41 | Index, 42 | }, 43 | route{ 44 | "Add New IP", 45 | "POST", 46 | "/ip", 47 | IPAdd, 48 | }, 49 | route{ 50 | "Show all IPs present", 51 | "GET", 52 | "/ip", 53 | ShowAllIPs, 54 | }, 55 | route{ 56 | "Show if particular IP is present", 57 | "GET", 58 | "/ip/{ip}", 59 | IPShow, 60 | }, 61 | route{ 62 | "Delete IP", 63 | "DELETE", 64 | "/ip/{ip}", 65 | IPDelete, 66 | }, 67 | } 68 | --------------------------------------------------------------------------------