Skip to content

Commit

Permalink
feat: add simple REST API (#106)
Browse files Browse the repository at this point in the history
* Added API endpoints for creating and removing proxy

* Added documentation and added option to enable or disable api

* Use Chi instead of gin as webserver

* Move env to main instead of handling it in http_api.go

* Moved getEnv function to main

* Removed manually registering proxies

* Added new method to create file with specific name and moved code to other methods

* Removed global variable

* Use ProxyConfig to handle JSON unmarshal

* Update documentation

* Update documentation

* Refactor api package

* Change StartWebserver to ListenAndServe to be consistent

* Changed http status codes

* Use Chi URL parameters

* Added return statements

* Changed http status code and removed fatal log

* Removed debug statement

* Consistent naming

* Removed global variable

* Rename http_api.go to api.go

* Cleaned up JSON unmarshal

* Removed unnecessary type conversion

* Simplify if statement
  • Loading branch information
base2code authored Nov 19, 2021
1 parent 66ac3f4 commit 9e6e7fc
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 17 deletions.
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ It works similar to Nginx for those of you who are familiar.
- [x] HAProxy Protocol Support
- [x] TCPShield/RealIP Protocol Support
- [X] Prometheus Support
- [ ] REST API
- [X] REST API

## Deploy

Expand All @@ -52,6 +52,9 @@ $ docker build --no-cache -t haveachin/infrared:latest https://github.com/haveac
`INFRARED_CONFIG_PATH` is the path to all your server configs [default: `"./configs/"`]
`INFRARED_RECEIVE_PROXY_PROTOCOL` if Infrared should be able to receive proxy protocol [default: `"false"`]

`INFRARED_API_ENABLED` if the api should be enabled [default: `"false"`]\
`INFRARED_API_BIND` change the http bind option [default: `"127.0.0.1:8080"`]

## Command-Line Flags

`-config-path` specifies the path to all your server configs [default: `"./configs/"`]
Expand Down Expand Up @@ -210,6 +213,48 @@ More info on [Portainer](https://www.portainer.io/).

</details>

## Rest API
**The API should not be accessible from the internet!**

### Enabling API
To enable the API the environment variable `INFRARED_API_ENABLED` must be set to `"true"`.
To change the http bind, set the env variable `INFRARED_API_BIND` to something like `"0.0.0.0:3000"` the default value is `"127.0.0.1:8080"`

### API Methods
#### Create new config

POST `/proxies/`\
Body must contain:
```json
{
"domainName": "mc.example.com",
"proxyTo": ":8080"
}
```
But all values (like in a normal config file) can be set.

The API then will create a file with the name of the domain (if the file exists it will be overwritten) and write the body to it. The proxy can now be visited.

-----
POST `/proxies/{fileName}`\
Body must contain:
```json
{
"domainName": "mc.example.com",
"proxyTo": ":8080"
}
```
But all values (like in a normal config file) can be set.

The server will create a file with the given filename (if the file exists it will be overwritten) and store the config in it.


### Remove config
DELETE `/proxies/{fileName}`\
Replace `:file` with the name of the proxy configuration file.

If the file was found it will be unloaded and deleted. Open connections do not close, but no new player can connect anymore.

## Prometheus exporter
The built-in prometheus exporter can be used to view metrics about infrareds operation.
When the command line flag `-enable-prometheus` is enabled it will bind to `:9100` by default, if you would like to use another port or use an application like [node_exporter](https://github.com/prometheus/node_exporter) that also uses port 9100 on the same machine you can change the port with the `-prometheus-bind` command line flag, example: `-prometheus-bind=":9070"`.
Expand Down
115 changes: 115 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package api

import (
"encoding/json"
"fmt"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/haveachin/infrared"
"io/ioutil"
"log"
"net/http"
"os"
)

// ListenAndServe StartWebserver Start Webserver if environment variable "api-enable" is set to true
func ListenAndServe(configPath string, apiBind string) {
fmt.Println("Starting WebAPI on " + apiBind)
router := chi.NewRouter()
router.Use(middleware.Logger)

router.Post("/proxies", addProxy(configPath))
router.Post("/proxies/{fileName}", addProxyWithName(configPath))
router.Delete("/proxies/{fileName}", removeProxy(configPath))

err := http.ListenAndServe(apiBind, router)
if err != nil {
log.Fatal(err)
return
}
}

func addProxy(configPath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rawData, err := ioutil.ReadAll(r.Body)
if err != nil || string(rawData) == "" {
w.WriteHeader(http.StatusBadRequest)
return
}

jsonIsValid := checkJSONAndRegister(rawData, "", configPath)
if jsonIsValid {
w.WriteHeader(http.StatusOK)
return
} else {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("{'error': 'domainName and proxyTo could not be found'}"))
return
}
}
}

func addProxyWithName(configPath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fileName := chi.URLParam(r, "fileName")

rawData, err := ioutil.ReadAll(r.Body)
if err != nil || string(rawData) == "" {
w.WriteHeader(http.StatusBadRequest)
return
}

jsonIsValid := checkJSONAndRegister(rawData, fileName, configPath)
if jsonIsValid {
w.WriteHeader(http.StatusOK)
return
} else {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("{'error': 'domainName and proxyTo could not be found'}"))
return
}
}
}

