diff --git a/bubble-server/src/main/java/bubble/ApiConstants.java b/bubble-server/src/main/java/bubble/ApiConstants.java index 5389647a..4db39472 100644 --- a/bubble-server/src/main/java/bubble/ApiConstants.java +++ b/bubble-server/src/main/java/bubble/ApiConstants.java @@ -4,6 +4,7 @@ import bubble.model.cloud.BubbleNode; import com.fasterxml.jackson.databind.JsonNode; import com.warrenstrange.googleauth.GoogleAuthenticator; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomUtils; import org.cobbzilla.util.io.FileUtil; import org.glassfish.grizzly.http.server.Request; @@ -24,8 +25,10 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.io.FileUtil.abs; import static org.cobbzilla.util.io.StreamUtil.stream2string; import static org.cobbzilla.util.json.JsonUtil.json; +import static org.cobbzilla.util.network.NetworkUtil.*; import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; +@Slf4j public class ApiConstants { @Getter(lazy=true) private static final String bubbleDefaultDomain = initDefaultDomain(); @@ -172,8 +175,13 @@ public class ApiConstants { } public static String getRemoteHost(Request req) { - final String remoteHost = req.getHeader("X-Forwarded-For"); - return remoteHost == null ? req.getRemoteAddr() : remoteHost; + final String xff = req.getHeader("X-Forwarded-For"); + final String remoteHost = xff == null ? req.getRemoteAddr() : xff; + if (isPublicIpv4(remoteHost)) return remoteHost; + final String publicIp = getFirstPublicIpv4(); + if (publicIp != null) return publicIp; + final String externalIp = getExternalIp(); + return isPublicIpv4(externalIp) ? externalIp : remoteHost; } public static String getUserAgent(ContainerRequest ctx) { return ctx.getHeaderString(USER_AGENT); } diff --git a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java index 52296666..9d1bfdfc 100644 --- a/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java +++ b/bubble-server/src/main/java/bubble/dao/account/AccountDAO.java @@ -1,18 +1,18 @@ package bubble.dao.account; import bubble.dao.account.message.AccountMessageDAO; +import bubble.dao.app.*; import bubble.dao.cloud.AnsibleRoleDAO; import bubble.dao.cloud.BubbleDomainDAO; import bubble.dao.cloud.BubbleFootprintDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.dao.device.DeviceDAO; -import bubble.dao.app.*; import bubble.model.account.*; +import bubble.model.app.*; import bubble.model.cloud.BubbleDomain; import bubble.model.cloud.BubbleNode; import bubble.model.cloud.CloudCredentials; import bubble.model.cloud.CloudService; -import bubble.model.app.*; import bubble.server.BubbleConfiguration; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.dao.AbstractCRUDDAO; @@ -25,6 +25,7 @@ import javax.transaction.Transactional; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import static bubble.ApiConstants.getRemoteHost; import static bubble.model.account.AccountTemplate.copyTemplateObjects; @@ -86,9 +87,10 @@ public class AccountDAO extends AbstractCRUDDAO { deviceDAO.ensureSpareDevice(accountUuid, thisNode.getNetwork(), true); } - // copy drivers, keep map of old uuid to new driver so we can map rules below if (account.hasParent()) { - daemon(new AccountInitializer(this, account, messageDAO)); + final AccountInitializer init = new AccountInitializer(account, this, messageDAO); + account.setAccountInitializer(init); + daemon(init); } return super.postCreate(account, context); @@ -103,11 +105,53 @@ public class AccountDAO extends AbstractCRUDDAO { return super.postUpdate(account, context); } - public void copyTemplates(Account account) { + public void copyTemplates(Account account, AtomicBoolean ready) { final String parent = account.getParent(); - final Map drivers = new HashMap<>(); final String acct = account.getUuid(); + final Map clouds = new HashMap<>(); + copyTemplateObjects(acct, parent, cloudDAO, new AccountTemplate.CopyTemplate<>() { + @Override public CloudService preCreate(CloudService parentEntity, CloudService accountEntity) { + return accountEntity.setDelegated(parentEntity.getUuid()) + .setCredentials(CloudCredentials.delegate(configuration.getThisNode(), configuration)) + .setTemplate(false); + } + @Override public void postCreate(CloudService parentEntity, CloudService accountEntity) { + clouds.put(parentEntity.getUuid(), accountEntity); + } + }); + + copyTemplateObjects(acct, parent, footprintDAO); + + //noinspection Convert2Diamond -- compilation breaks with <> + copyTemplateObjects(acct, parent, domainDAO, new AccountTemplate.CopyTemplate() { + @Override public BubbleDomain preCreate(BubbleDomain parentEntity, BubbleDomain accountEntity) { + final CloudService publicDns = findDnsCloudService(parentEntity, parentEntity.getPublicDns()); + if (publicDns == null) return null; + return accountEntity + .setDelegated(parentEntity.getUuid()) + .setPublicDns(publicDns.getUuid()); + } + + public CloudService findDnsCloudService(BubbleDomain parentEntity, String cloudDnsUuid) { + final CloudService dns = clouds.get(cloudDnsUuid); + if (dns == null) { + log.error("DNS service "+ cloudDnsUuid +" could not be found for domain "+parentEntity.getUuid()); + return null; + } + final CloudService acctPublicDns = cloudDAO.findByAccountAndName(acct, dns.getName()); + if (acctPublicDns == null) { + log.error("DNS service not found under account "+acct+": "+dns.getName()); + return null; + } + return dns; + } + }); + ready.set(true); + + copyTemplateObjects(acct, parent, roleDAO); + + final Map drivers = new HashMap<>(); copyTemplateObjects(acct, parent, driverDAO, new AccountTemplate.CopyTemplate<>() { @Override public void postCreate(RuleDriver parentEntity, RuleDriver accountEntity) { drivers.put(parentEntity.getUuid(), accountEntity); @@ -162,46 +206,6 @@ public class AccountDAO extends AbstractCRUDDAO { } }); - final Map clouds = new HashMap<>(); - - copyTemplateObjects(acct, parent, cloudDAO, new AccountTemplate.CopyTemplate<>() { - @Override public CloudService preCreate(CloudService parentEntity, CloudService accountEntity) { - return accountEntity.setDelegated(parentEntity.getUuid()) - .setCredentials(CloudCredentials.delegate(configuration.getThisNode(), configuration)) - .setTemplate(false); - } - @Override public void postCreate(CloudService parentEntity, CloudService accountEntity) { - clouds.put(parentEntity.getUuid(), accountEntity); - } - }); - - copyTemplateObjects(acct, parent, roleDAO); - copyTemplateObjects(acct, parent, footprintDAO); - - //noinspection Convert2Diamond -- compilation breaks with <> - copyTemplateObjects(acct, parent, domainDAO, new AccountTemplate.CopyTemplate() { - @Override public BubbleDomain preCreate(BubbleDomain parentEntity, BubbleDomain accountEntity) { - final CloudService publicDns = findDnsCloudService(parentEntity, parentEntity.getPublicDns()); - if (publicDns == null) return null; - return accountEntity - .setDelegated(parentEntity.getUuid()) - .setPublicDns(publicDns.getUuid()); - } - - public CloudService findDnsCloudService(BubbleDomain parentEntity, String cloudDnsUuid) { - final CloudService dns = clouds.get(cloudDnsUuid); - if (dns == null) { - log.error("DNS service "+ cloudDnsUuid +" could not be found for domain "+parentEntity.getUuid()); - return null; - } - final CloudService acctPublicDns = cloudDAO.findByAccountAndName(acct, dns.getName()); - if (acctPublicDns == null) { - log.error("DNS service not found under account "+acct+": "+dns.getName()); - return null; - } - return dns; - } - }); log.info("copyTemplates completed: "+acct); } diff --git a/bubble-server/src/main/java/bubble/dao/account/AccountInitializer.java b/bubble-server/src/main/java/bubble/dao/account/AccountInitializer.java index afd06904..a5f68292 100644 --- a/bubble-server/src/main/java/bubble/dao/account/AccountInitializer.java +++ b/bubble-server/src/main/java/bubble/dao/account/AccountInitializer.java @@ -9,6 +9,8 @@ import bubble.model.account.message.ActionTarget; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import java.util.concurrent.atomic.AtomicBoolean; + import static java.util.concurrent.TimeUnit.SECONDS; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.system.Sleep.sleep; @@ -17,11 +19,20 @@ import static org.cobbzilla.util.system.Sleep.sleep; public class AccountInitializer implements Runnable { public static final int MAX_ACCOUNT_INIT_RETRIES = 3; - public static final long COPY_WAIT_TIME = SECONDS.toMillis(3); + public static final long COPY_WAIT_TIME = SECONDS.toMillis(2); - private AccountDAO accountDAO; private Account account; + private AccountDAO accountDAO; private AccountMessageDAO messageDAO; + private AtomicBoolean ready = new AtomicBoolean(false); + + public AccountInitializer(Account account, AccountDAO accountDAO, AccountMessageDAO messageDAO) { + this.account = account; + this.accountDAO = accountDAO; + this.messageDAO = messageDAO; + } + + public boolean ready() { return ready.get(); } @Override public void run() { try { @@ -30,7 +41,7 @@ public class AccountInitializer implements Runnable { for (int i=0; i INIT_WAIT_TIMEOUT && !accountInitializer.ready()) { + throw invalidEx("err.accountInit.timeout"); + } + log.info("waitForAccountInit: ready in "+formatDuration(now() - start)); + return this; + } + @Transient @Getter @Setter private transient String apiToken; @Transient public String getToken() { return getApiToken(); } diff --git a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java index dcc87df9..161a273b 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AccountsResource.java @@ -84,7 +84,7 @@ public class AccountsResource { .setRemoteHost(getRemoteHost(req)) .setVerifyContact(true); final Account created = accountDAO.newAccount(req, reg, parent); - return ok(created); + return ok(created.waitForAccountInit()); } @GET @Path("/{id}"+EP_DOWNLOAD) 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 7114f2f5..b8097107 100644 --- a/bubble-server/src/main/java/bubble/resources/account/AuthResource.java +++ b/bubble-server/src/main/java/bubble/resources/account/AuthResource.java @@ -1,5 +1,6 @@ package bubble.resources.account; +import bubble.cloud.geoLocation.GeoLocation; import bubble.dao.SessionDAO; import bubble.dao.account.AccountDAO; import bubble.dao.account.AccountPolicyDAO; @@ -17,12 +18,15 @@ import bubble.service.account.StandardAccountMessageService; import bubble.service.backup.RestoreService; import bubble.service.boot.ActivationService; import bubble.service.boot.SageHelloService; +import bubble.service.cloud.GeoService; import bubble.service.notify.NotificationService; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.util.collection.NameAndValue; +import org.cobbzilla.util.string.LocaleUtil; import org.cobbzilla.wizard.auth.LoginRequest; import org.cobbzilla.wizard.stream.FileSendableResource; import org.cobbzilla.wizard.validation.ConstraintViolationBean; +import org.cobbzilla.wizard.validation.SimpleViolationException; import org.cobbzilla.wizard.validation.ValidationResult; import org.glassfish.grizzly.http.server.Request; import org.glassfish.jersey.server.ContainerRequest; @@ -34,7 +38,9 @@ import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import java.io.File; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; import static bubble.ApiConstants.*; @@ -44,6 +50,7 @@ import static bubble.model.cloud.BubbleNetwork.TAG_PARENT_ACCOUNT; import static bubble.model.cloud.notify.NotificationType.retrieve_backup; import static bubble.server.BubbleServer.getRestoreKey; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.http.HttpHeaders.ACCEPT_LANGUAGE; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.daemon.ZillaRuntime.now; import static org.cobbzilla.util.http.HttpContentTypes.APPLICATION_JSON; @@ -66,6 +73,7 @@ public class AuthResource { @Autowired private AccountMessageDAO accountMessageDAO; @Autowired private StandardAccountMessageService messageService; @Autowired private BubbleNodeDAO nodeDAO; + @Autowired private GeoService geoService; @Autowired private BubbleConfiguration configuration; @GET @Path(EP_CONFIGS) @@ -181,7 +189,7 @@ public class AuthResource { if (parent == null) return invalid("err.parent.notFound", "Parent account does not exist: "+parentUuid); final Account account = accountDAO.newAccount(req, request, parent); - return ok(account.setToken(sessionDAO.create(account))); + return ok(account.waitForAccountInit().setToken(sessionDAO.create(account))); } @POST @Path(EP_LOGIN) @@ -342,4 +350,46 @@ public class AuthResource { return ok_empty(); } + @GET @Path("/detect/locale") + public Response detectLocale(@Context Request req, + @Context ContainerRequest ctx) { + final Map locales = new HashMap<>(); + + final String langHeader = normalizeLangHeader(req); + if (langHeader != null) locales.put(ACCEPT_LANGUAGE, langHeader); + + final String remoteHost = getRemoteHost(req); + try { + final Account caller = userPrincipal(ctx); + final GeoLocation loc = geoService.locate(caller.getUuid(), remoteHost); + if (loc != null) { + final List found = LocaleUtil.getDefaultLocales(loc.getCountry()); + for (int i=0; i locales = new HashMap<>(); - - final String langHeader = normalizeLangHeader(req); - if (langHeader != null) locales.put(ACCEPT_LANGUAGE, langHeader); - - final String remoteHost = getRemoteHost(req); - try { - final Account caller = userPrincipal(ctx); - final GeoLocation loc = geoService.locate(caller.getUuid(), remoteHost); - if (loc != null) { - final List found = LocaleUtil.getDefaultLocales(loc.getCountry()); - for (int i=0; i localIps = NetworkUtil.configuredIps(); + @Getter(lazy=true) private final Set localIps = configuredIpsAndExternalIp(); @POST public Response receiveNotification(@Context Request req, @@ -150,7 +151,7 @@ public class InboundNotifyResource { if (fromKey != null) { if (!fromKey.getRemoteHost().equals(remoteHost)) { // if request is from 127.0.0.1, check to see if fromKey is for a local address - if (remoteHost.equals("127.0.0.1") && getLocalIps().contains(fromKey.getRemoteHost())) { + if (isLocalHost(remoteHost) && getLocalIps().contains(fromKey.getRemoteHost())) { log.debug("findFromKey: request from 127.0.0.1 is OK, key is local: "+fromKey.getRemoteHost()+ " (ips="+ StringUtil.toString(getLocalIps())+")"); } else { log.warn("findFromKey: remoteHost for for node " + fromNodeUuid + " (key=" + fromKeyUuid + ", remoteHost=" + fromKey.getRemoteHost() + ") does not match request: " + remoteHost+ " (ips="+ StringUtil.toString(getLocalIps())+")"); diff --git a/bubble-server/src/main/java/bubble/service/cloud/GeoService.java b/bubble-server/src/main/java/bubble/service/cloud/GeoService.java index db98398c..cc73bc35 100644 --- a/bubble-server/src/main/java/bubble/service/cloud/GeoService.java +++ b/bubble-server/src/main/java/bubble/service/cloud/GeoService.java @@ -8,6 +8,7 @@ import bubble.cloud.geoCode.GeoCodeResult; import bubble.cloud.geoCode.GeoCodeServiceDriver; import bubble.cloud.geoLocation.GeoLocation; import bubble.cloud.geoTime.GeoTimeZone; +import bubble.dao.account.AccountDAO; import bubble.dao.cloud.BubbleFootprintDAO; import bubble.dao.cloud.CloudServiceDAO; import bubble.model.account.Account; @@ -37,6 +38,7 @@ public class GeoService { // todo: move to config? public static final int LOC_MAX_DISTANCE = 50000; + @Autowired private AccountDAO accountDAO; @Autowired private CloudServiceDAO cloudDAO; @Autowired private BubbleFootprintDAO footprintDAO; @Autowired private BubbleConfiguration configuration; @@ -112,6 +114,7 @@ public class GeoService { public GeoTimeZone getTimeZone (Account account, String ip) { + if (account == null) account = accountDAO.findFirstAdmin(); final List geoServices = cloudDAO.findByAccountAndType(account.getUuid(), CloudServiceType.geoTime); if (geoServices.isEmpty()) throw new SimpleViolationException("err.geoTimeService.notFound"); geoServices.sort(SORT_PRIORITY); diff --git a/bubble-server/src/main/resources/message_templates/server/en_US/pre_auth/ResourceMessages.properties b/bubble-server/src/main/resources/message_templates/server/en_US/pre_auth/ResourceMessages.properties index 51f72e1a..408dc850 100644 --- a/bubble-server/src/main/resources/message_templates/server/en_US/pre_auth/ResourceMessages.properties +++ b/bubble-server/src/main/resources/message_templates/server/en_US/pre_auth/ResourceMessages.properties @@ -26,6 +26,7 @@ err.phone.invalid=SMS Phone is invalid err.phone.length=SMS Phone is too long err.country.invalid=Country is invalid err.parent.notFound=Parent account does not exist +err.accountInit.timeout=Timeout initializing new account # Login/Registration form form_label_title_login=Login diff --git a/bubble-web b/bubble-web index b500db5e..0cf7321e 160000 --- a/bubble-web +++ b/bubble-web @@ -1 +1 @@ -Subproject commit b500db5e9c4a0e621833e788dc4e6cf5fae5e2ee +Subproject commit 0cf7321e714801118c7ab13ca2202a68936c0dea diff --git a/utils/cobbzilla-utils b/utils/cobbzilla-utils index 862c6282..51b557f2 160000 --- a/utils/cobbzilla-utils +++ b/utils/cobbzilla-utils @@ -1 +1 @@ -Subproject commit 862c6282b7ecdf9b1146f4ecb005cf38be1c4e86 +Subproject commit 51b557f242ea52ed339ac6469adbc42b8873a3e9 diff --git a/utils/cobbzilla-wizard b/utils/cobbzilla-wizard index 86a153ec..54c08985 160000 --- a/utils/cobbzilla-wizard +++ b/utils/cobbzilla-wizard @@ -1 +1 @@ -Subproject commit 86a153ecc2531008afab4e3bfed471f9d957dab7 +Subproject commit 54c08985284f4f81dad3200b04e93384cbdffb12