# Conflicts - WIP: # bubble-server/src/main/java/bubble/service/stream/StandardAppPrimerService.javapull/58/head
@@ -28,16 +28,23 @@ you can probably figure out how to get things working. If you're running Mac OS | |||
more difficult. | |||
### Download a Bubble Distribution | |||
Download and unzip the latest Bubble release: | |||
* [ZIP file](https://git.bubblev.org/bubblev/bubble/archive/release/adventure.zip) | |||
* [tar.gz archive](https://git.bubblev.org/bubblev/bubble/archive/release/adventure.tar.gz) | |||
Download the [latest Bubble release](https://jenkins.bubblev.org/public/releases/bubble/latest/bubble.zip) | |||
Unzip or untar the archive that you downloaded. | |||
Open a command-line terminal. | |||
Unzip the file that you downloaded: | |||
unzip bubble.zip | |||
Change directories into the directory containing the files that were unzipped: | |||
cd bubble-Adventure_1.x.x # replace `Adventure_1.x.x` with the version that you downloaded | |||
### Install System Software | |||
You'll need to install some software for Bubble to work correctly. | |||
You'll need to install some software for Bubble to work correctly. Run: | |||
./bin/first_time_ubuntu.sh | |||
Run the `bin/first_time_ubuntu.sh` command. | |||
This will grab all the submodules and perform an initial build of all components. | |||
You only need to run this command once, ever, on a given system. | |||
@@ -51,23 +58,14 @@ distributions should work fine. If you are running a different OS or distributio | |||
And then edit it such that all the same packages get installed. | |||
Then submit a pull request and we can add support for your operating system to the main repository. | |||
### Build | |||
Before running Bubble, you need to build it. Run: | |||
./bin/first_time_setup.sh | |||
This will grab all the submodules and perform an initial build of all components. | |||
This will take a while to complete, please be patient. | |||
## Deployment Modes | |||
Bubble runs in three different modes. | |||
In order to launch and use your own Bubble that you can connect devices to and use, | |||
you will progress through each of these modes. | |||
Bubble runs in three different modes. You'll at least need to run a Local Launcher first, then | |||
decide if you want to use a Remote Launcher to manage multiple Bubble nodes, or just launch a single Bubble | |||
directly from the Local Launcher. | |||
#### Local Launcher Mode | |||
In this mode, Bubble runs locally on your machine. You'll setup the various cloud services required to run Bubble, | |||
and use the Local Launcher to fork a Remote Launcher. | |||
and use the Local Launcher to start a Remote Launcher or a Bubble Node. | |||
Learn more about setting up [Local Launcher Mode](docs/local-launcher.md) | |||
@@ -78,7 +76,7 @@ You cannot connect devices to a Bubble in Launcher Mode, you can only use it to | |||
Learn more about setting up [Remote Launcher Mode](docs/remote-launcher.md) | |||
#### Bubble Node Mode | |||
In this mode, the Bubble has been launched by a Remote Launcher and is a proper Bubble Node. | |||
In this mode, the Bubble has been launched by a Local Launcher or a Remote Launcher and is a proper Bubble Node. | |||
You can connect your devices to it and use it as your own private VPN and enhanced internet service. | |||
Learn more about launching a [Bubble Node](docs/launch-node.md) |
@@ -17,6 +17,10 @@ | |||
# | |||
# You install the JDK on the remote node first: apt install openjdk-11-jdk-headless | |||
# | |||
# Environment variables: | |||
# | |||
# BUBBLE_SSH_PORT : SSH port, default is 1202 | |||
# | |||
SCRIPT="${0}" | |||
SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | |||
. ${SCRIPT_DIR}/bubble_common | |||
@@ -34,8 +38,8 @@ mvn -DskipTests=true -Dcheckstyle.skip=true compile && rsync -avzc ./target/clas | |||
if [[ ! -z "${NO_RESTART}" && "${NO_RESTART}" == "norestart" ]] ; then | |||
echo "Patching but not restarting..." | |||
ssh ${HOST} "cd /tmp && cp ~bubble/api/bubble.jar . && cd classes && jar uvf ../bubble.jar . | egrep -v '*/\(*' && cat ../bubble.jar > ~bubble/api/bubble.jar" || die "Error patching remote jar" | |||
ssh -p ${BUBBLE_SSH_PORT} ${HOST} "cd /tmp && cp ~bubble/api/bubble.jar . && cd classes && jar uvf ../bubble.jar . | egrep -v '*/\(*' && cat ../bubble.jar > ~bubble/api/bubble.jar" || die "Error patching remote jar" | |||
else | |||
echo "Patching and restarting..." | |||
ssh ${HOST} "cd /tmp && cp ~bubble/api/bubble.jar . && cd classes && jar uvf ../bubble.jar . | egrep -v '*/\(*' && cat ../bubble.jar > ~bubble/api/bubble.jar && supervisorctl restart bubble" || die "Error patching remote jar" | |||
fi | |||
ssh -p ${BUBBLE_SSH_PORT} ${HOST} "cd /tmp && cp ~bubble/api/bubble.jar . && cd classes && jar uvf ../bubble.jar . | egrep -v '*/\(*' && cat ../bubble.jar > ~bubble/api/bubble.jar && supervisorctl restart bubble" || die "Error patching remote jar" | |||
fi |
@@ -14,6 +14,10 @@ | |||
# Patch the bubble.jar on a remote node. | |||
# This script updates the entire jar file, and takes a lot longer than bpatch | |||
# | |||
# Environment variables | |||
# | |||
# BUBBLE_SSH_PORT : SSH port, default is 1202 | |||
# | |||
SCRIPT="${0}" | |||
SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | |||
. ${SCRIPT_DIR}/bubble_common | |||
@@ -21,6 +25,10 @@ SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | |||
HOST=${1:?no host provided} | |||
NO_RESTART=${2} | |||
if [[ -z "${BUBBLE_SSH_PORT}" ]] ; then | |||
BUBBLE_SSH_PORT="1202" | |||
fi | |||
BUBBLE_SERVER_DIR="${SCRIPT_DIR}/../bubble-server" | |||
if [[ ! -d "${BUBBLE_SERVER_DIR}" ]] ; then | |||
die "bubble-server dir not found: ${BUBBLE_SERVER_DIR}" | |||
@@ -35,7 +43,7 @@ else | |||
fi | |||
if [[ ${ANY_CHANGES} -eq 0 ]] ; then | |||
echo "No changes, not repackaging jar" | |||
scp ./target/bubble*.jar ${HOST}:/tmp/bubble.jar || die "Error copying file to remote host ${HOST}" | |||
scp -P ${BUBBLE_SSH_PORT} ./target/bubble*.jar ${HOST}:/tmp/bubble.jar || die "Error copying file to remote host ${HOST}" | |||
else | |||
if [[ ${ANY_JAR} -eq 0 ]] ; then | |||
echo "No bubble jar file found, rebuilding" | |||
@@ -44,18 +52,18 @@ else | |||
find "./src/main" -type f -newer "$(find "./target" -type f -name "bubble*.jar" | head -1)" | |||
fi | |||
BUBBLE_PRODUCTION=1 mvn -DskipTests=true -Dcheckstyle.skip=true clean package || die "Error packaging jar" | |||
scp ./target/bubble*.jar ${HOST}:/tmp/bubble.jar || die "Error copying file to remote host ${HOST}" | |||
scp -P ${BUBBLE_SSH_PORT} ./target/bubble*.jar ${HOST}:/tmp/bubble.jar || die "Error copying file to remote host ${HOST}" | |||
fi | |||
if [[ ! -z "${NO_RESTART}" && "${NO_RESTART}" == "norestart" ]] ; then | |||
echo "Patching but not restarting..." | |||
ssh ${HOST} "cat /tmp/bubble.jar > ~bubble/api/bubble.jar" | |||
ssh -p ${BUBBLE_SSH_PORT} ${HOST} "cat /tmp/bubble.jar > ~bubble/api/bubble.jar" | |||
else | |||
echo "Patching and restarting..." | |||
ssh ${HOST} "cat /tmp/bubble.jar > ~bubble/api/bubble.jar && supervisorctl restart bubble" | |||
ssh -p ${BUBBLE_SSH_PORT} ${HOST} "cat /tmp/bubble.jar > ~bubble/api/bubble.jar && supervisorctl restart bubble" | |||
fi | |||
if unzip -Z -1 ./target/bubble*.jar | grep -q "^site/$" ; then | |||
echo "Deploying new web..." | |||
ssh ${HOST} "cd ~bubble && unzip -o /tmp/bubble.jar 'site/*' && chown -R bubble:bubble site" | |||
ssh -p ${BUBBLE_SSH_PORT} ${HOST} "cd ~bubble && unzip -o /tmp/bubble.jar 'site/*' && chown -R bubble:bubble site" | |||
fi |
@@ -78,6 +78,7 @@ if [[ ! -z "${BUBBLE_DIST_HOME}" ]] ; then | |||
if [[ ${IS_DEV} -eq 0 ]] ; then | |||
cd ${BUBBLE_DIST_TOP} && rm -f latest && ln -sf ${BUBBLE_VERSION} latest | |||
echo "${BUBBLE_VERSION}" > latest.txt | |||
cd ${BUBBLE_DIST_DIR} && ln -s "$(basename ${BUBBLE_DIST})" bubble.zip && ln -s "$(basename ${BUBBLE_DIST}).sha" bubble.zip.sha256 | |||
fi | |||
echo "Published release: ${BUBBLE_DIST}" | |||
fi |
@@ -0,0 +1,49 @@ | |||
#!/bin/bash | |||
# | |||
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||
# | |||
# | |||
# Perform a full reset of Bubble. Removes the database and all locally stored files | |||
# | |||
# Usage: reset_bubble_full | |||
# | |||
SCRIPT="${0}" | |||
SCRIPT_DIR=$(cd $(dirname ${SCRIPT}) && pwd) | |||
. ${SCRIPT_DIR}/bubble_common | |||
SELF_NODE_JSON="${HOME}/self_node.json" | |||
BUBBLE_VERSIONS_FILE="${HOME}/bubble_versions.properties" | |||
BUBBLE_LOCAL_STORAGE_DIR_DEFAULT=".bubble_local_storage" | |||
BUBBLE_LOCAL_STORAGE_DIR=$(${SCRIPT_DIR}/bin/bconst bubble.cloud.storage.local.LocalStorageDriver.BUBBLE_LOCAL_STORAGE_DIR) | |||
if [[ -z ${BUBBLE_LOCAL_STORAGE_DIR} ]] ; then | |||
echo "Error determining Bubble LocalStorage directory, using default: ${BUBBLE_LOCAL_STORAGE_DIR_DEFAULT}" | |||
BUBBLE_LOCAL_STORAGE_DIR=${BUBBLE_LOCAL_STORAGE_DIR_DEFAULT} | |||
fi | |||
BUBBLE_LOCAL_STORAGE_DIR="${HOME}/${BUBBLE_LOCAL_STORAGE_DIR}" | |||
BUBBLE_CLOUD_DATA_DIR_DEFAULT="bubble_cloudServiceData" | |||
BUBBLE_CLOUD_DATA_DIR=$(${SCRIPT_DIR}/bin/bconst bubble.ApiConstants.BUBBLE_CLOUD_SERVICE_DATA) | |||
if [[ -z ${BUBBLE_CLOUD_DATA_DIR} ]] ; then | |||
echo "Error determining Bubble Cloud Service Data directory, using default: " | |||
BUBBLE_CLOUD_DATA_DIR=${BUBBLE_CLOUD_DATA_DIR_DEFAULT} | |||
fi | |||
BUBBLE_CLOUD_DATA_DIR="${HOME}/${BUBBLE_CLOUD_DATA_DIR}" | |||
rm -f ${SELF_NODE_JSON} || die "Error removing ${SELF_NODE_JSON}" | |||
echo "> removed: ${SELF_NODE_JSON}" | |||
rm -f ${BUBBLE_VERSIONS_FILE} || die "Error removing ${BUBBLE_VERSIONS_FILE}" | |||
echo "> removed: ${BUBBLE_VERSIONS_FILE}" | |||
rm -rf ${BUBBLE_LOCAL_STORAGE_DIR} || die "Error removing ${BUBBLE_LOCAL_STORAGE_DIR}" | |||
echo "> removed: ${BUBBLE_LOCAL_STORAGE_DIR}" | |||
rm -rf ${BUBBLE_CLOUD_DATA_DIR} || die "Error removing ${BUBBLE_CLOUD_DATA_DIR}" | |||
echo "> removed: ${BUBBLE_CLOUD_DATA_DIR}" | |||
dropdb bubble || die "Error dropping bubble database" | |||
echo "> dropped database: bubble" | |||
echo "+++ Bubble Reset Complete" |
@@ -127,6 +127,7 @@ public class ApiConstants { | |||
} | |||
} | |||
public static final String BUBBLE_CLOUD_SERVICE_DATA = "bubble_cloudServiceData"; | |||
public static final File CACERTS_DIR = new File(HOME_DIR, "cacerts"); | |||
public static final File MITMPROXY_CERT_DIR = new File(HOME_DIR, "mitm_certs"); | |||
@@ -21,8 +21,7 @@ import java.util.stream.Collectors; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.notFoundEx; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Slf4j | |||
public class TlsPassthruAppConfigDriver extends AppConfigDriverBase { | |||
@@ -63,12 +62,14 @@ public class TlsPassthruAppConfigDriver extends AppConfigDriverBase { | |||
} | |||
private Set<FlexFeed> loadManageFlexFeeds(Account account, BubbleApp app) { | |||
if (!account.admin()) throw forbiddenEx(); | |||
final TlsPassthruConfig config = getConfig(account, app); | |||
config.getFlexSet(); // ensure names are initialized | |||
return config.getFlexFeedSet(); | |||
} | |||
private Set<FlexFqdn> loadManageFlexDomains(Account account, BubbleApp app) { | |||
if (!account.admin()) throw forbiddenEx(); | |||
final TlsPassthruConfig config = getConfig(account, app); | |||
return !config.hasFlexFqdnList() ? Collections.emptySet() : | |||
Arrays.stream(config.getFlexFqdnList()) | |||
@@ -154,6 +155,7 @@ public class TlsPassthruAppConfigDriver extends AppConfigDriverBase { | |||
} | |||
private List<TlsPassthruFqdn> addFlexFqdn(Account account, BubbleApp app, JsonNode data) { | |||
if (!account.admin()) throw forbiddenEx(); | |||
final JsonNode fqdnNode = data.get(PARAM_FLEX_FQDN); | |||
if (fqdnNode == null || fqdnNode.textValue() == null || empty(fqdnNode.textValue().trim())) { | |||
throw invalidEx("err.flexFqdn.flexFqdnRequired"); | |||
@@ -177,6 +179,7 @@ public class TlsPassthruAppConfigDriver extends AppConfigDriverBase { | |||
} | |||
private Set<TlsPassthruFeed> addFlexFeed(Account account, BubbleApp app, Map<String, String> params, JsonNode data) { | |||
if (!account.admin()) throw forbiddenEx(); | |||
final JsonNode urlNode = data.get(PARAM_FLEX_FEED_URL); | |||
if (urlNode == null || urlNode.textValue() == null || empty(urlNode.textValue().trim())) { | |||
throw invalidEx("err.flexFeedUrl.feedUrlRequired"); | |||
@@ -233,6 +236,7 @@ public class TlsPassthruAppConfigDriver extends AppConfigDriverBase { | |||
} | |||
private List<TlsPassthruFqdn> removeFlexFqdn(Account account, BubbleApp app, String id) { | |||
if (!account.admin()) throw forbiddenEx(); | |||
final AppRule rule = loadRule(account, app); | |||
loadDriver(account, rule, TlsPassthruRuleDriver.class); // validate proper driver | |||
final TlsPassthruConfig config = getConfig(account, app); | |||
@@ -245,6 +249,7 @@ public class TlsPassthruAppConfigDriver extends AppConfigDriverBase { | |||
} | |||
public Set<TlsPassthruFeed> removeFlexFeed(Account account, BubbleApp app, String id) { | |||
if (!account.admin()) throw forbiddenEx(); | |||
final AppRule rule = loadRule(account, app); | |||
loadDriver(account, rule, TlsPassthruRuleDriver.class); // validate proper driver | |||
final TlsPassthruConfig config = getConfig(account, app).removeFlexFeed(id); | |||
@@ -39,9 +39,9 @@ public abstract class ComputeServiceDriverBase | |||
@Override public void postSetup() { | |||
final String prefix = "postSetup("+getClass().getSimpleName()+"/"+cloud.getUuid()+"): "; | |||
if (configuration.isSelfSage()) { | |||
if (configuration.getThisNetwork().sage()) { | |||
if (cloud.delegated()) { | |||
log.info(prefix+"NOT starting NodeReaper for delegated driver"); | |||
log.info(prefix + "NOT starting NodeReaper for delegated driver"); | |||
} else { | |||
synchronized (reapers) { | |||
if (reapers.get(getCredentials()) == null) { | |||
@@ -10,6 +10,7 @@ import lombok.Cleanup; | |||
import lombok.Getter; | |||
import lombok.Setter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.commons.io.IOUtils; | |||
import org.cobbzilla.util.collection.NameAndValue; | |||
import org.cobbzilla.util.handlebars.HandlebarsUtil; | |||
import org.cobbzilla.util.http.HttpMeta; | |||
@@ -21,8 +22,7 @@ import org.cobbzilla.util.io.TempDir; | |||
import org.cobbzilla.wizard.cache.redis.RedisService; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import java.io.File; | |||
import java.io.IOException; | |||
import java.io.*; | |||
import java.util.List; | |||
import java.util.concurrent.TimeUnit; | |||
import java.util.regex.Pattern; | |||
@@ -139,9 +139,15 @@ public abstract class GeoLocateServiceDriverBase<T> extends CloudServiceDriverBa | |||
private File downloadDbFile(HttpRequestBean request, File archive) throws IOException { | |||
Exception lastEx = null; | |||
if (!archive.getParentFile().exists()) { | |||
mkdirOrDie(archive.getParentFile()); | |||
} | |||
for (int i=0; i<MAX_FILE_RETRIES; i++) { | |||
try { | |||
return HttpUtil.getResponse(request).toFile(archive); | |||
@Cleanup final InputStream in = HttpUtil.get(request.getUri(), NameAndValue.toMap(request.getHeaders())); | |||
@Cleanup final OutputStream out = new FileOutputStream(archive); | |||
IOUtils.copyLarge(in, out); | |||
return archive; | |||
} catch (Exception e) { | |||
lastEx = e; | |||
log.warn("downloadDbFile: "+shortError(e)); | |||
@@ -4,13 +4,22 @@ | |||
*/ | |||
package bubble.dao; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.model.account.Account; | |||
import org.cobbzilla.wizard.dao.AbstractSessionDAO; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Repository; | |||
@Repository | |||
public class SessionDAO extends AbstractSessionDAO<Account> { | |||
@Autowired private AccountDAO accountDAO; | |||
@Override protected boolean canStartSession(Account account) { return !account.suspended(); } | |||
@Override public String create(Account account) { | |||
account.setFirstAdmin(account.getUuid().equals(accountDAO.getFirstAdmin().getUuid())); | |||
return super.create(account); | |||
} | |||
} |
@@ -6,7 +6,6 @@ package bubble.dao.account; | |||
import bubble.cloud.CloudServiceDriver; | |||
import bubble.cloud.CloudServiceType; | |||
import bubble.cloud.compute.ComputeNodeSizeType; | |||
import bubble.dao.account.message.AccountMessageDAO; | |||
import bubble.dao.app.*; | |||
import bubble.dao.bill.AccountPaymentArchivedDAO; | |||
@@ -221,8 +220,8 @@ public class AccountDAO extends AbstractCRUDDAO<Account> implements SqlViewSearc | |||
final BubbleNetwork thisNetwork = selfNodeService.getThisNetwork(); | |||
if (parentEntity.delegated() | |||
&& thisNetwork != null | |||
&& thisNetwork.getInstallType() == AnsibleInstallType.node | |||
&& thisNetwork.getComputeSizeType() != ComputeNodeSizeType.local) { | |||
&& thisNetwork.node() | |||
&& thisNetwork.local()) { | |||
// on a node, sub-accounts can use the same cloud/config/credentials as their admin | |||
return accountEntity.setDelegated(parentEntity.getDelegated()) | |||
.setCredentialsJson(parentEntity.getCredentialsJson()) | |||
@@ -20,6 +20,7 @@ import java.io.File; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import static bubble.ApiConstants.BUBBLE_CLOUD_SERVICE_DATA; | |||
import static bubble.ApiConstants.HOME_DIR; | |||
import static org.cobbzilla.util.reflect.ReflectionUtil.getFirstTypeParam; | |||
import static org.cobbzilla.util.security.ShaUtil.sha256_hex; | |||
@@ -79,7 +80,7 @@ public abstract class AccountOwnedEntityDAO<E extends HasAccount> | |||
pathMiddle = cloudServiceUuid; | |||
} | |||
return new File(HOME_DIR + File.separator | |||
+ "bubble_cloudServiceData" + File.separator | |||
+ BUBBLE_CLOUD_SERVICE_DATA + File.separator | |||
+ pathMiddle + File.separator | |||
+ sha.substring(0, 2) + File.separator | |||
+ sha.substring(2, 4) + File.separator | |||
@@ -9,7 +9,6 @@ import bubble.dao.cloud.BubbleNetworkDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.account.AccountSshKey; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.server.BubbleConfiguration; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -53,7 +52,7 @@ public class AccountSshKeyDAO extends AccountOwnedEntityDAO<AccountSshKey> { | |||
final Account owner = accountDAO.findByUuid(key.getAccount()); | |||
final BubbleNetwork thisNetwork = configuration.getThisNetwork(); | |||
if (thisNetwork == null || thisNetwork.getInstallType() == AnsibleInstallType.sage) { | |||
if (thisNetwork == null || thisNetwork.sage()) { | |||
// only allow installation of a key on a sage if the user is the first admin and has no keys | |||
final Account firstAdmin = accountDAO.getFirstAdmin(); | |||
if (owner.getUuid().equals(firstAdmin.getUuid())) { | |||
@@ -65,7 +64,7 @@ public class AccountSshKeyDAO extends AccountOwnedEntityDAO<AccountSshKey> { | |||
} else { | |||
// admin keys are always installed on a node | |||
// never install key for non-admin | |||
key.setInstallSshKey(owner.admin() && thisNetwork.getInstallType() == AnsibleInstallType.node); | |||
key.setInstallSshKey(owner.admin() && thisNetwork.node()); | |||
} | |||
final String hash = sha256_hex(key.getSshPublicKey()); | |||
@@ -7,6 +7,7 @@ package bubble.dao.cloud; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountOwnedEntityDAO; | |||
import bubble.dao.bill.AccountPlanDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.bill.AccountPlan; | |||
import bubble.model.cloud.*; | |||
import bubble.server.BubbleConfiguration; | |||
@@ -24,6 +25,7 @@ import java.util.stream.Collectors; | |||
import static bubble.model.cloud.BubbleNetwork.validateHostname; | |||
import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; | |||
import static org.cobbzilla.wizard.model.Identifiable.UUID; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.forbiddenEx; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; | |||
@Repository @Slf4j | |||
@@ -46,9 +48,14 @@ public class BubbleNetworkDAO extends AccountOwnedEntityDAO<BubbleNetwork> { | |||
if (errors.hasSuggestedName()) network.setName(errors.getSuggestedName()); | |||
} | |||
if (!network.hasNickname()) network.setNickname(network.getName()); | |||
final AnsibleInstallType installType = network.hasForkHost() && configuration.isSageLauncher() | |||
final AnsibleInstallType installType = network.getLaunchType() == LaunchType.fork_sage && configuration.isSageLauncher() | |||
? AnsibleInstallType.sage | |||
: AnsibleInstallType.node; | |||
if (installType == AnsibleInstallType.sage) { | |||
// ensure caller is an admin | |||
final Account account = accountDAO.findByUuid(network.getAccount()); | |||
if (!account.admin()) throw forbiddenEx(); | |||
} | |||
network.setInstallType(installType); | |||
network.setSslPort(installType == AnsibleInstallType.sage ? 443 : configuration.getDefaultSslPort()); | |||
if (!network.hasLocale()) network.setLocale(getDEFAULT_LOCALE()); | |||
@@ -57,7 +57,7 @@ public class RekeyReaderMain extends BaseMain<RekeyOptions> { | |||
} | |||
protected Iterator<Identifiable> getEntityProducer(BubbleConfiguration fromConfig, AtomicReference<Exception> error) { | |||
return new FullEntityIterator(fromConfig, null, error); | |||
return new FullEntityIterator(fromConfig, null, null, null, error); | |||
} | |||
} |
@@ -141,6 +141,9 @@ public class Account extends IdentifiableBaseParentEntity implements TokenPrinci | |||
@Getter @Setter private Boolean admin = false; | |||
public boolean admin () { return bool(admin); } | |||
// set in SessionDAO so UI can know if the user is first admin | |||
@Transient @Getter @Setter private boolean firstAdmin = false; | |||
@ECIndex(unique=true, where="sage = true") @ECField(index=70) | |||
@Getter @Setter private Boolean sage = false; | |||
public boolean sage () { return bool(sage); } | |||
@@ -10,6 +10,7 @@ import bubble.model.account.HasNetwork; | |||
import bubble.model.cloud.BubbleDomain; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.model.cloud.LaunchType; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import lombok.Getter; | |||
import lombok.NoArgsConstructor; | |||
@@ -46,7 +47,7 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
public static final String[] UPDATE_FIELDS = {"description", "paymentMethod", "paymentMethodObject"}; | |||
public static final String[] CREATE_FIELDS = ArrayUtil.append(UPDATE_FIELDS, | |||
"name", "forkHost", "locale", "timezone", "domain", "network", | |||
"name", "launchType", "forkHost", "locale", "timezone", "domain", "network", | |||
"sshKey", "syncAccount", "launchLock", "sendErrors", "sendMetrics", "plan", "footprint"); | |||
@SuppressWarnings("unused") | |||
@@ -159,6 +160,9 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
@JsonIgnore @Transient @Getter @Setter private transient Account accountObject = null; | |||
public boolean hasAccountObject () { return account != null; } | |||
@Transient @Getter @Setter private transient LaunchType launchType = null; | |||
public boolean hasLaunchType () { return launchType != null; } | |||
@Transient @Getter @Setter private transient String forkHost = null; | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
@@ -195,6 +199,7 @@ public class AccountPlan extends IdentifiableBase implements HasNetwork { | |||
.setFootprint(getFootprint()) | |||
.setComputeSizeType(plan.getComputeSizeType()) | |||
.setStorage(storage.getUuid()) | |||
.setLaunchType(hasForkHost() && hasLaunchType() ? getLaunchType() : LaunchType.node) | |||
.setForkHost(hasForkHost() ? getForkHost() : null); | |||
} | |||
@@ -136,6 +136,10 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
@ECIndex @Column(nullable=false, updatable=false, length=60) @ECField(index=70) | |||
@Enumerated(EnumType.STRING) | |||
@Getter @Setter private AnsibleInstallType installType; | |||
public boolean sage() { return installType == AnsibleInstallType.sage; } | |||
public boolean notSage() { return !sage(); } | |||
public boolean node() { return installType == AnsibleInstallType.node; } | |||
public boolean notNode() { return !node(); } | |||
@ECSearchable @ECField(index=80) | |||
@ECForeignKey(entity=AccountSshKey.class) | |||
@@ -146,6 +150,7 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
@ECSearchable @ECField(index=90) | |||
@ECIndex @Column(nullable=false, updatable=false, length=20) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private ComputeNodeSizeType computeSizeType; | |||
public boolean local() { return computeSizeType == ComputeNodeSizeType.local; } | |||
@ECSearchable @ECField(index=100) | |||
@ECForeignKey(entity=BubbleFootprint.class) | |||
@@ -202,8 +207,11 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu | |||
public boolean hasForkHost () { return !empty(forkHost); } | |||
public boolean fork() { return hasForkHost(); } | |||
@ECSearchable @ECField(index=190) | |||
@Column(length=20) | |||
@ECField(index=190) @Column(length=20, updatable=false) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private LaunchType launchType = null; | |||
public boolean hasLaunchType () { return launchType != null; } | |||
@ECSearchable @ECField(index=200) @Column(length=20) | |||
@Enumerated(EnumType.STRING) @Getter @Setter private BubbleNetworkState state = created; | |||
public String hostFromFqdn(String fqdn) { | |||
@@ -131,6 +131,8 @@ public class BubbleNode extends IdentifiableBase implements HasNetwork, HasBubbl | |||
@ECIndex @Column(nullable=false, updatable=false, length=60) | |||
@Enumerated(EnumType.STRING) | |||
@Getter @Setter private AnsibleInstallType installType; | |||
public boolean sage() { return installType == AnsibleInstallType.sage; } | |||
public boolean node() { return installType == AnsibleInstallType.node; } | |||
@ECSearchable @ECField(index=50) | |||
@ECForeignKey(entity=BubbleNode.class, cascade=false) | |||
@@ -0,0 +1,17 @@ | |||
/** | |||
* Copyright (c) 2020 Bubble, Inc. All rights reserved. | |||
* For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||
*/ | |||
package bubble.model.cloud; | |||
import com.fasterxml.jackson.annotation.JsonCreator; | |||
import static bubble.ApiConstants.enumFromString; | |||
public enum LaunchType { | |||
node, fork_node, fork_sage; | |||
@JsonCreator public static LaunchType fromString(String v) { return enumFromString(LaunchType.class, v); } | |||
} |
@@ -34,6 +34,7 @@ public class DeviceStatus { | |||
@Getter @Setter private String bytesReceived; | |||
@Getter @Setter private String receivedUnits; | |||
@Getter @Setter private Integer lastHandshakeDays; | |||
@Getter @Setter private Integer lastHandshakeHours; | |||
@Getter @Setter private Integer lastHandshakeMinutes; | |||
@Getter @Setter private Integer lastHandshakeSeconds; | |||
@@ -101,6 +102,7 @@ public class DeviceStatus { | |||
String unit = parts[i+1].trim(); | |||
if (unit.endsWith(",")) unit = unit.substring(0, unit.length()-1); | |||
switch (unit) { | |||
case "day": case "days": setLastHandshakeDays(count); break; | |||
case "hour": case "hours": setLastHandshakeHours(count); break; | |||
case "minute": case "minutes": setLastHandshakeMinutes(count); break; | |||
case "second": case "seconds": setLastHandshakeSeconds(count); break; | |||
@@ -0,0 +1,19 @@ | |||
package bubble.model.device; | |||
import lombok.Getter; | |||
import lombok.experimental.Accessors; | |||
import java.util.Collection; | |||
@Accessors(chain=true) | |||
public class FlexRouterRemoveRoutes { | |||
@Getter private final FlexRouterPing ping; | |||
@Getter private final String[] routes; | |||
public FlexRouterRemoveRoutes (FlexRouter router, Collection<String> routes) { | |||
this.ping = router.pingObject(); | |||
this.routes = routes.toArray(String[]::new); | |||
} | |||
} |
@@ -8,6 +8,7 @@ import bubble.cloud.CloudAndRegion; | |||
import bubble.model.account.AccountContact; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.LaunchType; | |||
import bubble.model.cloud.NetLocation; | |||
import com.fasterxml.jackson.annotation.JsonIgnore; | |||
import lombok.Getter; | |||
@@ -51,6 +52,8 @@ public class NewNodeNotification { | |||
@Getter @Setter private Boolean fork; | |||
public boolean fork() { return fork != null && fork; } | |||
@Getter @Setter private LaunchType launchType; | |||
@Getter @Setter private String restoreKey; | |||
public boolean hasRestoreKey () { return !empty(restoreKey); } | |||
@@ -47,7 +47,7 @@ public class NotificationHandler_hello_from_sage extends ReceivedNotificationHan | |||
configuration.setSageVersion(payloadNode.getSageVersion()); | |||
// start the app upgrade service, if not running | |||
if (!appUpgradeService.getIsAlive()) appUpgradeService.start(); | |||
if (!appUpgradeService.getIsAlive() && appUpgradeService.shouldRun()) appUpgradeService.start(); | |||
} | |||
final BubbleNode thisNode = configuration.getThisNode(); | |||
@@ -7,7 +7,6 @@ package bubble.notify; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.account.AccountPolicyDAO; | |||
import bubble.dao.cloud.BubbleNodeDAO; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.notify.ReceivedNotification; | |||
import bubble.service.account.SyncAccountNotification; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -49,7 +48,7 @@ public class NotificationHandler_sync_account extends ReceivedNotificationHandle | |||
localAccount.getHashedPassword().setHashedPassword(incomingHashedPassword); | |||
// if we are a node, set skipSync so we don't get caught in an infinite loop | |||
// (the node would notify the sage, which would notify the node, ad infinitum) | |||
localAccount.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); | |||
localAccount.setSkipSync(configuration.getThisNetwork().node()); | |||
// update password, if we are a sage, this will notify all networks of password change | |||
accountDAO.update(localAccount); | |||
} | |||
@@ -63,7 +62,7 @@ public class NotificationHandler_sync_account extends ReceivedNotificationHandle | |||
} | |||
localPolicy.update(incomingPolicy); | |||
localPolicy.setAccountContactsJson(incomingPolicy.getAccountContactsJson()); | |||
localPolicy.setSkipSync(configuration.getThisNetwork().getInstallType() == AnsibleInstallType.node); | |||
localPolicy.setSkipSync(configuration.getThisNetwork().node()); | |||
accountPolicyDAO.update(localPolicy); | |||
} | |||
} | |||
@@ -432,8 +432,9 @@ public class AuthResource { | |||
final BubbleNetwork thisNetwork = configuration.getThisNetwork(); | |||
if (thisNetwork != null | |||
&& thisNetwork.syncAccount() | |||
&& thisNetwork.getInstallType() == AnsibleInstallType.node | |||
&& configuration.hasSageNode()) { | |||
&& thisNetwork.node() | |||
&& configuration.hasSageNode() | |||
&& !configuration.isSelfSage()) { | |||
// check if session is valid on sage | |||
@Cleanup final BubbleNodeClient sageClient = configuration.getSageNode().getApiQuickClient(configuration); | |||
try { | |||
@@ -27,7 +27,9 @@ import java.math.BigInteger; | |||
import java.net.InetAddress; | |||
import static bubble.ApiConstants.EP_STATUS; | |||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static org.cobbzilla.util.network.PortPicker.portIsAvailable; | |||
import static org.cobbzilla.util.system.Sleep.sleep; | |||
import static org.cobbzilla.wizard.resources.ResourceUtil.*; | |||
@Slf4j | |||
@@ -60,6 +62,7 @@ public class FlexRoutersResource extends AccountOwnedResource<FlexRouter, FlexRo | |||
@Override protected Object daoCreate(FlexRouter toCreate) { | |||
toCreate.setRegistered(true); | |||
final Object router = super.daoCreate(toCreate); | |||
sleep(SECONDS.toMillis(12), "waiting for refresh_flex_keys_monitor to write new flex SSH key"); | |||
flexRouterService.interruptSoon(); | |||
return router; | |||
} | |||
@@ -43,7 +43,7 @@ public class FilterHttpRequest { | |||
@Getter @Setter private String contentSecurityPolicy; | |||
public boolean hasContentSecurityPolicy () { return !empty(contentSecurityPolicy); } | |||
public static final Pattern NONCE_PATTERN = Pattern.compile(";\\s*script-src\\s+.*'nonce-([^']+)'"); | |||
public static final Pattern NONCE_PATTERN = Pattern.compile("\\s*script-src\\s+.*?'nonce-([^']+)'"); | |||
@Getter(lazy=true) private final String scriptNonce = initScriptNonce(); | |||
private String initScriptNonce () { | |||
@@ -36,6 +36,7 @@ import org.springframework.beans.factory.annotation.Autowired; | |||
import java.io.File; | |||
import java.io.InputStream; | |||
import java.io.InputStreamReader; | |||
import java.nio.charset.Charset; | |||
import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
@@ -51,7 +52,6 @@ import static org.cobbzilla.util.io.FileUtil.basename; | |||
import static org.cobbzilla.util.io.regex.RegexReplacementFilter.DEFAULT_PREFIX_REPLACEMENT_WITH_MATCH; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.security.ShaUtil.sha256_hex; | |||
import static org.cobbzilla.util.string.StringUtil.UTF8cs; | |||
public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
@@ -105,19 +105,21 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
} | |||
public static final String DEFAULT_INSERTION_REGEX = "<\\s*head[^>]*>"; | |||
public static final String DEFAULT_SCRIPT_OPEN = "<meta charset=\"UTF-8\"><script>"; | |||
public static final String CHARSET_VAR = "{{charset}}"; | |||
public static final String DEFAULT_SCRIPT_OPEN = "<meta charset=\""+CHARSET_VAR+"\"><script>"; | |||
public static final String NONCE_VAR = "{{nonce}}"; | |||
public static final String DEFAULT_SCRIPT_NONCE_OPEN = "<meta charset=\"UTF-8\"><script nonce=\""+NONCE_VAR+"\">"; | |||
public static final String DEFAULT_SCRIPT_NONCE_OPEN = "<meta charset=\""+CHARSET_VAR+"\"><script nonce=\""+NONCE_VAR+"\">"; | |||
public static final String DEFAULT_SCRIPT_CLOSE = "</script>"; | |||
protected static String insertionRegex (String customRegex) { | |||
return empty(customRegex) ? DEFAULT_INSERTION_REGEX : customRegex; | |||
} | |||
protected static String scriptOpen (FilterHttpRequest filterRequest, String customNonceOpen, String customNoNonceOpen) { | |||
return filterRequest.hasScriptNonce() | |||
protected static String scriptOpen (FilterHttpRequest filterRequest, String charset, String customNonceOpen, String customNoNonceOpen) { | |||
return (filterRequest.hasScriptNonce() | |||
? (empty(customNonceOpen) ? DEFAULT_SCRIPT_NONCE_OPEN : customNonceOpen).replace(NONCE_VAR, filterRequest.getScriptNonce()) | |||
: (empty(customNoNonceOpen) ? DEFAULT_SCRIPT_OPEN : customNoNonceOpen); | |||
: (empty(customNoNonceOpen) ? DEFAULT_SCRIPT_OPEN : customNoNonceOpen) | |||
).replace(CHARSET_VAR, charset); | |||
} | |||
protected static String scriptClose (String customClose) { | |||
@@ -153,6 +155,7 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
@Getter(lazy=true) private final String scriptClose = scriptClose(requestModConfig().getScriptClose()); | |||
protected InputStream filterInsertJs(InputStream in, | |||
Charset charset, | |||
FilterHttpRequest filterRequest, | |||
Map<String, Object> filterCtx, | |||
String bubbleJsTemplate, | |||
@@ -161,7 +164,7 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
boolean showIcon) { | |||
final RequestModifierConfig modConfig = requestModConfig(); | |||
final String replacement = DEFAULT_PREFIX_REPLACEMENT_WITH_MATCH | |||
+ scriptOpen(filterRequest, modConfig.getScriptOpenNonce(), modConfig.getScriptOpenNoNonce()) | |||
+ scriptOpen(filterRequest, charset.name(), modConfig.getScriptOpenNonce(), modConfig.getScriptOpenNoNonce()) | |||
+ getBubbleJs(filterRequest, filterCtx, bubbleJsTemplate, defaultSiteTemplate, siteJsInsertionVar, showIcon) | |||
+ getScriptClose(); | |||
@@ -187,7 +190,7 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
if (alternates != null) { | |||
final BubbleAlternateRegexReplacement firstAlt = alternates.get(0); | |||
if (log.isInfoEnabled()) log.info(prefix + "using alternate filter (0): " +firstAlt); | |||
reader = new RegexFilterReader(new InputStreamReader(in), firstAlt.regexFilter(filterRequest, replacement)) | |||
reader = new RegexFilterReader(new InputStreamReader(in, charset), firstAlt.regexFilter(filterRequest, replacement)) | |||
.setName(filterNamePrefix + "(alt0: "+firstAlt.getFqdnMatch()+") " + firstAlt.getInsertionRegex()) | |||
.setMaxMatches(1); | |||
for (int i=1; i<alternates.size(); i++) { | |||
@@ -200,7 +203,7 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
} else { | |||
if (log.isInfoEnabled()) log.info(prefix + "using default filter: " +getInsertionRegex()); | |||
reader = new RegexFilterReader(new InputStreamReader(in), new RegexReplacementFilter(getInsertionRegex(), replacement)) | |||
reader = new RegexFilterReader(new InputStreamReader(in, charset), new RegexReplacementFilter(getInsertionRegex(), replacement)) | |||
.setName(filterNamePrefix + getInsertionRegex()) | |||
.setMaxMatches(1); | |||
} | |||
@@ -213,7 +216,7 @@ public abstract class AbstractAppRuleDriver implements AppRuleDriver { | |||
} | |||
} | |||
return new ReaderInputStream(reader, UTF8cs); | |||
return new ReaderInputStream(reader, charset); | |||
} | |||
protected String getBubbleJs(FilterHttpRequest filterRequest, | |||
@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.Map; | |||
import java.util.Set; | |||
@@ -167,12 +168,12 @@ public interface AppRuleDriver { | |||
default InputStream doFilterRequest(InputStream in) { return in; } | |||
default InputStream filterResponse(FilterHttpRequest filterRequest, InputStream in) { | |||
if (hasNext()) return doFilterResponse(filterRequest, getNext().filterResponse(filterRequest, in)); | |||
return doFilterResponse(filterRequest, in); | |||
default InputStream filterResponse(FilterHttpRequest filterRequest, InputStream in, Charset charset) { | |||
if (hasNext()) return doFilterResponse(filterRequest, getNext().filterResponse(filterRequest, in, charset), charset); | |||
return doFilterResponse(filterRequest, in, charset); | |||
} | |||
default InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) { return in; } | |||
default InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in, Charset charset) { return in; } | |||
default String resolveResource(String res, Map<String, Object> ctx) { | |||
final String resource = locateResource(res); | |||
@@ -30,6 +30,7 @@ import org.glassfish.jersey.server.ContainerRequest; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.net.URI; | |||
import java.nio.charset.Charset; | |||
import java.util.*; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
@@ -331,7 +332,7 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver | |||
public static final String FILTER_CTX_DECISION = "decision"; | |||
public static final String BLOCK_STATS_JS = "BLOCK_STATS_JS"; | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) { | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in, Charset charset) { | |||
final FilterMatchersRequest request = filterRequest.getMatchersResponse().getRequest(); | |||
final String prefix = "doFilterResponse("+filterRequest.getId()+"): "; | |||
@@ -384,14 +385,14 @@ public class BubbleBlockRuleDriver extends TrafficAnalyticsRuleDriver | |||
} | |||
if (bubbleBlockConfig.inPageBlocks() && showStats) { | |||
if (log.isInfoEnabled()) log.info(prefix + "SEND: both inPageBlocks and showStats are true, filtering"); | |||
return filterInsertJs(in, filterRequest, filterCtx, BUBBLE_JS_TEMPLATE, getBubbleJsStatsTemplate(), BLOCK_STATS_JS, showStats); | |||
return filterInsertJs(in, charset, filterRequest, filterCtx, BUBBLE_JS_TEMPLATE, getBubbleJsStatsTemplate(), BLOCK_STATS_JS, showStats); | |||
} | |||
if (bubbleBlockConfig.inPageBlocks()) { | |||
if (log.isInfoEnabled()) log.info(prefix + "SEND: both inPageBlocks is true, filtering"); | |||
return filterInsertJs(in, filterRequest, filterCtx, BUBBLE_JS_TEMPLATE, EMPTY, BLOCK_STATS_JS, showStats); | |||
return filterInsertJs(in, charset, filterRequest, filterCtx, BUBBLE_JS_TEMPLATE, EMPTY, BLOCK_STATS_JS, showStats); | |||
} | |||
if (log.isInfoEnabled()) log.info(prefix+"inserting JS for stats into: "+request.getUrl()+" with Content-Type: "+filterRequest.getContentType()); | |||
return filterInsertJs(in, filterRequest, filterCtx, getBubbleJsStatsTemplate(), null, null, showStats); | |||
return filterInsertJs(in, charset, filterRequest, filterCtx, getBubbleJsStatsTemplate(), null, null, showStats); | |||
} | |||
protected String getBubbleJsStatsTemplate () { | |||
@@ -12,6 +12,7 @@ import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import static org.cobbzilla.util.io.FileUtil.basename; | |||
import static org.cobbzilla.util.io.StreamUtil.stream2string; | |||
@@ -38,10 +39,10 @@ public class JsUserBlockerRuleDriver extends AbstractAppRuleDriver implements Re | |||
return loadTemplate(getDefaultSiteJsTemplate(), basename(getRequestModifierConfig().getSiteJsTemplate())); | |||
} | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) { | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in, Charset charset) { | |||
if (!filterRequest.isHtml()) return in; | |||
final String bubbleJsTemplate = loadTemplate(BUBBLE_JS_TEMPLATE, BUBBLE_JS_TEMPLATE_NAME); | |||
final String siteJsTemplate = getSiteJsTemplate(); | |||
return filterInsertJs(in, filterRequest, null, bubbleJsTemplate, siteJsTemplate, CTX_APPLY_BLOCKS_JS, true); | |||
return filterInsertJs(in, charset, filterRequest, null, bubbleJsTemplate, siteJsTemplate, CTX_APPLY_BLOCKS_JS, true); | |||
} | |||
} |
@@ -18,13 +18,13 @@ import org.cobbzilla.util.io.regex.RegexInsertionFilter; | |||
import org.cobbzilla.util.io.regex.RegexStreamFilter; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.die; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
import static org.cobbzilla.util.string.StringUtil.UTF8cs; | |||
@Slf4j | |||
public class UserBlockerRuleDriver extends AbstractAppRuleDriver { | |||
@@ -60,14 +60,14 @@ public class UserBlockerRuleDriver extends AbstractAppRuleDriver { | |||
protected UserBlockerConfig configObject() { return json(getFullConfig(), UserBlockerConfig.class); } | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in) { | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in, Charset charset) { | |||
if (!filterRequest.isHtml()) return in; | |||
final String requestId = filterRequest.getId(); | |||
final UserBlockerStreamFilter filter = new UserBlockerStreamFilter(requestId, matcher, rule, configuration.getHttp().getBaseUri()); | |||
filter.configure(getFullConfig()); | |||
filter.setDataDAO(appDataDAO); | |||
RegexFilterReader reader = new RegexFilterReader(in, RESPONSE_BUFSIZ, filter).setName("mainFilterReader"); | |||
RegexFilterReader reader = new RegexFilterReader(in, charset, RESPONSE_BUFSIZ, filter).setName("mainFilterReader"); | |||
final UserBlockerConfig config = configObject(); | |||
if (config.hasCommentDecorator()) { | |||
@@ -110,7 +110,7 @@ public class UserBlockerRuleDriver extends AbstractAppRuleDriver { | |||
} | |||
} | |||
return new ReaderInputStream(reader, UTF8cs); | |||
return new ReaderInputStream(reader, charset); | |||
} | |||
protected String startElementRegex(String el) { return "(<\\s*" + el + "[^>]*>)"; } | |||
@@ -143,12 +143,13 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||
return selfNode != null && selfNode.selfSage(); | |||
} | |||
@JsonIgnore @Transient public boolean isSageLauncher() { | |||
return isSelfSage() || !hasSageNode(); | |||
final BubbleNetwork thisNetwork = getThisNetwork(); | |||
return (isSelfSage() || !hasSageNode()) && (thisNetwork == null || thisNetwork.sage()); | |||
} | |||
@JsonIgnore @Transient public boolean isSage() { | |||
final BubbleNetwork thisNetwork = getThisNetwork(); | |||
return thisNetwork != null && thisNetwork.getInstallType() == AnsibleInstallType.sage; | |||
return thisNetwork != null && thisNetwork.sage(); | |||
} | |||
@JsonIgnore @Transient public synchronized BubbleNetwork getThisNetwork () { | |||
@@ -356,7 +357,7 @@ public class BubbleConfiguration extends PgRestServerConfiguration | |||
{TAG_ALLOW_REGISTRATION, thisNetwork == null ? null : thisNetwork.getBooleanTag(TAG_ALLOW_REGISTRATION, false)}, | |||
{TAG_NETWORK_UUID, thisNetwork == null ? null : thisNetwork.getUuid()}, | |||
{TAG_SAGE_LAUNCHER, thisNetwork == null || isSageLauncher()}, | |||
{TAG_BUBBLE_NODE, isSageLauncher() || thisNetwork == null ? null : thisNetwork.getInstallType() == AnsibleInstallType.node}, | |||
{TAG_BUBBLE_NODE, isSageLauncher() || thisNetwork == null ? null : thisNetwork.node()}, | |||
{TAG_PAYMENTS_ENABLED, cloudDAO.paymentsEnabled()}, | |||
{TAG_PROMO_CODE_POLICY, getPromoCodePolicy().name()}, | |||
{TAG_REQUIRE_SEND_METRICS, requireSendMetrics()}, | |||
@@ -43,7 +43,7 @@ public class BubbleFirstTimeListener extends RestServerLifecycleListenerBase<Bub | |||
private static final FirstTimeType FIRST_TIME_TYPE_DEFAULT = FirstTimeType.install; | |||
private static AtomicReference<RedisService> redis = new AtomicReference<>(); | |||
private static final AtomicReference<RedisService> redis = new AtomicReference<>(); | |||
public static String getUnlockKey () { | |||
final RedisService r = redis.get(); | |||
return r == null ? null : r.get(UNLOCK_KEY); | |||
@@ -7,14 +7,13 @@ package bubble.server.listener; | |||
import bubble.dao.account.AccountDAO; | |||
import bubble.dao.cloud.CloudServiceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.CloudService; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.boot.SelfNodeService; | |||
import bubble.service.device.DeviceService; | |||
import bubble.service.cloud.NetworkMonitorService; | |||
import bubble.service.device.DeviceService; | |||
import bubble.service.device.StandardFlexRouterService; | |||
import bubble.service.stream.AppDataCleaner; | |||
import bubble.service.stream.AppPrimerService; | |||
@@ -111,7 +110,7 @@ public class NodeInitializerListener extends RestServerLifecycleListenerBase<Bub | |||
// and start AppDataCleaner | |||
if (thisNode != null) { | |||
final BubbleNetwork thisNetwork = c.getThisNetwork(); | |||
if (thisNetwork != null && thisNetwork.getInstallType() == AnsibleInstallType.node) { | |||
if (thisNetwork != null && thisNetwork.node()) { | |||
c.getBean(AppPrimerService.class).primeApps(); | |||
c.getBean(StandardFlexRouterService.class).start(); | |||
c.getBean(DeviceService.class).initDeviceSecurityLevels(); | |||
@@ -4,7 +4,6 @@ | |||
*/ | |||
package bubble.service.account; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.service.boot.SelfNodeService; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -83,7 +82,7 @@ public class MitmControlService { | |||
public void checkMitmInstalled() { | |||
final BubbleNetwork thisNetwork = selfNodeService.getThisNetwork(); | |||
if (thisNetwork == null || thisNetwork.getInstallType() != AnsibleInstallType.node) { | |||
if (thisNetwork == null || thisNetwork.notNode()) { | |||
throw invalidEx("err.mitm.notInstalled"); | |||
} | |||
} | |||
@@ -168,6 +168,8 @@ public class ActivationService { | |||
.setDomain(domain.getUuid()) | |||
.setDomainName(domain.getName()) | |||
.setComputeSizeType(ComputeNodeSizeType.local) | |||
.setInstallType(AnsibleInstallType.sage) | |||
.setLaunchType(LaunchType.fork_sage) | |||
.setName(request.getNetworkName()) | |||
.setTag(TAG_ALLOW_REGISTRATION, true) | |||
.setTag(TAG_PARENT_ACCOUNT, account.getUuid()) | |||
@@ -148,8 +148,10 @@ public class StandardSelfNodeService implements SelfNodeService { | |||
// start hello sage and spare devices services, if we have a sage that is not ourselves | |||
if (!c.isSage()) { | |||
log.info("onStart: starting SageHelloService"); | |||
c.getBean(SageHelloService.class).start(); | |||
if (thisNode.node() && !c.isSelfSage()) { | |||
log.info("onStart: starting SageHelloService"); | |||
c.getBean(SageHelloService.class).start(); | |||
} | |||
log.info("onStart: building spare devices for all account that are not root account"); | |||
background(() -> { | |||
@@ -165,7 +167,7 @@ public class StandardSelfNodeService implements SelfNodeService { | |||
} | |||
// start RefundService if payments are enabled and this is a SageLauncher | |||
if (c.paymentsEnabled() && c.isSageLauncher()) { | |||
if (c.paymentsEnabled() && c.isSageLauncher() && thisNode.sage()) { | |||
log.info("onStart: starting BillingService and RefundService"); | |||
c.getBean(BillingService.class).start(); | |||
c.getBean(StandardRefundService.class).start(); | |||
@@ -435,7 +437,7 @@ public class StandardSelfNodeService implements SelfNodeService { | |||
@Override public BubblePlan getThisPlan() { | |||
final BubbleNetwork network = safeGetThisNetwork(); | |||
if (network == null) return null; | |||
if (network.getInstallType() != AnsibleInstallType.node) return null; | |||
if (network.notNode()) return null; | |||
final AccountPlan accountPlan = accountPlanDAO.findByNetwork(network.getUuid()); | |||
if (accountPlan == null) return null; | |||
return planDAO.findByUuid(accountPlan.getPlan()); | |||
@@ -451,8 +453,7 @@ public class StandardSelfNodeService implements SelfNodeService { | |||
return ttl < 0 ? Optional.empty() : Optional.of(now() + ttl * 1000); | |||
} | |||
@Override | |||
public void setLogFlag(final boolean logFlag, @NonNull final Optional<Integer> ttlInSeconds) { | |||
@Override public void setLogFlag(final boolean logFlag, @NonNull final Optional<Integer> ttlInSeconds) { | |||
if (logFlag) { | |||
getNodeConfig().set_plaintext(REDIS_LOG_FLAG_KEY, "true", EX, | |||
ttlInSeconds.orElse(isSelfSage() ? TTL_LOG_FLAG_SAGE : TTL_LOG_FLAG_NODE)); | |||
@@ -16,6 +16,7 @@ import bubble.model.bill.BubblePlanApp; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.LaunchType; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.dbfilter.DatabaseFilterService; | |||
import com.github.jknack.handlebars.Handlebars; | |||
@@ -61,6 +62,7 @@ public class AnsiblePrepService { | |||
ComputeServiceDriver computeDriver, | |||
ValidationResult errors, | |||
boolean fork, | |||
LaunchType launchType, | |||
String restoreKey) throws IOException { | |||
final BubbleConfiguration c = configuration; | |||
@@ -113,7 +115,7 @@ public class AnsiblePrepService { | |||
} | |||
// Copy database with new encryption key | |||
final String key = dbFilter.copyDatabase(fork, network, node, account, planApps, new File(bubbleFilesDir, "bubble.sql.gz")); | |||
final String key = dbFilter.copyDatabase(fork, launchType, network, node, account, planApps, new File(bubbleFilesDir, "bubble.sql.gz")); | |||
ctx.put("dbEncryptionKey", key); | |||
// if this is a fork, and current server is local, then sage will be self | |||
@@ -4,7 +4,6 @@ | |||
*/ | |||
package bubble.service.cloud; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.notify.NewNodeNotification; | |||
import bubble.server.BubbleConfiguration; | |||
@@ -80,7 +79,7 @@ public class NodeLaunchMonitor extends SimpleDaemon { | |||
if (thisNetwork == null) { | |||
die("register: thisNetwork is null"); | |||
} else if (configuration.isSageLauncher() || thisNetwork.getInstallType() == AnsibleInstallType.sage) { | |||
} else if (thisNetwork.sage()) { | |||
if (log.isInfoEnabled()) log.info("register: first registration, starting launch monitor"); | |||
start(); | |||
@@ -352,7 +352,7 @@ public class StandardNetworkService implements NetworkService { | |||
if (!setupOk) return launchFailureCanRetry(node, "newNode: error setting up, all retries failed for node: "+node.getUuid()); | |||
// wait for node to be ready | |||
if (node.getInstallType() == AnsibleInstallType.node) { | |||
if (node.node()) { | |||
final long readyStart = now(); | |||
boolean ready = false; | |||
Exception lastEx = null; | |||
@@ -479,7 +479,7 @@ public class StandardNetworkService implements NetworkService { | |||
progressMeter.write(METER_TICK_PREPARING_ROLES); | |||
final Map<String, Object> ctx = ansiblePrep.prepAnsible( | |||
automation, bubbleFilesDir, account, network, node, computeDriver, | |||
errors, nn.fork(), nn.getRestoreKey()); | |||
errors, nn.fork(), nn.getLaunchType(), nn.getRestoreKey()); | |||
if (errors.isInvalid()) { | |||
progressMeter.error(METER_ERROR_ROLE_VALIDATION_ERRORS); | |||
fatalLaunchFailure(node, new MultiViolationException(errors.getViolationBeans())); | |||
@@ -519,7 +519,7 @@ public class StandardNetworkService implements NetworkService { | |||
writeFile(bubbleFilesDir, null, SAGE_KEY_JSON, json(BubbleNodeKey.sageMask(sageKey))); | |||
// write packer keys if launching sage | |||
if (network.getInstallType() == AnsibleInstallType.sage) { | |||
if (network.sage()) { | |||
final File packerPubKeyFile = new File(bubbleFilesDir, PACKER_KEY_NAME+".pub"); | |||
copyFile(packerService.getPackerPublicKey(), packerPubKeyFile); | |||
@@ -726,6 +726,7 @@ public class StandardNetworkService implements NetworkService { | |||
final NewNodeNotification newNodeRequest = new NewNodeNotification() | |||
.setFork(network.fork()) | |||
.setLaunchType(network.getLaunchType()) | |||
.setNodeHost(network) | |||
.setNetLocation(netLocation) | |||
.setLock(lock); | |||
@@ -12,6 +12,7 @@ import bubble.model.account.Account; | |||
import bubble.model.bill.BubblePlanApp; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.BubbleNode; | |||
import bubble.model.cloud.LaunchType; | |||
import bubble.server.BubbleConfiguration; | |||
import lombok.Cleanup; | |||
import lombok.extern.slf4j.Slf4j; | |||
@@ -53,11 +54,12 @@ public class DatabaseFilterService { | |||
public static final String ENV_OLD_DB_KEY = "OLD_DB_KEY"; | |||
public static final String ENV_NEW_DB_KEY = "NEW_DB_KEY"; | |||
public static final String[] FLYWAY_DUMP_OPTIONS = {"--table=flyway_schema_history", "--data-only"}; | |||
public static final String[] FLYWAY_DUMP_OPTIONS = {"--table="+getFlywayTableName(), "--data-only"}; | |||
@Autowired private BubbleConfiguration configuration; | |||
public String copyDatabase(boolean fork, | |||
LaunchType launchType, | |||
BubbleNetwork network, | |||
BubbleNode node, | |||
Account account, | |||
@@ -112,7 +114,7 @@ public class DatabaseFilterService { | |||
@Override public RekeyOptions getOptions() { return readerOptions; } | |||
@Override protected Iterator<Identifiable> getEntityProducer(BubbleConfiguration fromConfig, AtomicReference<Exception> error) { | |||
return fork | |||
? new FullEntityIterator(configuration, network, readerError) | |||
? new FullEntityIterator(configuration, account, network, launchType, readerError) | |||
: new FilteredEntityIterator(configuration, account, network, node, planApps, readerError); | |||
} | |||
}.runInBackground("RekeyReaderMain.reader", readerError::set); | |||
@@ -4,32 +4,49 @@ | |||
*/ | |||
package bubble.service.dbfilter; | |||
import bubble.dao.device.DeviceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.cloud.LaunchType; | |||
import bubble.server.BubbleConfiguration; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
import static bubble.model.device.Device.newUninitializedDevice; | |||
import static org.cobbzilla.wizard.dao.AbstractCRUDDAO.ORDER_CTIME_ASC; | |||
@Slf4j | |||
public class FullEntityIterator extends EntityIterator { | |||
private final BubbleConfiguration config; | |||
private final Account account; | |||
private final BubbleNetwork network; | |||
private final LaunchType launchType; | |||
public FullEntityIterator (BubbleConfiguration config, | |||
Account account, | |||
BubbleNetwork network, | |||
LaunchType launchType, | |||
AtomicReference<Exception> error) { | |||
super(error); | |||
this.config = config; | |||
this.network = network; | |||
this.account = account; | |||
this.launchType = launchType; | |||
} | |||
protected void iterate() { | |||
config.getEntityClasses() | |||
.forEach(c -> addEntities(true, c, config.getDaoForEntityClass(c).findAll(ORDER_CTIME_ASC), | |||
network, null, null)); | |||
if (account != null && launchType != null && launchType == LaunchType.fork_node) { | |||
// add an initial device so that algo starts properly the first time | |||
// name and totp key will be overwritten when the device is initialized for use | |||
log.info("iterate: creating a single dummy device for algo to start properly"); | |||
final var initDevice = newUninitializedDevice(network.getUuid(), account.getUuid()); | |||
add(config.getBean(DeviceDAO.class).create(initDevice)); | |||
} | |||
log.info("iterate: completed"); | |||
} | |||
@@ -9,6 +9,7 @@ import bubble.dao.device.FlexRouterDAO; | |||
import bubble.model.device.DeviceStatus; | |||
import bubble.model.device.FlexRouter; | |||
import bubble.model.device.FlexRouterPing; | |||
import bubble.model.device.FlexRouterRemoveRoutes; | |||
import bubble.service.cloud.GeoService; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Cleanup; | |||
@@ -17,6 +18,8 @@ import org.apache.http.client.HttpClient; | |||
import org.apache.http.client.config.RequestConfig; | |||
import org.apache.http.impl.client.CloseableHttpClient; | |||
import org.apache.http.impl.client.HttpClientBuilder; | |||
import org.cobbzilla.util.collection.ExpirationEvictionPolicy; | |||
import org.cobbzilla.util.collection.ExpirationMap; | |||
import org.cobbzilla.util.collection.SingletonSet; | |||
import org.cobbzilla.util.daemon.AwaitResult; | |||
import org.cobbzilla.util.daemon.SimpleDaemon; | |||
@@ -24,6 +27,7 @@ import org.cobbzilla.util.http.HttpRequestBean; | |||
import org.cobbzilla.util.http.HttpResponseBean; | |||
import org.cobbzilla.util.http.HttpUtil; | |||
import org.cobbzilla.util.io.FileUtil; | |||
import org.cobbzilla.util.string.StringUtil; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
@@ -35,11 +39,14 @@ import java.util.concurrent.ExecutorService; | |||
import java.util.concurrent.Future; | |||
import java.util.concurrent.atomic.AtomicBoolean; | |||
import java.util.concurrent.atomic.AtomicInteger; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
import java.util.stream.Collectors; | |||
import static bubble.ApiConstants.HOME_DIR; | |||
import static bubble.model.device.FlexRouterPing.MAX_PING_AGE; | |||
import static java.util.concurrent.TimeUnit.MINUTES; | |||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static java.util.function.Function.identity; | |||
import static org.cobbzilla.util.daemon.Await.awaitAll; | |||
import static org.cobbzilla.util.daemon.DaemonThreadFactory.fixedPool; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
@@ -57,13 +64,18 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute | |||
private static final long PING_SLEEP_FACTOR = SECONDS.toMillis(2); | |||
// HttpClient timeouts are in seconds | |||
public static final int DEFAULT_PING_TIMEOUT = (int) SECONDS.toSeconds(MAX_PING_AGE/2); | |||
public static final int DEFAULT_PING_TIMEOUT = (int) MAX_PING_AGE/2; | |||
public static final RequestConfig DEFAULT_PING_REQUEST_CONFIG = RequestConfig.custom() | |||
.setConnectTimeout(DEFAULT_PING_TIMEOUT) | |||
.setSocketTimeout(DEFAULT_PING_TIMEOUT) | |||
.setConnectionRequestTimeout(DEFAULT_PING_TIMEOUT).build(); | |||
public static final int DEFAULT_UPDATE_ROUTES_TIMEOUT = (int) SECONDS.toMillis(10); | |||
public static final RequestConfig DEFAULT_UPDATE_ROUTES_REQUEST_CONFIG = RequestConfig.custom() | |||
.setConnectTimeout(DEFAULT_UPDATE_ROUTES_TIMEOUT) | |||
.setSocketTimeout(DEFAULT_UPDATE_ROUTES_TIMEOUT) | |||
.setConnectionRequestTimeout(DEFAULT_UPDATE_ROUTES_TIMEOUT).build(); | |||
// wait for ssh key to be written | |||
private static final long FIRST_TIME_WAIT = SECONDS.toMillis(10); | |||
private static final long INTERRUPT_WAIT = FIRST_TIME_WAIT/2; | |||
@@ -71,14 +83,18 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute | |||
public static final long PING_ALL_TIMEOUT | |||
= (SECONDS.toMillis(1) * DEFAULT_PING_TIMEOUT * MAX_PING_TRIES) + FIRST_TIME_WAIT; | |||
public static final long UPDATE_ROUTES_ALL_TIMEOUT = SECONDS.toMillis(30); | |||
// thread pool size | |||
public static final int DEFAULT_MAX_TUNNELS = 5; | |||
private static CloseableHttpClient getHttpClient() { | |||
private static CloseableHttpClient getHttpClient(RequestConfig requestConfig) { | |||
return HttpClientBuilder.create() | |||
.setDefaultRequestConfig(DEFAULT_PING_REQUEST_CONFIG) | |||
.setDefaultRequestConfig(requestConfig) | |||
.build(); | |||
} | |||
private static CloseableHttpClient getPingHttpClient() { return getHttpClient(DEFAULT_PING_REQUEST_CONFIG); } | |||
private static CloseableHttpClient getUpdateRoutesHttpClient() { return getHttpClient(DEFAULT_UPDATE_ROUTES_REQUEST_CONFIG); } | |||
public static final long DEFAULT_SLEEP_TIME = MINUTES.toMillis(2); | |||
@@ -162,10 +178,52 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute | |||
} | |||
} | |||
private final AtomicReference<Set<String>> mostRecentFlexDomains = new AtomicReference<>(null); | |||
private final Map<String, String> recentlyRemovedFlexDomains = new ExpirationMap<>(MINUTES.toMillis(20), ExpirationEvictionPolicy.ctime_or_atime); | |||
public void updateFlexRoutes(Set<String> flexDomains) { | |||
synchronized (mostRecentFlexDomains) { | |||
final Set<String> mostRecentDomains = mostRecentFlexDomains.get() == null ? Collections.emptySet() : mostRecentFlexDomains.get(); | |||
if (!mostRecentDomains.isEmpty()) { | |||
// what should we remove now? | |||
final Set<String> routesToRemove = new HashSet<>(mostRecentDomains); | |||
routesToRemove.removeAll(flexDomains); | |||
// add to recently removed, we will remove all of these in case a previous update was missed | |||
recentlyRemovedFlexDomains.putAll(routesToRemove.stream().collect(Collectors.toMap(identity(), identity()))); | |||
// but exclude domains that are currently flex routed, we don't want to remove these | |||
for (String current : flexDomains) recentlyRemovedFlexDomains.remove(current); | |||
if (!recentlyRemovedFlexDomains.isEmpty()) { | |||
try { | |||
@Cleanup final CloseableHttpClient httpClient = getUpdateRoutesHttpClient(); | |||
final List<FlexRouter> routers = flexRouterDAO.findEnabledAndRegistered(); | |||
if (log.isDebugEnabled()) log.debug("updateFlexRoutes: updating "+routers.size()+" routers"); | |||
final List<Future<?>> futures = new ArrayList<>(); | |||
@Cleanup("shutdownNow") final ExecutorService exec = fixedPool(DEFAULT_MAX_TUNNELS, "StandardFlexRouterService.updateFlexRoutes"); | |||
final Set<String> routes = recentlyRemovedFlexDomains.keySet(); | |||
for (FlexRouter router : routers) { | |||
if (log.isDebugEnabled()) log.debug("updateFlexRoutes: starting job for router: " + router + " with routes to remove: "+StringUtil.toString(routes)); | |||
futures.add(exec.submit(new FlexRemoveRoutesJob(router, routes, httpClient))); | |||
} | |||
final AwaitResult<Boolean> awaitResult = awaitAll(futures, UPDATE_ROUTES_ALL_TIMEOUT); | |||
if (log.isTraceEnabled()) log.trace("updateFlexRoutes: awaitResult=" + awaitResult); | |||
} catch (Exception e) { | |||
log.error("updateFlexRoutes: " + shortError(e)); | |||
} | |||
} | |||
} else { | |||
if (log.isDebugEnabled()) log.debug("updateFlexRoutes: no routes to remove"); | |||
} | |||
mostRecentFlexDomains.set(flexDomains); | |||
} | |||
} | |||
@Override protected void process() { | |||
synchronized (interrupted) { interrupted.set(false); } | |||
try { | |||
@Cleanup final CloseableHttpClient httpClient = getHttpClient(); | |||
@Cleanup final CloseableHttpClient httpClient = getPingHttpClient(); | |||
final List<FlexRouter> routers = flexRouterDAO.findEnabledAndRegistered(); | |||
if (log.isTraceEnabled()) log.trace("process: starting, will ping "+routers.size()+" routers"); | |||
final List<Future<?>> futures = new ArrayList<>(); | |||
@@ -219,7 +277,7 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute | |||
} | |||
} catch (Exception e) { | |||
log.info(prefix+"error: "+shortError(e)); | |||
log.error(prefix+"error: "+shortError(e)); | |||
} | |||
setStatus(router, FlexRouterStatus.unreachable); | |||
} | |||
@@ -298,7 +356,7 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute | |||
boolean update = false; | |||
if (!active) { | |||
if (pollFailures.computeIfAbsent(router.getUuid(), k -> new AtomicInteger(0)).incrementAndGet() > MAX_POLL_FAILURES) { | |||
log.warn("process: too many poll failures for router ("+router+"), marking unregistered"); | |||
if (log.isWarnEnabled()) log.warn("process: too many poll failures for router ("+router+"), marking unregistered"); | |||
router.setRegistered(false); | |||
update = true; | |||
} | |||
@@ -320,4 +378,31 @@ public class StandardFlexRouterService extends SimpleDaemon implements FlexRoute | |||
} | |||
} | |||
@AllArgsConstructor | |||
private static class FlexRemoveRoutesJob implements Callable<Boolean> { | |||
private final FlexRouter router; | |||
private final Collection<String> routes; | |||
private final HttpClient httpClient; | |||
@Override public Boolean call() { | |||
final String removeUrl = router.proxyBaseUri() + "/remove"; | |||
final HttpRequestBean request = new HttpRequestBean(POST, removeUrl); | |||
final String prefix = "FlexRouterRemoveJob(" + router + ", "+ StringUtil.toString(routes)+"): "; | |||
request.setEntity(json(new FlexRouterRemoveRoutes(router, routes))); | |||
try { | |||
if (log.isDebugEnabled()) log.debug(prefix+"sending JSON message to remove routes..."); | |||
final HttpResponseBean response = HttpUtil.getResponse(request, httpClient); | |||
if (!response.isOk()) { | |||
log.error(prefix+"response not OK: "+response); | |||
} else { | |||
if (log.isDebugEnabled()) log.debug(prefix+"routes removed from router"); | |||
return true; | |||
} | |||
} catch (Exception e) { | |||
log.error(prefix+"error: "+shortError(e)); | |||
} | |||
return false; | |||
} | |||
} | |||
} |
@@ -5,6 +5,9 @@ | |||
package bubble.service.stream; | |||
import bubble.resources.stream.FilterHttpRequest; | |||
import bubble.service.stream.charset.BubbleCharSet; | |||
import bubble.service.stream.charset.CharsetDetector; | |||
import lombok.Cleanup; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.commons.io.IOUtils; | |||
@@ -21,9 +24,11 @@ import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.List; | |||
import java.util.Map; | |||
import static bubble.service.stream.charset.CharsetDetector.charSetDetectorForContentType; | |||
import static java.util.concurrent.TimeUnit.DAYS; | |||
import static java.util.concurrent.TimeUnit.SECONDS; | |||
import static org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY; | |||
@@ -32,15 +37,11 @@ import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; | |||
import static org.cobbzilla.util.io.NullInputStream.NULL_STREAM; | |||
@Slf4j | |||
class ActiveStreamState { | |||
public class ActiveStreamState { | |||
public static final int DEFAULT_BYTE_BUFFER_SIZE = (int) (8 * Bytes.KB); | |||
public static final long MAX_BYTE_BUFFER_SIZE = (64 * Bytes.KB); | |||
// do not wrap input with encoding stream until we have received at least this many bytes | |||
// this avoids errors when creating a GZIPInputStream when only one or a few bytes are available | |||
public static final long MIN_BYTES_BEFORE_WRAP = Bytes.KB; | |||
// If no data is readable for this long, shut down the underlying MultiStream | |||
public static final long UNDERFLOW_TIMEOUT = SECONDS.toMillis(60); | |||
@@ -69,6 +70,7 @@ class ActiveStreamState { | |||
private InputStream output = null; | |||
private long totalBytesWritten = 0; | |||
private long totalBytesRead = 0; | |||
private CharsetDetector charsetDetector; | |||
public ActiveStreamState(FilterHttpRequest request, | |||
List<AppRuleHarness> rules) { | |||
@@ -76,6 +78,7 @@ class ActiveStreamState { | |||
this.requestId = request.getId(); | |||
this.encoding = request.getEncoding(); | |||
this.firstRule = rules.get(0); | |||
this.charsetDetector = charSetDetectorForContentType(request.getContentType()); | |||
final String prefix = "ActiveStreamState("+reqId()+"): "; | |||
if (empty(rules)) { | |||
@@ -130,8 +133,13 @@ class ActiveStreamState { | |||
} | |||
// do not wrap input with encoding stream until we have received at least MIN_BYTES_BEFORE_WRAP bytes | |||
// this avoids errors when creating a GZIPInputStream when only one or a few bytes are available | |||
if (output == null && totalBytesWritten > MIN_BYTES_BEFORE_WRAP) { | |||
output = outputStream(firstRule.getDriver().filterResponse(request, inputStream(multiStream))); | |||
if (output == null && totalBytesWritten > StreamConstants.MIN_BYTES_BEFORE_WRAP) { | |||
log.info("addChunk: detecting charset using "+charsetDetector.getClass().getSimpleName()); | |||
final BubbleCharSet cs = getCharSet(false); | |||
log.info("addChunk: detected charset: "+cs); | |||
if (cs != null) { | |||
output = outputStream(firstRule.getDriver().filterResponse(request, inputStream(multiStream), cs.getCharset())); | |||
} | |||
} | |||
} | |||
} | |||
@@ -147,7 +155,27 @@ class ActiveStreamState { | |||
multiStream.addLastStream(chunkStream); | |||
} | |||
if (output == null) { | |||
output = outputStream(firstRule.getDriver().filterResponse(request, inputStream(multiStream))); | |||
log.info("addLastChunk: detecting charset using "+charsetDetector.getClass().getSimpleName()); | |||
final BubbleCharSet cs = getCharSet(true); | |||
log.info("addLastChunk: detected charset: "+cs); | |||
final Charset charset; | |||
if (cs == null) { | |||
log.warn(prefix("addLastChunk")+"no charset could be determined"); | |||
charset = null; | |||
} else { | |||
charset = cs.getCharset(); | |||
} | |||
output = outputStream(firstRule.getDriver().filterResponse(request, inputStream(multiStream), charset)); | |||
} | |||
} | |||
public BubbleCharSet getCharSet(boolean last) throws IOException { | |||
try { | |||
multiStream.mark((int) totalBytesWritten); | |||
@Cleanup final InputStream in = inputStream(multiStream); | |||
return charsetDetector.getCharSet(in, totalBytesWritten, last); | |||
} finally { | |||
multiStream.reset(); | |||
} | |||
} | |||
@@ -211,7 +239,7 @@ class ActiveStreamState { | |||
if (log.isDebugEnabled()) log.debug(prefix+"identity encoding, returning baseStream unmodified"); | |||
return baseStream; | |||
} else if (doNotWrap.containsKey(url)) { | |||
} else if (url == null || doNotWrap.containsKey(url)) { | |||
if (log.isDebugEnabled()) log.debug(prefix+"previous error wrapping encoding, returning baseStream unmodified"); | |||
encoding = null; | |||
return baseStream; | |||
@@ -9,12 +9,12 @@ import bubble.dao.app.*; | |||
import bubble.dao.device.DeviceDAO; | |||
import bubble.model.account.Account; | |||
import bubble.model.app.*; | |||
import bubble.model.cloud.AnsibleInstallType; | |||
import bubble.model.cloud.BubbleNetwork; | |||
import bubble.model.device.Device; | |||
import bubble.rule.AppRuleDriver; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.device.DeviceService; | |||
import bubble.service.device.StandardFlexRouterService; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.cobbzilla.util.collection.SingletonList; | |||
@@ -40,6 +40,7 @@ public class StandardAppPrimerService implements AppPrimerService { | |||
@Autowired private AppRuleDAO ruleDAO; | |||
@Autowired private RuleDriverDAO driverDAO; | |||
@Autowired private AppDataDAO dataDAO; | |||
@Autowired private StandardFlexRouterService flexRouterService; | |||
@Autowired private RedisService redis; | |||
@Autowired private BubbleConfiguration configuration; | |||
@@ -54,7 +55,7 @@ public class StandardAppPrimerService implements AppPrimerService { | |||
log.info("initPrimingEnabled: thisNetwork is null, not priming"); | |||
return false; | |||
} | |||
if (thisNetwork.getInstallType() != AnsibleInstallType.node) { | |||
if (thisNetwork.notNode()) { | |||
log.info("initPrimingEnabled: thisNetwork is not a node, not priming"); | |||
return false; | |||
} | |||
@@ -118,6 +119,13 @@ public class StandardAppPrimerService implements AppPrimerService { | |||
} | |||
if (accountDeviceIps.isEmpty()) return; | |||
// flex domains can only be managed by the first admin | |||
final Account firstAdmin = accountDAO.getFirstAdmin(); | |||
account.setFirstAdmin(account.getUuid().equals(firstAdmin.getUuid())); | |||
boolean updateFlexRouters = false; | |||
Set<String> flexDomains = null; | |||
Set<String> flexExcludeDomains = null; | |||
final List<BubbleApp> appsToPrime = singleApp == null | |||
? appDAO.findByAccount(account.getUuid()).stream() | |||
.filter(BubbleApp::canPrime) | |||
@@ -142,9 +150,84 @@ public class StandardAppPrimerService implements AppPrimerService { | |||
dataDAO.registerCallback(app.getUuid(), dataCallback.createCallback(account, app, configuration)); | |||
} | |||
for (Device device : devices) { | |||
defineRedisSets(account, accountDeviceIps, app, matchers, rule, driver, device); | |||
final Set<String> rejectDomains = new HashSet<>(); | |||
final Set<String> blockDomains = new HashSet<>(); | |||
final Set<String> whiteListDomains = new HashSet<>(); | |||
final Set<String> filterDomains = new HashSet<>(); | |||
for (AppMatcher matcher : matchers) { | |||
final AppRuleDriver appRuleDriver = rule.initDriver(app, driver, matcher, account, device); | |||
final Set<String> rejects = appRuleDriver.getPrimedRejectDomains(); | |||
if (empty(rejects)) { | |||
log.debug("_prime: no rejectDomains for device/app/rule/matcher: " + device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
rejectDomains.addAll(rejects); | |||
} | |||
final Set<String> blocks = appRuleDriver.getPrimedBlockDomains(); | |||
if (empty(blocks)) { | |||
log.debug("_prime: no blockDomains for device/app/rule/matcher: " + device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
blockDomains.addAll(blocks); | |||
} | |||
final Set<String> whiteList = appRuleDriver.getPrimedWhiteListDomains(); | |||
if (empty(whiteList)) { | |||
log.debug("_prime: no whiteListDomains for device/app/rule/matcher: " + device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
whiteListDomains.addAll(whiteList); | |||
} | |||
final Set<String> filters = appRuleDriver.getPrimedFilterDomains(); | |||
if (empty(filters)) { | |||
log.debug("_prime: no filterDomains for device/app/rule/matcher: " + device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
filterDomains.addAll(filters); | |||
} | |||
if (account.isFirstAdmin() && flexDomains == null) { | |||
final Set<String> flexes = appRuleDriver.getPrimedFlexDomains(); | |||
if (empty(flexes)) { | |||
log.debug("_prime: no flexDomains for device/app/rule/matcher: " + device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
flexDomains = new HashSet<>(flexes); | |||
} | |||
final Set<String> flexExcludes = appRuleDriver.getPrimedFlexExcludeDomains(); | |||
if (empty(flexExcludes)) { | |||
log.debug("_prime: no flexExcludeDomains for device/app/rule/matcher: " + device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
flexExcludeDomains = new HashSet<>(flexExcludes); | |||
} | |||
} | |||
} | |||
if (!empty(rejectDomains) || !empty(blockDomains) || !empty(filterDomains) || !empty(flexDomains) || !empty(flexExcludeDomains)) { | |||
for (String ip : accountDeviceIps.get(device.getUuid())) { | |||
if (!empty(rejectDomains)) { | |||
rejectDomains.removeAll(whiteListDomains); | |||
AppRuleDriver.defineRedisRejectSet(redis, ip, app.getName() + ":" + app.getUuid(), rejectDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(blockDomains)) { | |||
blockDomains.removeAll(whiteListDomains); | |||
AppRuleDriver.defineRedisBlockSet(redis, ip, app.getName() + ":" + app.getUuid(), blockDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(whiteListDomains)) { | |||
AppRuleDriver.defineRedisWhiteListSet(redis, ip, app.getName() + ":" + app.getUuid(), whiteListDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(filterDomains)) { | |||
AppRuleDriver.defineRedisFilterSet(redis, ip, app.getName() + ":" + app.getUuid(), filterDomains.toArray(String[]::new)); | |||
} | |||
if (account.isFirstAdmin() && (!empty(flexDomains) || !empty(flexExcludeDomains))) { | |||
updateFlexRouters = true; | |||
if (!empty(flexDomains)) { | |||
if (flexExcludeDomains != null) flexDomains.removeAll(flexExcludeDomains); | |||
AppRuleDriver.defineRedisFlexSet(redis, ip, app.getName() + ":" + app.getUuid(), flexDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(flexExcludeDomains)) { | |||
AppRuleDriver.defineRedisFlexExcludeSet(redis, ip, app.getName() + ":" + app.getUuid(), flexExcludeDomains.toArray(String[]::new)); | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
if (updateFlexRouters && !empty(flexDomains)) { | |||
flexRouterService.updateFlexRoutes(flexDomains); | |||
} | |||
} | |||
} catch (Exception e) { | |||
die("_prime: "+shortError(e), e); | |||
@@ -153,117 +236,4 @@ public class StandardAppPrimerService implements AppPrimerService { | |||
} | |||
} | |||
private void defineRedisSets(Account account, Map<String, List<String>> accountDeviceIps, BubbleApp app, | |||
List<AppMatcher> matchers, AppRule rule, RuleDriver driver, Device device) { | |||
final Set<String> rejectDomains = new HashSet<>(); | |||
final Set<String> blockDomains = new HashSet<>(); | |||
final Set<String> whiteListDomains = new HashSet<>(); | |||
final Set<String> filterDomains = new HashSet<>(); | |||
final Set<String> flexDomains = new HashSet<>(); | |||
final Set<String> flexExcludeDomains = new HashSet<>(); | |||
final Set<String> requestHeaderModifiers = new HashSet<>(); | |||
boolean areAllSetsEmpty = true; | |||
for (AppMatcher matcher : matchers) { | |||
final AppRuleDriver appRuleDriver = rule.initDriver(app, driver, matcher, account, device); | |||
final Set<String> rejects = appRuleDriver.getPrimedRejectDomains(); | |||
if (empty(rejects)) { | |||
log.debug("_prime: no rejectDomains for device/app/rule/matcher: " + device.getName() | |||
+ "/" + app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
rejectDomains.addAll(rejects); | |||
areAllSetsEmpty = false; | |||
} | |||
final Set<String> blocks = appRuleDriver.getPrimedBlockDomains(); | |||
if (empty(blocks)) { | |||
log.debug("_prime: no blockDomains for device/app/rule/matcher: " + device.getName() + "/" | |||
+ app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
blockDomains.addAll(blocks); | |||
areAllSetsEmpty = false; | |||
} | |||
final Set<String> whiteList = appRuleDriver.getPrimedWhiteListDomains(); | |||
if (empty(whiteList)) { | |||
log.debug("_prime: no whiteListDomains for device/app/rule/matcher: " + device.getName() + "/" | |||
+ app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
whiteListDomains.addAll(whiteList); | |||
areAllSetsEmpty = false; | |||
} | |||
final Set<String> filters = appRuleDriver.getPrimedFilterDomains(); | |||
if (empty(filters)) { | |||
log.debug("_prime: no filterDomains for device/app/rule/matcher: " + device.getName() + "/" | |||
+ app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
filterDomains.addAll(filters); | |||
areAllSetsEmpty = false; | |||
} | |||
final Set<String> flexes = appRuleDriver.getPrimedFlexDomains(); | |||
if (empty(flexes)) { | |||
log.debug("_prime: no flexDomains for device/app/rule/matcher: " + device.getName() + "/" | |||
+ app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
flexDomains.addAll(flexes); | |||
areAllSetsEmpty = false; | |||
} | |||
final Set<String> flexExcludes = appRuleDriver.getPrimedFlexExcludeDomains(); | |||
if (empty(flexExcludes)) { | |||
log.debug("_prime: no flexExcludeDomains for device/app/rule/matcher: " + device.getName() + "/" | |||
+ app.getName() + "/" + rule.getName() + "/" + matcher.getName()); | |||
} else { | |||
flexExcludeDomains.addAll(flexExcludes); | |||
areAllSetsEmpty = false; | |||
} | |||
final Set<String> modifiers = appRuleDriver.getPrimedResponseHeaderModifiers(); | |||
if (empty(modifiers)) { | |||
log.debug("_prime: no responseHeaderModifiers for device/app/rule/matcher: " | |||
+ device.getName() + "/" + app.getName() + "/" + rule.getName() + "/" | |||
+ matcher.getName()); | |||
} else { | |||
requestHeaderModifiers.addAll(modifiers); | |||
areAllSetsEmpty = false; | |||
} | |||
} | |||
if (areAllSetsEmpty) return; | |||
for (String ip : accountDeviceIps.get(device.getUuid())) { | |||
if (!empty(rejectDomains)) { | |||
AppRuleDriver.defineRedisRejectSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
rejectDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(blockDomains)) { | |||
AppRuleDriver.defineRedisBlockSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
blockDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(whiteListDomains)) { | |||
AppRuleDriver.defineRedisWhiteListSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
whiteListDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(filterDomains)) { | |||
AppRuleDriver.defineRedisFilterSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
filterDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(flexDomains)) { | |||
AppRuleDriver.defineRedisFlexSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
flexDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(flexExcludeDomains)) { | |||
AppRuleDriver.defineRedisFlexExcludeSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
flexExcludeDomains.toArray(String[]::new)); | |||
} | |||
if (!empty(requestHeaderModifiers)) { | |||
AppRuleDriver.defineRedisResponseHeaderModifiersSet(redis, ip, app.getName() + ":" + app.getUuid(), | |||
requestHeaderModifiers.toArray(String[]::new)); | |||
} | |||
} | |||
} | |||
} |
@@ -18,10 +18,13 @@ import bubble.resources.stream.FilterMatchersRequest; | |||
import bubble.rule.AppRuleDriver; | |||
import bubble.rule.FilterMatchDecision; | |||
import bubble.server.BubbleConfiguration; | |||
import bubble.service.stream.charset.BubbleCharSet; | |||
import bubble.service.stream.charset.CharsetDetector; | |||
import lombok.Cleanup; | |||
import lombok.Getter; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.apache.commons.io.IOUtils; | |||
import org.apache.commons.io.input.TeeInputStream; | |||
import org.apache.http.Header; | |||
import org.apache.http.StatusLine; | |||
import org.apache.http.client.methods.CloseableHttpResponse; | |||
@@ -38,6 +41,7 @@ import org.cobbzilla.util.collection.SingletonList; | |||
import org.cobbzilla.util.http.HttpClosingFilterInputStream; | |||
import org.cobbzilla.util.http.HttpMethods; | |||
import org.cobbzilla.util.http.URIBean; | |||
import org.cobbzilla.util.io.multi.MultiStream; | |||
import org.cobbzilla.wizard.cache.redis.RedisService; | |||
import org.cobbzilla.wizard.stream.ByteStreamingOutput; | |||
import org.cobbzilla.wizard.stream.SendableResource; | |||
@@ -48,9 +52,11 @@ import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.stereotype.Service; | |||
import javax.ws.rs.core.Response; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.List; | |||
@@ -64,7 +70,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; | |||
import static javax.ws.rs.core.HttpHeaders.CONTENT_LENGTH; | |||
import static org.apache.http.HttpHeaders.CONTENT_TYPE; | |||
import static org.apache.http.HttpHeaders.TRANSFER_ENCODING; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.*; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.empty; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.hashOf; | |||
import static org.cobbzilla.util.http.HttpStatusCodes.OK; | |||
import static org.cobbzilla.util.json.JsonUtil.COMPACT_MAPPER; | |||
import static org.cobbzilla.util.json.JsonUtil.json; | |||
@@ -121,6 +128,8 @@ public class StandardRuleEngineService implements RuleEngineService { | |||
return send(response); | |||
} | |||
// this method is only called by the ReverseProxyResource which is not used in production, | |||
// so we can be a little less strict about performance and other things | |||
public Response applyRulesAndSendResponse(ContainerRequest request, | |||
URIBean ub, | |||
FilterHttpRequest filterRequest) throws IOException { | |||
@@ -141,7 +150,22 @@ public class StandardRuleEngineService implements RuleEngineService { | |||
// filter response. when stream is closed, close http client | |||
final Header contentTypeHeader = proxyResponse.getFirstHeader(CONTENT_TYPE); | |||
filterRequest.setContentType(contentTypeHeader == null ? null : contentTypeHeader.getValue()); | |||
final InputStream responseEntity = firstRule.getDriver().filterResponse(filterRequest, new HttpClosingFilterInputStream(httpClient, proxyResponse)); | |||
final InputStream in = new HttpClosingFilterInputStream(httpClient, proxyResponse); | |||
// do we have a content length? | |||
final Header contentLengthHeader = proxyResponse.getFirstHeader(CONTENT_LENGTH); | |||
final Long contentLength = contentLengthHeader == null ? null : Long.parseLong(contentLengthHeader.getValue()); | |||
filterRequest.setContentLength(contentLength); | |||
final CharsetDetector charsetDetector = CharsetDetector.charSetDetectorForContentType(filterRequest.getContentType()); | |||
final long size = contentLength != null ? contentLength : 1024; | |||
final ByteArrayOutputStream stash = new ByteArrayOutputStream((int) size); | |||
final TeeInputStream teeIn = new TeeInputStream(in, stash); | |||
final BubbleCharSet cs = charsetDetector.getCharSet(teeIn, size, true); | |||
final Charset charset = cs == null ? null : cs.getCharset(); | |||
final MultiStream multiStream = new MultiStream(new ByteArrayInputStream(stash.toByteArray())); | |||
multiStream.addLastStream(in); | |||
final InputStream responseEntity = firstRule.getDriver().filterResponse(filterRequest, multiStream, charset); | |||
// send response | |||
return sendResponse(responseEntity, proxyResponse); | |||
@@ -0,0 +1,11 @@ | |||
package bubble.service.stream; | |||
import org.cobbzilla.util.system.Bytes; | |||
public class StreamConstants { | |||
// do not wrap input with encoding stream until we have received at least this many bytes | |||
// this avoids errors when creating a GZIPInputStream when only one or a few bytes are available | |||
public static final long MIN_BYTES_BEFORE_WRAP = Bytes.KB; | |||
} |
@@ -0,0 +1,27 @@ | |||
package bubble.service.stream.charset; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Getter; | |||
import lombok.ToString; | |||
import java.nio.charset.Charset; | |||
import java.util.Map; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import static org.cobbzilla.util.string.StringUtil.UTF8cs; | |||
@AllArgsConstructor @ToString(of="charset") | |||
public class BubbleCharSet { | |||
private static final Map<Charset, BubbleCharSet> cache = new ConcurrentHashMap<>(10); | |||
public static BubbleCharSet forCharSet(Charset cs) { | |||
return cache.computeIfAbsent(cs, BubbleCharSet::new); | |||
} | |||
public static final BubbleCharSet RAW = new BubbleCharSet(null); | |||
public static final BubbleCharSet UTF8 = forCharSet(UTF8cs); | |||
@Getter private final Charset charset; | |||
} |
@@ -0,0 +1,23 @@ | |||
package bubble.service.stream.charset; | |||
import java.io.InputStream; | |||
import static bubble.service.stream.charset.HtmlCharsetDetector.htmlCharSetDetector; | |||
import static org.cobbzilla.util.http.HttpContentTypes.isHtml; | |||
public interface CharsetDetector { | |||
CharsetDetector SKIP_CHARSET_DETECTION = new SkipCharsetDetection(); | |||
static CharsetDetector charSetDetectorForContentType(String contentType) { | |||
if (isHtml(contentType)) return htmlCharSetDetector(contentType); | |||
return SKIP_CHARSET_DETECTION; | |||
} | |||
BubbleCharSet getCharSet(InputStream in, long size, boolean last); | |||
class SkipCharsetDetection implements CharsetDetector { | |||
@Override public BubbleCharSet getCharSet(InputStream in, long size, boolean last) { return BubbleCharSet.RAW; } | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
package bubble.service.stream.charset; | |||
import lombok.AllArgsConstructor; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.Map; | |||
import java.util.concurrent.ConcurrentHashMap; | |||
import static bubble.service.stream.charset.HtmlStreamCharsetDetector.HTML_STREAM_CHARSET_DETECTOR; | |||
@Slf4j | |||
public abstract class HtmlCharsetDetector implements CharsetDetector { | |||
private static final Map<String, HtmlCharsetDetector> detectors = new ConcurrentHashMap<>(10); | |||
public static final String CONTENT_TYPE_CHARSET = "charset="; | |||
public static CharsetDetector htmlCharSetDetector(String contentType) { | |||
return detectors.computeIfAbsent(contentType, ct -> { | |||
final int csPos = ct.indexOf(CONTENT_TYPE_CHARSET); | |||
if (csPos == -1) return HTML_STREAM_CHARSET_DETECTOR; | |||
final String charsetName = ct.substring(csPos + CONTENT_TYPE_CHARSET.length()); | |||
try { | |||
final Charset cs = Charset.forName(charsetName); | |||
return new HtmlContentTypeCharSet(cs); | |||
} catch (Exception e) { | |||
log.error("htmlCharSetDetector: invalid charset, returning HtmlStreamCharsetDetector: "+charsetName); | |||
return HTML_STREAM_CHARSET_DETECTOR; | |||
} | |||
}); | |||
} | |||
@AllArgsConstructor | |||
public static class HtmlContentTypeCharSet extends HtmlCharsetDetector { | |||
private final Charset charset; | |||
@Override public BubbleCharSet getCharSet(InputStream in, long size, boolean last) { | |||
return BubbleCharSet.forCharSet(charset); | |||
} | |||
} | |||
} |
@@ -0,0 +1,92 @@ | |||
package bubble.service.stream.charset; | |||
import lombok.extern.slf4j.Slf4j; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.regex.Matcher; | |||
import java.util.regex.Pattern; | |||
import static bubble.service.stream.StreamConstants.MIN_BYTES_BEFORE_WRAP; | |||
import static org.cobbzilla.util.daemon.ZillaRuntime.shortError; | |||
import static org.cobbzilla.util.string.StringUtil.UTF8cs; | |||
@Slf4j | |||
public class HtmlStreamCharsetDetector extends HtmlCharsetDetector { | |||
public static final HtmlStreamCharsetDetector HTML_STREAM_CHARSET_DETECTOR = new HtmlStreamCharsetDetector(); | |||
private static final Pattern HTML_CONTENT_TYPE_EQUIV_CHARSET | |||
= Pattern.compile("<meta\\s+http-equiv\\s*=\\s*\"Content-Type\"\\s+content=\"[/\\w]+\\s*;\\s*charset=([-\\w]+)\\s*\"\\s*>", Pattern.CASE_INSENSITIVE); | |||
private static final Pattern HTML_META_CHARSET | |||
= Pattern.compile("<meta\\s+charset\\s*=\\s*\"([-\\w]+)\">", Pattern.CASE_INSENSITIVE); | |||
private static final Pattern HTML_CLOSE_HEAD | |||
= Pattern.compile("</head[^>]*>", Pattern.CASE_INSENSITIVE); | |||
@Override public BubbleCharSet getCharSet(InputStream in, long size, boolean last) { | |||
final byte[] buffer = new byte[(int) MIN_BYTES_BEFORE_WRAP]; | |||
int count; | |||
String fullData = null; | |||
try { | |||
final StringBuilder b = new StringBuilder(); | |||
int bytesRead = 0; | |||
boolean zeroRead = false; | |||
while (bytesRead < size && (count = in.read(buffer, 0, readSize(size, buffer.length, bytesRead))) != -1) { | |||
if (count == 0) { | |||
// reached end of multi-stream, if this is our second zero read, bail out | |||
if (zeroRead) { | |||
if (last) { | |||
if (log.isDebugEnabled()) log.debug("getCharSet: exhausted stream and no match found, returning UTF-8"); | |||
return BubbleCharSet.UTF8; | |||
} | |||
if (log.isDebugEnabled()) log.debug("getCharSet: two zero reads, must be at end of multi-stream, returning null"); | |||
return null; | |||
} | |||
zeroRead = true; | |||
} | |||
final String data = new String(buffer, 0, count); | |||
b.append(data); | |||
fullData = b.toString(); | |||
final Matcher metaMatcher = HTML_META_CHARSET.matcher(fullData); | |||
if (metaMatcher.find()) { | |||
return BubbleCharSet.forCharSet(safeCharSet(metaMatcher.group(1))); | |||
} | |||
final Matcher equivMatcher = HTML_CONTENT_TYPE_EQUIV_CHARSET.matcher(fullData); | |||
if (equivMatcher.find()) { | |||
return BubbleCharSet.forCharSet(safeCharSet(equivMatcher.group(1))); | |||
} | |||
final Matcher headCloseMatcher = HTML_CLOSE_HEAD.matcher(fullData); | |||
if (headCloseMatcher.find()) { | |||
if (log.isDebugEnabled()) log.debug("getCharSet: found head closing tag before any charset specifier, returning UTF-8"); | |||
return BubbleCharSet.UTF8; | |||
} | |||
} | |||
if (last) { | |||
if (log.isDebugEnabled()) log.debug("getCharSet: exhausted stream and no match found, returning UTF-8"); | |||
return BubbleCharSet.UTF8; | |||
} | |||
if (log.isDebugEnabled()) log.debug("getCharSet: exhausted stream and no match found, but more data may be coming, returning null"); | |||
return null; | |||
} catch (Exception e) { | |||
log.error("getCharSet: io error, returning UTF-8: "+shortError(e)); | |||
return BubbleCharSet.UTF8; | |||
} | |||
} | |||
private Charset safeCharSet(String csName) { | |||
try { | |||
return Charset.forName(csName); | |||
} catch (Exception e) { | |||
log.error("safeCharSet: invalid name, returning UTF-8: "+csName); | |||
return UTF8cs; | |||
} | |||
} | |||
private int readSize(long size, int bufsiz, int bytesRead) { | |||
return bytesRead + bufsiz < size ? bufsiz : (int) (bufsiz - (size - bytesRead)); | |||
} | |||
} |
@@ -50,24 +50,49 @@ public class AppUpgradeService extends SimpleDaemon { | |||
@Autowired private RuleDriverDAO driverDAO; | |||
@Autowired private StandardRuleEngineService ruleEngine; | |||
public boolean shouldRun () { | |||
final BubbleNetwork thisNetwork = configuration.getThisNetwork(); | |||
if (thisNetwork == null) { | |||
log.warn("shouldRun: thisNetwork is null, not running"); | |||
return false; | |||
} | |||
final BubbleNode thisNode = configuration.getThisNode(); | |||
if (thisNode == null) { | |||
log.warn("shouldRun: thisNode is null, not running"); | |||
return false; | |||
} | |||
final BubbleNode sageNode = configuration.getSageNode(); | |||
if (sageNode == null) { | |||
log.warn("shouldRun: sageNode is null, not running"); | |||
return false; | |||
} | |||
if (sageNode.getUuid().equals(thisNode.getUuid())) { | |||
log.warn("shouldRun: sageNode is thisNode, not running"); | |||
return false; | |||
} | |||
return true; | |||
} | |||
@Override protected void process() { | |||
final BubbleNetwork thisNetwork = configuration.getThisNetwork(); | |||
if (thisNetwork == null) { | |||
log.warn("process: thisNetwork is null, not running"); | |||
return; | |||
} | |||
final BubbleNode thisNode = configuration.getThisNode(); | |||
if (thisNode == null) { | |||
log.warn("process: thisNode is null, not running"); | |||
return; | |||
} | |||
final BubbleNode sageNode = configuration.getSageNode(); | |||
if (sageNode == null) { | |||
log.warn("process: sageNode is null, not running"); | |||
return; | |||
} | |||
if (sageNode.getUuid().equals(thisNode.getUuid())) { | |||
log.warn("process: sageNode is thisNode, not running"); | |||
return; | |||
} | |||
// excluding sage, are we the oldest running node? | |||
final List<BubbleNode> nodes = nodeDAO.findRunningByNetwork(thisNetwork.getUuid()).stream() | |||
@@ -1 +1 @@ | |||
bubble.version=Adventure 1.2.1 | |||
bubble.version=Adventure 1.2.4 |
@@ -0,0 +1 @@ | |||
ALTER TABLE bubble_network ADD COLUMN launch_type VARCHAR(200); |
@@ -1 +1 @@ | |||
Subproject commit 61fc9667c4c66c0bcbbb8eb4d128e8ab58f50869 | |||
Subproject commit 9d1ecac6514696721effe012e9c726896c368ebe |
@@ -48,6 +48,7 @@ | |||
"name": "manageFlexDomains", | |||
"scope": "app", | |||
"root": "true", | |||
"when": "account.firstAdmin === true", | |||
"fields": ["flexFqdn"], | |||
"actions": [ | |||
{"name": "removeFlexFqdn", "index": 10}, | |||
@@ -61,6 +62,7 @@ | |||
"name": "manageFlexFeeds", | |||
"scope": "app", | |||
"root": "true", | |||
"when": "account.firstAdmin === true", | |||
"fields": ["flexFeedName", "flexFeedUrl"], | |||
"actions": [ | |||
{"name": "removeFlexFeed", "index": 10}, | |||
@@ -25,7 +25,6 @@ from bubble_config import bubble_port, debug_capture_fqdn, \ | |||
from mitmproxy import http | |||
from mitmproxy.net.http import headers as nheaders | |||
from mitmproxy.proxy.protocol.async_stream_body import AsyncStreamBody | |||
from mitmproxy.proxy.protocol.request_capture import RequestCapture | |||
bubble_log = logging.getLogger(__name__) | |||
@@ -401,7 +400,7 @@ def is_bubble_health_check(path): | |||
def is_sage_request(ip, fqdns): | |||
return (ip == bubble_sage_ip4 or ip == bubble_sage_ip6) and bubble_sage_host in fqdns | |||
return fqdns is not None and (ip == bubble_sage_ip4 or ip == bubble_sage_ip6) and bubble_sage_host in fqdns | |||
def is_not_from_vpn(client_addr): | |||
@@ -416,7 +415,7 @@ def is_flex_domain(client_addr, server_addr, fqdns): | |||
return False | |||
fqdn = fqdns[0] | |||
if fqdn == bubble_host or fqdn == bubble_host_alias or fqdn == bubble_sage_host: | |||
if fqdn == bubble_host or fqdn == bubble_host_alias or (bubble_sage_host is not None and fqdn == bubble_sage_host): | |||
if bubble_log.isEnabledFor(DEBUG): | |||
bubble_log.debug('is_flex_domain: (early) returning False for: '+fqdn) | |||
return False | |||
@@ -54,19 +54,45 @@ def ensure_bubble_csp(csp, req_id): | |||
elif part.startswith(' script-src ') or part.startswith('script-src '): | |||
tokens = part.split() | |||
if "'self'" in tokens: | |||
# allows from self, check if there is an existing nonce. if so we will reuse it | |||
found_nonce = False | |||
for token in tokens: | |||
if " 'nonce-" in token: | |||
found_nonce = True | |||
break | |||
# if no nonce, then add our nonce | |||
if not found_nonce: | |||
found_unsafe_inline = "'unsafe-inline'" in tokens | |||
found_nonce = False | |||
found_sha = False | |||
for token in tokens: | |||
if not found_nonce and "'nonce-" in token: | |||
found_nonce = True | |||
if not found_sha and ("'sha256-" in token or "'sha384-" in token or "'sha512-" in token): | |||
found_sha = True | |||
if found_nonce and found_sha: | |||
break | |||
if found_unsafe_inline: | |||
if not found_sha and not found_nonce: | |||
# unsafe-inline is set, and there are no shas or nonces | |||
# then we can add ourselves as unsafe inline without any nonce | |||
new_csp = add_csp_part(new_csp, part) | |||
elif found_nonce: | |||
# unsafe-inline is set and there is a nonce, we keep the nonce | |||
new_csp = add_csp_part(new_csp, part) | |||
elif found_sha: | |||
# unsafe-inline is set and there is no nonce, but at least one sha is present | |||
# we must add a nonce for ourselves | |||
new_csp = add_csp_part(new_csp, " ".join(tokens) + " 'nonce-"+base64.b64encode(bytes(req_id, 'utf-8')).decode()+"' ") | |||
else: | |||
# unreachable, just for sanity | |||
new_csp = add_csp_part(new_csp, part) | |||
else: | |||
# does not allow from self, so add self with our nonce | |||
new_csp = add_csp_part(new_csp, tokens[0] + " 'self' 'nonce-"+base64.b64encode(bytes(req_id, 'utf-8')).decode()+"' " + " ".join(tokens[1:])) | |||
# unsafe-inline is not set | |||
if not found_nonce or found_sha: | |||
# there is no nonce or a sha is set, add unsafe-inline and our nonce | |||
new_csp = add_csp_part(new_csp, tokens[0] + " 'unsafe-inline' 'nonce-"+base64.b64encode(bytes(req_id, 'utf-8')).decode()+"' " + " ".join(tokens[1:])) | |||
elif found_nonce: | |||
# there is a nonce, keep it and add unsafe-inline | |||
new_csp = add_csp_part(new_csp, tokens[0] + " 'unsafe-inline' " + " ".join[tokens[1:]]) | |||
elif found_sha: | |||
# no nonce but a sha is set, add our nonce and unsafe-inline | |||
new_csp = add_csp_part(new_csp, tokens[0] + " 'unsafe-inline' 'nonce-"+base64.b64encode(bytes(req_id, 'utf-8')).decode()+"' " + " ".join(tokens[1:])) | |||
else: | |||
# unreachable, just for sanity | |||
new_csp = add_csp_part(new_csp, part) | |||
else: | |||
new_csp = add_csp_part(new_csp, part) | |||
return new_csp | |||
@@ -0,0 +1,138 @@ | |||
package bubble.test.filter; | |||
import bubble.model.app.AppMatcher; | |||
import bubble.model.app.AppRule; | |||
import bubble.resources.stream.FilterHttpRequest; | |||
import bubble.resources.stream.FilterMatchersRequest; | |||
import bubble.resources.stream.FilterMatchersResponse; | |||
import bubble.service.stream.ActiveStreamState; | |||
import bubble.service.stream.AppRuleHarness; | |||
import org.apache.commons.io.IOUtils; | |||
import org.cobbzilla.util.collection.SingletonList; | |||
import org.cobbzilla.util.http.HttpContentEncodingType; | |||
import org.junit.Test; | |||
import java.io.ByteArrayInputStream; | |||
import java.io.ByteArrayOutputStream; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import static org.cobbzilla.util.http.HttpContentEncodingType.*; | |||
import static org.cobbzilla.util.http.HttpContentTypes.TEXT_HTML; | |||
import static org.cobbzilla.util.io.StreamUtil.stream2bytes; | |||
import static org.cobbzilla.util.security.ShaUtil.sha256_base64; | |||
import static org.junit.Assert.*; | |||
public class CharsetDetectionTest { | |||
public static final byte[] WIN_1250_TEST = stream2bytes("charset_detection/meta-windows-1250.html"); | |||
public static final byte[] WIN_1250_LATE_TEST = stream2bytes("charset_detection/meta-windows-1250-late.html"); | |||
public static final byte[] WIN_1250_EQUIV_TEST = stream2bytes("charset_detection/equiv-windows-1250.html"); | |||
@Test public void testNonUTF8Charset () throws Exception { | |||
// read first chunk exactly 288 bytes, so it ends in the middle of "charset" | |||
_testNonUTF8Charset(WIN_1250_TEST, 288, null); | |||
} | |||
@Test public void testNonUTF8Charset_gzip () throws Exception { | |||
// for gzip we won't be able to break exactly on charset, | |||
// but try a small read anyway to make sure nothing breaks | |||
_testNonUTF8Charset(gzip.encode(WIN_1250_TEST), 288, gzip); | |||
} | |||
@Test public void testNonUTF8CharsetLate () throws Exception { | |||
_testNonUTF8Charset(WIN_1250_LATE_TEST, 1024, null); | |||
} | |||
@Test public void testNonUTF8CharsetLate_brotli () throws Exception { | |||
_testNonUTF8Charset(br.encode(WIN_1250_LATE_TEST), 1024, br); | |||
} | |||
@Test public void testNonUTF8CharsetEquiv () throws Exception { | |||
_testNonUTF8Charset(WIN_1250_EQUIV_TEST, 1024, null); | |||
} | |||
@Test public void testNonUTF8CharsetEquiv_deflate () throws Exception { | |||
_testNonUTF8Charset(deflate.encode(WIN_1250_EQUIV_TEST), 1024, deflate); | |||
} | |||
private void _testNonUTF8Charset(byte[] test, int initialReadSize, HttpContentEncodingType encoding) throws Exception { | |||
final FilterHttpRequest request = new FilterHttpRequest() | |||
.setMatchersResponse(new FilterMatchersResponse() | |||
.setMatchers(new SingletonList<>(new AppMatcher())) | |||
.setRequest(new FilterMatchersRequest() | |||
.setFqdn("example.com") | |||
.setUri("/test_"+sha256_base64(test)+".html"))) | |||
.setContentType(TEXT_HTML) | |||
.setEncoding(encoding); | |||
final List<AppRuleHarness> rules = new ArrayList<>(); | |||
final PassthruDriver driver = new PassthruDriver(); | |||
final AppRuleHarness passthruRuleHarness = passthruRuleHarness(driver); | |||
rules.add(passthruRuleHarness); | |||
final ActiveStreamState streamState = new ActiveStreamState(request, rules); | |||
final byte[] buffer = new byte[8192]; | |||
final ByteArrayInputStream in = new ByteArrayInputStream(test); | |||
final ByteArrayOutputStream out = new ByteArrayOutputStream(test.length); | |||
// add first chunk, no charset yet found | |||
final byte[] buf = new byte[8192]; | |||
final int initialActualRead = in.read(buffer, 0, initialReadSize); | |||
assertEquals("expected first read to read "+initialReadSize+"bytes", initialReadSize, initialActualRead); | |||
System.arraycopy(buffer, 0, buf, 0, initialReadSize); | |||
streamState.addChunk(new ByteArrayInputStream(buf, 0, initialReadSize), initialReadSize); | |||
Charset charset = driver.getLastSeenCharset(); | |||
assertNull("expected no charset to be found in the first chunk", charset); | |||
// do not expect to have found a charset yet | |||
InputStream response = streamState.getResponseStream(false); | |||
IOUtils.copyLarge(response, out); | |||
// add remaining chunks, while reading data back | |||
int count; | |||
Charset lastSeenCharset = null; | |||
int responseCount = 0; | |||
while ((count = in.read(buffer)) != -1) { | |||
System.arraycopy(buffer, 0, buf, 0, count); | |||
streamState.addChunk(new ByteArrayInputStream(buf, 0, count), count); | |||
response = streamState.getResponseStream(false); | |||
responseCount++; | |||
charset = driver.getLastSeenCharset(); | |||
if (charset != null) { | |||
if (lastSeenCharset == null) { | |||
lastSeenCharset = charset; | |||
} else { | |||
// charset cannot change | |||
assertEquals("expected charset to be same as lastSeenCharset", lastSeenCharset, charset); | |||
} | |||
// charset must be windows-1250 | |||
assertEquals("expected windows-1250 charset", "windows-1250", charset.name()); | |||
} | |||
IOUtils.copyLarge(response, out); | |||
} | |||
assertNotNull("expected to find a charset", lastSeenCharset); | |||
assertEquals("expected windows-1250 charset", "windows-1250", lastSeenCharset.name()); | |||
// add last empty chunk | |||
streamState.addLastChunk(new ByteArrayInputStream(new byte[0]), 0); | |||
// read the data back | |||
response = streamState.getResponseStream(true); | |||
IOUtils.copyLarge(response, out); | |||
final byte[] actualBytes = out.toByteArray(); | |||
final String expectedHtml = new String(encoding == null ? test : encoding.decode(test), lastSeenCharset); | |||
final String actualHtml = new String(encoding == null ? actualBytes : encoding.decode(actualBytes), lastSeenCharset); | |||
assertEquals("expected output to be same as input", expectedHtml, actualHtml); | |||
} | |||
private AppRuleHarness passthruRuleHarness(PassthruDriver driver) { | |||
final AppRuleHarness appRuleHarness = new AppRuleHarness(new AppMatcher(), new AppRule()); | |||
appRuleHarness.setDriver(driver); | |||
return appRuleHarness; | |||
} | |||
} |
@@ -0,0 +1,21 @@ | |||
package bubble.test.filter; | |||
import bubble.resources.stream.FilterHttpRequest; | |||
import bubble.rule.AbstractAppRuleDriver; | |||
import lombok.Getter; | |||
import java.io.InputStream; | |||
import java.nio.charset.Charset; | |||
public class PassthruDriver extends AbstractAppRuleDriver { | |||
@Getter private Charset lastSeenCharset; | |||
@Override public InputStream doFilterResponse(FilterHttpRequest filterRequest, InputStream in, Charset charset) { | |||
this.lastSeenCharset = charset; | |||
return super.doFilterResponse(filterRequest, in, charset); | |||
} | |||
@Override public boolean couldModify(FilterHttpRequest request) { return true; } | |||
} |
@@ -1 +1 @@ | |||
Subproject commit 66d46695a64ff58934560c4b35aa43a0ab32fbe2 | |||
Subproject commit 61a7f288e515a185f2055bbe1e513943d54ac66c |
@@ -5,11 +5,19 @@ Bubble Developer Guide | |||
These instructions presume you are running a newly setup Ubuntu 20.04 system. | |||
Either the Ubuntu Server or Desktop distribution will work. | |||
Other Debian-based systems will probably also work fine. | |||
See below for other Linux distributions and other operating systems. | |||
## First-Time System Setup | |||
You'll need to install some software for Bubble to work correctly. | |||
After you clone this repository, run: | |||
./bin/first_time_ubuntu.sh | |||
This runs some `apt` commands to install various bits of software needed to run Bubble. | |||
If you are running on a non-Ubuntu system, copy that file to something like: | |||
./bin/first_time_myoperatingsystem.sh | |||
@@ -51,3 +59,12 @@ This will update and rebuild all submodules, and the main bubble jar file. | |||
## Running in development | |||
Run the `bin/run.sh` script to start the Bubble server. | |||
## Resetting everything | |||
If you want to "start over", run: | |||
./bin/reset_bubble_full | |||
This will remove local files stored by Bubble, and drop the bubble database. | |||
If you run `./bin/run.sh` again, it will be like running it for the first time. |
@@ -0,0 +1,37 @@ | |||
# Launching a Bubble from a Local Launcher | |||
These instructions assume you have already set up a [Local Launcher](local-launcher.md). | |||
## Login | |||
Login to your Local Launcher using the root admin account that was created during activation. | |||
Because the login field must be an email address, use the special email address `root@local.local` to login | |||
with the admin account. | |||
## Launch Bubble | |||
You should see a "Launch Bubble" screen. | |||
In the "Bubble Type" drop-down, choose "Fork Bubble" | |||
Choose your configuration options, then click the "Launch Your Bubble!" button kick things off. | |||
The screen will refresh and show a progress meter. A typical launch will take about 10 minutes. | |||
## Your Very First Bubble | |||
The very first Bubble you launch will build a [Packer](https://packer.io) image that will be used for this and | |||
subsequent launches. | |||
This process adds about 20-25 minutes to the launch process. | |||
While the packer image is building, the progress meter will appear to be "stuck" at 1%. This is normal. | |||
If you're curious, you can observe the packer image being built in your Bubble logs. | |||
This only happens the first time you launch a Bubble. | |||
Later launches can skip this step, because Bubble will detect that the packer image already exists. | |||
## Install Bubble Apps | |||
While your Bubble is launching, take a moment to | |||
[install the Bubble Native app](https://support.getbubblenow.com/hc/en-us/articles/360050801634-Connect-a-device-to-your-Bubble) | |||
on each device you plan on connecting to your Bubble. | |||
When your Bubble finishes launching, it will show a "Connect to Bubble" button. Click this and you'll be connected | |||
to your Bubble Node. |
@@ -0,0 +1,40 @@ | |||
# Launching a Bubble from a Remote Launcher | |||
These instructions assume you have already set up a [Remote Launcher](remote-launcher.md). | |||
## Create a user | |||
Click the "Sign Up" button in the header to create a new user account. | |||
Note: You *could* sign in using the root account and launch a Bubble from there, but this is discouraged for security reasons. | |||
It is *highly recommended* to launch new Bubbles using a regular user account, and not your Remote Launcher root account. | |||
You should use the root account on the Remote Launcher only to manage the system itself. | |||
## Verify user | |||
After you create a user, the Remote Launcher will send an email with a verification link. | |||
Click the link in the email to verify your new account. | |||
## Launch Bubble | |||
After you click the verification link, you should see a "Launch Bubble" screen. | |||
Choose your configuration options, then click the "Launch Your Bubble!" button kick things off. | |||
The screen will refresh and show a progress meter. A typical launch will take about 10 minutes. | |||
## Your Very First Bubble | |||
The very first Bubble you launch will build a [Packer](https://packer.io) image that will be used for this and | |||
subsequent launches. | |||
This process adds about 20-25 minutes to the launch process. | |||
While the packer image is building, the progress meter will appear to be "stuck" at 1%. This is normal. | |||
If you're curious, you can observe the packer image being built in your Bubble logs. | |||
This only happens the first time you launch a Bubble. | |||
Later launches can skip this step, because Bubble will detect that the packer image already exists. | |||
## Install Bubble Apps | |||
While your Bubble is launching, take a moment to | |||
[install the Bubble Native app](https://support.getbubblenow.com/hc/en-us/articles/360050801634-Connect-a-device-to-your-Bubble) | |||
on each device you plan on connecting to your Bubble. | |||
When your Bubble finishes launching, it will show a "Connect to Bubble" button. Click this and you'll be connected | |||
to your Bubble Node. |
@@ -2,35 +2,17 @@ | |||
A Bubble in Node Mode is a proper Bubble that allows you to connect your devices to it, and manages network traffic | |||
on behalf of those devices. | |||
Launching a Bubble in Node Mode requires a Bubble in Remote Launcher Mode. If you don't have | |||
a Bubble in Remote Launcher Mode, go back and follow the [Remote Launcher Mode instructions](remote-launcher.md). | |||
Launching a Bubble in Node Mode requires a Bubble in Local or Remote Launcher Mode. If you don't have | |||
a Bubble in Local or Remote Launcher Mode, go back and follow the [Local Launcher Mode instructions](local-launcher.md) | |||
and/or the [Remote Launcher Mode instructions](remote-launcher.md). | |||
## Create a user | |||
Load your Remote Launcher's home page in a web browser. | |||
Load your Launcher's home page in a web browser. | |||
Click the "Sign Up" button in the header to create a new user account. | |||
## From a Remote Launcher | |||
If you're launching from a [Remote Launcher](remote-launcher.md), follow [these instructions](launch-node-from-remote.md) | |||
Note: You *could* sign in using the root account and launch a Bubble from there, but this is discouraged for security reasons. | |||
It is *highly recommended* to launch new Bubbles using a regular user account, and not your Remote Launcher root account. | |||
You should use the root account on the Remote Launcher only to manage the system itself. | |||
## Verify user | |||
After you create a user, the Remote Launcher will send an email with a verification link. | |||
Click the link in the email to verify your new account. | |||
## Launch Bubble | |||
After you click the verification link, you should see a "Launch Bubble" screen. | |||
Choose your configuration options, then click the "Launch Your Bubble!" button kick things off. | |||
The screen will refresh and show a progress meter. | |||
While your Bubble is launching, take a moment to | |||
[install the Bubble Native app](https://support.getbubblenow.com/hc/en-us/articles/360050801634-Connect-a-device-to-your-Bubble) | |||
on each device you plan on connecting to your Bubble. | |||
When your Bubble finishes launching, it will show a "Connect to Bubble" button. Click this and you'll be connected | |||
to your Bubble Node. | |||
## From a Local Launcher | |||
If you're launching from a [Local Launcher](local-launcher.md), follow [these instructions](launch-node-from-local.md) | |||
## Connect Devices | |||
[Install the Bubble Native app](https://support.getbubblenow.com/hc/en-us/articles/360050801634-Connect-a-device-to-your-Bubble) | |||
@@ -38,7 +20,7 @@ on each device you plan on connecting to your Bubble. | |||
Start the app. At the bottom of the login screen, you should see "Running your own Bubble? Set Launcher". | |||
Click or tap "Set Launcher" and enter the hostname of your Remote Launcher. | |||
Click or tap "Set Launcher" and enter the hostname of your new Bubble Node. | |||
Then fill out the "Email" and "Password" fields, and click or tap the "Sign In" button. | |||
Your Bubble should automatically connect and configure the device. If it does not, click or tap the "Connect" button. | |||
@@ -3,31 +3,42 @@ A Bubble in Local Launcher Mode is the starting point for standing up a Bubble i | |||
[Remote Launcher Mode](remote-launcher.md), which you can then use to launch proper Bubbles | |||
in [Bubble Node Mode](launch-node.md). | |||
### Run Bubble | |||
## Run Bubble | |||
Run the `./bin/run.sh` script on your local machine to start Bubble in Local Launcher mode. | |||
Once the server is running, it will try to open a browser window to continue configuration. | |||
It will also print out the URL, so if the browser doesn't start correctly, you can paste this | |||
into your browser's location bar. | |||
### Activation | |||
## Activation | |||
Your Bubble is running locally in a "blank" mode. It needs an initial "root" account and some basic services configured. | |||
In order to activate your Local Launcher, you'll need accounts and/or API keys from several cloud providers. | |||
Have these account credentials handy. Be prepared to sign up for new accounts where needed. | |||
#### Activate via Web UI | |||
### Activate via Web UI | |||
The browser-based admin UI should be displaying an "Activate" page. Complete the information on this page and submit the | |||
data. The Bubble Launcher will create an initial "root" account and other basic system configurations. | |||
#### Activate via command line | |||
### Activate via command line | |||
Make a copy of the file `config/activation.json` and edit the copy. There are comments in the file to guide you. | |||
To activate your Local Launcher Bubble, run this command: | |||
`./bin/bactivate /path/to/activation.json` | |||
./bin/bactivate /path/to/activation.json | |||
After running the above, refresh the web page that opened when the server started. You should see a login page. | |||
You can now login as the admin user using the email address `root@local.local` and the password from your `activation.json` file. | |||
You are now read to Launch Bubble in [Remote Launcher Mode](remote-launcher.md) | |||
### Resetting everything | |||
If you want to "start over", run: | |||
./bin/reset_bubble_full | |||
This will remove local files stored by Bubble, and drop the bubble database. | |||
If you run `./bin/run.sh` again, it will be like running it for the first time. | |||
## Next Steps | |||
You are now read to launch a new Bubble in [Bubble Mode](launch-node.md), or | |||
a Remote Launcher via [Remote Launcher Mode](remote-launcher.md) |
@@ -15,7 +15,7 @@ address `root@local.local` and the password from your `activation.json` file. | |||
After you log in, you should see a "Launch Bubble" screen. | |||
If you don't see this screen, click the "My Bubble" link in the header. | |||
In the "Bubble Type" drop-down box, choose "Fork". | |||
In the "Bubble Type" drop-down box, choose "Fork Launcher". | |||
In the "Fork Host" field, enter the fully-qualified domain name (FQDN) that the Bubble will be known as. | |||
@@ -26,8 +26,22 @@ In the "Domain" field, choose the domain that corresponds to the FQDN you entere | |||
Configure the remaining fields as you desire. When you're ready, click the "Launch Your Bubble!" button. | |||
The screen will refresh and show a progress meter. | |||
The screen will refresh and show a progress meter. A typical launch will take about 10 minutes. | |||
## Your Very Remote Launcher | |||
The very first Remote Launcher you launch will build a [Packer](https://packer.io) image that will be used for this and | |||
subsequent launches. | |||
This process adds about 20-25 minutes to the launch process. | |||
While the packer image is building, the progress meter will appear to be "stuck" at 1%. This is normal. | |||
If you're curious, you can observe the packer image being built in your Bubble logs. | |||
This only happens the first time you launch a Remote Launcher. | |||
Later launches can skip this step, because Bubble will detect that the packer image already exists. | |||
## Next Steps | |||
When your Bubble finishes launching, it will show a "Connect to Bubble" button. Click this and you'll be connected | |||
to your Remote Launcher. | |||
You are now ready to [Launch a Bubble](launch-node.md) | |||
You are now ready to [Launch a Bubble](launch-node.md) from the Remote Launcher |
@@ -73,6 +73,7 @@ | |||
<include>bubble.test.promo.AccountCreditTest</include> | |||
<include>bubble.test.promo.MultiplePromotionsTest</include> | |||
<include>bubble.test.system.DriverTest</include> | |||
<include>bubble.test.filter.CharsetDetectionTest</include> | |||
<include>bubble.test.filter.ProxyTest</include> | |||
<include>bubble.test.filter.TrafficAnalyticsTest</include> | |||
<include>bubble.test.filter.BlockSummaryTest</include> | |||
@@ -80,6 +81,7 @@ | |||
<include>bubble.test.system.BackupTest</include> | |||
<include>bubble.test.system.NetworkTest</include> | |||
<include>bubble.abp.spec.BlockListTest</include> | |||
<include>org.cobbzilla.util.io.regex.RegexFilterReaderTest</include> | |||
</includes> | |||
</configuration> | |||
</plugin> | |||
@@ -1 +1 @@ | |||
Subproject commit bcb92061c3482ba4803c0030c78fb550206a0511 | |||
Subproject commit 2324f5d196fa52931f07e85c830bf9c10465b8f8 |
@@ -1 +1 @@ | |||
Subproject commit dfafe62c7eb3413cf1210e40e551094458f4d9d0 | |||
Subproject commit a9d3d69f1112a47b8d69f9343cc66caf5bab1383 |
@@ -1 +1 @@ | |||
Subproject commit cd9cf18c90a373f024163c048d57cf574ec97293 | |||
Subproject commit e91152f296a04ce1e4363ae90e1cfde70cdee4d5 |