Skip to content
Merged
31 changes: 26 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,35 @@ You can set environment variables to configure the proxy before running it.
|-----------------|-------------------------------------|------------------------|
| `ORIGIN_URL` | The origin server URL to proxy | `http://dummyjson.com` |
| `PROXY_PORT` | Port for the proxy server to listen | `1337` |
| `PROXY_CACHE` | Cache storage (redis/memory) | `memory` |
| `PROXY_TTL` | Cache TTL (seconds) | `60` |
| `ZAP_ENV` | Logging environment (dev/prod) | `dev` |
| `REDIS_HOST` | Redis server host | `redis_proxio` |
| `REDIS_HOST` | Redis server host (used only if cache = redis) | `redis_proxio` |
| `REDIS_PORT` | Redis server port | `6379` |
| `REDIS_USERNAME` | Redis username | `proxio` |
| `REDIS_PASSWORD` | Redis password | *(empty)* |
| `REDIS_DATABASE` | Redis database number | `0` |

### Notes on Caching
- **Default cache storage is `memory`**
- When using memory cache, **you do not need a Redis instance**
- Switch to Redis by setting:
```env
PROXY_CACHE="redis"
```

## Run Proxio

```bash
# Run with default environment variables
./proxio

# Or set custom variables inline
ORIGIN_URL="http://example.com" PROXY_PORT="8080" ./proxio
ORIGIN_URL="http://example.com" \
PROXY_PORT="8080" \
PROXY_CACHE="redis" \
PROXY_TTL="120" \
./proxio
```
Cached responses will be stored in Redis and served quickly on repeated requests.

Expand All @@ -56,12 +70,20 @@ docker build -t proxio .
docker run -d \
-e ORIGIN_URL="http://example.com" \
-e PROXY_PORT="8080" \
-p 8080:8080 \
proxio
```

If using redis:
```bash
docker run -d \
-e PROXY_CACHE="redis" \
-e REDIS_HOST="redis" \
-p 8080:8080 \
proxio
```

## Using Docker
## Using Container Registries
```bash
# Pull from Docker Hub
docker pull me3di/proxio:latest
Expand All @@ -71,8 +93,6 @@ docker pull ghcr.io/pyr33x/proxio:latest

# Or build locally
docker build -t proxio .

