diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 8dfc437e..f05f831d 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -92,6 +92,7 @@ public class ApiConstants { public static final String AUTH_ENDPOINT = "/auth"; public static final String EP_ACTIVATE = "/activate"; 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_LOGIN = "/login"; public static final String EP_APP_LOGIN = "/appLogin"; diff --git a/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java b/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java index 24a2604b..bca1dc71 100644 --- a/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java +++ b/bubble-server/src/main/java/bubble/dao/device/DeviceDAO.java @@ -10,8 +10,8 @@ import bubble.dao.app.AppDataDAO; import bubble.model.device.BubbleDeviceType; import bubble.model.device.Device; import bubble.server.BubbleConfiguration; -import lombok.NonNull; import bubble.service.cloud.DeviceIdService; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; import org.hibernate.criterion.Order; import org.springframework.beans.factory.annotation.Autowired; @@ -20,24 +20,29 @@ import org.springframework.stereotype.Repository; import javax.transaction.Transactional; import java.io.File; import java.util.List; +import java.util.Optional; import static bubble.ApiConstants.HOME_DIR; import static bubble.model.device.Device.UNINITIALIZED_DEVICE_LIKE; 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.touch; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; +import static org.cobbzilla.util.system.Sleep.sleep; @Repository @Slf4j public class DeviceDAO extends AccountOwnedEntityDAO { 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_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 AccountDAO accountDAO; @Autowired private AppDataDAO dataDAO; @Autowired private DeviceIdService deviceIdService; @@ -58,38 +63,54 @@ public class DeviceDAO extends AccountOwnedEntityDAO { return super.preCreate(device); } + private static final Object createLock = new Object(); + @Transactional @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 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) { @@ -100,6 +121,7 @@ public class DeviceDAO extends AccountOwnedEntityDAO { toUpdate.update(updateRequest); final var updated = super.update(toUpdate); deviceIdService.setDeviceSecurityLevel(updated); + refreshVpnUsers(); return updated; } diff --git a/bubble-server/src/main/java/bubble/model/device/Device.java b/bubble-server/src/main/java/bubble/model/device/Device.java index 80f464c0..39f7a15a 100644 --- a/bubble-server/src/main/java/bubble/model/device/Device.java +++ b/bubble-server/src/main/java/bubble/model/device/Device.java @@ -4,6 +4,7 @@ */ package bubble.model.device; +import bubble.ApiConstants; import bubble.model.account.Account; import bubble.model.account.HasAccount; import bubble.model.cloud.BubbleNetwork; @@ -18,12 +19,11 @@ import org.cobbzilla.wizard.model.IdentifiableBase; import org.cobbzilla.wizard.model.entityconfig.annotations.*; 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 java.io.File; + import static bubble.ApiConstants.EP_DEVICES; import static bubble.model.device.BubbleDeviceType.other; 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_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 (String uuid) { setUuid(uuid); } diff --git a/bubble-server/src/main/java/bubble/resources/BubbleMagicResource.java b/bubble-server/src/main/java/bubble/resources/BubbleMagicResource.java index 5ea11b21..bcd27f73 100644 --- a/bubble-server/src/main/java/bubble/resources/BubbleMagicResource.java +++ b/bubble-server/src/main/java/bubble/resources/BubbleMagicResource.java @@ -4,11 +4,8 @@ */ package bubble.resources; -import bubble.server.BubbleConfiguration; -import bubble.service.cloud.RequestCoordinationService; import lombok.extern.slf4j.Slf4j; import org.glassfish.jersey.server.ContainerRequest; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.ws.rs.Consumes; @@ -28,9 +25,6 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.ok; @Service @Slf4j public class BubbleMagicResource { - @Autowired private BubbleConfiguration configuration; - @Autowired private RequestCoordinationService requestService; - @GET public Response get(@Context ContainerRequest ctx) { return ok("you are ok. the magic is ok too."); diff --git a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java index d1fa89c7..1327ee61 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -13,6 +13,7 @@ import bubble.dao.bill.AccountPaymentMethodDAO; import bubble.dao.bill.BubblePlanDAO; import bubble.dao.cloud.BubbleNodeDAO; import bubble.dao.cloud.BubbleNodeKeyDAO; +import bubble.dao.device.DeviceDAO; import bubble.model.CertType; import bubble.model.account.*; import bubble.model.account.message.*; @@ -92,6 +93,7 @@ public class AuthResource { @Autowired private StandardAuthenticatorService authenticatorService; @Autowired private PromotionService promoService; @Autowired private DeviceIdService deviceIdService; + @Autowired private DeviceDAO deviceDAO; @Autowired private BubbleNodeKeyDAO nodeKeyDAO; @Autowired private NodeManagerService nodeManagerService; @@ -107,6 +109,20 @@ public class AuthResource { 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) public Response isActivated(@Context ContainerRequest ctx) { return ok(accountDAO.activated()); } diff --git a/bubble-server/src/main/java/bubble/resources/account/VpnConfigResource.java b/bubble-server/src/main/java/bubble/resources/account/VpnConfigResource.java index 3886dd40..4d784469 100644 --- a/bubble-server/src/main/java/bubble/resources/account/VpnConfigResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/VpnConfigResource.java @@ -4,7 +4,6 @@ */ package bubble.resources.account; -import bubble.ApiConstants; import bubble.model.account.Account; import bubble.model.device.Device; import lombok.extern.slf4j.Slf4j; @@ -30,14 +29,12 @@ import static org.cobbzilla.wizard.resources.ResourceUtil.*; @Slf4j public class VpnConfigResource { - public static final String VPN_CONFIG_PATH = ApiConstants.HOME_DIR + "/configs/localhost/wireguard/"; - private Device device; public VpnConfigResource(Device device) { this.device = device; } public File getQRfile() { - final File qrFile = new File(VPN_CONFIG_PATH+device.getUuid()+".png"); + final File qrFile = device.qrFile(); if (!qrFile.exists()) { // todo: try to regenerate algo users? log.error("qrCode: file not found: "+abs(qrFile)); @@ -47,7 +44,7 @@ public class VpnConfigResource { } public File getVpnConfFile() { - final File confFile = new File(VPN_CONFIG_PATH+device.getUuid()+".conf"); + final File confFile = device.vpnConfFile(); if (!confFile.exists()) { // todo: try to regenerate algo users? log.error("confFile: file not found: "+abs(confFile)); diff --git a/bubble-server/src/main/java/bubble/service/account/StandardSyncPasswordService.java b/bubble-server/src/main/java/bubble/service/account/StandardSyncPasswordService.java index d9050ba9..1c06e6b8 100644 --- a/bubble-server/src/main/java/bubble/service/account/StandardSyncPasswordService.java +++ b/bubble-server/src/main/java/bubble/service/account/StandardSyncPasswordService.java @@ -35,6 +35,10 @@ public class StandardSyncPasswordService implements SyncPasswordService { log.warn("syncPassword: thisNetwork was null, sync_password is impossible"); return; } + if (!account.admin()) { + log.info("syncPassword: not syncing non-admin password"); + return; + } final AnsibleInstallType installType = thisNetwork.getInstallType(); final SyncPasswordNotification notification = new SyncPasswordNotification(account); if (installType == AnsibleInstallType.sage) { diff --git a/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterConstants.java b/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterConstants.java index befc83e3..a61e0bab 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterConstants.java +++ b/bubble-server/src/main/java/bubble/service/cloud/NodeProgressMeterConstants.java @@ -60,9 +60,9 @@ public class NodeProgressMeterConstants { {METER_TICK_LAUNCHING_NODE, 1}, {METER_TICK_PREPARING_ROLES, 5}, {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 getStandardTicks(NewNodeNotification nn) { diff --git a/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java b/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java index 132305c9..1bcf3fda 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java +++ b/bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java @@ -4,6 +4,7 @@ */ package bubble.service.cloud; +import bubble.client.BubbleNodeClient; import bubble.cloud.CloudAndRegion; import bubble.cloud.compute.ComputeServiceDriver; 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.getProgressMeterPrefix; 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.cobbzilla.util.daemon.Await.awaitAll; 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 PLAN_ENABLE_TIMEOUT = PURCHASE_DELAY + SECONDS.toMillis(10); 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 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()); + // 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. final BubbleNetworkState finalState = nn.hasRestoreKey() ? BubbleNetworkState.restoring : BubbleNetworkState.running; if (network.getState() != finalState) { @@ -355,6 +380,7 @@ public class StandardNetworkService implements NetworkService { node.setState(BubbleNodeState.running); nodeDAO.update(node); progressMeter.completed(); + log.info("newNode: ready in "+formatDuration(now() - start)); } catch (Exception e) { log.error("newNode: "+e, e); diff --git a/bubble-server/src/main/resources/META-INF/bubble/bubble.properties b/bubble-server/src/main/resources/META-INF/bubble/bubble.properties index 463bfc38..2f277528 100644 --- a/bubble-server/src/main/resources/META-INF/bubble/bubble.properties +++ b/bubble-server/src/main/resources/META-INF/bubble/bubble.properties @@ -1 +1 @@ -bubble.version=0.12.5 +bubble.version=0.12.6 diff --git a/bubble-server/src/main/resources/bubble/node_progress_meter_ticks.json b/bubble-server/src/main/resources/bubble/node_progress_meter_ticks.json index 5343c000..8df87692 100644 --- a/bubble-server/src/main/resources/bubble/node_progress_meter_ticks.json +++ b/bubble-server/src/main/resources/bubble/node_progress_meter_ticks.json @@ -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]" } ] diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties index d4a36cc4..25da1cbb 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/post_auth/ResourceMessages.properties @@ -359,7 +359,7 @@ meter_tick_running_ansible=Whipping the batter... # Launch progress meter: install ticks meter_tick_ansible_deps=Mixing the pie filling... 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_certbot=Checking the temperature... 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_mitmproxy_set_cert=Letting the pie cool a bit... 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_config_node=Configuration installation #meter_tick_nginx_dhparam=Securing SSL libraries @@ -404,11 +404,10 @@ meter_unknown_error=An unknown error occurred title_launch_help_html=Next Steps message_launch_help_html=

Your Bubble will take about 10 minutes to launch and configure itself.

-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=

Congratulations! Your Bubble is now running.

message_launch_support=

Having trouble? Any questions? Check our our {{messages.link_support}} resources.

- -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 msg_network_state_created=initialized diff --git a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties index b43493d5..3ff441be 100644 --- a/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/en_US/server/pre_auth/ResourceMessages.properties @@ -190,6 +190,7 @@ err.name.invalid=Name is invalid 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.mismatch=Name mismatch +err.node.notReady=Bubble is not ready yet err.password.required=Password is required 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 diff --git a/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users.sh b/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users.sh index c2739988..eca53638 100644 --- a/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users.sh +++ b/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users.sh @@ -4,7 +4,11 @@ # LOG=/tmp/bubble.algo_refresh_users.log +ALGO_BASE=/root/ansible/roles/algo/algo +REFRESH_MARKER=${ALGO_BASE}/.refreshing_users + function die { + rm -f ${REFRESH_MARKER} echo 1>&2 "${1}" log "${1}" exit 1 @@ -14,7 +18,6 @@ function log { echo "$(date): ${1}" >> ${LOG} } -ALGO_BASE=/root/ansible/roles/algo/algo if [[ ! -d ${ALGO_BASE} ]] ; then die "Algo VPN directory ${ALGO_BASE} not found" fi @@ -27,32 +30,61 @@ if [[ ! -f "${ALGO_BASE}/config.cfg.hbs" ]] ; then die "No ${ALGO_BASE}/config.cfg.hbs found" 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..." -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} \ No newline at end of file diff --git a/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users_monitor.sh b/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users_monitor.sh index a7d8f4b0..eb006d61 100644 --- a/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users_monitor.sh +++ b/bubble-server/src/main/resources/packer/roles/algo/files/algo_refresh_users_monitor.sh @@ -41,9 +41,8 @@ log "Watching marker file..." while : ; do if [[ $(stat -c %Y ${BUBBLE_USER_MARKER}) -gt $(stat -c %Y ${ALGO_USER_MARKER}) ]] ; then touch ${ALGO_USER_MARKER} - sleep 5s log "Refreshing VPN users..." /usr/local/bin/algo_refresh_users.sh && log "VPN users successfully refreshed" || log "Error refreshing Algo VPN users" fi - sleep 10s + sleep 2s done diff --git a/bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_conn_check.py b/bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_conn_check.py index ad4909af..d65b90fa 100644 --- a/bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_conn_check.py +++ b/bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_conn_check.py @@ -223,13 +223,13 @@ def next_layer(next_layer): check = check_connection(client_addr, server_addr, fqdns, security_level) 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) next_layer_replacement = RawTCPLayer(next_layer.ctx, ignore=True) next_layer.reply.send(next_layer_replacement) 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) next_layer.__class__ = TlsBlock diff --git a/bubble-server/src/test/resources/models/tests/auth/device_crud.json b/bubble-server/src/test/resources/models/tests/auth/device_crud.json index 1c9e238d..73615205 100644 --- a/bubble-server/src/test/resources/models/tests/auth/device_crud.json +++ b/bubble-server/src/test/resources/models/tests/auth/device_crud.json @@ -108,8 +108,8 @@ "response": { "check": [ {"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"} ] } },