Przeglądaj źródła

Merge branch 'master' into kris/request_protector_app

# Conflicts - WIP:
#	bubble-server/src/main/java/bubble/service/stream/StandardAppPrimerService.java
pull/58/head
Kristijan Mitrovic 4 lat temu
rodzic
commit
7baec383e6
77 zmienionych plików z 8764 dodań i 1779 usunięć
  1. +18
    -20
      README.md
  2. +7
    -3
      bin/bpatch
  3. +13
    -5
      bin/bpatchfull
  4. +1
    -0
      bin/build_dist
  5. +49
    -0
      bin/reset_bubble_full
  6. +1
    -0
      bubble-server/src/main/java/bubble/ApiConstants.java
  7. +7
    -2
      bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppConfigDriver.java
  8. +2
    -2
      bubble-server/src/main/java/bubble/cloud/compute/ComputeServiceDriverBase.java
  9. +9
    -3
      bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocateServiceDriverBase.java
  10. +9
    -0
      bubble-server/src/main/java/bubble/dao/SessionDAO.java
  11. +2
    -3
      bubble-server/src/main/java/bubble/dao/account/AccountDAO.java
  12. +2
    -1
      bubble-server/src/main/java/bubble/dao/account/AccountOwnedEntityDAO.java
  13. +2
    -3
      bubble-server/src/main/java/bubble/dao/account/AccountSshKeyDAO.java
  14. +8
    -1
      bubble-server/src/main/java/bubble/dao/cloud/BubbleNetworkDAO.java
  15. +1
    -1
      bubble-server/src/main/java/bubble/main/rekey/RekeyReaderMain.java
  16. +3
    -0
      bubble-server/src/main/java/bubble/model/account/Account.java
  17. +6
    -1
      bubble-server/src/main/java/bubble/model/bill/AccountPlan.java
  18. +10
    -2
      bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java
  19. +2
    -0
      bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java
  20. +17
    -0
      bubble-server/src/main/java/bubble/model/cloud/LaunchType.java
  21. +2
    -0
      bubble-server/src/main/java/bubble/model/device/DeviceStatus.java
  22. +19
    -0
      bubble-server/src/main/java/bubble/model/device/FlexRouterRemoveRoutes.java
  23. +3
    -0
      bubble-server/src/main/java/bubble/notify/NewNodeNotification.java
  24. +1
    -1
      bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java
  25. +2
    -3
      bubble-server/src/main/java/bubble/notify/NotificationHandler_sync_account.java
  26. +3
    -2
      bubble-server/src/main/java/bubble/resources/account/AuthResource.java
  27. +3
    -0
      bubble-server/src/main/java/bubble/resources/device/FlexRoutersResource.java
  28. +1
    -1
      bubble-server/src/main/java/bubble/resources/stream/FilterHttpRequest.java
  29. +13
    -10
      bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java
  30. +5
    -4
      bubble-server/src/main/java/bubble/rule/AppRuleDriver.java
  31. +5
    -4
      bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java
  32. +3
    -2
      bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java
  33. +4
    -4
      bubble-server/src/main/java/bubble/rule/social/block/UserBlockerRuleDriver.java
  34. +4
    -3
      bubble-server/src/main/java/bubble/server/BubbleConfiguration.java
  35. +1
    -1
      bubble-server/src/main/java/bubble/server/listener/BubbleFirstTimeListener.java
  36. +2
    -3
      bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java
  37. +1
    -2
      bubble-server/src/main/java/bubble/service/account/MitmControlService.java
  38. +2
    -0
      bubble-server/src/main/java/bubble/service/boot/ActivationService.java
  39. +7
    -6
      bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java
  40. +3
    -1
      bubble-server/src/main/java/bubble/service/cloud/AnsiblePrepService.java
  41. +1
    -2
      bubble-server/src/main/java/bubble/service/cloud/NodeLaunchMonitor.java
  42. +4
    -3
      bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java
  43. +4
    -2
      bubble-server/src/main/java/bubble/service/dbfilter/DatabaseFilterService.java
  44. +17
    -0
      bubble-server/src/main/java/bubble/service/dbfilter/FullEntityIterator.java
  45. +92
    -7
      bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java
  46. +37
    -9
      bubble-server/src/main/java/bubble/service/stream/ActiveStreamState.java
  47. +86
    -116
      bubble-server/src/main/java/bubble/service/stream/StandardAppPrimerService.java
  48. +26
    -2
      bubble-server/src/main/java/bubble/service/stream/StandardRuleEngineService.java
  49. +11
    -0
      bubble-server/src/main/java/bubble/service/stream/StreamConstants.java
  50. +27
    -0
      bubble-server/src/main/java/bubble/service/stream/charset/BubbleCharSet.java
  51. +23
    -0
      bubble-server/src/main/java/bubble/service/stream/charset/CharsetDetector.java
  52. +43
    -0
      bubble-server/src/main/java/bubble/service/stream/charset/HtmlCharsetDetector.java
  53. +92
    -0
      bubble-server/src/main/java/bubble/service/stream/charset/HtmlStreamCharsetDetector.java
  54. +27
    -2
      bubble-server/src/main/java/bubble/service/upgrade/AppUpgradeService.java
  55. +1
    -1
      bubble-server/src/main/resources/META-INF/bubble/bubble.properties
  56. +0
    -1486
      bubble-server/src/main/resources/apps.json
  57. +1
    -0
      bubble-server/src/main/resources/db/migration/V2020091801__add_network_launch_type.sql
  58. +1
    -1
      bubble-server/src/main/resources/messages
  59. +2
    -0
      bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru.json
  60. +2
    -3
      bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_api.py
  61. +37
    -11
      bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_modify.py
  62. +138
    -0
      bubble-server/src/test/java/bubble/test/filter/CharsetDetectionTest.java
  63. +21
    -0
      bubble-server/src/test/java/bubble/test/filter/PassthruDriver.java
  64. +2552
    -0
      bubble-server/src/test/resources/charset_detection/equiv-windows-1250.html
  65. +2569
    -0
      bubble-server/src/test/resources/charset_detection/meta-windows-1250-late.html
  66. +2554
    -0
      bubble-server/src/test/resources/charset_detection/meta-windows-1250.html
  67. +1
    -1
      bubble-web
  68. +17
    -0
      docs/dev.md
  69. +37
    -0
      docs/launch-node-from-local.md
  70. +40
    -0
      docs/launch-node-from-remote.md
  71. +9
    -27
      docs/launch-node.md
  72. +17
    -6
      docs/local-launcher.md
  73. +17
    -3
      docs/remote-launcher.md
  74. +2
    -0
      pom.xml
  75. +1
    -1
      utils/cobbzilla-parent
  76. +1
    -1
      utils/cobbzilla-utils
  77. +1
    -1
      utils/cobbzilla-wizard

