Skip to content

Commit

Permalink
ip api integration
Browse files Browse the repository at this point in the history
  • Loading branch information
gweebg committed Jan 27, 2024
1 parent 080ead0 commit f5270c9
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 43 deletions.
17 changes: 4 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,13 @@
3. App should expose an API for the changes and checks;
4. App should have extensive logging;
5. App should be able to notify users by email;
6. App configuration should be done via a YAML file.
6. App configuration should be done via a YAML file;
7. If watcher is configured for both v4 and v6, it should spawn two processes;
8.

### User Requirements

1. User can chose ways to be notified if a change happens;
1. User can choose ways to be notified if a change happens;
2. User can define the delta between checks;
3. User can specify a script to run if the address has, or not changed;
4. User can choose to check IPv6, IPv4 or both ips.

### Configuration

1. url only needs v4 or v6, or both to be specified
2. url must be a valid address
3. response_type is mandatory
4. response_type can be json | text
5. field is mandatory when response_type is json
6. field must be present in the response from the api and represents the ip field
7. default source is always the first, the others are used as fallbac1.
8. timeout is specified in seconds
26 changes: 6 additions & 20 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,18 @@ package main

import (
"github.com/gweebg/ipwatcher/internal/config"
"github.com/gweebg/ipwatcher/internal/fetch"
"github.com/gweebg/ipwatcher/internal/utils"
"log"
)

func main() {

config.Init()

v4 := "v4"
v6 := "v6"
f := "field"
const version string = "v6"
addr, err := fetch.RequestAddress(version)

s := config.Source{
Name: "mock",
Url: config.SourceUrl{
V4: &v4,
V6: &v6,
},
Type: "json",
Field: &f,
}
err := config.AddSource(s)

if err != nil {

log.Print(err.Error())
} else {
log.Println("good")
}
utils.Check(err, "")
log.Println(addr)
}
15 changes: 15 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"github.com/gweebg/ipwatcher/internal/utils"
"github.com/spf13/viper"
"strings"
)

var config *viper.Viper
Expand All @@ -13,6 +14,20 @@ type SourceUrl struct {
V6 *string `mapstructure:"v6"`
}

func (s SourceUrl) GetUrl(version string) (string, error) {

if strings.ToLower(version) == "v4" && s.V4 != nil {
return *s.V4, nil
}

if strings.ToLower(version) == "v6" && s.V6 != nil {
return *s.V6, nil
}

return "", errors.New("'" + version + "' is not specified in SourceUrl")

}

type Source struct {
Name string `mapstructure:"name"`
Url SourceUrl `mapstructure:"url"`
Expand Down
117 changes: 107 additions & 10 deletions internal/fetch/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,122 @@ package fetch
import (
"bytes"
"encoding/json"
"errors"
"io"
"log"
"net"
"net/http"
"strings"

"github.com/gweebg/ipwatcher/internal/config"
"github.com/gweebg/ipwatcher/internal/utils"
)

func SendHTTPRequest(method, url string, headers map[string]string, payload interface{}) (*http.Response, error) {
func RequestAddress(version string) (string, error) {

jsonPayload, err := json.Marshal(payload)
if err != nil {
return nil, err
conf := config.GetConfig()
sources, err := config.GetSources()
utils.Check(err, "")

address := ""

const forceSource = "service.force_source"
for _, source := range sources {

// if 'force_source' is set, and it is different from the current source, we skip it
if conf.IsSet(forceSource) && conf.Get(forceSource) != source.Name {
continue
}

url, err := source.Url.GetUrl(version)
if err != nil {
log.Printf("source '%v' does not have any 'IP%v' url specified, skipping\n", source.Name, version)
continue
}

response, err := sendRequest(url)
if err != nil {
log.Printf("failed to send request to source '%v', skipping\n", source.Name)
continue
}

address = parseResponse(response, source)

valid := net.ParseIP(address)
if valid == nil {
log.Printf("source '%v' did not return a valid IP address: '%v', skipping\n", source.Name, address)
continue
}

log.Printf("valid address from source '%v'\n", source.Name)
break
}

req, err := http.NewRequest(method, url, bytes.NewBuffer(jsonPayload))
if err != nil {
return nil, err
// if address is still empty after querying the urls then, user needs to try others
if address == "" {
return address, errors.New(
"none of the specified sources returned a valid address or 'force_source' name missmatch",
)
}

return address, nil
}

func parseResponse(response *http.Response, source config.Source) string {

// check if http response status code is 'positive' (200<=status<300)
if !(response.StatusCode >= 200 && response.StatusCode < 300) {
log.Printf("'%v' returned %d\n", source.Name, response.StatusCode)
return ""
}

// handle the different Content-Types
contentType := response.Header.Get("Content-Type")

// todo: abstract these if's for easier plug and play
if source.Type == "text" && strings.Contains(contentType, "text/plain") {

var address bytes.Buffer
_, err := io.Copy(&address, response.Body)
if err != nil {
log.Printf("error reading response body from source '%v'", source.Name)
return ""
}

return address.String()
}

if source.Type == "json" && strings.Contains(contentType, "application/json") {

var responseBody map[string]interface{}
err := json.NewDecoder(response.Body).Decode(&responseBody)
if err != nil {
log.Printf("error decoding JSON response from source '%v'", source.Name)
return ""
}

address, ok := responseBody[*source.Field]
if !ok {
log.Printf("expected field '%v' present on response:\n\t%v", *source.Field, responseBody)
return ""
}

return address.(string)
}

req.Header.Set("Content-Type", "application/json")
log.Printf("content type between response and config mismatch, expected '%s' but got '%s'\n", source.Type, contentType)
return ""
}

func sendRequest(url string) (*http.Response, error) {

for key, value := range headers {
req.Header.Set(key, value)
req, err := http.NewRequest(
"GET",
url,
nil,
)
if err != nil {
return nil, err
}

client := &http.Client{}
Expand Down

0 comments on commit f5270c9

Please sign in to comment.