func removeProxy(configPath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
file := chi.URLParam(r, "fileName")
fmt.Println(file)

err := os.Remove(configPath + "/" + file)
if err != nil {
w.WriteHeader(http.StatusNoContent)
w.Write([]byte(err.Error()))
return
}
}
}

// Helper method to check for domainName and proxyTo in a given JSON array
// If the filename is empty the domain will be used as the filename - files with the same name will be overwritten
func checkJSONAndRegister(rawData []byte, filename string, configPath string) (successful bool) {
var cfg infrared.ProxyConfig
err := json.Unmarshal(rawData, &cfg)
if err != nil {
fmt.Println(err)
return false
}

if cfg.DomainName == "" || cfg.ProxyTo == "" {
return false
}

path := configPath + "/" + filename
// If fileName is empty use domainName as filename
if filename == "" {
path = configPath + "/" + cfg.DomainName
}

err = os.WriteFile(path, rawData, 0644)
if err != nil {
fmt.Println(err)
return false
}

return true
}
23 changes: 17 additions & 6 deletions cmd/infrared/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"flag"
"github.com/haveachin/infrared/api"
"log"
"os"
"strconv"
Expand All @@ -13,20 +14,24 @@ const (
envPrefix = "INFRARED_"
envConfigPath = envPrefix + "CONFIG_PATH"
envReceiveProxyProtocol = envPrefix + "RECEIVE_PROXY_PROTOCOL"
envApiEnabled = envPrefix + "API_ENABLED"
envApiBind = envPrefix + "API_BIND"
)

const (
clfConfigPath = "config-path"
clfReceiveProxyProtocol = "receive-proxy-protocol"
clfPrometheusEnabled = "enable-prometheus"
clfPrometheusBind = "prometheus-bind"
clfPrometheusEnabled = "enable-prometheus"
clfPrometheusBind = "prometheus-bind"
)

var (
configPath = "./configs"
receiveProxyProtocol = false
prometheusEnabled = false
prometheusBind = ":9100"
prometheusEnabled = false
prometheusBind = ":9100"
apiEnabled = false
apiBind = "127.0.0.1:8080"
)