+ 18
- 20
README.md Wyświetl plik

@@ -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)

+ 7
- 3
bin/bpatch Wyświetl plik

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

+ 13
- 5
bin/bpatchfull Wyświetl plik

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

+ 1
- 0
bin/build_dist Wyświetl plik

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

+ 49
- 0
bin/reset_bubble_full Wyświetl plik

@@ -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"

+ 1
- 0
bubble-server/src/main/java/bubble/ApiConstants.java Wyświetl plik

@@ -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");



+ 7
- 2
bubble-server/src/main/java/bubble/app/passthru/TlsPassthruAppConfigDriver.java Wyświetl plik

@@ -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);


+ 2
- 2
bubble-server/src/main/java/bubble/cloud/compute/ComputeServiceDriverBase.java Wyświetl plik

@@ -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) {


+ 9
- 3
bubble-server/src/main/java/bubble/cloud/geoLocation/GeoLocateServiceDriverBase.java Wyświetl plik

@@ -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));


+ 9
- 0
bubble-server/src/main/java/bubble/dao/SessionDAO.java Wyświetl plik

@@ -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);
}

}

+ 2
- 3
bubble-server/src/main/java/bubble/dao/account/AccountDAO.java Wyświetl plik

@@ -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())


+ 2
- 1
bubble-server/src/main/java/bubble/dao/account/AccountOwnedEntityDAO.java Wyświetl plik

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


+ 2
- 3
bubble-server/src/main/java/bubble/dao/account/AccountSshKeyDAO.java Wyświetl plik

@@ -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());


+ 8
- 1
bubble-server/src/main/java/bubble/dao/cloud/BubbleNetworkDAO.java Wyświetl plik

@@ -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());


+ 1
- 1
bubble-server/src/main/java/bubble/main/rekey/RekeyReaderMain.java Wyświetl plik

@@ -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);
}

}

+ 3
- 0
bubble-server/src/main/java/bubble/model/account/Account.java Wyświetl plik

@@ -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); }


+ 6
- 1
bubble-server/src/main/java/bubble/model/bill/AccountPlan.java Wyświetl plik

@@ -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);
}



+ 10
- 2
bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java Wyświetl plik

