Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 129 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ CloudProxy exposes an API and modern UI for managing your proxy infrastructure.
* Modern UI with real-time updates
* Interactive API documentation
* Multi-provider support
* Multiple accounts per provider
* Automatic proxy rotation
* Health monitoring
* Easy scaling controls
Expand Down Expand Up @@ -158,6 +159,43 @@ my_request = requests.get("https://api.ipify.org", proxies=proxies)

For detailed API documentation, see [API Documentation](docs/api.md).

## Multi-Account Provider Support

CloudProxy now supports multiple accounts per provider, allowing you to:

- Use multiple API keys or access tokens for the same provider
- Configure different regions, sizes, and scaling parameters per account
- Organize proxies by account/instance for better management
- Scale each account independently

Each provider can have multiple "instances", which represent different accounts or configurations. Each instance has its own:

- Scaling parameters (min/max)
- Region settings
- Size configuration
- API credentials
- IP addresses

To configure multiple instances, use environment variables with the instance name in the format:
```
PROVIDERNAME_INSTANCENAME_VARIABLE
```

For example, to configure two DigitalOcean accounts:
```shell
# Default DigitalOcean account
DIGITALOCEAN_ENABLED=True
DIGITALOCEAN_ACCESS_TOKEN=your_first_token
DIGITALOCEAN_DEFAULT_REGION=lon1
DIGITALOCEAN_DEFAULT_MIN_SCALING=2

# Second DigitalOcean account
DIGITALOCEAN_SECONDACCOUNT_ENABLED=True
DIGITALOCEAN_SECONDACCOUNT_ACCESS_TOKEN=your_second_token
DIGITALOCEAN_SECONDACCOUNT_REGION=nyc1
DIGITALOCEAN_SECONDACCOUNT_MIN_SCALING=3
```

## CloudProxy API Examples

