diff --git a/automation/roles/bubble/files/bubble_role.json b/automation/roles/bubble/files/bubble_role.json index 909a098e..d6564889 100644 --- a/automation/roles/bubble/files/bubble_role.json +++ b/automation/roles/bubble/files/bubble_role.json @@ -12,6 +12,7 @@ {"name": "default_locale", "value": "[[network.locale]]"}, {"name": "bubble_version", "value": "[[configuration.version]]"}, {"name": "bubble_host", "value": "[[node.fqdn]]"}, + {"name": "bubble_cname", "value": "[[node.networkDomain]]"}, {"name": "admin_user", "value": "[[node.user]]"}, {"name": "db_encoding", "value": "UTF-8"}, {"name": "db_locale", "value": "en_US"}, diff --git a/automation/roles/bubble/templates/bubble.env.j2 b/automation/roles/bubble/templates/bubble.env.j2 index 50b40bb2..14384f3d 100644 --- a/automation/roles/bubble/templates/bubble.env.j2 +++ b/automation/roles/bubble/templates/bubble.env.j2 @@ -1,4 +1,4 @@ -export PUBLIC_BASE_URI=https://{{ bubble_host }}:{{ ssl_port }} +export PUBLIC_BASE_URI=https://{{ bubble_cname }}:{{ ssl_port }} export SELF_NODE={{ node_uuid }} export SAGE_NODE={{ sage_node }} export LETSENCRYPT_EMAIL={{ letsencrypt_email }} diff --git a/automation/roles/nginx/files/bubble_role.json b/automation/roles/nginx/files/bubble_role.json index 83fe4af9..04083834 100644 --- a/automation/roles/nginx/files/bubble_role.json +++ b/automation/roles/nginx/files/bubble_role.json @@ -4,6 +4,7 @@ "template": true, "config": [ {"name": "server_name", "value": "[[node.fqdn]]"}, + {"name": "server_alias", "value": "[[node.networkDomain]]"}, {"name": "letsencrypt_email", "value": "[[configuration.letsencryptEmail]]"}, {"name": "ssl_port", "value": "[[configuration.nginxPort]]"}, {"name": "admin_port", "value": "[[node.adminPort]]"} diff --git a/automation/roles/nginx/files/certbot_renew.sh b/automation/roles/nginx/files/certbot_renew.sh new file mode 100644 index 00000000..293535e2 --- /dev/null +++ b/automation/roles/nginx/files/certbot_renew.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +service mitmproxy stop && service nginx stop && certbot renew --standalone --non-interactive || echo "Error updating SSL certificates" +service mitmproxy restart +service nginx restart diff --git a/automation/roles/nginx/files/init_certbot.sh b/automation/roles/nginx/files/init_certbot.sh index 39f56fcb..922e169d 100755 --- a/automation/roles/nginx/files/init_certbot.sh +++ b/automation/roles/nginx/files/init_certbot.sh @@ -2,12 +2,13 @@ LE_EMAIL="${1}" SERVER_NAME="${2}" +SERVER_ALIAS="${3}" if [[ $(find /etc/letsencrypt/accounts -type f -name regr.json | xargs grep -l \"${LE_EMAIL}\" | wc -l | tr -d ' ') -eq 0 ]] ; then certbot register --agree-tos -m ${LE_EMAIL} --non-interactive fi if [[ ! -f /etc/letsencrypt/live/${SERVER_NAME}/fullchain.pem ]] ; then - certbot certonly --standalone --non-interactive -d ${SERVER_NAME} + certbot certonly --standalone --non-interactive -d ${SERVER_NAME} -d ${SERVER_ALIAS} else certbot renew --standalone --non-interactive fi diff --git a/automation/roles/nginx/tasks/main.yml b/automation/roles/nginx/tasks/main.yml index 960bb7c9..61bfa62a 100644 --- a/automation/roles/nginx/tasks/main.yml +++ b/automation/roles/nginx/tasks/main.yml @@ -48,7 +48,15 @@ mode: 0555 - name: Init certbot - shell: init_certbot.sh {{ letsencrypt_email }} {{ server_name }} + shell: init_certbot.sh {{ letsencrypt_email }} {{ server_name }} {{ server_alias }} + +- name: Install certbot_renew.sh weekly cron job + copy: + src: "certbot_renew.sh" + dest: /etc/cron.weekly/certbot_renew.sh + owner: root + group: root + mode: 0755 # see https://weakdh.org/sysadmin.html - name: Create a strong dhparam.pem diff --git a/automation/roles/nginx/templates/site.conf.j2 b/automation/roles/nginx/templates/site.conf.j2 index 66b5c1c2..f811d294 100644 --- a/automation/roles/nginx/templates/site.conf.j2 +++ b/automation/roles/nginx/templates/site.conf.j2 @@ -1,6 +1,6 @@ server { - server_name {{ server_name }}; + server_name {{ server_name }} {{ server_alias }}; listen {{ ssl_port }} ssl http2; ssl_certificate /etc/letsencrypt/live/{{ server_name }}/fullchain.pem; diff --git a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java index 26e40dac..5d564b71 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/AccountPlanDAO.java @@ -15,11 +15,13 @@ import bubble.server.BubbleConfiguration; import bubble.service.bill.RefundService; import bubble.service.cloud.NetworkService; import lombok.extern.slf4j.Slf4j; +import org.cobbzilla.wizard.validation.ValidationResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; import java.util.List; +import static bubble.model.cloud.BubbleNetwork.validateHostname; import static java.util.concurrent.TimeUnit.SECONDS; import static org.cobbzilla.util.daemon.ZillaRuntime.background; import static org.cobbzilla.util.daemon.ZillaRuntime.now; @@ -69,6 +71,9 @@ public class AccountPlanDAO extends AccountOwnedEntityDAO { } @Override public Object preCreate(AccountPlan accountPlan) { + final ValidationResult errors = validateHostname(accountPlan); + if (errors.isInvalid()) throw invalidEx(errors); + if (configuration.paymentsEnabled()) { if (!accountPlan.hasPaymentMethodObject()) throw invalidEx("err.paymentMethod.required"); if (!accountPlan.getPaymentMethodObject().hasUuid()) throw invalidEx("err.paymentMethod.required"); diff --git a/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java b/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java index c00adf7a..db5e5844 100644 --- a/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java +++ b/bubble-server/src/main/java/bubble/dao/bill/BubblePlanDAO.java @@ -4,6 +4,7 @@ import bubble.dao.account.AccountOwnedEntityDAO; import bubble.model.bill.BubblePlan; import bubble.model.cloud.BubbleNode; import bubble.server.BubbleConfiguration; +import org.hibernate.criterion.Order; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -14,6 +15,8 @@ public class BubblePlanDAO extends AccountOwnedEntityDAO { @Override public boolean dbFilterIncludeAll() { return true; } + @Override public Order getDefaultSortOrder() { return Order.asc("priority"); } + @Override public BubblePlan findByUuid(String uuid) { final BubblePlan plan = super.findByUuid(uuid); if (plan != null) return plan; diff --git a/bubble-server/src/main/java/bubble/dao/cloud/BubbleNetworkDAO.java b/bubble-server/src/main/java/bubble/dao/cloud/BubbleNetworkDAO.java index a394e539..9b148c15 100644 --- a/bubble-server/src/main/java/bubble/dao/cloud/BubbleNetworkDAO.java +++ b/bubble-server/src/main/java/bubble/dao/cloud/BubbleNetworkDAO.java @@ -12,6 +12,7 @@ import bubble.service.boot.SelfNodeService; import bubble.service.cloud.NetworkService; import lombok.extern.slf4j.Slf4j; import org.cobbzilla.wizard.model.IdentifiableBase; +import org.cobbzilla.wizard.validation.ValidationResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Repository; @@ -19,7 +20,9 @@ import java.io.IOException; import java.util.List; import java.util.stream.Collectors; +import static bubble.model.cloud.BubbleNetwork.validateHostname; import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; +import static org.cobbzilla.wizard.resources.ResourceUtil.invalidEx; @Repository @Slf4j public class BubbleNetworkDAO extends AccountOwnedEntityDAO { @@ -34,6 +37,9 @@ public class BubbleNetworkDAO extends AccountOwnedEntityDAO { @Autowired private BubbleConfiguration configuration; @Override public Object preCreate(BubbleNetwork network) { + final ValidationResult errors = validateHostname(network); + if (errors.isInvalid()) throw invalidEx(errors); + if (!network.hasLocale()) network.setLocale(getDEFAULT_LOCALE()); return super.preCreate(network); } diff --git a/bubble-server/src/main/java/bubble/model/account/AccountSshKey.java b/bubble-server/src/main/java/bubble/model/account/AccountSshKey.java index e844368c..8ea830ed 100644 --- a/bubble-server/src/main/java/bubble/model/account/AccountSshKey.java +++ b/bubble-server/src/main/java/bubble/model/account/AccountSshKey.java @@ -42,7 +42,6 @@ public class AccountSshKey extends IdentifiableBase implements HasAccount { @HasValue(message="err.name.required") @ECIndex(unique=true) @Column(nullable=false, updatable=false, length=100) @Getter @Setter private String name; - public boolean hasName () { return !empty(name); } @ECSearchable @ECField(index=20) @ECForeignKey(entity=Account.class) diff --git a/bubble-server/src/main/java/bubble/model/account/HasAccount.java b/bubble-server/src/main/java/bubble/model/account/HasAccount.java index 6394565d..32f8a0ed 100644 --- a/bubble-server/src/main/java/bubble/model/account/HasAccount.java +++ b/bubble-server/src/main/java/bubble/model/account/HasAccount.java @@ -10,6 +10,5 @@ public interface HasAccount extends Identifiable, NamedEntity, SqlViewSearchResu E setAccount (String account); default boolean hasAccount () { return getAccount() != null; } String getName(); - default boolean hasName() { return getName() != null; } } diff --git a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java index 03848d68..1b10497e 100644 --- a/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/AccountPlan.java @@ -22,6 +22,7 @@ import javax.persistence.Transient; import javax.validation.constraints.Size; import static bubble.model.bill.BillPeriod.BILL_START_END_FORMAT; +import static bubble.model.cloud.BubbleNetwork.NETWORK_NAME_MAXLEN; import static org.cobbzilla.util.daemon.ZillaRuntime.bool; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; @@ -50,8 +51,8 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { // mirrors network name @ECSearchable(filter=true) @ECField(index=10) - @Size(max=100, message="err.name.length") - @Column(length=100, nullable=false) + @Size(max=NETWORK_NAME_MAXLEN, message="err.name.length") + @Column(length=NETWORK_NAME_MAXLEN, nullable=false) @Getter @Setter private String name; @ECSearchable @ECField(index=20) @@ -138,6 +139,9 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { @Transient @Getter @Setter private transient AccountPaymentMethod paymentMethodObject = null; public boolean hasPaymentMethodObject () { return paymentMethodObject != null; } + @Transient @Getter @Setter private transient String forkHost = null; + public boolean hasForkHost () { return !empty(forkHost); } + public BubbleNetwork bubbleNetwork(Account account, BubbleDomain domain, BubblePlan plan, @@ -153,7 +157,8 @@ public class AccountPlan extends IdentifiableBase implements HasAccount { .setDomainName(domain.getName()) .setFootprint(getFootprint()) .setComputeSizeType(plan.getComputeSizeType()) - .setStorage(storage.getUuid()); + .setStorage(storage.getUuid()) + .setForkHost(hasForkHost() ? getForkHost() : null); } } diff --git a/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java b/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java index 735f0c8e..6b94cfd3 100644 --- a/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java +++ b/bubble-server/src/main/java/bubble/model/bill/BubblePlan.java @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import org.cobbzilla.util.collection.HasPriority; import org.cobbzilla.wizard.model.IdentifiableBase; import org.cobbzilla.wizard.model.entityconfig.annotations.*; import org.cobbzilla.wizard.validation.HasValue; @@ -32,13 +33,13 @@ import static org.cobbzilla.util.reflect.ReflectionUtil.copy; }) @Entity @NoArgsConstructor @Accessors(chain=true) @ECIndexes({ @ECIndex(unique=true, of={"account", "name"}) }) -public class BubblePlan extends IdentifiableBase implements HasAccount { +public class BubblePlan extends IdentifiableBase implements HasAccount, HasPriority { public static final int MAX_CHARGENAME_LEN = 12; public static final String[] CREATE_FIELDS = { - "name", "chargeName", "enabled", "price", "period", "computeSizeType", - "nodesIncluded", "additionalPerNodePrice", + "name", "chargeName", "enabled", "priority", "price", "period", + "computeSizeType", "nodesIncluded", "additionalPerNodePrice", "storageGbIncluded", "additionalStoragePerGbPrice", "bandwidthGbIncluded", "additionalBandwidthPerGbPrice" }; @@ -77,27 +78,30 @@ public class BubblePlan extends IdentifiableBase implements HasAccount { @Getter @Setter private Boolean enabled = true; public boolean enabled () { return enabled == null || enabled; } - @ECSearchable @ECField(index=50) + @ECSearchable @ECField(index=50) @Column(nullable=false) + @ECIndex @Getter @Setter private Integer priority = 1; + + @ECSearchable @ECField(index=60) @ECIndex @Column(nullable=false) @Getter @Setter private Long price; - @ECSearchable @ECField(index=60) + @ECSearchable @ECField(index=70) @ECIndex @Column(nullable=false, length=10) @Getter @Setter private String currency = "USD"; - @ECSearchable @ECField(index=70) + @ECSearchable @ECField(index=80) @Enumerated(EnumType.STRING) @Column(nullable=false, updatable=false, length=20) @Getter @Setter private BillPeriod period = BillPeriod.monthly; - @ECSearchable @ECField(index=80) + @ECSearchable @ECField(index=90) @ECIndex @Column(nullable=false, updatable=false, length=20) @Enumerated(EnumType.STRING) @Getter @Setter private ComputeNodeSizeType computeSizeType; - @ECSearchable @ECField(index=90) + @ECSearchable @ECField(index=100) @Column(nullable=false, updatable=false) @Getter @Setter private Integer nodesIncluded; - @ECSearchable @ECField(index=100) + @ECSearchable @ECField(index=110) @Column(nullable=false, updatable=false) @Getter @Setter private Integer additionalPerNodePrice; diff --git a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java index 5a6b1b7f..21cba2a4 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java +++ b/bubble-server/src/main/java/bubble/model/cloud/BubbleNetwork.java @@ -14,9 +14,11 @@ import lombok.experimental.Accessors; import org.cobbzilla.util.collection.ArrayUtil; import org.cobbzilla.wizard.model.Identifiable; import org.cobbzilla.wizard.model.IdentifiableBase; +import org.cobbzilla.wizard.model.NamedEntity; import org.cobbzilla.wizard.model.entityconfig.EntityFieldType; import org.cobbzilla.wizard.model.entityconfig.annotations.*; import org.cobbzilla.wizard.validation.HasValue; +import org.cobbzilla.wizard.validation.ValidationResult; import org.hibernate.annotations.Type; import javax.persistence.*; @@ -33,6 +35,8 @@ import static bubble.server.BubbleConfiguration.getDEFAULT_LOCALE; import static org.cobbzilla.util.daemon.ZillaRuntime.die; import static org.cobbzilla.util.daemon.ZillaRuntime.empty; import static org.cobbzilla.util.reflect.ReflectionUtil.copy; +import static org.cobbzilla.util.string.ValidationRegexes.HOST_PART_PATTERN; +import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENCRYPTED_STRING; import static org.cobbzilla.wizard.model.crypto.EncryptedTypes.ENC_PAD; @@ -67,9 +71,13 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu @Transient @JsonIgnore public String getNetwork () { return getUuid(); } + public static final int NETWORK_NAME_MAXLEN = 100; + public static final int NETWORK_NAME_MINLEN = 4; + @ECSearchable @ECField(index=10) @HasValue(message="err.name.required") - @ECIndex @Column(nullable=false, updatable=false, length=200) + @Size(min=NETWORK_NAME_MINLEN, max=NETWORK_NAME_MAXLEN, message="err.name.length") + @ECIndex @Column(nullable=false, updatable=false, length=NETWORK_NAME_MAXLEN) @Getter @Setter private String name; @ECSearchable @ECField(index=20) @@ -143,4 +151,22 @@ public class BubbleNetwork extends IdentifiableBase implements HasNetwork, HasBu } return die("hostFromFqdn("+fqdn+"): expected suffix ."+getNetworkDomain()); } + + public static ValidationResult validateHostname(NamedEntity request) { + return validateHostname(request, new ValidationResult()); + } + + public static ValidationResult validateHostname(NamedEntity request, ValidationResult errors) { + if (!request.hasName()) { + errors.addViolation("err.name.required"); + } else if (!validateRegexMatches(HOST_PART_PATTERN, request.getName())) { + errors.addViolation("err.name.invalid"); + } else if (request.getName().length() > NETWORK_NAME_MAXLEN) { + errors.addViolation("err.name.length"); + } else if (request.getName().length() < NETWORK_NAME_MINLEN) { + errors.addViolation("err.name.tooShort"); + } + return errors; + } + } diff --git a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java index c75b52d9..696a81fe 100644 --- a/bubble-server/src/main/java/bubble/model/cloud/CloudService.java +++ b/bubble-server/src/main/java/bubble/model/cloud/CloudService.java @@ -102,7 +102,7 @@ public class CloudService extends IdentifiableBaseParentEntity implements Accoun @ECIndex @Column(nullable=false, updatable=false, length=20) @Getter @Setter private CloudServiceType type; - @ECSearchable @ECField(index=50) + @ECSearchable @ECField(index=50) @Column(nullable=false) @ECIndex @Getter @Setter private Integer priority = 1; @ECSearchable @ECField(index=60) diff --git a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java index f1adf608..7c452ccb 100644 --- a/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java +++ b/bubble-server/src/main/java/bubble/resources/bill/AccountPlansResource.java @@ -38,6 +38,8 @@ import java.util.List; import java.util.stream.Collectors; import static bubble.ApiConstants.*; +import static org.cobbzilla.util.string.ValidationRegexes.HOST_PART_PATTERN; +import static org.cobbzilla.util.string.ValidationRegexes.validateRegexMatches; import static org.cobbzilla.wizard.resources.ResourceUtil.*; @Slf4j @@ -115,6 +117,16 @@ public class AccountPlansResource extends AccountOwnedResource