@@ -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) {


+ 2
- 0
bubble-server/src/main/java/bubble/model/cloud/BubbleNode.java Wyświetl plik

@@ -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)


+ 17
- 0
bubble-server/src/main/java/bubble/model/cloud/LaunchType.java Wyświetl plik

@@ -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); }

}

+ 2
- 0
bubble-server/src/main/java/bubble/model/device/DeviceStatus.java Wyświetl plik

@@ -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;


+ 19
- 0
bubble-server/src/main/java/bubble/model/device/FlexRouterRemoveRoutes.java Wyświetl plik

@@ -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);
}

}

+ 3
- 0
bubble-server/src/main/java/bubble/notify/NewNodeNotification.java Wyświetl plik

@@ -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); }



+ 1
- 1
bubble-server/src/main/java/bubble/notify/NotificationHandler_hello_from_sage.java Wyświetl plik

@@ -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();


+ 2
- 3
bubble-server/src/main/java/bubble/notify/NotificationHandler_sync_account.java Wyświetl plik

@@ -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);
}
}


+ 3
- 2
bubble-server/src/main/java/bubble/resources/account/AuthResource.java Wyświetl plik

@@ -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 {


+ 3
- 0
bubble-server/src/main/java/bubble/resources/device/FlexRoutersResource.java Wyświetl plik

@@ -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;
}


+ 1
- 1
bubble-server/src/main/java/bubble/resources/stream/FilterHttpRequest.java Wyświetl plik

@@ -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 () {


+ 13
- 10
bubble-server/src/main/java/bubble/rule/AbstractAppRuleDriver.java Wyświetl plik

@@ -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,


+ 5
- 4
bubble-server/src/main/java/bubble/rule/AppRuleDriver.java Wyświetl plik

@@ -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);


+ 5
- 4
bubble-server/src/main/java/bubble/rule/bblock/BubbleBlockRuleDriver.java Wyświetl plik

@@ -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 () {


+ 3
- 2
bubble-server/src/main/java/bubble/rule/social/block/JsUserBlockerRuleDriver.java Wyświetl plik

@@ -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);
}
}

+ 4
- 4
bubble-server/src/main/java/bubble/rule/social/block/UserBlockerRuleDriver.java Wyświetl plik

@@ -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 + "[^>]*>)"; }


+ 4
- 3
bubble-server/src/main/java/bubble/server/BubbleConfiguration.java Wyświetl plik

@@ -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()},


+ 1
- 1
bubble-server/src/main/java/bubble/server/listener/BubbleFirstTimeListener.java Wyświetl plik

@@ -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);


+ 2
- 3
bubble-server/src/main/java/bubble/server/listener/NodeInitializerListener.java Wyświetl plik

@@ -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();


+ 1
- 2
bubble-server/src/main/java/bubble/service/account/MitmControlService.java Wyświetl plik

@@ -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");
}
}


+ 2
- 0
bubble-server/src/main/java/bubble/service/boot/ActivationService.java Wyświetl plik

@@ -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())


+ 7
- 6
bubble-server/src/main/java/bubble/service/boot/StandardSelfNodeService.java Wyświetl plik

@@ -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));


+ 3
- 1
bubble-server/src/main/java/bubble/service/cloud/AnsiblePrepService.java Wyświetl plik

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


+ 1
- 2
bubble-server/src/main/java/bubble/service/cloud/NodeLaunchMonitor.java Wyświetl plik

@@ -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();



+ 4
- 3
bubble-server/src/main/java/bubble/service/cloud/StandardNetworkService.java Wyświetl plik

@@ -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);


+ 4
- 2
bubble-server/src/main/java/bubble/service/dbfilter/DatabaseFilterService.java Wyświetl plik

@@ -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);


+ 17
- 0
bubble-server/src/main/java/bubble/service/dbfilter/FullEntityIterator.java Wyświetl plik

@@ -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");
}



+ 92
- 7
bubble-server/src/main/java/bubble/service/device/StandardFlexRouterService.java Wyświetl plik

@@ -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;
}
}

}

+ 37
- 9
bubble-server/src/main/java/bubble/service/stream/ActiveStreamState.java Wyświetl plik

@@ -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;


+ 86
- 116
bubble-server/src/main/java/bubble/service/stream/StandardAppPrimerService.java Wyświetl plik

@@ -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));
}
}
}

}

+ 26
- 2
bubble-server/src/main/java/bubble/service/stream/StandardRuleEngineService.java Wyświetl plik

@@ -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);


