Skip to content

Commit

Permalink
working service
Browse files Browse the repository at this point in the history
  • Loading branch information
SomeoneWeird committed Sep 30, 2024
1 parent 0786f42 commit 599cf80
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 2 deletions.
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dev:
go run main.go -bind :9999
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
# reflector
Dogebox reflector service
# Dogebox reflector service

This service is a basic in-memory key-pair cache. It is used by the Dogebox (by default) to persist its local, internal IP address somewhere that the users client can find it once the Dogebox has switched networks.

### API

#### GET /:token

Fetch the IP submitted via token. Is a one-shot fetch, will be removed after you have retrieved it.

Return `200` `{ "ip": "1.2.3.4" }` if found.

Returns `404` if not found.

#### POST /

Required body: `{ "token": "abc", "ip": "1.2.3.4" }`

Returns `201` on create.

Rate limited to 1 per minute via IP.
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/dogeorg/reflector

go 1.23.1

require (
github.com/go-chi/chi/v5 v5.1.0
github.com/patrickmn/go-cache v2.1.0+incompatible
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
45 changes: 45 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"flag"
"log"
"net/http"
"time"

"github.com/dogeorg/reflector/pkg/api"
"github.com/dogeorg/reflector/pkg/database"
reflectormiddleware "github.com/dogeorg/reflector/pkg/middleware"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)

func main() {
// Initialize database
db, err := database.NewDatabase()
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer db.Close()

// Create router
r := chi.NewRouter()

// Middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

// Routes
r.Post("/", func(w http.ResponseWriter, r *http.Request) {
reflectormiddleware.RateLimiter(time.Minute, 1)(api.CreateEntry(db)).ServeHTTP(w, r)
})

r.Get("/{token}", api.GetIP(db))

// Parse command-line arguments
bindAddr := flag.String("bind", ":8080", "Bind address and port for the server")
flag.Parse()

// Start server
log.Printf("Server starting on %s\n", *bindAddr)
log.Fatal(http.ListenAndServe(*bindAddr, r))
}
67 changes: 67 additions & 0 deletions pkg/api/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package api

import (
"encoding/json"
"net/http"
"regexp"

"github.com/dogeorg/reflector/pkg/database"
"github.com/go-chi/chi/v5"
)

type Entry struct {
Token string `json:"token"`
IP string `json:"ip"`
}

func CreateEntry(db *database.Database) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var entry Entry
if err := json.NewDecoder(r.Body).Decode(&entry); err != nil {
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
return
}

if !isValidToken(entry.Token) || !isValidIP(entry.IP) {
http.Error(w, "Invalid token or IP format", http.StatusBadRequest)
return
}

if err := db.SaveEntry(entry.Token, entry.IP); err != nil {
http.Error(w, "Failed to save entry", http.StatusInternalServerError)
return
}

w.WriteHeader(http.StatusCreated)
}
}

func GetIP(db *database.Database) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := chi.URLParam(r, "token")

ip, err := db.GetIP(token)
if err != nil {
http.Error(w, "IP not found", http.StatusNotFound)
return
}

if err := db.DeleteEntry(token); err != nil {
http.Error(w, "Failed to remove entry", http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"ip": ip})
}
}

func isValidToken(token string) bool {
return len(token) <= 20
}

func isValidIP(ip string) bool {
ipPattern := `^(\d{1,3}\.){3}\d{1,3}$`
match, _ := regexp.MatchString(ipPattern, ip)
return match && len(ip) <= 15
}
48 changes: 48 additions & 0 deletions pkg/database/database.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package database

import (
"fmt"
"sync"
"time"

"github.com/patrickmn/go-cache"
)

type Database struct {
cache *cache.Cache
mu sync.Mutex
}

func NewDatabase() (*Database, error) {
c := cache.New(2*time.Hour, 10*time.Minute)
return &Database{cache: c}, nil
}

func (db *Database) Close() error {
// No need to close in-memory cache
return nil
}

func (db *Database) SaveEntry(token, ip string) error {
db.mu.Lock()
defer db.mu.Unlock()
db.cache.Set(token, ip, cache.DefaultExpiration)
return nil
}

func (db *Database) DeleteEntry(token string) error {
db.mu.Lock()
defer db.mu.Unlock()
db.cache.Delete(token)
return nil
}

func (db *Database) GetIP(token string) (string, error) {
db.mu.Lock()
defer db.mu.Unlock()
ip, found := db.cache.Get(token)
if !found {
return "", fmt.Errorf("IP not found for token: %s", token)
}
return ip.(string), nil
}
45 changes: 45 additions & 0 deletions pkg/middleware/ratelimiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package reflectormiddleware

import (
"net/http"
"sync"
"time"
)

func RateLimiter(interval time.Duration, limit int) func(http.Handler) http.Handler {
type client struct {
count int
lastSeen time.Time
}

var (
mu sync.Mutex
clients = make(map[string]*client)
)

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr

mu.Lock()
if _, found := clients[ip]; !found {
clients[ip] = &client{}
}
c := clients[ip]
now := time.Now()
if now.Sub(c.lastSeen) > interval {
c.count = 0
c.lastSeen = now
}
c.count++
if c.count > limit {
mu.Unlock()
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
mu.Unlock()

next.ServeHTTP(w, r)
})
}
}

0 comments on commit 599cf80

Please sign in to comment.