func envBool(name string, value bool) bool {
Expand Down Expand Up @@ -55,13 +60,15 @@ func envString(name string, value string) string {
func initEnv() {
configPath = envString(envConfigPath, configPath)
receiveProxyProtocol = envBool(envReceiveProxyProtocol, receiveProxyProtocol)
apiEnabled = envBool(envApiEnabled, apiEnabled)
apiBind = envString(envApiBind, apiBind)
}

func initFlags() {
flag.StringVar(&configPath, clfConfigPath, configPath, "path of all proxy configs")
flag.BoolVar(&receiveProxyProtocol, clfReceiveProxyProtocol, receiveProxyProtocol, "should accept proxy protocol")
flag.BoolVar(&prometheusEnabled, clfPrometheusEnabled, prometheusEnabled, "should run prometheus client exposing metrics")
flag.StringVar(&prometheusBind, clfPrometheusBind, prometheusBind, "bind address and/or port for prometheus")
flag.BoolVar(&prometheusEnabled, clfPrometheusEnabled, prometheusEnabled, "should run prometheus client exposing metrics")
flag.StringVar(&prometheusBind, clfPrometheusBind, prometheusBind, "bind address and/or port for prometheus")
flag.Parse()
}

Expand Down Expand Up @@ -109,6 +116,10 @@ func main() {
}
}()

if apiEnabled {
go api.ListenAndServe(configPath, apiBind)
}

if prometheusEnabled {
gateway.EnablePrometheus(prometheusBind)
}
Expand Down
10 changes: 5 additions & 5 deletions gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ var (

type Gateway struct {
listeners sync.Map
proxies sync.Map
Proxies sync.Map
closed chan bool
wg sync.WaitGroup
receiveProxyProtocol bool
Expand Down Expand Up @@ -77,15 +77,15 @@ func (gateway *Gateway) Close() {

func (gateway *Gateway) CloseProxy(proxyUID string) {
log.Println("Closing proxy with UID", proxyUID)
v, ok := gateway.proxies.LoadAndDelete(proxyUID)
v, ok := gateway.Proxies.LoadAndDelete(proxyUID)
if !ok {
return
}
proxiesActive.Dec()
proxy := v.(*Proxy)

closeListener := true
gateway.proxies.Range(func(k, v interface{}) bool {
gateway.Proxies.Range(func(k, v interface{}) bool {
otherProxy := v.(*Proxy)
if proxy.ListenTo() == otherProxy.ListenTo() {
closeListener = false
Expand All @@ -109,7 +109,7 @@ func (gateway *Gateway) RegisterProxy(proxy *Proxy) error {
// Register new Proxy
proxyUID := proxy.UID()
log.Println("Registering proxy with UID", proxyUID)
gateway.proxies.Store(proxyUID, proxy)
gateway.Proxies.Store(proxyUID, proxy)
proxiesActive.Inc()

proxy.Config.removeCallback = func() {
Expand Down Expand Up @@ -201,7 +201,7 @@ func (gateway *Gateway) serve(conn Conn, addr string) error {
proxyUID := proxyUID(hs.ParseServerAddress(), addr)

log.Printf("[i] %s requests proxy with UID %s", connRemoteAddr, proxyUID)
v, ok := gateway.proxies.Load(proxyUID)
v, ok := gateway.Proxies.Load(proxyUID)
if !ok {
// Client send an invalid address/port; we don't have a v for that address
return errors.New("no proxy with uid " + proxyUID)
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ require (
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.4.9
github.com/go-chi/chi/v5 v5.0.6
github.com/gofrs/uuid v4.0.0+incompatible
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.4 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/pires/go-proxyproto v0.4.2
github.com/prometheus/client_golang v1.10.0 // indirect
github.com/prometheus/client_golang v1.10.0
github.com/sirupsen/logrus v1.7.0 // indirect
golang.org/x/net v0.0.0-20210119194325-5f4716e94777
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
Expand Down
6 changes: 3 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-chi/chi/v5 v5.0.6 h1:CHIMAkr36TRf/zYvOqNKklMDxEm9HuqdiK+syK+tYtw=
github.com/go-chi/chi/v5 v5.0.6/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
Expand Down Expand Up @@ -105,7 +107,6 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
Expand Down Expand Up @@ -394,7 +395,6 @@ golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand Down Expand Up @@ -476,10 +476,10 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
Expand Down

0 comments on commit 9e6e7fc

Please sign in to comment.