Skip to content

Commit e736225

Browse files
committed
Implement standard client Store interface
Define and use a standard Store interface for the client library. This will allow us to extend the client to greater uses later. As an example, a RAM-based caching store has been implemented. Fixes palner#11 Signed-off-by: Seán C McCord <[email protected]>
1 parent 71c0326 commit e736225

File tree

4 files changed

+521
-175
lines changed

4 files changed

+521
-175
lines changed

clients/go/apiban-iptables/apiban-iptables-client.go

+13-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import (
2929
"log"
3030
"os"
3131
"runtime"
32+
"strconv"
33+
"time"
3234

3335
"github.com/coreos/go-iptables/iptables"
3436
"github.com/palner/apiban/clients/go/apiban"
@@ -120,24 +122,26 @@ func main() {
120122
log.Fatalln("failed to initialize IPTables:", err)
121123
}
122124

125+
lastTimestamp, err := strconv.ParseInt(apiconfig.LKID, 10, 64)
126+
if err != nil {
127+
// If we don't have a valid timestamp, try 1 year ago
128+
lastTimestamp = time.Now().AddDate(-1, 0, 0).Unix()
129+
}
130+
123131
// Get list of banned ip's from APIBAN.org
124-
res, err := apiban.Banned(apiconfig.APIKEY, apiconfig.LKID)
132+
list, err := apiban.NewOfficialStore(apiconfig.APIKEY).ListFromTime(time.Unix(lastTimestamp, 0))
125133
if err != nil {
126134
log.Fatalln("failed to get banned list:", err)
127135
}
128136

129-
if res.ID == apiconfig.LKID {
137+
if len(list) == 0 {
130138
log.Print("Great news... no new bans to add. Exiting...")
131139
os.Exit(0)
132140
}
133141

134-
if len(res.IPs) == 0 {
135-
log.Print("No IP addresses detected. Exiting.")
136-
os.Exit(0)
137-
}
142+
for _, l := range list {
143+
blockedip := l.IP.String()
138144

139-
for _, ip := range res.IPs {
140-
blockedip := ip + "/32"
141145
err = ipt.AppendUnique("filter", "APIBAN", "-s", blockedip, "-d", "0/0", "-j", "REJECT")
142146
if err != nil {
143147
log.Print("Adding rule failed. ", err.Error())
@@ -147,7 +151,7 @@ func main() {
147151
}
148152

149153
// Update the config with the updated LKID
150-
apiconfig.LKID = res.ID
154+
apiconfig.LKID = strconv.FormatInt(list[len(list)-1].Timestamp.Unix(), 10)
151155
if err := apiconfig.Update(); err != nil {
152156
log.Fatalln(err)
153157
}

clients/go/apiban/apiban.go

+24-166
Original file line numberDiff line numberDiff line change
@@ -22,183 +22,41 @@
2222
package apiban
2323

2424
import (
25-
"bytes"
26-
"encoding/json"
27-
"errors"
28-
"fmt"
29-
"io"
30-
"net/http"
25+
"net"
26+
"time"
3127
)
3228

33-
const (
34-
// RootURL is the base URI of the APIBAN.org API server
35-
RootURL = "https://apiban.org/api/"
36-
)
37-
38-
// ErrBadRequest indicates a 400 response was received;
39-
//
40-
// NOTE: this is used by the server to indicate both that an IP address is not
41-
// blocked (when calling Check) and that the list is complete (when calling
42-
// Banned)
43-
var ErrBadRequest = errors.New("Bad Request")
44-
45-
// Entry describes a set of blocked IP addresses from APIBAN.org
46-
type Entry struct {
47-
48-
// ID is the timestamp of the next Entry
49-
ID string `json:"ID"`
50-
51-
// IPs is the list of blocked IP addresses in this entry
52-
IPs []string `json:"ipaddress"`
53-
}
54-
55-
// Banned returns a set of banned addresses, optionally limited to the
56-
// specified startFrom ID. If no startFrom is supplied, the entire current list will
57-
// be pulled.
58-
func Banned(key string, startFrom string) (*Entry, error) {
59-
if key == "" {
60-
return nil, errors.New("API Key is required")
61-
}
62-
63-
if startFrom == "" {
64-
startFrom = "100" // NOTE: arbitrary ID copied from reference source
65-
}
66-
67-
out := &Entry{
68-
ID: startFrom,
69-
}
70-
71-
for {
72-
e, err := queryServer(http.DefaultClient, fmt.Sprintf("%s%s/banned/%s", RootURL, key, out.ID))
73-
if err != nil {
74-
return nil, err
75-
}
76-
77-
if e.ID == "none" {
78-
// List complete
79-
return out, nil
80-
}
81-
if e.ID == "" {
82-
return nil, errors.New("empty ID received")
83-
}
29+
// Store defines and interface for storing and retrieving entries in the APIBan database, local or remote
30+
type Store interface {
8431

85-
// Set the next ID
86-
out.ID = e.ID
32+
// Add inserts the given Listing into the store
33+
Add(l *Listing) (*Listing, error)
8734

88-
// Aggregate the received IPs
89-
out.IPs = append(out.IPs, e.IPs...)
90-
}
91-
}
92-
93-
// Check queries APIBAN.org to see if the provided IP address is blocked.
94-
func Check(key string, ip string) (bool, error) {
95-
if key == "" {
96-
return false, errors.New("API Key is required")
97-
}
98-
if ip == "" {
99-
return false, errors.New("IP address is required")
100-
}
101-
102-
entry, err := queryServer(http.DefaultClient, fmt.Sprintf("%s%s/check/%s", RootURL, key, ip))
103-
if err == ErrBadRequest {
104-
// Not blocked
105-
return false, nil
106-
}
107-
if err != nil {
108-
return false, err
109-
}
110-
if entry == nil {
111-
return false, errors.New("empty entry received")
112-
}
35+
// Exists checks to see whether the given IP matches a Listing in the store, returning the first matching Listing.
36+
Exists(ip net.IP) (*Listing, error)
11337

114-
// IP address is blocked
115-
return true, nil
116-
}
117-
118-
func queryServer(c *http.Client, u string) (*Entry, error) {
119-
resp, err := http.Get(u)
120-
if err != nil {
121-
return nil, err
122-
}
123-
defer resp.Body.Close()
38+
// List retrieves the contents of the store
39+
List() ([]*Listing, error)
12440

125-
// StatusBadRequest (400) has a number of special cases to handle
126-
if resp.StatusCode == http.StatusBadRequest {
127-
return processBadRequest(resp)
128-
}
129-
if resp.StatusCode > 400 && resp.StatusCode < 500 {
130-
return nil, fmt.Errorf("client error (%d) from apiban.org: %s from %q", resp.StatusCode, resp.Status, u)
131-
}
132-
if resp.StatusCode >= 500 {
133-
return nil, fmt.Errorf("server error (%d) from apiban.org: %s from %q", resp.StatusCode, resp.Status, u)
134-
}
135-
if resp.StatusCode > 299 {
136-
return nil, fmt.Errorf("unhandled error (%d) from apiban.org: %s from %q", resp.StatusCode, resp.Status, u)
137-
}
41+
// ListFromTime retrieves the contents of the store from the given timestamp
42+
ListFromTime(t time.Time) ([]*Listing, error)
13843

139-
entry := new(Entry)
140-
if err = json.NewDecoder(resp.Body).Decode(entry); err != nil {
141-
return nil, fmt.Errorf("failed to decode server response: %w", err)
142-
}
44+
// Remove deletes the given Listing from the store.
45+
Remove(id string) error
14346

144-
return entry, nil
47+
// Reset empties the store
48+
Reset() error
14549
}
14650

147-
func processBadRequest(resp *http.Response) (*Entry, error) {
148-
var buf bytes.Buffer
149-
if _, err := buf.ReadFrom(resp.Body); err != nil {
150-
return nil, fmt.Errorf("failed to read response body: %w", err)
151-
}
152-
153-
// Read the bytes buffer into a new bytes.Reader
154-
r := bytes.NewReader(buf.Bytes())
155-
156-
// First, try decoding as a normal entry
157-
e := new(Entry)
158-
if err := json.NewDecoder(r).Decode(e); err == nil {
159-
// Successfully decoded normal entry
160-
161-
switch e.ID {
162-
case "none":
163-
// non-error case
164-
case "unauthorized":
165-
return nil, errors.New("unauthorized")
166-
default:
167-
// unhandled case
168-
return nil, ErrBadRequest
169-
}
170-
171-
if len(e.IPs) > 0 {
172-
switch e.IPs[0] {
173-
case "no new bans":
174-
return e, nil
175-
}
176-
}
177-
178-
// Unhandled case
179-
return nil, ErrBadRequest
180-
}
181-
182-
// Next, try decoding as an errorEntry
183-
if _, err := r.Seek(0, io.SeekStart); err != nil {
184-
return nil, fmt.Errorf("failed to re-seek to beginning of response buffer: %w", err)
185-
}
51+
// Listing is an individually-listed IP address or subnet
52+
type Listing struct {
18653

187-
type errorEntry struct {
188-
AddressCode string `json:"ipaddress"`
189-
IDCode string `json:"ID"`
190-
}
54+
// ID is the unique identifier for this Listing; for official APIBAN v1 entries, this is simply the IP address.
55+
ID string
19156

192-
ee := new(errorEntry)
193-
if err := json.NewDecoder(r).Decode(ee); err != nil {
194-
return nil, fmt.Errorf("failed to decode Bad Request response: %w", err)
195-
}
57+
// Timestamp is the time at which this Listing was added to the apiban.org database
58+
Timestamp time.Time
19659

197-
switch ee.AddressCode {
198-
case "rate limit exceeded":
199-
return nil, errors.New("rate limit exceeded")
200-
default:
201-
// unhandled case
202-
return nil, ErrBadRequest
203-
}
60+
// IP is the IP address or IP network which is in the apiban.org database
61+
IP net.IPNet
20462
}

0 commit comments

Comments
 (0)