### List available proxy servers
Expand Down Expand Up @@ -287,7 +325,31 @@ For detailed API documentation, see [API Documentation](docs/api.md).
"max_scaling": 2
},
"size": "s-1vcpu-1gb",
"region": "lon1"
"region": "lon1",
"instances": {
"default": {
"enabled": true,
"ips": ["192.168.1.1"],
"scaling": {
"min_scaling": 2,
"max_scaling": 2
},
"size": "s-1vcpu-1gb",
"region": "lon1",
"display_name": "Default Account"
},
"secondary": {
"enabled": true,
"ips": ["192.168.1.2"],
"scaling": {
"min_scaling": 1,
"max_scaling": 3
},
"size": "s-1vcpu-1gb",
"region": "nyc1",
"display_name": "US Account"
}
}
},
"aws": {
"enabled": false,
Expand Down Expand Up @@ -335,6 +397,72 @@ For detailed API documentation, see [API Documentation](docs/api.md).
}
}
```

### Get provider instance
#### Request

`GET /providers/digitalocean/secondary`

curl -X 'GET' 'http://localhost:8000/providers/digitalocean/secondary' -H 'accept: application/json'

#### Response
```json
{
"metadata": {
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2024-02-24T08:00:00Z"
},
"message": "Provider 'digitalocean' instance 'secondary' configuration retrieved successfully",
"provider": "digitalocean",
"instance": "secondary",
"config": {
"enabled": true,
"ips": ["192.168.1.2"],
"scaling": {
"min_scaling": 1,
"max_scaling": 3
},
"size": "s-1vcpu-1gb",
"region": "nyc1",
"display_name": "US Account"
}
}
```

### Update provider instance scaling
#### Request

`PATCH /providers/digitalocean/secondary`

curl -X 'PATCH' 'http://localhost:8000/providers/digitalocean/secondary' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{"min_scaling": 2, "max_scaling": 5}'

#### Response
```json
{
"metadata": {
"request_id": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2024-02-24T08:00:00Z"
},
"message": "Provider 'digitalocean' instance 'secondary' scaling configuration updated successfully",
"provider": "digitalocean",
"instance": "secondary",
"config": {
"enabled": true,
"ips": ["192.168.1.2"],
"scaling": {
"min_scaling": 2,
"max_scaling": 5
},
"size": "s-1vcpu-1gb",
"region": "nyc1",
"display_name": "US Account"
}
}
```

CloudProxy runs on a schedule of every 30 seconds, it will check if the minimum scaling has been met, if not then it will deploy the required number of proxies. The new proxy info will appear in IPs once they are deployed and ready to be used.

<!-- ROADMAP -->
Expand Down
82 changes: 62 additions & 20 deletions cloudproxy-ui/src/components/ListProxies.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
<template>
<div>
<div
v-for="provider in sortedProviders"
:key="provider.key"
v-for="provider in sortedProviderInstances"
:key="`${provider.providerKey}-${provider.instanceKey}`"
class="provider-section"
>
<div class="provider-header">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center">
<div class="provider-icon-wrapper me-2">
<i
:class="'bi bi-' + getProviderIcon(provider.key)"
:class="'bi bi-' + getProviderIcon(provider.providerKey)"
class="provider-icon"
style="font-size: 1.5rem;"
/>
</div>
<h2 class="mb-0">
{{ formatProviderName(provider.key) }}
{{ provider.data.display_name || formatProviderName(provider.providerKey, provider.instanceKey) }}
</h2>
</div>
<form
class="scaling-control"
@submit.prevent="updateProvider(provider.key, provider.data.scaling.min_scaling)"
@submit.prevent="updateProvider(provider.providerKey, provider.instanceKey, provider.data.scaling.min_scaling)"
>
<div class="d-flex align-items-center">
<span
Expand All @@ -46,7 +46,7 @@
min="0"
max="100"
class="form-control custom-spinbutton"
@change="updateProvider(provider.key, $event.target.value)"
@change="updateProvider(provider.providerKey, provider.instanceKey, $event.target.value)"
>
</div>
</form>
Expand Down Expand Up @@ -198,37 +198,74 @@ export default {
auth_enabled: true
});

const sortedProviders = computed(() => {
// Convert data object to array of {key, data} pairs
const providers = Object.entries(data.value).map(([key, providerData]) => ({
key,
data: providerData
}));
const sortedProviderInstances = computed(() => {
// Create array of provider instances
const providers = [];

// Loop through each provider
Object.entries(data.value).forEach(([providerKey, providerData]) => {
// Handle both old format (without instances) and new format (with instances)
if (providerData.instances) {
// New format with instances
Object.entries(providerData.instances).forEach(([instanceKey, instanceData]) => {
providers.push({
providerKey,
instanceKey,
data: instanceData
});
});
} else {
// Old format for backward compatibility
providers.push({
providerKey,
instanceKey: 'default',
data: providerData
});
}
});

// Sort enabled providers first, then by name
return providers.sort((a, b) => {
if (a.data.enabled && !b.data.enabled) return -1;
if (!a.data.enabled && b.data.enabled) return 1;
return a.key.localeCompare(b.key);

// If same provider type, sort by instance name
if (a.providerKey === b.providerKey) {
// Keep 'default' instance first
if (a.instanceKey === 'default') return -1;
if (b.instanceKey === 'default') return 1;
return a.instanceKey.localeCompare(b.instanceKey);
}

return a.providerKey.localeCompare(b.providerKey);
});
});

const formatProviderName = (name) => {
const formatProviderName = (name, instance = 'default') => {
const specialCases = {
'digitalocean': 'DigitalOcean',
'aws': 'AWS',
'gcp': 'GCP',
'hetzner': 'Hetzner'
'hetzner': 'Hetzner',
'azure': 'Azure'
};
return specialCases[name] || name.charAt(0).toUpperCase() + name.slice(1);

const providerName = specialCases[name] || name.charAt(0).toUpperCase() + name.slice(1);

if (instance === 'default') {
return providerName;
} else {
return `${providerName} (${instance})`;
}
};

const getProviderIcon = (provider) => {
const icons = {
digitalocean: 'water',
aws: 'cloud-fill',
gcp: 'google',
hetzner: 'hdd-rack'
hetzner: 'hdd-rack',
azure: 'microsoft'
};
return icons[provider] || 'cloud-fill';
};
Expand Down Expand Up @@ -289,10 +326,15 @@ export default {
}
};

const updateProvider = async (provider, min_scaling) => {
const updateProvider = async (provider, instance, min_scaling) => {
try {
let update_url = `/providers/${provider}`;
if (instance !== 'default') {
update_url += `/${instance}`;
}

const update_res = await fetch(
"/providers/" + provider,
update_url,
{
method: "PATCH",
headers: {
Expand Down Expand Up @@ -373,7 +415,7 @@ export default {
data,
listremove_data,
auth,
sortedProviders,
sortedProviderInstances,
formatProviderName,
getProviderIcon,
removeProxy,
Expand Down
2 changes: 1 addition & 1 deletion cloudproxy/check.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def fetch_ip(ip_address):

def check_alive(ip_address):
try:
result = requests.get("http://ipecho.net/plain", proxies={'http': "http://" + ip_address + ":8899"}, timeout=3)
result = requests.get("http://ipecho.net/plain", proxies={'http': "http://" + ip_address + ":8899"}, timeout=10)
if result.status_code in (200, 407):
return True
else:
Expand Down
Loading