@@ -92,6 +92,7 @@ public class ApiConstants { | |||||
public static final String AUTH_ENDPOINT = "/auth"; | public static final String AUTH_ENDPOINT = "/auth"; | ||||
public static final String EP_ACTIVATE = "/activate"; | public static final String EP_ACTIVATE = "/activate"; | ||||
public static final String EP_CONFIGS = "/configs"; | public static final String EP_CONFIGS = "/configs"; | ||||
public static final String EP_READY = "/ready"; | |||||
public static final String EP_REGISTER = "/register"; | public static final String EP_REGISTER = "/register"; | ||||
public static final String EP_LOGIN = "/login"; | public static final String EP_LOGIN = "/login"; | ||||
public static final String EP_APP_LOGIN = "/appLogin"; | public static final String EP_APP_LOGIN = "/appLogin"; | ||||
@@ -10,8 +10,8 @@ import bubble.dao.app.AppDataDAO; | |||||
import bubble.model.device.BubbleDeviceType; | import bubble.model.device.BubbleDeviceType; | ||||
import bubble.model.device.Device; | import bubble.model.device.Device; | ||||
import bubble.server.BubbleConfiguration; | import bubble.server.BubbleConfiguration; | ||||
import lombok.NonNull; | |||||
import bubble.service.cloud.DeviceIdService; | import bubble.service.cloud.DeviceIdService; | ||||
import lombok.NonNull; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.hibernate.criterion.Order; | import org.hibernate.criterion.Order; | ||||
import org.springframework.beans.factory.annotation.Autowired; | import org.springframework.beans.factory.annotation.Autowired; | ||||
@@ -20,24 +20,29 @@ import org.springframework.stereotype.Repository; | |||||
import javax.transaction.Transactional; | import javax.transaction.Transactional; | ||||
import java.io.File; | import java.io.File; | ||||
import java.util.List; | import java.util.List; | ||||
import java.util.Optional; | |||||
import static bubble.ApiConstants.HOME_DIR; | import static bubble.ApiConstants.HOME_DIR; | ||||
import static bubble.model.device.Device.UNINITIALIZED_DEVICE_LIKE; | import static bubble.model.device.Device.UNINITIALIZED_DEVICE_LIKE; | ||||
import static bubble.model.device.Device.newUninitializedDevice; | import static bubble.model.device.Device.newUninitializedDevice; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||||
import static java.util.concurrent.TimeUnit.MINUTES; | |||||
import static java.util.concurrent.TimeUnit.SECONDS; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||||
import static org.cobbzilla.util.daemon.ZillaRuntime.now; | |||||
import static org.cobbzilla.util.io.FileUtil.abs; | import static org.cobbzilla.util.io.FileUtil.abs; | ||||
import static org.cobbzilla.util.io.FileUtil.touch; | import static org.cobbzilla.util.io.FileUtil.touch; | ||||
import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | import static org.cobbzilla.util.reflect.ReflectionUtil.copy; | ||||
import static org.cobbzilla.util.system.Sleep.sleep; | |||||
@Repository @Slf4j | @Repository @Slf4j | ||||
public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | ||||
private static final File VPN_REFRESH_USERS_FILE = new File(HOME_DIR, ".algo_refresh_users"); | private static final File VPN_REFRESH_USERS_FILE = new File(HOME_DIR, ".algo_refresh_users"); | ||||
private static final short SPARE_DEVICES_PER_ACCOUNT_MAX = 10; | private static final short SPARE_DEVICES_PER_ACCOUNT_MAX = 10; | ||||
private static final short SPARE_DEVICES_PER_ACCOUNT_THRESHOLD = 5; | |||||
private static final short SPARE_DEVICES_PER_ACCOUNT_THRESHOLD = 10; | |||||
private static final long DEVICE_INIT_TIMEOUT = MINUTES.toMillis(5); | |||||
@Autowired private BubbleConfiguration configuration; | @Autowired private BubbleConfiguration configuration; | ||||
@Autowired private AccountDAO accountDAO; | |||||
@Autowired private AppDataDAO dataDAO; | @Autowired private AppDataDAO dataDAO; | ||||
@Autowired private DeviceIdService deviceIdService; | @Autowired private DeviceIdService deviceIdService; | ||||
@@ -58,38 +63,54 @@ public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | |||||
return super.preCreate(device); | return super.preCreate(device); | ||||
} | } | ||||
private static final Object createLock = new Object(); | |||||
@Transactional | @Transactional | ||||
@Override public Device create(@NonNull final Device device) { | @Override public Device create(@NonNull final Device device) { | ||||
if (device.uninitialized()) return super.create(device); | |||||
device.initDeviceType(); | |||||
final var accountUuid = device.getAccount(); | |||||
final var uninitializedDevices = findByAccountAndUninitialized(accountUuid); | |||||
var newDevicesCreated = false; | |||||
if (uninitializedDevices.size() <= SPARE_DEVICES_PER_ACCOUNT_THRESHOLD | |||||
&& !configuration.getBean(AccountDAO.class).findByUuid(accountUuid).isRoot()) { | |||||
newDevicesCreated = ensureAllSpareDevices(accountUuid, device.getNetwork()); | |||||
synchronized (createLock) { | |||||
if (device.uninitialized()) return super.create(device); | |||||
device.initDeviceType(); | |||||
final var accountUuid = device.getAccount(); | |||||
var uninitializedDevices = findByAccountAndUninitialized(accountUuid); | |||||
if (uninitializedDevices.size() <= SPARE_DEVICES_PER_ACCOUNT_THRESHOLD | |||||
&& !configuration.getBean(AccountDAO.class).findByUuid(accountUuid).isRoot()) { | |||||
if (ensureAllSpareDevices(accountUuid, device.getNetwork())) refreshVpnUsers(); | |||||
} | |||||
final Device result; | |||||
uninitializedDevices = findByAccountAndUninitialized(accountUuid); | |||||
if (uninitializedDevices.isEmpty()) { | |||||
log.warn("create: no uninitialized devices for account " + accountUuid); | |||||
// just create the device now: | |||||
device.initTotpKey(); | |||||
result = super.create(device); | |||||
} else { | |||||
final Device uninitialized; | |||||
Optional<Device> availableDevice = uninitializedDevices.stream().filter(Device::configsOk).findAny(); | |||||
final long start = now(); | |||||
while (availableDevice.isEmpty() && now() - start < DEVICE_INIT_TIMEOUT) { | |||||
if (configuration.testMode()) { | |||||
log.warn("create: no available uninitialized devices and in test mode, using first uninitialized device..."); | |||||
availableDevice = Optional.of(uninitializedDevices.get(0)); | |||||
} else { | |||||
// wait for configs to be ok | |||||
log.warn("create: no available uninitialized devices, waiting..."); | |||||
sleep(SECONDS.toMillis(5), "waiting for available uninitialized device"); | |||||
availableDevice = uninitializedDevices.stream().filter(Device::configsOk).findAny(); | |||||
} | |||||
} | |||||
if (availableDevice.isEmpty()) return die("create: timeout waiting for available uninitialized device"); | |||||
uninitialized = availableDevice.get(); | |||||
copy(uninitialized, device); | |||||
result = super.update(uninitialized); | |||||
} | |||||
deviceIdService.setDeviceSecurityLevel(result); | |||||
return result; | |||||
} | } | ||||
final Device result; | |||||
// run the above creation of spare devices in parallel, but if there were no spare devices loaded before that, | |||||
// create a brand new entry here: | |||||
if (uninitializedDevices.isEmpty()) { | |||||
log.info("create: no uninitialized devices for account " + accountUuid); | |||||
// just create the device now: | |||||
device.initTotpKey(); | |||||
result = super.create(device); | |||||
newDevicesCreated = true; | |||||
} else { | |||||
final var uninitialized = uninitializedDevices.get(0); | |||||
copy(uninitialized, device); | |||||
result = super.update(uninitialized); | |||||
} | |||||
if (newDevicesCreated) refreshVpnUsers(); | |||||
deviceIdService.setDeviceSecurityLevel(result); | |||||
return result; | |||||
} | } | ||||
@Override @NonNull public Device update(@NonNull final Device updateRequest) { | @Override @NonNull public Device update(@NonNull final Device updateRequest) { | ||||
@@ -100,6 +121,7 @@ public class DeviceDAO extends AccountOwnedEntityDAO<Device> { | |||||
toUpdate.update(updateRequest); | toUpdate.update(updateRequest); | ||||
final var updated = super.update(toUpdate); | final var updated = super.update(toUpdate); | ||||
deviceIdService.setDeviceSecurityLevel(updated); | deviceIdService.setDeviceSecurityLevel(updated); | ||||
refreshVpnUsers(); | |||||
return updated; | return updated; | ||||
} | } | ||||
@@ -4,6 +4,7 @@ | |||||
*/ | */ | ||||
package bubble.model.device; | package bubble.model.device; | ||||
import bubble.ApiConstants; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.account.HasAccount; | import bubble.model.account.HasAccount; | ||||
import bubble.model.cloud.BubbleNetwork; | import bubble.model.cloud.BubbleNetwork; | ||||
@@ -18,12 +19,11 @@ import org.cobbzilla.wizard.model.IdentifiableBase; | |||||
import org.cobbzilla.wizard.model.entityconfig.annotations.*; | import org.cobbzilla.wizard.model.entityconfig.annotations.*; | ||||
import org.hibernate.annotations.Type; | import org.hibernate.annotations.Type; | ||||
import javax.persistence.Column; | |||||
import javax.persistence.Entity; | |||||
import javax.persistence.EnumType; | |||||
import javax.persistence.Enumerated; | |||||
import javax.persistence.*; | |||||
import javax.validation.constraints.Size; | import javax.validation.constraints.Size; | ||||
import java.io.File; | |||||
import static bubble.ApiConstants.EP_DEVICES; | import static bubble.ApiConstants.EP_DEVICES; | ||||
import static bubble.model.device.BubbleDeviceType.other; | import static bubble.model.device.BubbleDeviceType.other; | ||||
import static bubble.model.device.BubbleDeviceType.uninitialized; | import static bubble.model.device.BubbleDeviceType.uninitialized; | ||||
@@ -50,6 +50,12 @@ public class Device extends IdentifiableBase implements HasAccount { | |||||
public static final String UNINITIALIZED_DEVICE = "__uninitialized_device__"; | public static final String UNINITIALIZED_DEVICE = "__uninitialized_device__"; | ||||
public static final String UNINITIALIZED_DEVICE_LIKE = UNINITIALIZED_DEVICE+"%"; | public static final String UNINITIALIZED_DEVICE_LIKE = UNINITIALIZED_DEVICE+"%"; | ||||
public static final String VPN_CONFIG_PATH = ApiConstants.HOME_DIR + "/configs/localhost/wireguard/"; | |||||
public File qrFile () { return new File(Device.VPN_CONFIG_PATH+getUuid()+".png"); } | |||||
public File vpnConfFile () { return new File(Device.VPN_CONFIG_PATH+getUuid()+".conf"); } | |||||
public boolean configsOk () { return qrFile().exists() && vpnConfFile().exists(); } | |||||
public Device (Device other) { copy(this, other, CREATE_FIELDS); } | public Device (Device other) { copy(this, other, CREATE_FIELDS); } | ||||
public Device (String uuid) { setUuid(uuid); } | public Device (String uuid) { setUuid(uuid); } | ||||
@@ -4,11 +4,8 @@ | |||||
*/ | */ | ||||
package bubble.resources; | package bubble.resources; | ||||
import bubble.server.BubbleConfiguration; | |||||
import bubble.service.cloud.RequestCoordinationService; | |||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.glassfish.jersey.server.ContainerRequest; | import org.glassfish.jersey.server.ContainerRequest; | ||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import org.springframework.stereotype.Service; | import org.springframework.stereotype.Service; | ||||
import javax.ws.rs.Consumes; | import javax.ws.rs.Consumes; | ||||
@@ -28,9 +25,6 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.ok; | |||||
@Service @Slf4j | @Service @Slf4j | ||||
public class BubbleMagicResource { | public class BubbleMagicResource { | ||||
@Autowired private BubbleConfiguration configuration; | |||||
@Autowired private RequestCoordinationService requestService; | |||||
@GET | @GET | ||||
public Response get(@Context ContainerRequest ctx) { | public Response get(@Context ContainerRequest ctx) { | ||||
return ok("you are ok. the magic is ok too."); | return ok("you are ok. the magic is ok too."); | ||||
@@ -13,6 +13,7 @@ import bubble.dao.bill.AccountPaymentMethodDAO; | |||||
import bubble.dao.bill.BubblePlanDAO; | import bubble.dao.bill.BubblePlanDAO; | ||||
import bubble.dao.cloud.BubbleNodeDAO; | import bubble.dao.cloud.BubbleNodeDAO; | ||||
import bubble.dao.cloud.BubbleNodeKeyDAO; | import bubble.dao.cloud.BubbleNodeKeyDAO; | ||||
import bubble.dao.device.DeviceDAO; | |||||
import bubble.model.CertType; | import bubble.model.CertType; | ||||
import bubble.model.account.*; | import bubble.model.account.*; | ||||
import bubble.model.account.message.*; | import bubble.model.account.message.*; | ||||
@@ -92,6 +93,7 @@ public class AuthResource { | |||||
@Autowired private StandardAuthenticatorService authenticatorService; | @Autowired private StandardAuthenticatorService authenticatorService; | ||||
@Autowired private PromotionService promoService; | @Autowired private PromotionService promoService; | ||||
@Autowired private DeviceIdService deviceIdService; | @Autowired private DeviceIdService deviceIdService; | ||||
@Autowired private DeviceDAO deviceDAO; | |||||
@Autowired private BubbleNodeKeyDAO nodeKeyDAO; | @Autowired private BubbleNodeKeyDAO nodeKeyDAO; | ||||
@Autowired private NodeManagerService nodeManagerService; | @Autowired private NodeManagerService nodeManagerService; | ||||
@@ -107,6 +109,20 @@ public class AuthResource { | |||||
return ok(configuration.getPublicSystemConfigs()); | return ok(configuration.getPublicSystemConfigs()); | ||||
} | } | ||||
@GET @Path(EP_READY) | |||||
public Response getNodeIsReady(@Context ContainerRequest ctx) { | |||||
try { | |||||
if (deviceDAO.findByAccountAndUninitialized(accountDAO.getFirstAdmin().getUuid()) | |||||
.stream() | |||||
.anyMatch(Device::configsOk)) { | |||||
return ok(); | |||||
} | |||||
} catch (Exception e) { | |||||
log.warn("getNodeIsReady: "+shortError(e)); | |||||
} | |||||
return invalid("err.node.notReady"); | |||||
} | |||||
@GET @Path(EP_ACTIVATE) | @GET @Path(EP_ACTIVATE) | ||||
public Response isActivated(@Context ContainerRequest ctx) { return ok(accountDAO.activated()); } | public Response isActivated(@Context ContainerRequest ctx) { return ok(accountDAO.activated()); } | ||||
@@ -4,7 +4,6 @@ | |||||
*/ | */ | ||||
package bubble.resources.account; | package bubble.resources.account; | ||||
import bubble.ApiConstants; | |||||
import bubble.model.account.Account; | import bubble.model.account.Account; | ||||
import bubble.model.device.Device; | import bubble.model.device.Device; | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
@@ -30,14 +29,12 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||||
@Slf4j | @Slf4j | ||||
public class VpnConfigResource { | public class VpnConfigResource { | ||||
public static final String VPN_CONFIG_PATH = ApiConstants.HOME_DIR + "/configs/localhost/wireguard/"; | |||||
private Device device; | private Device device; | ||||
public VpnConfigResource(Device device) { this.device = device; } | public VpnConfigResource(Device device) { this.device = device; } | ||||
public File getQRfile() { | public File getQRfile() { | ||||
final File qrFile = new File(VPN_CONFIG_PATH+device.getUuid()+".png"); | |||||
final File qrFile = device.qrFile(); | |||||
if (!qrFile.exists()) { | if (!qrFile.exists()) { | ||||
// todo: try to regenerate algo users? | // todo: try to regenerate algo users? | ||||
log.error("qrCode: file not found: "+abs(qrFile)); | log.error("qrCode: file not found: "+abs(qrFile)); | ||||
@@ -47,7 +44,7 @@ public class VpnConfigResource { | |||||
} | } | ||||
public File getVpnConfFile() { | public File getVpnConfFile() { | ||||
final File confFile = new File(VPN_CONFIG_PATH+device.getUuid()+".conf"); | |||||
final File confFile = device.vpnConfFile(); | |||||
if (!confFile.exists()) { | if (!confFile.exists()) { | ||||
// todo: try to regenerate algo users? | // todo: try to regenerate algo users? | ||||
log.error("confFile: file not found: "+abs(confFile)); | log.error("confFile: file not found: "+abs(confFile)); | ||||
@@ -35,6 +35,10 @@ public class StandardSyncPasswordService implements SyncPasswordService { | |||||
log.warn("syncPassword: thisNetwork was null, sync_password is impossible"); | log.warn("syncPassword: thisNetwork was null, sync_password is impossible"); | ||||
return; | return; | ||||
} | } | ||||
if (!account.admin()) { | |||||
log.info("syncPassword: not syncing non-admin password"); | |||||
return; | |||||
} | |||||
final AnsibleInstallType installType = thisNetwork.getInstallType(); | final AnsibleInstallType installType = thisNetwork.getInstallType(); | ||||
final SyncPasswordNotification notification = new SyncPasswordNotification(account); | final SyncPasswordNotification notification = new SyncPasswordNotification(account); | ||||
if (installType == AnsibleInstallType.sage) { | if (installType == AnsibleInstallType.sage) { | ||||
@@ -60,9 +60,9 @@ public class NodeProgressMeterConstants { | |||||
{METER_TICK_LAUNCHING_NODE, 1}, | {METER_TICK_LAUNCHING_NODE, 1}, | ||||
{METER_TICK_PREPARING_ROLES, 5}, | {METER_TICK_PREPARING_ROLES, 5}, | ||||
{METER_TICK_PREPARING_INSTALL, 7}, | {METER_TICK_PREPARING_INSTALL, 7}, | ||||
{METER_TICK_STARTING_INSTALL, 33}, | |||||
{METER_TICK_COPYING_ANSIBLE, 34}, | |||||
{METER_TICK_RUNNING_ANSIBLE, 37} | |||||
{METER_TICK_STARTING_INSTALL, 23}, | |||||
{METER_TICK_COPYING_ANSIBLE, 24}, | |||||
{METER_TICK_RUNNING_ANSIBLE, 27} | |||||
}); | }); | ||||
public static List<NodeProgressMeterTick> getStandardTicks(NewNodeNotification nn) { | public static List<NodeProgressMeterTick> getStandardTicks(NewNodeNotification nn) { | ||||
@@ -4,6 +4,7 @@ | |||||
*/ | */ | ||||
package bubble.service.cloud; | package bubble.service.cloud; | ||||
import bubble.client.BubbleNodeClient; | |||||
import bubble.cloud.CloudAndRegion; | import bubble.cloud.CloudAndRegion; | ||||
import bubble.cloud.compute.ComputeServiceDriver; | import bubble.cloud.compute.ComputeServiceDriver; | ||||
import bubble.dao.account.AccountDAO; | import bubble.dao.account.AccountDAO; | ||||
@@ -74,7 +75,8 @@ import static bubble.service.boot.StandardSelfNodeService.*; | |||||
import static bubble.service.cloud.NodeProgressMeter.getProgressMeterKey; | import static bubble.service.cloud.NodeProgressMeter.getProgressMeterKey; | ||||
import static bubble.service.cloud.NodeProgressMeter.getProgressMeterPrefix; | import static bubble.service.cloud.NodeProgressMeter.getProgressMeterPrefix; | ||||
import static bubble.service.cloud.NodeProgressMeterConstants.*; | import static bubble.service.cloud.NodeProgressMeterConstants.*; | ||||
import static java.util.concurrent.TimeUnit.*; | |||||
import static java.util.concurrent.TimeUnit.MINUTES; | |||||
import static java.util.concurrent.TimeUnit.SECONDS; | |||||
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; | import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; | ||||
import static org.cobbzilla.util.daemon.Await.awaitAll; | import static org.cobbzilla.util.daemon.Await.awaitAll; | ||||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | import static org.cobbzilla.util.daemon.ZillaRuntime.*; | ||||
@@ -104,6 +106,7 @@ public class StandardNetworkService implements NetworkService { | |||||
private static final long NET_DEADLOCK_TIMEOUT = MINUTES.toMillis(20); | private static final long NET_DEADLOCK_TIMEOUT = MINUTES.toMillis(20); | ||||
private static final long PLAN_ENABLE_TIMEOUT = PURCHASE_DELAY + SECONDS.toMillis(10); | private static final long PLAN_ENABLE_TIMEOUT = PURCHASE_DELAY + SECONDS.toMillis(10); | ||||
private static final long NODE_START_JOB_TIMEOUT = MINUTES.toMillis(30); | private static final long NODE_START_JOB_TIMEOUT = MINUTES.toMillis(30); | ||||
private static final long NODE_READY_TIMEOUT = MINUTES.toMillis(6); | |||||
@Autowired private AccountDAO accountDAO; | @Autowired private AccountDAO accountDAO; | ||||
@Autowired private AccountSshKeyDAO sshKeyDAO; | @Autowired private AccountSshKeyDAO sshKeyDAO; | ||||
@@ -346,6 +349,28 @@ public class StandardNetworkService implements NetworkService { | |||||
} | } | ||||
if (!setupOk) return die("newNode: error setting up, all retries failed for node: "+node.getUuid()); | if (!setupOk) return die("newNode: error setting up, all retries failed for node: "+node.getUuid()); | ||||
// wait for node to be ready | |||||
final long readyStart = now(); | |||||
boolean ready = false; | |||||
BubbleNodeClient nodeClient = null; | |||||
while (now() - readyStart < NODE_READY_TIMEOUT) { | |||||
sleep(SECONDS.toMillis(2), "newNode: waiting for node ("+node.id()+") to be ready"); | |||||
if (nodeKeyDAO.findFirstByNode(node.getUuid()) == null) continue; | |||||
try { | |||||
if (nodeClient == null) nodeClient = node.getApiQuickClient(configuration); | |||||
if (nodeClient.get(AUTH_ENDPOINT + EP_READY).isSuccess()) { | |||||
log.info("newNode: node ("+node.id()+") is ready!"); | |||||
ready = true; | |||||
break; | |||||
} | |||||
} catch (Exception e) { | |||||
log.warn("newNode: node ("+node.id()+") error checking if ready: "+shortError(e)); | |||||
} | |||||
} | |||||
if (!ready) { | |||||
return die("newNode: timeout waiting for node ("+node.id()+") to be ready"); | |||||
} | |||||
// we are good. | // we are good. | ||||
final BubbleNetworkState finalState = nn.hasRestoreKey() ? BubbleNetworkState.restoring : BubbleNetworkState.running; | final BubbleNetworkState finalState = nn.hasRestoreKey() ? BubbleNetworkState.restoring : BubbleNetworkState.running; | ||||
if (network.getState() != finalState) { | if (network.getState() != finalState) { | ||||
@@ -355,6 +380,7 @@ public class StandardNetworkService implements NetworkService { | |||||
node.setState(BubbleNodeState.running); | node.setState(BubbleNodeState.running); | ||||
nodeDAO.update(node); | nodeDAO.update(node); | ||||
progressMeter.completed(); | progressMeter.completed(); | ||||
log.info("newNode: ready in "+formatDuration(now() - start)); | |||||
} catch (Exception e) { | } catch (Exception e) { | ||||
log.error("newNode: "+e, e); | log.error("newNode: "+e, e); | ||||
@@ -1 +1 @@ | |||||
bubble.version=0.12.5 | |||||
bubble.version=0.12.6 |
@@ -1,13 +1,13 @@ | |||||
[ | [ | ||||
{ "percent": 38,"messageKey":"ansible_deps", "match": "prefix", "pattern":"Building wheel for PyYAML (setup.py): started" }, | |||||
{ "percent": 41,"messageKey":"config_node", "match": "prefix", "pattern":"PLAY [Configure new bubble node]" }, | |||||
{ "percent": 42,"messageKey":"nginx_dhparam", "match": "prefix", "pattern":"TASK [nginx : Create a strong dhparam.pem]" }, | |||||
{ "percent": 55,"messageKey":"nginx_dh_conf", "match": "prefix", "pattern":"TASK [Create dhparam nginx conf]" }, | |||||
{ "percent": 56,"messageKey":"nginx_certbot", "match": "prefix", "pattern":"TASK [nginx : Init certbot]" }, | |||||
{ "percent": 58,"messageKey":"bubble_db", "match": "prefix", "pattern":"TASK [bubble : Populate database]" }, | |||||
{ "percent": 65,"messageKey":"algo_sh", "match": "prefix", "pattern":"TASK [Write install_algo.sh template]" }, | |||||
{ "percent": 92,"messageKey":"restart_algo", "match": "prefix", "pattern":"TASK [Restart algo monitors]" }, | |||||
{ "percent": 95,"messageKey":"mitmproxy_set_cert","match": "prefix", "pattern":"TASK [mitmproxy : Set the cert name]" }, | |||||
{ "percent": 98,"messageKey":"touch_first_setup", "match": "prefix", "pattern":"TASK [finalizer : Touch first-time setup file]" }, | |||||
{ "percent": 100,"messageKey":"ssh_keys", "match": "prefix", "pattern":"TASK [finalizer : Ensure authorized SSH keys are up-to-date]" } | |||||
{ "percent": 29,"messageKey":"ansible_deps", "match": "prefix", "pattern":"Building wheel for PyYAML (setup.py): started" }, | |||||
{ "percent": 31,"messageKey":"config_node", "match": "prefix", "pattern":"PLAY [Configure new bubble node]" }, | |||||
{ "percent": 32,"messageKey":"nginx_dhparam", "match": "prefix", "pattern":"TASK [nginx : Create a strong dhparam.pem]" }, | |||||
{ "percent": 41,"messageKey":"nginx_dh_conf", "match": "prefix", "pattern":"TASK [Create dhparam nginx conf]" }, | |||||
{ "percent": 42,"messageKey":"nginx_certbot", "match": "prefix", "pattern":"TASK [nginx : Init certbot]" }, | |||||
{ "percent": 44,"messageKey":"bubble_db", "match": "prefix", "pattern":"TASK [bubble : Populate database]" }, | |||||
{ "percent": 49,"messageKey":"algo_sh", "match": "prefix", "pattern":"TASK [Write install_algo.sh template]" }, | |||||
{ "percent": 69,"messageKey":"restart_algo", "match": "prefix", "pattern":"TASK [Restart algo monitors]" }, | |||||
{ "percent": 72,"messageKey":"mitmproxy_set_cert","match": "prefix", "pattern":"TASK [mitmproxy : Set the cert name]" }, | |||||
{ "percent": 76,"messageKey":"touch_first_setup", "match": "prefix", "pattern":"TASK [finalizer : Touch first-time setup file]" }, | |||||
{ "percent": 81,"messageKey":"ssh_keys", "match": "prefix", "pattern":"TASK [finalizer : Ensure authorized SSH keys are up-to-date]" } | |||||
] | ] |
@@ -359,7 +359,7 @@ meter_tick_running_ansible=Whipping the batter... | |||||
# Launch progress meter: install ticks | # Launch progress meter: install ticks | ||||
meter_tick_ansible_deps=Mixing the pie filling... | meter_tick_ansible_deps=Mixing the pie filling... | ||||
meter_tick_config_node=Filling the pie... | meter_tick_config_node=Filling the pie... | ||||
meter_tick_nginx_dhparam=Gently adding the lattice top crust... | |||||
meter_tick_nginx_dhparam=Gently weaving the lattice top crust... | |||||
meter_tick_nginx_dh_conf=Glazing the top crust... | meter_tick_nginx_dh_conf=Glazing the top crust... | ||||
meter_tick_nginx_certbot=Checking the temperature... | meter_tick_nginx_certbot=Checking the temperature... | ||||
meter_tick_bubble_db=Putting pie into the oven... | meter_tick_bubble_db=Putting pie into the oven... | ||||
@@ -367,7 +367,7 @@ meter_tick_algo_sh=Baking the pie... | |||||
meter_tick_restart_algo=Removing pie from the oven... | meter_tick_restart_algo=Removing pie from the oven... | ||||
meter_tick_mitmproxy_set_cert=Letting the pie cool a bit... | meter_tick_mitmproxy_set_cert=Letting the pie cool a bit... | ||||
meter_tick_touch_first_setup=Setting the table... | meter_tick_touch_first_setup=Setting the table... | ||||
meter_tick_ssh_keys=Hey everybody, the pie is ready! | |||||
meter_tick_ssh_keys=Ringing the bell... | |||||
#meter_tick_ansible_deps=Installing installer dependencies | #meter_tick_ansible_deps=Installing installer dependencies | ||||
#meter_tick_config_node=Configuration installation | #meter_tick_config_node=Configuration installation | ||||
#meter_tick_nginx_dhparam=Securing SSL libraries | #meter_tick_nginx_dhparam=Securing SSL libraries | ||||
@@ -404,11 +404,10 @@ meter_unknown_error=An unknown error occurred | |||||
title_launch_help_html=Next Steps | title_launch_help_html=Next Steps | ||||
message_launch_help_html=<p>Your Bubble will take about 10 minutes to launch and configure itself.</p> | message_launch_help_html=<p>Your Bubble will take about 10 minutes to launch and configure itself.</p> | ||||
message_launch_help_apps=While you wait for your Bubble to be ready, please install the Bubble app on each of your devices. | |||||
message_launch_help_apps=Please install the Bubble app on each of your devices to connect them to your Bubble. | |||||
message_launch_success_help_html=<p>Congratulations! Your Bubble is now running.</p> | message_launch_success_help_html=<p>Congratulations! Your Bubble is now running.</p> | ||||
message_launch_support=<p>Having trouble? Any questions? Check our our <a target="_blank" rel="noopener noreferrer" href="/support">{{messages.link_support}}</a> resources.</p> | message_launch_support=<p>Having trouble? Any questions? Check our our <a target="_blank" rel="noopener noreferrer" href="/support">{{messages.link_support}}</a> resources.</p> | ||||
message_launch_success_apps=Install the Bubble app on each of your devices and get connected to your Bubble! | |||||
message_launch_success_apps=Install the Bubble app on each of your devices to start using your Bubble! | |||||
# Network statuses | # Network statuses | ||||
msg_network_state_created=initialized | msg_network_state_created=initialized | ||||
@@ -190,6 +190,7 @@ err.name.invalid=Name is invalid | |||||
err.name.networkNameAlreadyExists=Name is already in use | err.name.networkNameAlreadyExists=Name is already in use | ||||
err.name.regexFailed=Name must start with a letter and can only contain letters, numbers, hyphens, periods and underscores | err.name.regexFailed=Name must start with a letter and can only contain letters, numbers, hyphens, periods and underscores | ||||
err.name.mismatch=Name mismatch | err.name.mismatch=Name mismatch | ||||
err.node.notReady=Bubble is not ready yet | |||||
err.password.required=Password is required | err.password.required=Password is required | ||||
err.password.tooShort=Password must be at least 8 characters | err.password.tooShort=Password must be at least 8 characters | ||||
err.password.invalid=Password must contain at least one letter, one number, and one special character | err.password.invalid=Password must contain at least one letter, one number, and one special character | ||||
@@ -4,7 +4,11 @@ | |||||
# | # | ||||
LOG=/tmp/bubble.algo_refresh_users.log | LOG=/tmp/bubble.algo_refresh_users.log | ||||
ALGO_BASE=/root/ansible/roles/algo/algo | |||||
REFRESH_MARKER=${ALGO_BASE}/.refreshing_users | |||||
function die { | function die { | ||||
rm -f ${REFRESH_MARKER} | |||||
echo 1>&2 "${1}" | echo 1>&2 "${1}" | ||||
log "${1}" | log "${1}" | ||||
exit 1 | exit 1 | ||||
@@ -14,7 +18,6 @@ function log { | |||||
echo "$(date): ${1}" >> ${LOG} | echo "$(date): ${1}" >> ${LOG} | ||||
} | } | ||||
ALGO_BASE=/root/ansible/roles/algo/algo | |||||
if [[ ! -d ${ALGO_BASE} ]] ; then | if [[ ! -d ${ALGO_BASE} ]] ; then | ||||
die "Algo VPN directory ${ALGO_BASE} not found" | die "Algo VPN directory ${ALGO_BASE} not found" | ||||
fi | fi | ||||
@@ -27,32 +30,61 @@ if [[ ! -f "${ALGO_BASE}/config.cfg.hbs" ]] ; then | |||||
die "No ${ALGO_BASE}/config.cfg.hbs found" | die "No ${ALGO_BASE}/config.cfg.hbs found" | ||||
fi | fi | ||||
START_TIME=$(date +%s) | |||||
REFRESH_TIMEOUT=300 | |||||
OK_TO_REFRESH=0 | |||||
if [[ -f ${REFRESH_MARKER} ]] ; then | |||||
log "Refresh marker exists: ${REFRESH_MARKER}, waiting for previous refresh run to finish" | |||||
while [[ $(expr $(date +%s) - ${START_TIME}) -lt ${REFRESH_TIMEOUT} ]] ; do | |||||
if [[ ! -f ${REFRESH_MARKER} ]] ; then | |||||
OK_TO_REFRESH=1 | |||||
break | |||||
fi | |||||
sleep 1s | |||||
done | |||||
if [[ ${OK_TO_REFRESH} -eq 0 ]] ; then | |||||
log "Timeout waiting for previous refresh, continuing anyway" | |||||
fi | |||||
touch ${REFRESH_MARKER} | |||||
fi | |||||
ALGO_CONFIG="${ALGO_BASE}/config.cfg" | |||||
ALGO_CONFIG_SHA="$(sha256sum ${ALGO_CONFIG} | cut -f1 -d' ')" | |||||
log "Regenerating algo config..." | log "Regenerating algo config..." | ||||
java -cp /home/bubble/api/bubble.jar bubble.main.BubbleMain generate-algo-conf --algo-config ${ALGO_BASE}/config.cfg.hbs || die "Error writing algo config.cfg" | |||||
log "Updating algo VPN users..." | |||||
cd ${ALGO_BASE} && \ | |||||
python3 -m virtualenv --python="$(command -v python3)" .env \ | |||||
&& source .env/bin/activate \ | |||||
&& python3 -m pip install -U pip virtualenv \ | |||||
&& python3 -m pip install -r requirements.txt \ | |||||
&& ansible-playbook users.yml --tags update-users --skip-tags debug \ | |||||
-e "ca_password=$(cat ${CA_PASS_FILE}) | |||||
provider=local | |||||
server=localhost | |||||
store_cakey=true | |||||
ondemand_cellular=false | |||||
ondemand_wifi=false | |||||
store_pki=true | |||||
dns_adblocking=false | |||||
ssh_tunneling=false | |||||
endpoint={{ endpoint }} | |||||
server_name={{ server_name }}" 2>&1 | tee -a ${LOG} || die "Error running algo users.yml" | |||||
# Archive configs in a place that the BackupService can pick them up | |||||
log "Sync'ing algo VPN users to bubble..." | |||||
CONFIGS_BACKUP=/home/bubble/.BUBBLE_ALGO_CONFIGS.tgz | |||||
cd ${ALGO_BASE} && tar czf ${CONFIGS_BACKUP} configs && chgrp bubble ${CONFIGS_BACKUP} && chmod 660 ${CONFIGS_BACKUP} || die "Error backing up algo configs" | |||||
cd /home/bubble && rm -rf configs/* && tar xzf ${CONFIGS_BACKUP} && chgrp -R bubble configs && chown -R bubble configs && chmod 500 configs || die "Error unpacking algo configs to bubble home" | |||||
log "VPN users successfully sync'd to bubble" | |||||
java -cp /home/bubble/api/bubble.jar bubble.main.BubbleMain generate-algo-conf --algo-config ${ALGO_CONFIG}.hbs || die "Error writing algo config.cfg" | |||||
NEW_ALGO_CONFIG_SHA="$(sha256sum ${ALGO_CONFIG} | cut -f1 -d' ')" | |||||
if [[ ! -z "${ALGO_CONFIG_SHA}" && "${ALGO_CONFIG_SHA}" == "${NEW_ALGO_CONFIG_SHA}" ]] ; then | |||||
log "Algo configuration is unchanged, not refreshing: ${ALGO_CONFIG}" | |||||
else | |||||
log "Updating algo VPN users..." | |||||
cd ${ALGO_BASE} && \ | |||||
python3 -m virtualenv --python="$(command -v python3)" .env \ | |||||
&& source .env/bin/activate \ | |||||
&& python3 -m pip install -U pip virtualenv \ | |||||
&& python3 -m pip install -r requirements.txt \ | |||||
&& ansible-playbook users.yml --tags update-users --skip-tags debug \ | |||||
-e "ca_password=$(cat ${CA_PASS_FILE}) | |||||
provider=local | |||||
server=localhost | |||||
store_cakey=true | |||||
ondemand_cellular=false | |||||
ondemand_wifi=false | |||||
store_pki=true | |||||
dns_adblocking=false | |||||
ssh_tunneling=false | |||||
endpoint={{ endpoint }} | |||||
server_name={{ server_name }}" 2>&1 | tee -a ${LOG} || die "Error running algo users.yml" | |||||
# Archive configs in a place that the BackupService can pick them up | |||||
log "Sync'ing algo VPN users to bubble..." | |||||
CONFIGS_BACKUP=/home/bubble/.BUBBLE_ALGO_CONFIGS.tgz | |||||
cd ${ALGO_BASE} && tar czf ${CONFIGS_BACKUP} configs && chgrp bubble ${CONFIGS_BACKUP} && chmod 660 ${CONFIGS_BACKUP} || die "Error backing up algo configs" | |||||
cd /home/bubble && rm -rf configs/* && tar xzf ${CONFIGS_BACKUP} && chgrp -R bubble configs && chown -R bubble configs && chmod 500 configs || die "Error unpacking algo configs to bubble home" | |||||
log "VPN users successfully sync'd to bubble. Refresh completed in $(expr $(date +%s) - ${START_TIME}) seconds" | |||||
fi | |||||
rm -f ${REFRESH_MARKER} |
@@ -41,9 +41,8 @@ log "Watching marker file..." | |||||
while : ; do | while : ; do | ||||
if [[ $(stat -c %Y ${BUBBLE_USER_MARKER}) -gt $(stat -c %Y ${ALGO_USER_MARKER}) ]] ; then | if [[ $(stat -c %Y ${BUBBLE_USER_MARKER}) -gt $(stat -c %Y ${ALGO_USER_MARKER}) ]] ; then | ||||
touch ${ALGO_USER_MARKER} | touch ${ALGO_USER_MARKER} | ||||
sleep 5s | |||||
log "Refreshing VPN users..." | log "Refreshing VPN users..." | ||||
/usr/local/bin/algo_refresh_users.sh && log "VPN users successfully refreshed" || log "Error refreshing Algo VPN users" | /usr/local/bin/algo_refresh_users.sh && log "VPN users successfully refreshed" || log "Error refreshing Algo VPN users" | ||||
fi | fi | ||||
sleep 10s | |||||
sleep 2s | |||||
done | done |
@@ -223,13 +223,13 @@ def next_layer(next_layer): | |||||
check = check_connection(client_addr, server_addr, fqdns, security_level) | check = check_connection(client_addr, server_addr, fqdns, security_level) | ||||
if check is None or ('passthru' in check and check['passthru']): | if check is None or ('passthru' in check and check['passthru']): | ||||
bubble_log('next_layer: enabling passthru for server_addr' + server_addr+', fqdns='+str(fqdns)) | |||||
bubble_log('next_layer: enabling passthru for server=' + server_addr+', fqdns='+str(fqdns)) | |||||
bubble_activity_log(client_addr, server_addr, 'tls_passthru', fqdns) | bubble_activity_log(client_addr, server_addr, 'tls_passthru', fqdns) | ||||
next_layer_replacement = RawTCPLayer(next_layer.ctx, ignore=True) | next_layer_replacement = RawTCPLayer(next_layer.ctx, ignore=True) | ||||
next_layer.reply.send(next_layer_replacement) | next_layer.reply.send(next_layer_replacement) | ||||
elif 'block' in check and check['block']: | elif 'block' in check and check['block']: | ||||
bubble_log('next_layer: enabling block for server_addr' + server_addr+', fqdns='+str(fqdns)) | |||||
bubble_log('next_layer: enabling block for server=' + server_addr+', fqdns='+str(fqdns)) | |||||
bubble_activity_log(client_addr, server_addr, 'conn_block', fqdns) | bubble_activity_log(client_addr, server_addr, 'conn_block', fqdns) | ||||
next_layer.__class__ = TlsBlock | next_layer.__class__ = TlsBlock | ||||
@@ -108,8 +108,8 @@ | |||||
"response": { | "response": { | ||||
"check": [ | "check": [ | ||||
{"condition": "json.length === 2"}, | {"condition": "json.length === 2"}, | ||||
{"condition": "json[0].getName() === 'root-renamed-device'"}, | |||||
{"condition": "json[1].getName() === 'user-added-device'"} | |||||
{"condition": "_find(json, function (d) { return d.getName() == 'user-added-device'; }) != null"}, | |||||
{"condition": "_find(json, function (d) { return d.getName() == 'root-renamed-device'; }) != null"} | |||||
] | ] | ||||
} | } | ||||
}, | }, | ||||