+ 11
- 0
bubble-server/src/main/java/bubble/service/stream/StreamConstants.java Wyświetl plik

@@ -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;

}

+ 27
- 0
bubble-server/src/main/java/bubble/service/stream/charset/BubbleCharSet.java Wyświetl plik

@@ -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;

}

+ 23
- 0
bubble-server/src/main/java/bubble/service/stream/charset/CharsetDetector.java Wyświetl plik

@@ -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; }
}

}

+ 43
- 0
bubble-server/src/main/java/bubble/service/stream/charset/HtmlCharsetDetector.java Wyświetl plik

@@ -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);
}
}

}

+ 92
- 0
bubble-server/src/main/java/bubble/service/stream/charset/HtmlStreamCharsetDetector.java Wyświetl plik

@@ -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));
}

}

+ 27
- 2
bubble-server/src/main/java/bubble/service/upgrade/AppUpgradeService.java Wyświetl plik

@@ -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-server/src/main/resources/META-INF/bubble/bubble.properties Wyświetl plik

@@ -1 +1 @@
bubble.version=Adventure 1.2.1
bubble.version=Adventure 1.2.4

+ 0
- 1486
bubble-server/src/main/resources/apps.json
Plik diff jest za duży
Wyświetl plik


+ 1
- 0
bubble-server/src/main/resources/db/migration/V2020091801__add_network_launch_type.sql Wyświetl plik

@@ -0,0 +1 @@
ALTER TABLE bubble_network ADD COLUMN launch_type VARCHAR(200);

+ 1
- 1
bubble-server/src/main/resources/messages

@@ -1 +1 @@
Subproject commit 61fc9667c4c66c0bcbbb8eb4d128e8ab58f50869
Subproject commit 9d1ecac6514696721effe012e9c726896c368ebe

+ 2
- 0
bubble-server/src/main/resources/models/apps/passthru/bubbleApp_passthru.json Wyświetl plik

@@ -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},


+ 2
- 3
bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_api.py Wyświetl plik

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


+ 37
- 11
bubble-server/src/main/resources/packer/roles/mitmproxy/files/bubble_modify.py Wyświetl plik

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


+ 138
- 0
bubble-server/src/test/java/bubble/test/filter/CharsetDetectionTest.java Wyświetl plik

@@ -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;
}

}

+ 21
- 0
bubble-server/src/test/java/bubble/test/filter/PassthruDriver.java Wyświetl plik

@@ -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; }

}

+ 2552
- 0
bubble-server/src/test/resources/charset_detection/equiv-windows-1250.html
Plik diff jest za duży
Wyświetl plik


+ 2569
- 0
bubble-server/src/test/resources/charset_detection/meta-windows-1250-late.html
Plik diff jest za duży
Wyświetl plik


+ 2554
- 0
bubble-server/src/test/resources/charset_detection/meta-windows-1250.html
Plik diff jest za duży
Wyświetl plik


+ 1
- 1
bubble-web

@@ -1 +1 @@
Subproject commit 66d46695a64ff58934560c4b35aa43a0ab32fbe2
Subproject commit 61a7f288e515a185f2055bbe1e513943d54ac66c

+ 17
- 0
docs/dev.md Wyświetl plik

@@ -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.

+ 37
- 0
docs/launch-node-from-local.md Wyświetl plik

@@ -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.

+ 40
- 0
docs/launch-node-from-remote.md Wyświetl plik

@@ -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.

+ 9
- 27
docs/launch-node.md Wyświetl plik

@@ -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.


+ 17
- 6
docs/local-launcher.md Wyświetl plik

@@ -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)

+ 17
- 3
docs/remote-launcher.md Wyświetl plik

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

+ 2
- 0
pom.xml Wyświetl plik

@@ -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
utils/cobbzilla-parent

@@ -1 +1 @@
Subproject commit bcb92061c3482ba4803c0030c78fb550206a0511
Subproject commit 2324f5d196fa52931f07e85c830bf9c10465b8f8

+ 1
- 1
utils/cobbzilla-utils

@@ -1 +1 @@
Subproject commit dfafe62c7eb3413cf1210e40e551094458f4d9d0
Subproject commit a9d3d69f1112a47b8d69f9343cc66caf5bab1383

+ 1
- 1
utils/cobbzilla-wizard

@@ -1 +1 @@
Subproject commit cd9cf18c90a373f024163c048d57cf574ec97293
Subproject commit e91152f296a04ce1e4363ae90e1cfde70cdee4d5

Ładowanie…
Anuluj
Zapisz