# Run with Docker
docker run -d -p 8080:8080 ghcr.io/pyr33x/proxio:latest
```

Expand All @@ -82,6 +102,7 @@ Run with:
```bash
docker compose up -d --build
```
If `PROXY_CACHE=memory`, Redis won't be used even if it’s running.

## Contributing
Feel free to open issues or submit pull requests. Any help is appreciated!
Expand Down
28 changes: 16 additions & 12 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,17 @@ import (
"time"

"github.com/pyr33x/proxio/pkg/err"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
)

type Caching interface {
Get(ctx context.Context, key string) string
Put(ctx context.Context, key string, value any) error
type Store interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Clear(ctx context.Context) error
}

type Cache struct {
rdb *redis.Client
store Store
logger *zap.Logger
expiration time.Duration
}
Expand All @@ -30,11 +29,11 @@ type CacheValue struct {
Body []byte
}

func NewCacheRepository(rdb *redis.Client, logger *zap.Logger) *Cache {
func NewCacheRepository(store Store, logger *zap.Logger, ttl time.Duration) *Cache {
return &Cache{
rdb: rdb,
store: store,
logger: logger,
expiration: 60 * time.Second,
expiration: ttl,
}
}

Expand All @@ -44,7 +43,7 @@ func (c *Cache) Get(ctx context.Context, key string) (*CacheValue, bool) {
return nil, false
}

raw, err := c.rdb.Get(ctx, key).Bytes()
raw, err := c.store.Get(ctx, key)
if err != nil {
c.logger.Info("cache miss",
zap.String("key", key),
Expand All @@ -53,6 +52,12 @@ func (c *Cache) Get(ctx context.Context, key string) (*CacheValue, bool) {
return nil, false
}

// raw is nil (key doesn't exist)
if raw == nil {
c.logger.Info("cache miss", zap.String("key", key), zap.String("state", "MISS"))
return nil, false
}

var val CacheValue
if err := json.Unmarshal(raw, &val); err != nil {
return nil, false
Expand All @@ -76,8 +81,7 @@ func (c *Cache) Put(ctx context.Context, key string, value CacheValue) error {
return err
}

err = c.rdb.Set(ctx, key, b, c.expiration).Err()
if err != nil {
if err := c.store.Set(ctx, key, b, c.expiration); err != nil {
c.logger.Error("failed to write to cache",
zap.String("key", key),
zap.Duration("expiration", c.expiration),
Expand All @@ -90,5 +94,5 @@ func (c *Cache) Put(ctx context.Context, key string, value CacheValue) error {
}

func (c *Cache) Clear(ctx context.Context) error {
return c.rdb.FlushAll(ctx).Err()
return c.store.Clear(ctx)
}
58 changes: 58 additions & 0 deletions internal/cache/memory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package cache

import (
"context"
"sync"
"time"
)

type memoryEntry struct {
value []byte
expiration time.Time
}

type memoryStore struct {
data sync.Map
}

func NewMemoryStore() *memoryStore {
return &memoryStore{}
}

func (m *memoryStore) Get(ctx context.Context, key string) ([]byte, error) {
raw, ok := m.data.Load(key)
if !ok {
return nil, nil
}

entry := raw.(memoryEntry)

if !entry.expiration.IsZero() && time.Now().After(entry.expiration) {
m.data.Delete(key)
return nil, nil
}

return entry.value, nil
}

func (m *memoryStore) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
var exp time.Time
if ttl > 0 {
exp = time.Now().Add(ttl)
}

m.data.Store(key, memoryEntry{
value: value,
expiration: exp,
})

return nil
}

func (m *memoryStore) Clear(ctx context.Context) error {
m.data.Range(func(key, _ any) bool {
m.data.Delete(key)
return true
})
return nil
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
28 changes: 28 additions & 0 deletions internal/cache/redis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cache

import (
"context"
"time"

"github.com/redis/go-redis/v9"
)

type redisStore struct {
client *redis.Client
}

func NewRedisStore(client *redis.Client) *redisStore {
return &redisStore{client: client}
}

func (r *redisStore) Get(ctx context.Context, key string) ([]byte, error) {
return r.client.Get(ctx, key).Bytes()
}

func (r *redisStore) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {
return r.client.Set(ctx, key, value, ttl).Err()
}

func (r *redisStore) Clear(ctx context.Context) error {
return r.client.FlushDB(ctx).Err()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
16 changes: 14 additions & 2 deletions internal/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ type OriginServer struct {
}

func NewProxyServer(ctx context.Context, cfg *config.Config, logger *zap.Logger) *http.Server {
rdb := redis.New(ctx, &cfg.Redis, logger).GetClient()
var cacheStorage cache.Store

if cfg.Server.Proxy.Cache == "redis" {
adapter := redis.New(ctx, &cfg.Redis, logger)
if adapter == nil {
logger.Warn("failed to connect to redis, falling back to memory store")
cacheStorage = cache.NewMemoryStore()
} else {
cacheStorage = cache.NewRedisStore(adapter.GetClient())
}
} else {
cacheStorage = cache.NewMemoryStore()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

srv := &Server{
Proxy: ProxyServer{
Expand All @@ -36,7 +48,7 @@ func NewProxyServer(ctx context.Context, cfg *config.Config, logger *zap.Logger)
Origin: OriginServer{
URL: cfg.Server.Origin.URL,
},
Cache: cache.NewCacheRepository(rdb, logger),
Cache: cache.NewCacheRepository(cacheStorage, logger, time.Duration(cfg.Server.Proxy.TTL)*time.Second),
logger: logger,
}

Expand Down
8 changes: 6 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ type Origin struct {
}

type Proxy struct {
Port string
Port string
Cache string
TTL int64
}

type Zap struct {
Expand All @@ -44,7 +46,9 @@ func New() *Config {
URL: envy.GetString("ORIGIN_URL", "http://dummyjson.com"),
},
Proxy: Proxy{
Port: envy.GetString("PROXY_PORT", "1337"),
Port: envy.GetString("PROXY_PORT", "1337"),
Cache: envy.GetString("PROXY_CACHE", "memory"),
TTL: envy.GetInt64("PROXY_TTL", 60), // in seconds
},
},
Zap: Zap{
Expand Down