Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
fredposner committed Nov 24, 2023
0 parents commit 1c60b62
Show file tree
Hide file tree
Showing 10 changed files with 1,073 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These are supported funding model platforms

github: palner
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# ---> macOS
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
8 changes: 8 additions & 0 deletions .markdownlint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"default": true,
"MD003": { "style": "atx_closed" },
"MD007": { "indent": 4 },
"MD013": false,
"MD040": false,
"no-hard-tabs": false
}
18 changes: 18 additions & 0 deletions .vscode/spellright.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
golang
redis
IPTABLES
iptables
opn
htables
autoexpire
rtimer
ipaddresses
ipban
Voi
rtc
ipset
opnsense
ipsets
Nsense
homeserver
apiban
339 changes: 339 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# APIBAN-tools

apiban-tools is brought to you by [apiban.org](https://apiban.org) and is an open source set of tools to help automate apiban based actions, such as `check`, `get`, etc.

For more information, please visit apiban.org.

**APIBAN is made possible by the generosity of our [sponsors](https://apiban.org/doc.html#sponsors).**

## Getting Help

Help is provided by LOD (<https://www.lod.com>) and an APIBAN room ([#apiban:matrix.lod.com](https://matrix.to/#/#apiban:matrix.lod.com)) is available on the LOD Matrix server. The software is provided under the GPLv2 license.

## Warranty

ABIBAN data is provided in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
264 changes: 264 additions & 0 deletions apiban.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* Copyright (C) 2020-2021 Fred Posner (palner.com)
*
* This file is part of APIBAN.org.
*
* apiban is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version
*
* apiban is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/

package golib

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
)

var (
// RootURL is the base URI of the APIBAN.org API server
RootURL = "https://apiban.org/api/"
)

// ErrBadRequest indicates a 400 response was received;
//
// NOTE: this is used by the server to indicate both that an IP address is not
// blocked (when calling Check) and that the list is complete (when calling
// Banned)
var ErrBadRequest = errors.New("bad request")

// Entry describes a set of blocked IP addresses from APIBAN.org
type Entry struct {

// ID is the timestamp of the next Entry
ID string `json:"ID"`

// IPs is the list of blocked IP addresses in this entry
IPs []string `json:"ipaddress"`
}

type ApiGetJson struct {
SetType string `json:"set"`
ID string `json:"id"`
}

type ApiGetCheck struct {
SetType string `json:"set"`
IP string `json:"ipaddress"`
}

// Banned returns a set of banned addresses, optionally limited to the
// specified startFrom ID. If no startFrom is supplied, the entire current list will
// be pulled.
func Banned(key string, startFrom string, set string) (*Entry, error) {
if key == "" {
return nil, errors.New("API Key is required")
}

if key == "MY API KEY" {
return nil, errors.New("API Key is required")
}

if startFrom == "" {
startFrom = "100" // NOTE: arbitrary ID copied from reference source
}

if set == "" {
set = "sip"
}

p := &ApiGetJson{
ID: startFrom,
SetType: set,
}

p_json, err := json.Marshal(p)
if err != nil {
return nil, errors.New("error making json payload")
}

out := &Entry{
ID: startFrom,
}

url := RootURL + "get"
for {
e, err := queryServerv2(key, p_json, url)
if err != nil {
return nil, err
}

if e.ID == "none" {
// List complete
return out, nil
}
if e.ID == "" {
return nil, errors.New("empty ID received")
}

// Set the next ID
out.ID = e.ID

// Aggregate the received IPs
out.IPs = append(out.IPs, e.IPs...)
}
}

// Check queries APIBAN.org to see if the provided IP address is blocked.
func Check(key string, ip string, set string) (bool, error) {
if key == "" {
return false, errors.New("API Key is required")
}
if ip == "" {
return false, errors.New("IP address is required")
}

if set == "" {
set = "sip"
}

p := &ApiGetCheck{
IP: ip,
SetType: set,
}

p_json, err := json.Marshal(p)
if err != nil {
return false, errors.New("error making json payload")
}

url := RootURL + "check"
entry, err := queryServerv2(key, p_json, url)
if err == ErrBadRequest {
// Not blocked
return false, nil
}
if err != nil {
return false, err
}
if entry == nil {
return false, errors.New("empty entry received")
} else if len(entry.IPs) == 1 {
if entry.IPs[0] == "not blocked" {
// Not blocked
return false, nil
}
}

// IP address is blocked
return true, nil
}

func queryServerv2(key string, p_json []byte, url string) (*Entry, error) {
sendbody := strings.NewReader(string(p_json))
bearer := "Bearer " + key
req, err := http.NewRequest("POST", url, sendbody)
req.Header.Add("Authorization", bearer)
if err != nil {
// handle err
return nil, err
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("query error: %s", err.Error())
}
defer resp.Body.Close()

switch {
case resp.StatusCode == http.StatusBadRequest ||
resp.StatusCode == http.StatusNotFound ||
resp.StatusCode == http.StatusForbidden:
return processBadRequest(resp)
case resp.StatusCode == http.StatusOK:
break
case resp.StatusCode > 400 && resp.StatusCode < 500:
return nil, fmt.Errorf("client error (%d) from apiban.org: %s from %q", resp.StatusCode, resp.Status, url)
case resp.StatusCode >= 500:
return nil, fmt.Errorf("server error (%d) from apiban.org: %s from %q", resp.StatusCode, resp.Status, url)
case resp.StatusCode > 299:
return nil, fmt.Errorf("unhandled error (%d) from apiban.org: %s from %q", resp.StatusCode, resp.Status, url)
}

entry := new(Entry)
if err = json.NewDecoder(resp.Body).Decode(entry); err != nil {
return nil, fmt.Errorf("failed to decode server response: %s", err.Error())
}

return entry, nil
}

func processBadRequest(resp *http.Response) (*Entry, error) {
var buf bytes.Buffer
if _, err := buf.ReadFrom(resp.Body); err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}

// Read the bytes buffer into a new bytes.Reader
r := bytes.NewReader(buf.Bytes())

// First, try decoding as a normal entry
e := new(Entry)
if err := json.NewDecoder(r).Decode(e); err == nil {
// Successfully decoded normal entry

switch e.ID {
case "none":
// non-error case
case "unauthorized":
return nil, errors.New("unauthorized")
default:
// unhandled case
return nil, ErrBadRequest
}

if len(e.IPs) > 0 {
switch e.IPs[0] {
case "no new bans":
return e, nil
}
}

// Unhandled case
return nil, ErrBadRequest
}

// Next, try decoding as an errorEntry
if _, err := r.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to re-seek to beginning of response buffer: %w", err)
}

type errorEntry struct {
AddressCode string `json:"ipaddress"`
IDCode string `json:"ID"`
}

ee := new(errorEntry)
if err := json.NewDecoder(r).Decode(ee); err != nil {
return nil, fmt.Errorf("failed to decode Bad Request response: %s", err.Error())
}

switch ee.AddressCode {
case "rate limit exceeded":
return nil, errors.New("rate limit exceeded")
default:
// unhandled case
return nil, ErrBadRequest
}
}
Loading

0 comments on commit 1c60b62

Please sign in to comment.