Repository: pritunl/pritunl-cloud Branch: master Commit: e75523642c38 Files: 1089 Total size: 7.3 MB Directory structure: gitextract_pa6e1wrf/ ├── .gitattributes ├── .gitignore ├── CHANGES ├── LICENSE ├── README.md ├── acme/ │ ├── acme.go │ ├── challenge.go │ ├── constants.go │ └── utils.go ├── advisory/ │ ├── advisory.go │ ├── constants.go │ └── utils.go ├── agent/ │ ├── agent.go │ ├── constants/ │ │ └── constants.go │ ├── imds/ │ │ ├── imds.go │ │ ├── journal.go │ │ ├── sync.go │ │ └── utils.go │ ├── logging/ │ │ ├── file.go │ │ ├── handler.go │ │ ├── logging.go │ │ └── systemd.go │ └── utils/ │ ├── sanitize.go │ └── sys.go ├── aggregate/ │ ├── block.go │ ├── deployment.go │ ├── disk.go │ ├── domain.go │ ├── instance.go │ ├── pod.go │ └── shape.go ├── ahandlers/ │ ├── alert.go │ ├── audit.go │ ├── auth.go │ ├── authority.go │ ├── balancer.go │ ├── block.go │ ├── certificate.go │ ├── check.go │ ├── completion.go │ ├── csrf.go │ ├── datacenter.go │ ├── devices.go │ ├── disk.go │ ├── domain.go │ ├── event.go │ ├── firewall.go │ ├── handlers.go │ ├── image.go │ ├── instance.go │ ├── license.go │ ├── log.go │ ├── node.go │ ├── organization.go │ ├── plan.go │ ├── pod.go │ ├── policy.go │ ├── pool.go │ ├── relations.go │ ├── secret.go │ ├── session.go │ ├── settings.go │ ├── shape.go │ ├── static.go │ ├── storage.go │ ├── subscription.go │ ├── theme.go │ ├── user.go │ ├── vpc.go │ └── zone.go ├── alert/ │ ├── alert.go │ ├── constants.go │ └── utils.go ├── alertevent/ │ ├── alertevent.go │ └── utils.go ├── arp/ │ └── arp.go ├── audit/ │ ├── audit.go │ ├── constants.go │ └── utils.go ├── auth/ │ ├── auth.go │ ├── authzero.go │ ├── azure.go │ ├── constants.go │ ├── errortypes.go │ ├── google.go │ ├── handler.go │ ├── jumpcloud.go │ ├── saml.go │ ├── state.go │ ├── sync.go │ └── utils.go ├── authority/ │ ├── authority.go │ ├── constants.go │ └── utils.go ├── authorizer/ │ ├── authorizer.go │ ├── constants.go │ └── utils.go ├── backup/ │ └── backup.go ├── balancer/ │ ├── balancer.go │ ├── constants.go │ └── utils.go ├── block/ │ ├── block.go │ ├── constants.go │ ├── errortypes.go │ ├── ip.go │ └── utils.go ├── bridges/ │ └── bridges.go ├── certificate/ │ ├── certificate.go │ ├── constants.go │ └── utils.go ├── cloud/ │ ├── cloud.go │ └── oracle.go ├── cloudinit/ │ ├── cloudinit.go │ ├── query.go │ └── utils.go ├── cmd/ │ ├── backup.go │ ├── dhcp.go │ ├── imds.go │ ├── instance.go │ ├── log.go │ ├── mtu.go │ ├── node.go │ ├── optimize.go │ └── settings.go ├── colorize/ │ └── colorize.go ├── completion/ │ └── completion.go ├── compositor/ │ └── compositor.go ├── config/ │ └── config.go ├── constants/ │ └── constants.go ├── cookie/ │ ├── cookie.go │ └── utils.go ├── crypto/ │ └── crypto.go ├── csrf/ │ └── csrf.go ├── data/ │ ├── disk.go │ ├── image.go │ ├── resize.go │ ├── sync.go │ └── utils.go ├── database/ │ ├── base.go │ ├── client.go │ ├── collection.go │ ├── database.go │ ├── errors.go │ ├── index.go │ └── utils.go ├── datacenter/ │ ├── constants.go │ ├── datacenter.go │ └── utils.go ├── defaults/ │ └── defaults.go ├── demo/ │ ├── alert.go │ ├── authority.go │ ├── balancer.go │ ├── block.go │ ├── certificate.go │ ├── datacenter.go │ ├── demo.go │ ├── disk.go │ ├── domain.go │ ├── firewall.go │ ├── instance.go │ ├── log.go │ ├── node.go │ ├── organization.go │ ├── plan.go │ ├── pod.go │ ├── policy.go │ ├── pool.go │ ├── rand.go │ ├── secret.go │ ├── shape.go │ ├── storage.go │ ├── subscription.go │ ├── user.go │ ├── vpc.go │ └── zone.go ├── deploy/ │ ├── deploy.go │ ├── deployments.go │ ├── disks.go │ ├── imds.go │ ├── instances.go │ ├── ipset.go │ ├── iptables.go │ ├── namespace.go │ ├── network.go │ └── services.go ├── deployment/ │ ├── constants.go │ ├── deployment.go │ └── utils.go ├── device/ │ ├── constants.go │ ├── device.go │ ├── facet.go │ └── utils.go ├── dhcpc/ │ ├── constants.go │ ├── dhcpc.go │ ├── imds.go │ ├── lease.go │ ├── lease4.go │ ├── lease6.go │ ├── systemd.go │ └── utils.go ├── dhcps/ │ ├── dhcp4.go │ ├── dhcp6.go │ ├── ndp.go │ └── systemd.go ├── disk/ │ ├── constants.go │ ├── disk.go │ ├── sort.go │ └── utils.go ├── dns/ │ ├── aws.go │ ├── cloudflare.go │ ├── constants.go │ ├── dns.go │ ├── errors.go │ ├── google.go │ ├── oracle.go │ └── utils.go ├── dnss/ │ ├── constants.go │ ├── database.go │ ├── dnss.go │ ├── plugin.go │ └── response.go ├── domain/ │ ├── constants.go │ ├── domain.go │ ├── record.go │ ├── sort.go │ └── utils.go ├── drive/ │ ├── drive.go │ └── utils.go ├── engine/ │ ├── bash.go │ ├── constants.go │ ├── engine.go │ ├── parser.go │ └── python.go ├── errortypes/ │ └── errortypes.go ├── eval/ │ ├── constants.go │ ├── errortypes.go │ ├── eval.go │ └── utils.go ├── event/ │ ├── event.go │ ├── listener.go │ └── socket.go ├── features/ │ ├── qemu.go │ └── systemd.go ├── finder/ │ ├── constants.go │ └── resources.go ├── firewall/ │ ├── constants.go │ ├── firewall.go │ ├── spec.go │ └── utils.go ├── geo/ │ └── geo.go ├── go.mod ├── go.sum ├── guest/ │ ├── guest.go │ └── power.go ├── hnetwork/ │ ├── hnetwork.go │ └── utils.go ├── hugepages/ │ └── hugepages.go ├── image/ │ ├── constants.go │ ├── errortypes.go │ ├── image.go │ ├── sort.go │ └── utils.go ├── imds/ │ ├── config.go │ ├── imds.go │ ├── resource/ │ │ ├── resource.go │ │ └── utils.go │ ├── server/ │ │ ├── config/ │ │ │ └── config.go │ │ ├── constants/ │ │ │ └── constants.go │ │ ├── errortypes/ │ │ │ └── errortypes.go │ │ ├── handlers/ │ │ │ ├── certificate.go │ │ │ ├── dhcp.go │ │ │ ├── handlers.go │ │ │ ├── instance.go │ │ │ ├── node.go │ │ │ ├── query.go │ │ │ ├── secret.go │ │ │ ├── sync.go │ │ │ └── vpc.go │ │ ├── router/ │ │ │ └── router.go │ │ ├── server.go │ │ ├── state/ │ │ │ └── state.go │ │ └── utils/ │ │ ├── files.go │ │ ├── misc.go │ │ └── request.go │ ├── systemd.go │ └── types/ │ ├── certificate.go │ ├── config.go │ ├── constants.go │ ├── domain.go │ ├── instance.go │ ├── journal.go │ ├── node.go │ ├── pod.go │ ├── secret.go │ ├── state.go │ └── vpc.go ├── info/ │ └── instance.go ├── instance/ │ ├── constants.go │ ├── errortypes.go │ ├── instance.go │ └── utils.go ├── interfaces/ │ └── interfaces.go ├── ip/ │ ├── interface.go │ └── ip.go ├── iproute/ │ ├── address.go │ ├── bridge.go │ └── iface.go ├── ipset/ │ ├── names.go │ ├── sets.go │ ├── state.go │ └── utils.go ├── iptables/ │ ├── iptables.go │ ├── lock.go │ ├── rules.go │ ├── state.go │ ├── update.go │ └── utils.go ├── ipvs/ │ ├── constants.go │ ├── ipvs.go │ ├── service.go │ └── target.go ├── iscsi/ │ └── iscsi.go ├── iso/ │ └── iso.go ├── journal/ │ ├── constants.go │ ├── journal.go │ ├── store.go │ └── utils.go ├── lock/ │ └── lvm.go ├── log/ │ ├── constants.go │ ├── log.go │ └── utils.go ├── logger/ │ ├── database.go │ ├── file.go │ ├── formatter.go │ ├── hook.go │ ├── limiter.go │ ├── logger.go │ ├── sender.go │ └── writer.go ├── lvm/ │ ├── lv.go │ └── vgs.go ├── main.go ├── middlewear/ │ ├── gzip.go │ └── middlewear.go ├── mtu/ │ └── mtu.go ├── netconf/ │ ├── address.go │ ├── base.go │ ├── bridge.go │ ├── clear.go │ ├── external.go │ ├── host.go │ ├── iface.go │ ├── imds.go │ ├── internal.go │ ├── ip.go │ ├── netconf.go │ ├── nodeport.go │ ├── oracle.go │ ├── space.go │ ├── utils.go │ ├── validate.go │ └── vlan.go ├── node/ │ ├── block.go │ ├── certificate.go │ ├── constants.go │ ├── interfaces.go │ ├── node.go │ ├── oracle.go │ └── utils.go ├── nodeport/ │ ├── constants.go │ ├── mapping.go │ ├── network.go │ ├── nodeport.go │ └── utils.go ├── nonce/ │ └── nonce.go ├── notification/ │ └── notification.go ├── oracle/ │ ├── iface.go │ ├── metadata.go │ ├── oracle.go │ ├── provider.go │ ├── routetable.go │ ├── subnet.go │ ├── utils.go │ └── vnic.go ├── organization/ │ ├── organization.go │ └── utils.go ├── paths/ │ ├── paths.go │ └── utils.go ├── pci/ │ ├── pci.go │ └── utils.go ├── permission/ │ ├── permission.go │ └── user.go ├── plan/ │ ├── constants.go │ ├── data.go │ ├── plan.go │ └── utils.go ├── planner/ │ ├── planner.go │ └── utils.go ├── pod/ │ ├── pod.go │ └── utils.go ├── policy/ │ ├── constants.go │ ├── policy.go │ └── utils.go ├── pool/ │ ├── constants.go │ ├── pool.go │ └── utils.go ├── proxy/ │ ├── constants.go │ ├── domain.go │ ├── errortypes.go │ ├── proxy.go │ ├── resolver.go │ ├── reverse.go │ ├── transport.go │ ├── types.go │ ├── utils.go │ └── ws.go ├── qemu/ │ ├── constants.go │ ├── data.go │ ├── disk.go │ ├── manage.go │ ├── network.go │ ├── power.go │ ├── qemu.go │ ├── routes.go │ ├── sort.go │ ├── usb.go │ └── utils.go ├── qga/ │ └── qga.go ├── qmp/ │ ├── backup.go │ ├── disk.go │ ├── errors.go │ ├── password.go │ ├── power.go │ ├── qmp.go │ └── vnc.go ├── qms/ │ ├── disk.go │ ├── power.go │ ├── qms.go │ ├── usb.go │ ├── utils.go │ └── vnc.go ├── redirect/ │ ├── acme.go │ ├── crypto/ │ │ └── crypto.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── utils.go ├── relations/ │ ├── definitions/ │ │ ├── block.go │ │ ├── certificate.go │ │ ├── datacenter.go │ │ ├── definitions.go │ │ ├── firewall.go │ │ ├── instance.go │ │ ├── node.go │ │ ├── organization.go │ │ ├── pod.go │ │ ├── policy.go │ │ ├── secret.go │ │ ├── shape.go │ │ ├── vpc.go │ │ └── zone.go │ ├── registry.go │ ├── relations.go │ ├── response.go │ └── utils.go ├── render/ │ ├── constants.go │ └── render.go ├── requires/ │ ├── errors.go │ └── requires.go ├── rokey/ │ ├── cache.go │ ├── rokey.go │ └── utils.go ├── router/ │ ├── certificates.go │ ├── constants.go │ └── router.go ├── scheduler/ │ ├── constants.go │ ├── scheduler.go │ ├── unit.go │ └── utils.go ├── secondary/ │ ├── constants.go │ ├── duo.go │ ├── errors.go │ ├── okta.go │ ├── onelogin.go │ ├── secondary.go │ └── utils.go ├── secret/ │ ├── constants.go │ ├── oracle.go │ ├── secret.go │ └── utils.go ├── session/ │ ├── constants.go │ ├── session.go │ └── utils.go ├── settings/ │ ├── acme.go │ ├── auth.go │ ├── hypervisor.go │ ├── local.go │ ├── registry.go │ ├── router.go │ ├── settings.go │ ├── system.go │ └── telemetry.go ├── setup/ │ └── iptables.go ├── shape/ │ ├── constants.go │ ├── node.go │ ├── shape.go │ └── utils.go ├── signature/ │ ├── signature.go │ └── utils.go ├── spec/ │ ├── constants.go │ ├── domain.go │ ├── firewall.go │ ├── instance.go │ ├── journal.go │ ├── node.go │ ├── spec.go │ └── utils.go ├── state/ │ ├── arps.go │ ├── authorities.go │ ├── authorities_preload.go │ ├── datacenter.go │ ├── deployments.go │ ├── disks.go │ ├── domains.go │ ├── firewalls.go │ ├── firewalls_preload.go │ ├── instances.go │ ├── instances_preload.go │ ├── network.go │ ├── package.go │ ├── pools.go │ ├── runtimes.go │ ├── schedulers.go │ ├── state.go │ ├── state_old.go │ ├── virtuals.go │ ├── vpcs.go │ ├── zone.go │ └── zones.go ├── static/ │ ├── file.go │ └── static.go ├── storage/ │ ├── constants.go │ ├── storage.go │ └── utils.go ├── store/ │ ├── address.go │ ├── arp.go │ ├── disks.go │ ├── routes.go │ ├── usb.go │ └── virt.go ├── subscription/ │ └── subscription.go ├── sync/ │ ├── auth.go │ ├── nodes.go │ ├── sync.go │ └── vm.go ├── systemd/ │ ├── systemd.go │ └── utils.go ├── task/ │ ├── acme.go │ ├── advisory.go │ ├── backing.go │ ├── balancer.go │ ├── blocks.go │ ├── cache.go │ ├── constants.go │ ├── deployments.go │ ├── domains.go │ ├── imds.go │ ├── job.go │ ├── notification.go │ ├── scheduler.go │ ├── spec.go │ ├── specindex.go │ ├── storage.go │ └── task.go ├── telemetry/ │ ├── constants.go │ ├── telemetry.go │ ├── updates.go │ └── utils.go ├── tools/ │ ├── autoindex.py │ ├── build_run.sh │ ├── builder.py │ ├── generate_demo_data.py │ ├── generate_files.py │ ├── package/ │ │ ├── PKGBUILD │ │ └── README.md │ ├── pritunl-cloud-redirect.service │ ├── pritunl-cloud-redirect.socket │ ├── pritunl-cloud.service │ ├── tsc_run.sh │ ├── virt-install/ │ │ ├── README.md │ │ ├── download.sh │ │ ├── install/ │ │ │ ├── almalinux10.sh │ │ │ ├── almalinux8.sh │ │ │ ├── almalinux9.sh │ │ │ ├── alpinelinux.sh │ │ │ ├── archlinux.sh │ │ │ ├── fedora43.sh │ │ │ ├── fedora44.sh │ │ │ ├── freebsd.sh │ │ │ ├── oraclelinux10.sh │ │ │ ├── oraclelinux7.sh │ │ │ ├── oraclelinux8.sh │ │ │ ├── oraclelinux9.sh │ │ │ ├── rockylinux10.sh │ │ │ ├── rockylinux8.sh │ │ │ ├── rockylinux9.sh │ │ │ ├── ubuntu24.sh │ │ │ └── ubuntu26.sh │ │ └── setup/ │ │ ├── alpine.sh │ │ ├── arch.sh │ │ ├── debian.sh │ │ ├── fedora.sh │ │ ├── freebsd.sh │ │ ├── rhel10.sh │ │ ├── rhel7.sh │ │ ├── rhel8.sh │ │ └── rhel9.sh │ └── webpack_run.sh ├── tpm/ │ ├── tpm.go │ └── utils.go ├── twilio/ │ ├── twilio.go │ └── utils.go ├── uhandlers/ │ ├── alert.go │ ├── auth.go │ ├── authority.go │ ├── balancer.go │ ├── certificate.go │ ├── check.go │ ├── completion.go │ ├── csrf.go │ ├── datacenter.go │ ├── devices.go │ ├── disk.go │ ├── domain.go │ ├── event.go │ ├── firewall.go │ ├── handlers.go │ ├── image.go │ ├── instance.go │ ├── license.go │ ├── node.go │ ├── organization.go │ ├── plan.go │ ├── pod.go │ ├── pool.go │ ├── relations.go │ ├── secret.go │ ├── shape.go │ ├── static.go │ ├── theme.go │ ├── utils.go │ ├── vpc.go │ └── zone.go ├── unit/ │ ├── unit.go │ └── utils.go ├── upgrade/ │ ├── created.go │ ├── instance.go │ ├── journal.go │ ├── node.go │ ├── objectid.go │ ├── roles.go │ ├── state.go │ ├── upgrade.go │ └── zone_datacenter.go ├── usb/ │ ├── usb.go │ └── utils.go ├── user/ │ ├── constants.go │ ├── user.go │ └── utils.go ├── useragent/ │ └── useragent.go ├── utils/ │ ├── crypto.go │ ├── dns.go │ ├── files.go │ ├── filter.go │ ├── limiter.go │ ├── math.go │ ├── misc.go │ ├── multilock.go │ ├── multitimeoutlock.go │ ├── network.go │ ├── proc.go │ ├── prompt.go │ ├── psutil_freebsd.go │ ├── psutil_linux.go │ ├── randomname.go │ ├── request.go │ ├── sort.go │ ├── timeoutlock.go │ ├── unix.go │ └── webauthn.go ├── validator/ │ └── validator.go ├── version/ │ ├── cache.go │ ├── utils.go │ └── version.go ├── virtiofs/ │ ├── systemd.go │ ├── utils.go │ └── virtiofs.go ├── vm/ │ ├── constants.go │ ├── sort.go │ ├── utils.go │ └── vm.go ├── vmdk/ │ └── utils.go ├── vpc/ │ ├── constants.go │ ├── ip.go │ ├── subnet.go │ ├── utils.go │ └── vpc.go ├── vxlan/ │ └── vxlan.go ├── www/ │ ├── .gitignore │ ├── README.md │ ├── app/ │ │ ├── Alert.ts │ │ ├── App.tsx │ │ ├── Constants.ts │ │ ├── Csrf.ts │ │ ├── EditorThemes.ts │ │ ├── Event.ts │ │ ├── EventEmitter.ts │ │ ├── License.ts │ │ ├── Loader.ts │ │ ├── References.d.ts │ │ ├── Router.ts │ │ ├── Styles.tsx │ │ ├── Theme.ts │ │ ├── actions/ │ │ │ ├── AlertActions.ts │ │ │ ├── AuditActions.ts │ │ │ ├── AuthorityActions.ts │ │ │ ├── BalancerActions.ts │ │ │ ├── BlockActions.ts │ │ │ ├── CertificateActions.ts │ │ │ ├── CompletionActions.ts │ │ │ ├── DatacenterActions.ts │ │ │ ├── DeviceActions.ts │ │ │ ├── DiskActions.ts │ │ │ ├── DomainActions.ts │ │ │ ├── FirewallActions.ts │ │ │ ├── ImageActions.ts │ │ │ ├── InstanceActions.ts │ │ │ ├── LogActions.ts │ │ │ ├── NodeActions.ts │ │ │ ├── OrganizationActions.ts │ │ │ ├── PlanActions.ts │ │ │ ├── PodActions.ts │ │ │ ├── PolicyActions.ts │ │ │ ├── PoolActions.ts │ │ │ ├── RelationsActions.ts │ │ │ ├── SecretActions.ts │ │ │ ├── SessionActions.ts │ │ │ ├── SettingsActions.ts │ │ │ ├── ShapeActions.ts │ │ │ ├── StorageActions.ts │ │ │ ├── SubscriptionActions.ts │ │ │ ├── UserActions.ts │ │ │ ├── VpcActions.ts │ │ │ └── ZoneActions.ts │ │ ├── completion/ │ │ │ ├── Cache.ts │ │ │ ├── Engine.ts │ │ │ └── Types.ts │ │ ├── components/ │ │ │ ├── AdvisoryDialog.tsx │ │ │ ├── Alert.tsx │ │ │ ├── AlertDetailed.tsx │ │ │ ├── AlertNew.tsx │ │ │ ├── Alerts.tsx │ │ │ ├── AlertsFilter.tsx │ │ │ ├── AlertsPage.tsx │ │ │ ├── Audit.tsx │ │ │ ├── Audits.tsx │ │ │ ├── AuditsPage.tsx │ │ │ ├── Authorities.tsx │ │ │ ├── AuthoritiesFilter.tsx │ │ │ ├── AuthoritiesPage.tsx │ │ │ ├── Authority.tsx │ │ │ ├── AuthorityDetailed.tsx │ │ │ ├── AuthorityNew.tsx │ │ │ ├── Balancer.tsx │ │ │ ├── BalancerBackend.tsx │ │ │ ├── BalancerDetailed.tsx │ │ │ ├── BalancerDomain.tsx │ │ │ ├── BalancerNew.tsx │ │ │ ├── Balancers.tsx │ │ │ ├── BalancersFilter.tsx │ │ │ ├── BalancersPage.tsx │ │ │ ├── Block.tsx │ │ │ ├── BlockDetailed.tsx │ │ │ ├── BlockNew.tsx │ │ │ ├── Blocks.tsx │ │ │ ├── BlocksFilter.tsx │ │ │ ├── BlocksPage.tsx │ │ │ ├── Certificate.tsx │ │ │ ├── CertificateDetailed.tsx │ │ │ ├── CertificateDomain.tsx │ │ │ ├── CertificateNew.tsx │ │ │ ├── Certificates.tsx │ │ │ ├── CertificatesFilter.tsx │ │ │ ├── CertificatesPage.tsx │ │ │ ├── ConfirmButton.tsx │ │ │ ├── CopyButton.tsx │ │ │ ├── Datacenter.tsx │ │ │ ├── DatacenterDetailed.tsx │ │ │ ├── DatacenterNew.tsx │ │ │ ├── Datacenters.tsx │ │ │ ├── DatacentersFilter.tsx │ │ │ ├── DatacentersPage.tsx │ │ │ ├── Device.tsx │ │ │ ├── Devices.tsx │ │ │ ├── Disk.tsx │ │ │ ├── DiskDetailed.tsx │ │ │ ├── DiskNew.tsx │ │ │ ├── Disks.tsx │ │ │ ├── DisksFilter.tsx │ │ │ ├── DisksPage.tsx │ │ │ ├── Domain.tsx │ │ │ ├── DomainDetailed.tsx │ │ │ ├── DomainNew.tsx │ │ │ ├── DomainRecord.tsx │ │ │ ├── Domains.tsx │ │ │ ├── DomainsFilter.tsx │ │ │ ├── DomainsPage.tsx │ │ │ ├── Editor.tsx │ │ │ ├── Firewall.tsx │ │ │ ├── FirewallDetailed.tsx │ │ │ ├── FirewallNew.tsx │ │ │ ├── FirewallRule.tsx │ │ │ ├── Firewalls.tsx │ │ │ ├── FirewallsFilter.tsx │ │ │ ├── FirewallsPage.tsx │ │ │ ├── Help.tsx │ │ │ ├── Image.tsx │ │ │ ├── ImageDetailed.tsx │ │ │ ├── Images.tsx │ │ │ ├── ImagesFilter.tsx │ │ │ ├── ImagesPage.tsx │ │ │ ├── Instance.tsx │ │ │ ├── InstanceDetailed.tsx │ │ │ ├── InstanceImages.tsx │ │ │ ├── InstanceIscsiDevice.tsx │ │ │ ├── InstanceLicense.tsx │ │ │ ├── InstanceMount.tsx │ │ │ ├── InstanceNew.tsx │ │ │ ├── InstanceNodePort.tsx │ │ │ ├── Instances.tsx │ │ │ ├── InstancesFilter.tsx │ │ │ ├── InstancesPage.tsx │ │ │ ├── LoadingBar.tsx │ │ │ ├── LoadingCircle.tsx │ │ │ ├── Log.tsx │ │ │ ├── LogViewer.tsx │ │ │ ├── Logs.tsx │ │ │ ├── LogsFilter.tsx │ │ │ ├── LogsPage.tsx │ │ │ ├── Main.tsx │ │ │ ├── MarkdownMemo.tsx │ │ │ ├── Node.tsx │ │ │ ├── NodeBlock.tsx │ │ │ ├── NodeDeploy.tsx │ │ │ ├── NodeDetailed.tsx │ │ │ ├── NodeShare.tsx │ │ │ ├── Nodes.tsx │ │ │ ├── NodesFilter.tsx │ │ │ ├── NodesPage.tsx │ │ │ ├── NonState.tsx │ │ │ ├── Organization.tsx │ │ │ ├── OrganizationDetailed.tsx │ │ │ ├── OrganizationNew.tsx │ │ │ ├── OrganizationSelect.tsx │ │ │ ├── Organizations.tsx │ │ │ ├── OrganizationsFilter.tsx │ │ │ ├── OrganizationsPage.tsx │ │ │ ├── Page.tsx │ │ │ ├── PageButton.tsx │ │ │ ├── PageCreate.tsx │ │ │ ├── PageCustom.tsx │ │ │ ├── PageDateTime.tsx │ │ │ ├── PageHeader.tsx │ │ │ ├── PageInfo.tsx │ │ │ ├── PageInput.tsx │ │ │ ├── PageInputButton.tsx │ │ │ ├── PageInputSwitch.tsx │ │ │ ├── PageNew.tsx │ │ │ ├── PageNumInput.tsx │ │ │ ├── PagePanel.tsx │ │ │ ├── PageSave.tsx │ │ │ ├── PageSelect.tsx │ │ │ ├── PageSelectButton.tsx │ │ │ ├── PageSelectButtonConfirm.tsx │ │ │ ├── PageSelector.tsx │ │ │ ├── PageSplit.tsx │ │ │ ├── PageSwitch.tsx │ │ │ ├── PageTextArea.tsx │ │ │ ├── Plan.tsx │ │ │ ├── PlanDetailed.tsx │ │ │ ├── PlanEditor.tsx │ │ │ ├── PlanNew.tsx │ │ │ ├── PlanStatement.tsx │ │ │ ├── Plans.tsx │ │ │ ├── PlansFilter.tsx │ │ │ ├── PlansPage.tsx │ │ │ ├── Pod.tsx │ │ │ ├── PodDeploy.tsx │ │ │ ├── PodDeployment.tsx │ │ │ ├── PodDeploymentEdit.tsx │ │ │ ├── PodDetailed.tsx │ │ │ ├── PodEditor.tsx │ │ │ ├── PodMigrate.tsx │ │ │ ├── PodNew.tsx │ │ │ ├── PodUnit.tsx │ │ │ ├── PodWorkspace.tsx │ │ │ ├── Pods.tsx │ │ │ ├── PodsFilter.tsx │ │ │ ├── PodsPage.tsx │ │ │ ├── Policies.tsx │ │ │ ├── PoliciesFilter.tsx │ │ │ ├── PoliciesPage.tsx │ │ │ ├── Policy.tsx │ │ │ ├── PolicyDetailed.tsx │ │ │ ├── PolicyNew.tsx │ │ │ ├── PolicyRule.tsx │ │ │ ├── Pool.tsx │ │ │ ├── PoolDetailed.tsx │ │ │ ├── PoolNew.tsx │ │ │ ├── Pools.tsx │ │ │ ├── PoolsFilter.tsx │ │ │ ├── PoolsPage.tsx │ │ │ ├── Relations.tsx │ │ │ ├── RouterLink.tsx │ │ │ ├── RouterRedirect.tsx │ │ │ ├── RouterRoute.tsx │ │ │ ├── RouterRoutes.tsx │ │ │ ├── SearchInput.tsx │ │ │ ├── Secret.tsx │ │ │ ├── SecretDetailed.tsx │ │ │ ├── SecretNew.tsx │ │ │ ├── Secrets.tsx │ │ │ ├── SecretsFilter.tsx │ │ │ ├── SecretsPage.tsx │ │ │ ├── Session.tsx │ │ │ ├── Sessions.tsx │ │ │ ├── Settings.tsx │ │ │ ├── SettingsProvider.tsx │ │ │ ├── SettingsSecondaryProvider.tsx │ │ │ ├── Shape.tsx │ │ │ ├── ShapeDetailed.tsx │ │ │ ├── ShapeNew.tsx │ │ │ ├── Shapes.tsx │ │ │ ├── ShapesFilter.tsx │ │ │ ├── ShapesPage.tsx │ │ │ ├── Storage.tsx │ │ │ ├── StorageDetailed.tsx │ │ │ ├── StorageNew.tsx │ │ │ ├── Storages.tsx │ │ │ ├── StoragesFilter.tsx │ │ │ ├── StoragesPage.tsx │ │ │ ├── Subscription.tsx │ │ │ ├── Switch.tsx │ │ │ ├── SwitchNull.tsx │ │ │ ├── User.tsx │ │ │ ├── UserDetailed.tsx │ │ │ ├── Users.tsx │ │ │ ├── UsersFilter.tsx │ │ │ ├── UsersPage.tsx │ │ │ ├── Vpc.tsx │ │ │ ├── VpcArp.tsx │ │ │ ├── VpcDetailed.tsx │ │ │ ├── VpcLinkUri.tsx │ │ │ ├── VpcMap.tsx │ │ │ ├── VpcNew.tsx │ │ │ ├── VpcRoute.tsx │ │ │ ├── VpcSubnet.tsx │ │ │ ├── Vpcs.tsx │ │ │ ├── VpcsFilter.tsx │ │ │ ├── VpcsPage.tsx │ │ │ ├── Zone.tsx │ │ │ ├── ZoneDetailed.tsx │ │ │ ├── ZoneNew.tsx │ │ │ ├── Zones.tsx │ │ │ ├── ZonesFilter.tsx │ │ │ └── ZonesPage.tsx │ │ ├── dispatcher/ │ │ │ ├── Base.ts │ │ │ ├── Dispatcher.ts │ │ │ ├── EventDispatcher.ts │ │ │ └── LoadingDispatcher.ts │ │ ├── stores/ │ │ │ ├── AlertsStore.ts │ │ │ ├── AuditsStore.ts │ │ │ ├── AuthoritiesStore.ts │ │ │ ├── BalancersStore.ts │ │ │ ├── BlocksStore.ts │ │ │ ├── CertificatesStore.ts │ │ │ ├── CompletionStore.ts │ │ │ ├── DatacentersStore.ts │ │ │ ├── DevicesStore.ts │ │ │ ├── DisksStore.ts │ │ │ ├── DomainsNameStore.ts │ │ │ ├── DomainsStore.ts │ │ │ ├── FirewallsStore.ts │ │ │ ├── ImagesDatacenterStore.ts │ │ │ ├── ImagesStore.ts │ │ │ ├── InstancesNodeStore.ts │ │ │ ├── InstancesStore.ts │ │ │ ├── LoadingStore.ts │ │ │ ├── LogsStore.ts │ │ │ ├── NodesStore.ts │ │ │ ├── NodesZoneStore.ts │ │ │ ├── OrganizationsStore.ts │ │ │ ├── PlansStore.ts │ │ │ ├── PodsStore.ts │ │ │ ├── PodsUnitStore.ts │ │ │ ├── PoliciesStore.ts │ │ │ ├── PoolsStore.ts │ │ │ ├── SecretsStore.ts │ │ │ ├── SessionsStore.ts │ │ │ ├── SettingsStore.ts │ │ │ ├── ShapesStore.ts │ │ │ ├── StoragesStore.ts │ │ │ ├── SubscriptionStore.ts │ │ │ ├── UserStore.ts │ │ │ ├── UsersStore.ts │ │ │ ├── VpcsNameStore.ts │ │ │ ├── VpcsStore.ts │ │ │ └── ZonesStore.ts │ │ ├── types/ │ │ │ ├── AgentTypes.ts │ │ │ ├── AlertTypes.ts │ │ │ ├── AuditTypes.ts │ │ │ ├── AuthorityTypes.ts │ │ │ ├── BalancerTypes.ts │ │ │ ├── BlockTypes.ts │ │ │ ├── CertificateTypes.ts │ │ │ ├── CompletionTypes.ts │ │ │ ├── DatacenterTypes.ts │ │ │ ├── DeviceTypes.ts │ │ │ ├── DiskTypes.ts │ │ │ ├── DomainTypes.ts │ │ │ ├── FirewallTypes.ts │ │ │ ├── GlobalTypes.ts │ │ │ ├── ImageTypes.ts │ │ │ ├── InstanceTypes.ts │ │ │ ├── LoadingTypes.ts │ │ │ ├── LogTypes.ts │ │ │ ├── NodeTypes.ts │ │ │ ├── OrganizationTypes.ts │ │ │ ├── PlanTypes.ts │ │ │ ├── PodTypes.ts │ │ │ ├── PolicyTypes.ts │ │ │ ├── PoolTypes.ts │ │ │ ├── RelationTypes.ts │ │ │ ├── RouterTypes.ts │ │ │ ├── SecretTypes.ts │ │ │ ├── SessionTypes.ts │ │ │ ├── SettingsTypes.ts │ │ │ ├── ShapeTypes.ts │ │ │ ├── StorageTypes.ts │ │ │ ├── SubscriptionTypes.ts │ │ │ ├── UserTypes.ts │ │ │ ├── VpcTypes.ts │ │ │ └── ZoneTypes.ts │ │ └── utils/ │ │ ├── AgentUtils.ts │ │ └── MiscUtils.tsx │ ├── build.sh │ ├── build_remote.sh │ ├── dist/ │ │ ├── index.html │ │ ├── login.html │ │ ├── static/ │ │ │ ├── blueprint-datetime2.css │ │ │ ├── blueprint-icons.css │ │ │ ├── blueprint3.css │ │ │ ├── blueprint5.css │ │ │ ├── global.css │ │ │ └── normalize.css │ │ └── uindex.html │ ├── dist-dev/ │ │ ├── index.html │ │ ├── login.html │ │ ├── static/ │ │ │ ├── blueprint-datetime2.css │ │ │ ├── blueprint-icons.css │ │ │ ├── blueprint3.css │ │ │ ├── blueprint5.css │ │ │ ├── global.css │ │ │ └── normalize.css │ │ └── uindex.html │ ├── index.html │ ├── index_dist.html │ ├── login.html │ ├── package.json │ ├── styles/ │ │ ├── blueprint.css │ │ └── global.css │ ├── tsconfig.json │ ├── uindex.html │ ├── uindex_dist.html │ ├── webpack.config.js │ └── webpack.dev.config.js └── zone/ ├── constants.go ├── utils.go └── zone.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ www/app/EditorThemes.ts linguist-vendored *.html linguist-vendored *.css linguist-vendored *.js linguist-vendored ================================================ FILE: .gitignore ================================================ .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes Icon? ehthumbs.db Thumbs.db *.pyc *.egg *.egg-info build_keys.json /pritunl-cloud /builder/builder /agent/agent /imds/server/server ================================================ FILE: CHANGES ================================================ pritunl-cloud changelog ======================= <%= version %> Add Google Cloud DNS support Add support to manual renew certificates Fix host network bridge management issues Optimize database indexes Version 2.0.3665.99 2025-12-06 ------------------------------ Add internal DNS system Add arch linux support Improve Route53 DNS management Improve instance layout in web console Improve instance web console VNC Version 2.0.3616.97 2025-10-18 ------------------------------ Add journal kind to pods Add support for QEMU 10 Improve QEMU tuning Fix relations query Fix RHEL 7 IPv6 network configuration Add imds service recovery Add update advisories Web console layout improvements Version 2.0.3592.49 2025-09-24 ------------------------------ Add pod system Add imds service Add secrets storage Add LVM storage pools Add DNS TXT verification for Lets Encrypt Add support for custom vpc arp entries Add node port support Add dhcp client and server Add cloud script support Version 1.2.2933.86 2023-12-05 ------------------------------ Fix fast login issues Version 1.2.2900.76 2023-11-02 ------------------------------ Improve single sign-on fast login Version 1.2.2853.96 2023-09-16 ------------------------------ Add VPC network maps Fix QEMU desktop GUI on Fedora 38 Version 1.2.2759.95 2023-06-14 ------------------------------ Add support for Fedora 38 Add fast login Version 1.2.2653.26 2023-02-28 ------------------------------ Improve instance backup support Version 1.2.2626.74 2023-02-01 ------------------------------ Fix kernel compatibility issue Version 1.2.2626.25 2023-02-01 ------------------------------ Support RHEL 9 Web interface design changes Fix network configuration issue with IPv6 instances Version 1.2.2415.36 2022-07-05 ------------------------------ Update cloud init configuration Version 1.2.2415.30 2022-07-05 ------------------------------ Update QEMU configuration Version 1.2.2414.59 2022-07-04 ------------------------------ Add FreeBSD support Add Windows 11 support Add instance TPM support Add instance DHCP server Configure node firewall in node init Version 1.2.2400.46 2022-06-20 ------------------------------ Fix QEMU feature detection Version 1.2.2400.30 2022-06-20 ------------------------------ Add AlmaLinux image labels Version 1.2.2399.48 2022-06-19 ------------------------------ Show running QEMU version in instance info Improve Lets Encrypt certificates Version 1.2.2397.83 2022-06-17 ------------------------------ Support QEMU 7.0.0 Support Azure users with more then 50 groups Version 1.2.2336.5 2022-04-17 ----------------------------- Fix compatibility issue with Fedora update Version 1.2.2335.81 2022-04-16 ------------------------------ Add support for MongoDB unix socket Version 1.2.2334.82 2022-04-15 ------------------------------ Support IPv6 only instances Version 1.2.2334.69 2022-04-15 ------------------------------ Fix issue with instance startup Version 1.2.2333.97 2022-04-14 ------------------------------ Improve instance management Version 1.2.2333.91 2022-04-14 ------------------------------ Improve instance IPv6 networking Version 1.2.2333.80 2022-04-14 ------------------------------ Improve instance networking Version 1.2.2330.66 2022-04-11 ------------------------------ Add option to disable public IPv6 address Version 1.2.2330.2 2022-04-11 ----------------------------- Improve node init support Improve instance isolation Version 1.2.2324.79 2022-04-05 ------------------------------ Improve IPv6 networking Version 1.2.2324.56 2022-04-05 ------------------------------ Improve node init support Add mtu check command Fix static IPv6 issues Version 1.2.2301.94 2022-03-13 ------------------------------ Add support for internal only jumbo frames Update image labels for Ubuntu Version 1.2.2289.56 2022-03-01 ------------------------------ Improve node init support Version 1.2.2287.69 2022-02-27 ------------------------------ Improve host networking Version 1.2.2286.62 2022-02-26 ------------------------------ Fix issues with static IPv6 configuration Version 1.2.2262.39 2022-02-02 ------------------------------ Fix U2F migration to WebAuthn issues Version 1.2.2261.49 2022-02-01 ------------------------------ Web interface improvements Version 1.2.2261.45 2022-02-01 ------------------------------ Improve Oracle Cloud networking Version 1.2.2260.40 2022-01-31 ------------------------------ Add hugepages support Add support for Azure graph API Add instance gui Add instance spice server Add instance source destination check Add JumpCloud single sign-on Add instance root password option Improve instance sandboxing Version 1.2.2184.82 2021-11-16 ------------------------------ Add Oracle Cloud vnic support Improve qemu support Version 1.2.2144.31 2021-10-07 ------------------------------ Web interface improvements Version 1.2.2141.15 2021-10-04 ------------------------------ Fix zone defaults Version 1.2.2140.92 2021-10-03 ------------------------------ Support instance networking without bridge Version 1.2.2091.92 2021-08-15 ------------------------------ Improve instance networking Version 1.2.2011.58 2021-05-27 ------------------------------ Add uefi instance support Add secure boot instance support Add pci passthrough Add disk resizing Add disk passthrough Add iscsi instance disk support Add instance iso support Support add and remove USB devices on running instance Support add and remove disks on running instance Support wildcard certificates Format username capitalization on update Move instance runtime files to run directory Web interface improvements Version 1.2.1807.79 2020-11-04 ------------------------------ Improve online disk backup Add VNC client to web console Add local backup command Web interface improvements Version 1.2.1772.58 2020-09-30 ------------------------------ Fix issue with multiple instance disks Fix startup issue Version 1.0.1743.55 2020-09-01 ------------------------------ Web server improvements Version 1.0.1623.75 2020-05-04 ------------------------------ Add web socket support to load balancers Version 1.2.1595.50 2020-04-06 ------------------------------ Add support for network alias interfaces Version 1.2.1594.77 2020-04-05 ------------------------------ Improve node management Version 1.2.1594.70 2020-04-05 ------------------------------ Networking improvements Version 1.2.1589.78 2020-03-31 ------------------------------ Add VPC subnets Move default data directory Support IPv6 static blocks Interface improvements Add load balancers Version 1.0.1452.55 2019-11-15 ------------------------------ Improve acme v2 support Version 1.0.1448.49 2019-11-11 ------------------------------ Support acme v2 Instance networking improvements Web interface improvements Version 1.0.1413.18 2019-10-07 ------------------------------ Add support for USB passthrough Version 1.0.1338.74 2019-07-24 ------------------------------ Improve instance management Version 1.0.1337.5 2019-07-23 ----------------------------- Improve instance management Add uptime to instance info Generate random default password Version 1.0.1213.83 2019-03-21 ------------------------------ Web interface improvements Version 1.0.1213.82 2019-03-21 ------------------------------ Add option to disable instance public and host address Version 1.0.1206.81 2019-03-14 ------------------------------ Add VGA option to node settings Version 1.0.1200.31 2019-03-08 ------------------------------ Improve network performance Version 1.0.1200.25 2019-03-08 ------------------------------ Improve instance management Version 1.0.1199.30 2019-03-07 ------------------------------ Fix instance creation issue Version 1.0.1199.28 2019-03-07 ------------------------------ Add instance vnc support Version 1.0.1197.13 2019-03-05 ------------------------------ Set instance hostname Version 1.0.1189.79 2019-02-25 ------------------------------ Improve U2F support Web interface improvements Version 1.0.1181.24 2019-02-17 ------------------------------ Add redirect server option to node settings Version 1.0.1180.14 2019-02-16 ------------------------------ Add support for Oracle Cloud Version 1.0.1179.24 2019-02-15 ------------------------------ Add host networking Version 1.0.1173.24 2019-02-09 ------------------------------ Add VXLan support Version 1.0.1169.1 2019-02-05 ----------------------------- Fix node configuration Version 1.0.1168.95 2019-02-04 ------------------------------ Improve node management Version 1.0.1168.94 2019-02-04 ------------------------------ Improve node management Version 1.0.1168.33 2019-02-04 ------------------------------ Add support for static instance IP addresses Improve instance management Version 1.0.1155.32 2019-01-22 ------------------------------ Add support for Oracle Cloud archive storage Version 1.0.1154.31 2019-01-21 ------------------------------ Improve instance startup Version 1.0.1154.28 2019-01-21 ------------------------------ Improve instance management Version 1.0.1153.36 2019-01-20 ------------------------------ Improve storage management Version 1.0.1153.8 2019-01-20 ----------------------------- Add storage class to images Version 1.0.1151.28 2019-01-18 ------------------------------ Improve instance destroy Version 1.0.1150.31 2019-01-17 ------------------------------ Add copy to clipboard to web console Version 1.0.1149.32 2019-01-16 ------------------------------ Add automatic backup scheduling Version 1.0.1147.30 2019-01-14 ------------------------------ Add disk backup and restore Version 1.0.1144.28 2019-01-11 ------------------------------ Jumbo frames support Instance management interface improvements Version 1.0.1142.87 2019-01-09 ------------------------------ Reliability improvements Version 1.0.1141.35 2019-01-08 ------------------------------ Improve instance management Version 1.0.1140.33 2019-01-07 ------------------------------ Add instance and disk delete protection Web interface improvements Version 1.0.1135.30 2019-01-02 ------------------------------ Node management improvements Version 1.0.1134.7 2019-01-01 ----------------------------- Improve node scalability Improve firewall performance Version 1.0.1129.93 2018-12-27 ------------------------------ Linked disk improvements Version 1.0.1129.29 2018-12-27 ------------------------------ Add support for multiple network interfaces Add support for linked disk images Version 1.0.1124.31 2018-12-22 ------------------------------ Improve instances interface in web console Version 1.0.1118.32 2018-12-16 ------------------------------ Virtual machine improvements Version 1.0.1108.99 2018-12-06 ------------------------------ Fix issue with Azure single sign-on Version 1.0.1091.33 2018-11-19 ------------------------------ Initial release ================================================ FILE: LICENSE ================================================ Copyright (c) 2013-2026 Pritunl LICENSE SUMMARY * License does not expire * Can be used on unlimited sites, servers * Source-code or binary products cannot be resold or distributed * Non-commercial use only * Can modify source-code but cannot distribute modifications (derivative works) PREAMBLE This Agreement, signed on Nov 12, 2014 [hereinafter: Effective Date] governs the relationship between you, a private person, (hereinafter: Licensee) and Pritunl, a private person whose principal place of business is United States (Hereinafter: Licensor). This Agreement sets the terms, rights, restrictions and obligations on using [Pritunl] (hereinafter: The Software) created and owned by Licensor, as detailed herein LICENSE GRANT Licensor hereby grants Licensee a Personal, Non-assignable & non-transferable, Non-commercial, Royalty free, Without the rights to create derivative works, Non-exclusive license, all with accordance with the terms set forth and other legal restrictions set forth in 3rd party software used while running Software. Limited: Licensee may use Software for the purpose of: * Running Software on Licensee's Website[s] and Server[s] * Allowing 3rd Parties to run Software on Licensee's Website[s] and Server[s] * Publishing Software's output to Licensee and 3rd Parties * Modify Software to suit Licensee's needs and specifications. Non Assignable & Non-Transferable: Licensee may not assign or transfer his rights and duties under this license. Non-Commercial: Licensee may not use Software for commercial purposes. for the purpose of this license, commercial purposes means that a 3rd party has to pay in order to access Software or that the Website that runs Software is behind a paywall. TERM & TERMINATION The Term of this license shall be until terminated. Licensor may terminate this Agreement, including Licensee's license in the case where Licensee: * became insolvent or otherwise entered into any liquidation process; or exported The Software to any jurisdiction where licensor may not enforce his rights under this agreements in; or * Licensee was in breach of any of this license's terms and conditions and such breach was not cured, immediately upon notification; or * Licensee in breach of any of the terms of clause 2 to this license; or * Licensee otherwise entered into any arrangement which caused Licensor to be unable to enforce his rights under this License. UPGRADES, UPDATES AND FIXES Licensor may provide Licensee, from time to time, with Upgrades, Updates or Fixes, as detailed herein and according to his sole discretion. Licensee hereby warrants to keep The Software up-to-date and install all relevant updates and fixes, and may, at his sole discretion, purchase upgrades, according to the rates set by Licensor. Licensor shall provide any update or Fix free of charge; however, nothing in this Agreement shall require Licensor to provide Updates or Fixes. Upgrades: for the purpose of this license, an Upgrade shall be a material amendment in The Software, which contains new features and or major performance improvements and shall be marked as a new version number. For example, should Licensee purchase The Software under version 1.X.X, an upgrade shall commence under number 2.0.0. Updates: for the purpose of this license, an update shall be a minor amendment in The Software, which may contain new features or minor improvements and shall be marked as a new sub-version number. For example, should Licensee purchase The Software under version 1.1.X, an upgrade shall commence under number 1.2.0. Fix: for the purpose of this license, a fix shall be a minor amendment in The Software, intended to remove bugs or alter minor features which impair the The Software's functionality. A fix shall be marked as a new sub-sub-version number. For example, should Licensee purchase Software under version 1.1.1, an upgrade shall commence under number 1.1.2. SUPPORT Software is provided under an AS-IS basis and without any support, updates or maintenance. Nothing in this Agreement shall require Licensor to provide Licensee with support or fixes to any bug, failure, mis-performance or other defect in The Software. Bug Notification: Licensee may provide Licensor of details regarding any bug, defect or failure in The Software promptly and with no delay from such event; Licensee shall comply with Licensor's request for information regarding bugs, defects or failures and furnish him with information, screenshots and try to reproduce such bugs, defects or failures. Feature Request: Licensee may request additional features in Software, provided, however, that (i) Licensee shall waive any claim or right in such feature should feature be developed by Licensor; (ii) Licensee shall be prohibited from developing the feature, or disclose such feature request, or feature, to any 3rd party directly competing with Licensor or any 3rd party which may be, following the development of such feature, in direct competition with Licensor; (iii) Licensee warrants that feature does not infringe any 3rd party patent, trademark, trade-secret or any other intellectual property right; and (iv) Licensee developed, envisioned or created the feature solely by himself. Contribution: Licensee may submit additional code for the Software, provided, however, that (i) Licensee hereby irrevocably assigns all right, title, and interest in such contribution to Licensor upon submission; (ii) Licensee hereby grants Licensor a perpetual, worldwide, royalty-free, irrevocable license to use, modify, sublicense, and relicense such contribution under any license terms Licensor chooses, including without limitation less restrictive licenses such as AGPL; and (iii) Licensee represents and warrants that the contribution is Licensee's original work and does not infringe upon any third-party intellectual property rights; and (iv) Licensee also waives any moral rights in the contribution to the extent permitted by law. LIABILITY To the extent permitted under Law, The Software is provided under an AS-IS basis. Licensor shall never, and without any limit, be liable for any damage, cost, expense or any other payment incurred by Licensee as a result of Software's actions, failure, bugs and/or any other interaction between The Software and Licensee's end-equipment, computers, other software or any 3rd party, end-equipment, computer or services. Moreover, Licensor shall never be liable for any defect in source code written by Licensee when relying on The Software or using The Software's source code. WARRANTY Intellectual Property: Licensor hereby warrants that The Software does not violate or infringe any 3rd party claims in regards to intellectual property, patents and/or trademarks and that to the best of its knowledge no legal action has been taken against it for any infringement or violation of any 3rd party intellectual property rights. No-Warranty: The Software is provided without any warranty; Licensor hereby disclaims any warranty that The Software shall be error free, without defects or code which may cause damage to Licensee's computers or to Licensee, and that Software shall be functional. Licensee shall be solely liable to any damage, defect or loss incurred as a result of operating software and undertake the risks contained in running The Software on License's Server[s] and Website[s]. Prior Inspection: Licensee hereby states that he inspected The Software thoroughly and found it satisfactory and adequate to his needs, that it does not interfere with his regular operation and that it does meet the standards and scope of his computer systems and architecture. Licensee found that The Software interacts with his development, website and server environment and that it does not infringe any of End User License Agreement of any software Licensee may use in performing his services. Licensee hereby waives any claims regarding The Software's incompatibility, performance, results and features, and warrants that he inspected the The Software. INDEMNIFICATION Licensee hereby warrants to hold Licensor harmless and indemnify Licensor for any lawsuit brought against it in regards to Licensee's use of The Software in means that violate, breach or otherwise circumvent this license, Licensor's intellectual property rights or Licensor's title in The Software. Licensor shall promptly notify Licensee in case of such legal action and request Licensee's consent prior to any settlement in relation to such lawsuit or claim. GOVERNING LAW, JURISDICTION Licensee hereby agrees not to initiate class-action lawsuits against Licensor in relation to this license and to compensate Licensor for any legal fees, cost or attorney fees should any claim brought by Licensee against Licensor be denied, in part or in full. ================================================ FILE: README.md ================================================ # pritunl-cloud: declarative kvm virtualization [![github](https://img.shields.io/badge/github-pritunl-181717.svg?style=flat)](https://github.com/pritunl) [![twitter](https://img.shields.io/badge/twitter-pritunl-55acee.svg?style=flat)](https://twitter.com/pritunl) [![substack](https://img.shields.io/badge/substack-pritunl-ff6719.svg?style=flat)](https://pritunl.substack.com/) [![forum](https://img.shields.io/badge/discussion-forum-ffffff.svg?style=flat)](https://forum.pritunl.com) [Pritunl-Cloud](https://cloud.pritunl.com) is a declarative KVM virtualization platform with shell and python based live updating templates. Documentation and more information can be found at [docs.pritunl.com](https://docs.pritunl.com/kb/cloud) [![pritunl](img/logo_code.png)](https://docs.pritunl.com/kb/cloud) ## Install from Source ```bash # Install Required Tools sudo dnf -y install git-core iptables net-tools ipset ipvsadm xorriso qemu-kvm qemu-img swtpm-tools sudo rm -rf /usr/local/go wget https://go.dev/dl/go1.25.5.linux-amd64.tar.gz echo "9e9b755d63b36acf30c12a9a3fc379243714c1c6d3dd72861da637f336ebb35b go1.25.5.linux-amd64.tar.gz" | sha256sum -c - && sudo tar -C /usr/local -xf go1.25.5.linux-amd64.tar.gz rm -f go1.25.5.linux-amd64.tar.gz tee -a ~/.bashrc << EOF export GOPATH=\$HOME/go export GOROOT=/usr/local/go export PATH=/usr/local/go/bin:\$PATH EOF source ~/.bashrc # Install MongoDB sudo dnf -y install podman git clone https://github.com/pritunl/toolbox.git cd toolbox/mongodb-container sudo podman build --rm -t mongo . cd sudo mkdir /var/lib/mongo sudo chown 277:277 /var/lib/mongo sudo tee /etc/containers/systemd/mongodb-podman.container << EOF [Unit] Description=MongoDB Podman Service [Container] Image=localhost/mongo ContainerName=mongodb Environment=DB_NAME=pritunl-cloud Environment=CACHE_SIZE=1 User=mongodb Volume=/var/lib/mongo:/data/db:Z PublishPort=127.0.0.1:27017:27017 PodmanArgs=--cpus=1 --memory=2g [Service] Restart=always [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl start mongodb-podman.service sleep 3 sudo cat /var/lib/mongo/credentials.txt # Build Pritunl Cloud (update with latest version from releases) go install -v github.com/pritunl/pritunl-cloud@2.0.3616.97 go install -v github.com/pritunl/pritunl-cloud/redirect@2.0.3616.97 go install -v github.com/pritunl/pritunl-cloud/agent@2.0.3616.97 GOOS=freebsd GOARCH=amd64 go install -v github.com/pritunl/pritunl-cloud/agent@2.0.3616.97 # Install Systemd Units sudo cp $(ls -d ~/go/pkg/mod/github.com/pritunl/pritunl-cloud@v* | sort -V | tail -n 1)/tools/pritunl-cloud.service /etc/systemd/system/ sudo cp $(ls -d ~/go/pkg/mod/github.com/pritunl/pritunl-cloud@v* | sort -V | tail -n 1)/tools/pritunl-cloud-redirect.socket /etc/systemd/system/ sudo cp $(ls -d ~/go/pkg/mod/github.com/pritunl/pritunl-cloud@v* | sort -V | tail -n 1)/tools/pritunl-cloud-redirect.service /etc/systemd/system/ sudo systemctl daemon-reload sudo useradd -r -s /sbin/nologin -c 'Pritunl web server' pritunl-cloud-web # Install Pritunl Cloud sudo mkdir -p /usr/share/pritunl-cloud/www/ sudo cp -r $(ls -d ~/go/pkg/mod/github.com/pritunl/pritunl-cloud@v* | sort -V | tail -n 1)/www/dist/. /usr/share/pritunl-cloud/www/ sudo cp ~/go/bin/pritunl-cloud /usr/bin/pritunl-cloud sudo cp ~/go/bin/redirect /usr/bin/pritunl-cloud-redirect sudo cp ~/go/bin/agent /usr/bin/pritunl-cloud-agent sudo cp ~/go/bin/freebsd_amd64/agent /usr/bin/pritunl-cloud-agent-bsd sudo systemctl enable --now pritunl-cloud ``` ## License Please refer to the [`LICENSE`](LICENSE) file for a copy of the license. ================================================ FILE: acme/acme.go ================================================ package acme import ( "context" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/dns" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" "golang.org/x/crypto/acme" ) func Generate(db *database.Database, cert *certificate.Certificate) ( err error) { acmeType := cert.AcmeType if acmeType == "" { acmeType = certificate.AcmeHTTP } acmeAuth := cert.AcmeAuth logrus.WithFields(logrus.Fields{ "certificate": cert.Name, "domains": cert.AcmeDomains, "acme_type": acmeType, "acme_auth": acmeAuth, }).Info("acme: Generating acme certificate") if cert.AcmeDomains == nil || len(cert.AcmeDomains) == 0 { err = &errortypes.UnknownError{ errors.Wrap(err, "acme: No acme domains"), } return } var dnsSvc dns.Service if acmeType == certificate.AcmeDNS { var secr *secret.Secret if cert.Organization.IsZero() { secr, err = secret.Get(db, cert.AcmeSecret) if err != nil { return } } else { secr, err = secret.GetOrg(db, cert.Organization, cert.AcmeSecret) if err != nil { return } } if secr == nil { err = &errortypes.UnknownError{ errors.Wrap(err, "acme: ACME secret not found"), } return } if acmeAuth == certificate.AcmeAWS { dnsSvc = &dns.Aws{} } else if acmeAuth == certificate.AcmeCloudflare { dnsSvc = &dns.Cloudflare{} } else if acmeAuth == certificate.AcmeOracleCloud { dnsSvc = &dns.Oracle{} } else if acmeAuth == certificate.AcmeGoogleCloud { dnsSvc = &dns.Google{} } else { err = &errortypes.UnknownError{ errors.Wrapf(err, "acme: Unknown acme auth type %s", acmeAuth), } return } err = dnsSvc.Connect(db, secr) if err != nil { return } } var acctKey *rsa.PrivateKey if cert.AcmeAccount != "" { acctBlock, _ := pem.Decode([]byte(cert.AcmeAccount)) if acctBlock == nil { err = &errortypes.ParseError{ errors.New("acme: Failed to decode account key"), } return } acctKey, err = x509.ParsePKCS1PrivateKey(acctBlock.Bytes) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to parse account key"), } return } } else { acctKey, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "acme: Failed to generate account key"), } return } acctBlock := &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(acctKey), } cert.AcmeAccount = string(pem.EncodeToMemory(acctBlock)) err = cert.CommitFields(db, set.NewSet("acme_account")) if err != nil { return } } acct := &acme.Account{} client := &acme.Client{ DirectoryURL: AcmeDirectory, Key: acctKey, } _, err = client.Register(context.Background(), acct, acme.AcceptTOS) if err != nil { if err == acme.ErrAccountAlreadyExists { err = nil } else { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Failed to register account"), } return } } order, err := client.AuthorizeOrder( context.Background(), acme.DomainIDs(cert.AcmeDomains...)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Failed to authorize order"), } return } if order.Status == acme.StatusReady { err = create(db, cert, client, order) if err != nil { return } return } else if order.Status != acme.StatusPending { err = &errortypes.RequestError{ errors.Newf( "acme: Authorize order status '%s' not pending", order.Status, ), } return } authzUrls := order.AuthzURLs for _, authzUrl := range authzUrls { authz, e := client.GetAuthorization( context.Background(), authzUrl) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "acme: Failed to get authorization"), } return } if authz.Status != acme.StatusPending { continue } var authzChal *acme.Challenge for _, c := range authz.Challenges { if acmeType == certificate.AcmeDNS { if c.Type == "dns-01" { authzChal = c break } } else { if c.Type == "http-01" { authzChal = c break } } } if authzChal == nil { revoke(client, authzUrls) err = &errortypes.RequestError{ errors.New( "acme: Authorization challenge not available"), } return } var chal *Challenge var chalToken string var chalDomain string if acmeType == certificate.AcmeDNS { chalToken, err = client.DNS01ChallengeRecord(authzChal.Token) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Challenge record error"), } return } chalDomain = fmt.Sprintf( "_acme-challenge.%s.", authz.Identifier.Value) ops := []*dns.Operation{ &dns.Operation{ Operation: dns.UPSERT, Value: "\"" + chalToken + "\"", }, } err = dnsSvc.DnsCommit(db, chalDomain, "TXT", ops) if err != nil { return } defer func() { delOps := []*dns.Operation{ &dns.Operation{ Operation: dns.DELETE, Value: "\"" + chalToken + "\"", }, } e := dnsSvc.DnsCommit(db, chalDomain, "TXT", delOps) if e != nil { logrus.WithFields(logrus.Fields{ "certificate": cert.Name, "domain": chalDomain, "acme_type": acmeType, "acme_auth": acmeAuth, "error": e, }).Error("acme: Failed to remove DNS TXT record") } }() matched, e := DnsTxtWait(chalDomain, chalToken) if e != nil { err = e return } if !matched { logrus.WithFields(logrus.Fields{ "certificate": cert.Name, "domain": chalDomain, "acme_type": acmeType, "acme_auth": acmeAuth, }).Warning("acme: Local DNS TXT test lookup failed") } time.Sleep(time.Duration(settings.Acme.DnsDelay) * time.Second) } else { resp, e := client.HTTP01ChallengeResponse(authzChal.Token) if e != nil { revoke(client, authzUrls) err = &errortypes.RequestError{ errors.Wrap(e, "acme: Challenge response failed"), } return } chal = &Challenge{ Id: authzChal.Token, Resource: resp, Timestamp: time.Now(), } err = chal.Insert(db) if err != nil { return } chalMsg := &ChallengeMsg{ Token: authzChal.Token, Response: resp, } err = chalMsg.Publish(db) if err != nil { return } time.Sleep(300 * time.Millisecond) } _, err = client.Accept(context.Background(), authzChal) if err != nil { revoke(client, authzUrls) err = &errortypes.RequestError{ errors.Wrap(err, "acme: Authorization accept failed"), } return } _, err = client.WaitAuthorization( context.Background(), authzChal.URI) if err != nil { revoke(client, authzUrls) err = &errortypes.RequestError{ errors.Wrap(err, "acme: Authorization wait failed"), } return } if chal != nil { err = chal.Remove(db) if err != nil { revoke(client, authzUrls) return } } } order, err = client.WaitOrder(context.Background(), order.URI) if err != nil { revoke(client, authzUrls) err = &errortypes.RequestError{ errors.Wrap(err, "acme: Order wait failed"), } return } if order.Status != acme.StatusReady { err = &errortypes.RequestError{ errors.Newf( "acme: Authorize order status '%s' not ready", order.Status, ), } return } err = create(db, cert, client, order) if err != nil { return } return } func create(db *database.Database, cert *certificate.Certificate, client *acme.Client, order *acme.Order) (err error) { var csr []byte var keyPem []byte if settings.System.AcmeKeyAlgorithm == "ec" { csr, keyPem, err = newEcCsr(cert.AcmeDomains) if err != nil { return } } else { csr, keyPem, err = newRsaCsr(cert.AcmeDomains) if err != nil { return } } derChain, _, err := client.CreateOrderCert( context.Background(), order.FinalizeURL, csr, true, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Create order cert failed"), } return } certPem := "" for _, der := range derChain { certBlock := &pem.Block{ Type: "CERTIFICATE", Bytes: der, } if certPem != "" { certPem += "\n" } certPem += strings.TrimSpace(string(pem.EncodeToMemory(certBlock))) } cert.Key = strings.TrimSpace(string(keyPem)) cert.Certificate = certPem cert.AcmeHash = cert.Hash() _, err = cert.Validate(db) if err != nil { return } err = cert.CommitFields(db, set.NewSet( "key", "certificate", "acme_hash", "info")) if err != nil { return } event.PublishDispatch(db, "certificate.change") return } func Renew(db *database.Database, cert *certificate.Certificate, force bool) ( err error) { if cert.Type != certificate.LetsEncrypt { return } if force || cert.AcmeHash != cert.Hash() || (cert.Info != nil && !cert.Info.ExpiresOn.IsZero() && time.Until(cert.Info.ExpiresOn) < 168*time.Hour) { err = Generate(db, cert) if err != nil { return } } return } func RenewBackground(cert *certificate.Certificate, force bool) { go func() { db := database.GetDatabase() defer db.Close() err := Renew(db, cert, force) if err != nil { logrus.WithFields(logrus.Fields{ "certificate_id": cert.Id.Hex(), "certificate_name": cert.Name, "error": err, }).Error("task: Failed to renew certificate") } }() } ================================================ FILE: acme/challenge.go ================================================ package acme import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/event" ) type Challenge struct { Id string `bson:"_id"` Resource string `bson:"resource"` Timestamp time.Time `bson:"timestamp"` } func (c *Challenge) Insert(db *database.Database) (err error) { coll := db.AcmeChallenges() _, err = coll.InsertOne(db, c) if err != nil { err = database.ParseError(err) return } return } func (c *Challenge) Remove(db *database.Database) (err error) { coll := db.AcmeChallenges() _, err = coll.DeleteOne(db, &bson.M{ "_id": c.Id, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } type ChallengeMsg struct { Token string `bson:"token" json:"token"` Response string `bson:"response" json:"response"` } func (c *ChallengeMsg) Publish(db *database.Database) (err error) { err = event.Publish(db, "acme", c) if err != nil { return } return } ================================================ FILE: acme/constants.go ================================================ package acme const ( AcmeDirectory = "https://acme-v02.api.letsencrypt.org/directory" AcmePath = "/.well-known/acme-challenge/" ) ================================================ FILE: acme/utils.go ================================================ package acme import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "net" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "golang.org/x/crypto/acme" ) func revoke(client *acme.Client, authzUrls []string) { if authzUrls == nil { return } for _, authzUrl := range authzUrls { authz, err := client.GetAuthorization( context.Background(), authzUrl) if err != nil { continue } if authz.Status != acme.StatusPending { continue } _ = client.RevokeAuthorization(context.Background(), authzUrl) } } func ParsePath(path string) string { split := strings.SplitN(path, AcmePath, 2) if len(split) == 2 { return split[1] } return "" } func DnsTxtWait(domain, val string) (found bool, err error) { start := time.Now() iterDelay := time.Duration(settings.Acme.DnsRetryRate) * time.Second timeout := time.Duration(settings.Acme.DnsTimeout) * time.Second for i := 0; i < 60; i++ { if time.Since(start) > timeout { return } time.Sleep(iterDelay) records, e := net.LookupTXT(domain) if e != nil { continue } for _, record := range records { if record == val { found = true return } } } return } func GetChallenge(token string) (challenge *Challenge, err error) { db := database.GetDatabase() defer db.Close() coll := db.AcmeChallenges() challenge = &Challenge{} err = coll.FindOneId(token, challenge) if err != nil { return } return } func newRsaCsr(domains []string) (csr []byte, keyPem []byte, err error) { key, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "acme: Failed to generate private key"), } return } csrReq := &x509.CertificateRequest{ SignatureAlgorithm: x509.SHA256WithRSA, PublicKeyAlgorithm: x509.RSA, PublicKey: key.Public(), Subject: pkix.Name{ CommonName: domains[0], }, DNSNames: domains, } csr, err = x509.CreateCertificateRequest(rand.Reader, csrReq, key) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "acme: Failed to create certificate request"), } return } certKeyByte := x509.MarshalPKCS1PrivateKey(key) certKeyBlock := &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: certKeyByte, } keyPem = pem.EncodeToMemory(certKeyBlock) return } func newEcCsr(domains []string) (csr []byte, keyPem []byte, err error) { key, err := ecdsa.GenerateKey( elliptic.P384(), rand.Reader, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "acme: Failed to generate private key"), } return } csrReq := &x509.CertificateRequest{ SignatureAlgorithm: x509.ECDSAWithSHA256, PublicKeyAlgorithm: x509.ECDSA, PublicKey: key.Public(), Subject: pkix.Name{ CommonName: domains[0], }, DNSNames: domains, } csr, err = x509.CreateCertificateRequest(rand.Reader, csrReq, key) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "acme: Failed to create certificate request"), } return } certKeyByte, err := x509.MarshalECPrivateKey(key) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to parse private key"), } return } certKeyBlock := &pem.Block{ Type: "EC PRIVATE KEY", Bytes: certKeyByte, } keyPem = pem.EncodeToMemory(certKeyBlock) return } ================================================ FILE: advisory/advisory.go ================================================ package advisory import ( "net/http" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" ) var client = &http.Client{ Timeout: 10 * time.Second, } type Advisory struct { Id string `bson:"id" json:"id"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Status string `bson:"status" json:"status"` Description string `bson:"description" json:"description"` Score float64 `bson:"score" json:"score"` Severity string `bson:"severity" json:"severity"` Vector string `bson:"vector" json:"vector"` Complexity string `bson:"complexity" json:"complexity"` Privileges string `bson:"privileges" json:"privileges"` Interaction string `bson:"interaction" json:"interaction"` Scope string `bson:"scope" json:"scope"` Confidentiality string `bson:"confidentiality" json:"confidentiality"` Integrity string `bson:"integrity" json:"integrity"` Availability string `bson:"availability" json:"availability"` } func (a *Advisory) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { if a.Id == "" { errData = &errortypes.ErrorData{ Error: "id_required", Message: "Missing required advisory ID", } return } if a.Timestamp.IsZero() { a.Timestamp = time.Now() } if !cveIdReg.MatchString(a.Id) { errData = &errortypes.ErrorData{ Error: "id_invalid", Message: "Invalid advisory ID", } return } if a.Status != "" && !ValidStatuses.Contains(a.Status) { errData = &errortypes.ErrorData{ Error: "invalid_status", Message: "Invalid advisory status", } return } if a.Score < 0 || a.Score > 10 { errData = &errortypes.ErrorData{ Error: "invalid_score", Message: "Advisory score must be between 0 and 10", } return } if a.Severity != "" && !ValidSeverities.Contains(a.Severity) { errData = &errortypes.ErrorData{ Error: "invalid_severity", Message: "Invalid advisory severity", } return } if a.Vector != "" && !ValidVectors.Contains(a.Vector) { errData = &errortypes.ErrorData{ Error: "invalid_vector", Message: "Invalid advisory attack vector", } return } if a.Complexity != "" && !ValidComplexities.Contains(a.Complexity) { errData = &errortypes.ErrorData{ Error: "invalid_complexity", Message: "Invalid advisory attack complexity", } return } if a.Privileges != "" && !ValidPrivileges.Contains(a.Privileges) { errData = &errortypes.ErrorData{ Error: "invalid_privileges", Message: "Invalid advisory privileges required", } return } if a.Interaction != "" && !ValidInteractions.Contains(a.Interaction) { errData = &errortypes.ErrorData{ Error: "invalid_interaction", Message: "Invalid advisory user interaction", } return } if a.Scope != "" && !ValidScopes.Contains(a.Scope) { errData = &errortypes.ErrorData{ Error: "invalid_scope", Message: "Invalid advisory scope", } return } if a.Confidentiality != "" && !ValidImpacts.Contains(a.Confidentiality) { errData = &errortypes.ErrorData{ Error: "invalid_confidentiality", Message: "Invalid advisory confidentiality impact", } return } if a.Integrity != "" && !ValidImpacts.Contains(a.Integrity) { errData = &errortypes.ErrorData{ Error: "invalid_integrity", Message: "Invalid advisory integrity impact", } return } if a.Availability != "" && !ValidImpacts.Contains(a.Availability) { errData = &errortypes.ErrorData{ Error: "invalid_availability", Message: "Invalid advisory availability impact", } return } return } func (a *Advisory) IsFresh() bool { if a == nil { return false } if (a.Status == Analyzed || a.Status == Deferred) && time.Since(a.Timestamp) < time.Duration( settings.Telemetry.NvdFinalTtl)*time.Second { return true } if time.Since(a.Timestamp) < time.Duration( settings.Telemetry.NvdTtl)*time.Second { return true } return false } func (a *Advisory) Commit(db *database.Database) (err error) { coll := db.Advisories() _, err = coll.UpdateOne( db, &bson.M{ "_id": a.Id, }, &bson.M{ "$set": &bson.M{ "timestamp": a.Timestamp, "status": a.Status, "description": a.Description, "score": a.Score, "severity": a.Severity, "vector": a.Vector, "complexity": a.Complexity, "privileges": a.Privileges, "interaction": a.Interaction, "scope": a.Scope, "confidentiality": a.Confidentiality, "integrity": a.Integrity, "availability": a.Availability, }, }, options.UpdateOne().SetUpsert(true), ) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: advisory/constants.go ================================================ package advisory import ( "regexp" "github.com/dropbox/godropbox/container/set" ) const ( None = "none" Low = "low" Medium = "medium" High = "high" Critical = "critical" Network = "network" Adjacent = "adjacent" Local = "local" Physical = "physical" Required = "required" Unchanged = "unchanged" Changed = "changed" Analyzed = "analyzed" AwaitingAnalysis = "awaiting_analysis" Rejected = "rejected" Undergoing = "undergoing_analysis" Modified = "modified" Deferred = "deferred" Pending = "pending" nvdApi = "https://services.nvd.nist.gov/rest/json/cves/2.0" ) var ( cveIdReg = regexp.MustCompile(`^CVE-\d{4}-\d{4,}$`) ValidStatuses = set.NewSet( Analyzed, AwaitingAnalysis, Rejected, Undergoing, Modified, Deferred, ) ValidSeverities = set.NewSet( None, Low, Medium, High, Critical, ) ValidVectors = set.NewSet( Network, Adjacent, Local, Physical, ) ValidComplexities = set.NewSet( Low, High, ) ValidPrivileges = set.NewSet( None, Low, High, ) ValidInteractions = set.NewSet( None, Required, ) ValidScopes = set.NewSet( Unchanged, Changed, ) ValidImpacts = set.NewSet( None, Low, High, ) ) ================================================ FILE: advisory/utils.go ================================================ package advisory import ( "encoding/json" "net/http" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" ) var ( lastCall time.Time ) type nvdCvssData struct { VectorString string `json:"vectorString"` BaseScore float64 `json:"baseScore"` BaseSeverity string `json:"baseSeverity"` AttackVector string `json:"attackVector"` AttackComplexity string `json:"attackComplexity"` PrivilegesRequired string `json:"privilegesRequired"` UserInteraction string `json:"userInteraction"` Scope string `json:"scope"` ConfidentialityImpact string `json:"confidentialityImpact"` IntegrityImpact string `json:"integrityImpact"` AvailabilityImpact string `json:"availabilityImpact"` } type nvdCvssMetric struct { Type string `json:"type"` CvssData nvdCvssData `json:"cvssData"` } type nvdMetrics struct { CvssMetricV31 []nvdCvssMetric `json:"cvssMetricV31"` } type nvdDescription struct { Lang string `json:"lang"` Value string `json:"value"` } type nvdCve struct { ID string `json:"id"` VulnStatus string `json:"vulnStatus"` Descriptions []nvdDescription `json:"descriptions"` Metrics nvdMetrics `json:"metrics"` } type nvdVulnerability struct { Cve nvdCve `json:"cve"` } type nvdResponse struct { TotalResults int `json:"totalResults"` Vulnerabilities []nvdVulnerability `json:"vulnerabilities"` } func normalizeStatus(status string) string { switch status { case "Analyzed": return Analyzed case "Awaiting Analysis": return AwaitingAnalysis case "Rejected": return Rejected case "Undergoing Analysis": return Undergoing case "Modified": return Modified case "Deferred": return Deferred default: return strings.ToLower(strings.ReplaceAll(status, " ", "_")) } } func normalizeValue(val string) string { switch strings.ToUpper(val) { case "NONE": return None case "LOW": return Low case "MEDIUM": return Medium case "HIGH": return High case "CRITICAL": return Critical case "NETWORK": return Network case "ADJACENT_NETWORK", "ADJACENT": return Adjacent case "LOCAL": return Local case "PHYSICAL": return Physical case "REQUIRED": return Required case "UNCHANGED": return Unchanged case "CHANGED": return Changed default: return strings.ToLower(val) } } func getOne(db *database.Database, query *bson.M) (adv *Advisory, err error) { coll := db.Advisories() adv = &Advisory{} err = coll.FindOne(db, query).Decode(adv) if err != nil { err = database.ParseError(err) return } return } func getOneNvd(db *database.Database, cveId string) ( adv *Advisory, err error) { req, err := http.NewRequest("GET", nvdApi, nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "advisory: Failed to create request"), } return } query := req.URL.Query() query.Set("cveId", cveId) req.URL.RawQuery = query.Encode() nvdApiKey := settings.Telemetry.NvdApiKey if nvdApiKey != "" { req.Header.Set("apiKey", nvdApiKey) } resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "advisory: Request failed"), } return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = &errortypes.RequestError{ errors.Newf("advisory: Bad status status %d", resp.StatusCode), } return } nvdResp := &nvdResponse{} err = json.NewDecoder(resp.Body).Decode(nvdResp) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "advisory: Failed to decode response"), } return } if nvdResp.TotalResults == 0 || len(nvdResp.Vulnerabilities) == 0 { err = &errortypes.NotFoundError{ errors.New("advisory: Not found"), } return } cve := nvdResp.Vulnerabilities[0].Cve adv = &Advisory{ Id: strings.ToUpper(cve.ID), Timestamp: time.Now(), Status: normalizeStatus(cve.VulnStatus), } for _, desc := range cve.Descriptions { if desc.Lang == "en" { adv.Description = desc.Value break } } metrics := cve.Metrics.CvssMetricV31 if len(metrics) > 0 { var cvss *nvdCvssMetric for i := range metrics { if metrics[i].Type == "Primary" { cvss = &metrics[i] break } } if cvss == nil { cvss = &metrics[0] } adv.Score = cvss.CvssData.BaseScore adv.Severity = normalizeValue(cvss.CvssData.BaseSeverity) adv.Vector = normalizeValue(cvss.CvssData.AttackVector) adv.Complexity = normalizeValue(cvss.CvssData.AttackComplexity) adv.Privileges = normalizeValue(cvss.CvssData.PrivilegesRequired) adv.Interaction = normalizeValue(cvss.CvssData.UserInteraction) adv.Scope = normalizeValue(cvss.CvssData.Scope) adv.Confidentiality = normalizeValue( cvss.CvssData.ConfidentialityImpact) adv.Integrity = normalizeValue(cvss.CvssData.IntegrityImpact) adv.Availability = normalizeValue(cvss.CvssData.AvailabilityImpact) } errData, err := adv.Validate(db) if err != nil { return } if errData != nil { err = errData.GetError() return } err = adv.Commit(db) if err != nil { return } return } func GetOneLimit(db *database.Database, cveId string) ( adv *Advisory, err error) { adv, err = getOne(db, &bson.M{ "_id": cveId, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { adv = nil } else { return } } if adv.IsFresh() { return } since := time.Since(lastCall) var limit time.Duration if settings.Telemetry.NvdApiKey != "" { limit = time.Duration(settings.Telemetry.NvdApiAuthLimit) * time.Second } else { limit = time.Duration(settings.Telemetry.NvdApiLimit) * time.Second } if since < limit { time.Sleep(limit - since) } lastCall = time.Now() adv, err = getOneNvd(db, cveId) if err != nil { return } return } func GetOne(db *database.Database, cveId string) (adv *Advisory, err error) { adv, err = getOne(db, &bson.M{ "_id": cveId, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { adv = nil } else { return } } if adv.IsFresh() { return } adv, err = getOneNvd(db, cveId) if err != nil { return } return } func Remove(db *database.Database, advId bson.ObjectID) (err error) { coll := db.Advisories() _, err = coll.DeleteOne(db, &bson.M{ "_id": advId, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } return } ================================================ FILE: agent/agent.go ================================================ package main import ( "flag" "fmt" "os" "runtime/debug" "strings" "syscall" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/agent/constants" "github.com/pritunl/pritunl-cloud/agent/imds" "github.com/pritunl/pritunl-cloud/agent/utils" "github.com/pritunl/pritunl-cloud/engine" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/types" pritunl_utils "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/logger" ) const help = ` Usage: pci COMMAND Commands: get Get value from IMDS image Sanitize host files and initiate shutdown for imaging version Show version ` var ( daemon = flag.Bool("daemon", false, "Fork agent to background") ) func main() { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("agent: Panic in main") time.Sleep(3 * time.Second) os.Exit(1) } }() flag.Usage = func() { fmt.Printf(help) } flag.Parse() logger.Init( logger.SetTimeFormat(""), ) logger.AddHandler(func(record *logger.Record) { fmt.Print(record.String()) }) command := flag.Arg(0) if command == "engine" || command == "agent" { envStateData := os.Getenv("IMDS_STATE") envStateDatas := strings.Split(envStateData, ":") if len(envStateDatas) != 2 { fmt.Println("pritunl-cloud-agent: Invalid state") os.Exit(1) return } stage := envStateDatas[0] envState := envStateDatas[1] if stage == "run" { err := imds.SetState(envState) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to write imds state") utils.DelayExit(1, 1*time.Second) return } } else { confState := imds.GetState() if envState != "" && confState != envState { fmt.Println("pritunl-cloud-agent: Waiting for state") os.Exit(0) return } } if *daemon { err := daemonFork() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to fork to background") utils.DelayExit(1, 1*time.Second) return } } } switch command { case "get": ids := &imds.Imds{} err := ids.Init(nil) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Initialize failed") utils.DelayExit(1, 1*time.Second) return } defer ids.Close() val, err := ids.Get(flag.Arg(1)) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Get imds failed") utils.DelayExit(1, 1*time.Second) return } fmt.Print(val) break case "engine": eng := &engine.Engine{ OnStatus: imds.SetStatus, } ids := &imds.Imds{} err := ids.Init(eng) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to init imds") utils.DelayExit(1, 1*time.Second) return } defer ids.Close() err = ids.OpenLog() if err != nil { return } err = ids.SyncReady(5 * time.Minute) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync imds initial") utils.DelayExit(1, 1*time.Second) return } image := false phase := engine.Reboot switch flag.Arg(1) { case engine.Image: image = true phase = engine.Initial break case engine.Initial: phase = engine.Initial break } err = eng.Init() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to init engine") utils.DelayExit(1, 1*time.Second) return } data, err := utils.Read("/etc/pritunl-deploy.md") if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to read deploy spec") utils.DelayExit(1, 1*time.Second) return } blocks, err := engine.Parse(data) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to parse deploy spec") utils.DelayExit(1, 1*time.Second) return } ids.RunSync(image) runStatus := types.Running fatal, err := eng.Run(phase, blocks) if err != nil { runStatus = types.Fault if fatal { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Fatal initial engine run error") utils.DelayExit(1, 1*time.Second) return } else { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Non-fatal initial engine run error") err = nil } } if !image { ids.SetInitialized() err = ids.SyncStatus(runStatus) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync status") utils.DelayExit(1, 1*time.Second) return } err = ids.Wait() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to run") utils.DelayExit(1, 1*time.Second) return } } time.Sleep(500 * time.Millisecond) _, err = ids.Sync() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync") utils.DelayExit(1, 1*time.Second) return } break case "agent": ids := &imds.Imds{} err := ids.Init(nil) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to init imds") utils.DelayExit(1, 1*time.Second) return } defer ids.Close() err = ids.OpenLog() if err != nil { return } err = ids.SyncReady(5 * time.Minute) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync imds initial") utils.DelayExit(1, 1*time.Second) return } ids.RunSync(false) ids.SetInitialized() err = ids.SyncStatus(types.Running) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync status") utils.DelayExit(1, 1*time.Second) return } err = ids.Wait() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to run") utils.DelayExit(1, 1*time.Second) return } time.Sleep(500 * time.Millisecond) _, err = ids.Sync() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync") utils.DelayExit(1, 1*time.Second) return } break case "image": ids := &imds.Imds{} err := ids.Init(nil) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Iniatilize failed") utils.DelayExit(1, 1*time.Second) return } err = utils.Sanitize() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Sanitize failed") utils.DelayExit(1, 1*time.Second) return } err = ids.SyncStatus(types.Imaged) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Sync status failed") utils.DelayExit(1, 1*time.Second) return } err = utils.SanitizeImds() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Sanitize imds failed") utils.DelayExit(1, 1*time.Second) return } break case "status": mem, err := pritunl_utils.GetMemInfo() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("imds: Failed to get memory") utils.DelayExit(1, 1*time.Second) return } fmt.Println("Memory Information:") fmt.Printf(" Total Memory: %d KB\n", mem.Total) fmt.Printf(" Free Memory: %d KB\n", mem.Free) fmt.Printf(" Available Memory: %d KB\n", mem.Available) fmt.Printf(" Buffers: %d KB\n", mem.Buffers) fmt.Printf(" Cached: %d KB\n", mem.Cached) fmt.Printf(" Used Memory: %d KB\n", mem.Used) fmt.Printf(" Used Percentage: %.2f%%\n", mem.UsedPercent) fmt.Printf(" Dirty: %d KB\n", mem.Dirty) fmt.Println("\nSwap Information:") fmt.Printf(" Swap Total: %d KB\n", mem.SwapTotal) fmt.Printf(" Swap Free: %d KB\n", mem.SwapFree) fmt.Printf(" Swap Used: %d KB\n", mem.SwapUsed) fmt.Printf(" Swap Used Percentage: %.2f%%\n", mem.SwapUsedPercent) fmt.Println("\nHugePages Information:") fmt.Printf(" HugePages Total: %d\n", mem.HugePagesTotal) fmt.Printf(" HugePages Free: %d\n", mem.HugePagesFree) fmt.Printf(" HugePages Reserved: %d\n", mem.HugePagesReserved) fmt.Printf(" HugePages Used: %d\n", mem.HugePagesUsed) fmt.Printf(" HugePages Used Percent: %.2f%%\n", mem.HugePagesUsedPercent) fmt.Printf(" HugePage Size: %d KB\n", mem.HugePageSize) load, err := pritunl_utils.LoadAverage() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("imds: Failed to get load") utils.DelayExit(1, 1*time.Second) return } fmt.Println("\nLoad Average Information:") fmt.Printf(" CPU Units: %d\n", load.CpuUnits) fmt.Printf(" Load Average (1 min): %.2f%%\n", load.Load1) fmt.Printf(" Load Average (5 min): %.2f%%\n", load.Load5) fmt.Printf(" Load Average (15 min): %.2f%%\n", load.Load15) break case "version": fmt.Printf("pci v%s\n", constants.Version) break default: fmt.Printf(help) } return } func daemonFork() (err error) { fmt.Println("pritunl-cloud-agent: Forking daemon process") app, err := os.Executable() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "agent: Failed to get executable path"), } return } args := []string{app} args = append(args, flag.Args()...) devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "agent: Failed to open /dev/null"), } return } _, err = os.StartProcess(app, args, &os.ProcAttr{ Files: []*os.File{nil, devNull, devNull}, Sys: &syscall.SysProcAttr{ Setsid: true, }, }) if err != nil { devNull.Close() err = &errortypes.ExecError{ errors.Wrap(err, "agent: Failed to fork agent process"), } return } os.Exit(0) return } ================================================ FILE: agent/constants/constants.go ================================================ package constants const ( Version = "1.0.3248.95" ImdsConfPath = "/etc/pritunl-imds.json" ImdsLogPath = "/var/log/pritunl-imds.log" ) ================================================ FILE: agent/imds/imds.go ================================================ package imds import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "runtime/debug" "strings" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/agent/constants" "github.com/pritunl/pritunl-cloud/agent/logging" "github.com/pritunl/pritunl-cloud/engine" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/telemetry" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/logger" "github.com/pritunl/tools/set" ) var ( client = &http.Client{ Timeout: 10 * time.Second, } curSyncHash uint32 ) type Imds struct { Address string `json:"address"` Port int `json:"port"` Secret string `json:"secret"` State string `json:"state"` engine *engine.Engine `json:"-"` initialized bool `json:"-"` waiter sync.WaitGroup `json:"-"` journals map[string]*Journal `json:"-"` syncLock sync.Mutex `json:"-"` logger *logging.Redirect `json:"-"` } func (m *Imds) NewRequest(method, pth string, data interface{}) ( req *http.Request, err error) { u := &url.URL{} u.Scheme = "http" u.Host = fmt.Sprintf("%s:%d", m.Address, m.Port) u.Path = pth var body io.Reader if data != nil { reqDataBuf := &bytes.Buffer{} err = json.NewEncoder(reqDataBuf).Encode(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Failed to parse request data"), } return } body = reqDataBuf } req, err = http.NewRequest(method, u.String(), body) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to create imds request"), } return } req.Header.Set("User-Agent", "pritunl-imds") req.Header.Set("Auth-Token", m.Secret) if data != nil { req.Header.Set("Content-Type", "application/json") } return } func (m *Imds) Get(query string) (val string, err error) { query = strings.TrimPrefix(query, "+") req, err := m.NewRequest("GET", "/query"+query, nil) resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "agent: Imds request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData := &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } if errData != nil && errData.Message != "" { body = errData.Message } err = &errortypes.RequestError{ errors.Newf( "agent: Imds server get error %d - %s", resp.StatusCode, body), } return } data, err := ioutil.ReadAll(resp.Body) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "agent: Imds failed to read body"), } return } val = string(data) return } type SyncResp struct { Spec string `json:"spec"` Hash uint32 `json:"hash"` Journals []*types.Journal `json:"journals"` } func (m *Imds) SyncReady(timeout time.Duration) (err error) { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() ticker := time.NewTicker(200 * time.Millisecond) defer ticker.Stop() var lastErr error for { select { case <-ctx.Done(): if lastErr != nil { err = lastErr } else { err = &errortypes.RequestError{ errors.New("agent: Initial config timeout"), } } return case <-ticker.C: ready, e := m.Sync() if e != nil { lastErr = e continue } if !ready { continue } return nil } } } func (m *Imds) Sync() (ready bool, err error) { m.syncLock.Lock() defer m.syncLock.Unlock() data, err := m.GetState(curSyncHash) if err != nil { return } if m.logger != nil { data.Output = m.logger.GetOutput() } if m.journals != nil { journals := map[string][]*types.Entry{} for _, jrnl := range m.journals { journals[jrnl.Key] = jrnl.Handler.GetOutput() } if len(journals) > 0 { data.Journals = journals } } else { m.journals = map[string]*Journal{} } req, err := m.NewRequest("PUT", "/sync", data) if err != nil { return } resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Imds request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData := &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } if errData != nil && errData.Message != "" { body = errData.Message } err = &errortypes.RequestError{ errors.Newf("agent: Imds server sync error %d - %s", resp.StatusCode, body), } return } respData := &SyncResp{} err = json.NewDecoder(resp.Body).Decode(respData) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to decode imds sync resp"), } return } ready = true if respData.Hash == 0 { ready = false } else if curSyncHash == 0 { curSyncHash = respData.Hash } else if respData.Hash != curSyncHash && respData.Spec != "" && m.engine != nil && m.initialized { curSyncHash = respData.Hash logger.WithFields(logger.Fields{ "spec_len": len(respData.Spec), "hash": int(respData.Hash), }).Info("agent: Queuing engine reload") m.engine.Queue(respData.Spec) } activeJournals := set.NewSet() for unit := range m.journals { activeJournals.Add(unit) } for _, jrnlConf := range respData.Journals { if activeJournals.Contains(jrnlConf.Key) { activeJournals.Remove(jrnlConf.Key) jrnl := m.journals[jrnlConf.Key] if jrnl != nil { if jrnl.Index != jrnlConf.Index || jrnl.Key != jrnlConf.Key || jrnl.Type != jrnlConf.Type || jrnl.Unit != jrnlConf.Unit || jrnl.Path != jrnlConf.Path { jrnl.Close() delete(m.journals, jrnlConf.Key) } else { continue } } } jrnl := &Journal{ Index: jrnlConf.Index, Key: jrnlConf.Key, Type: jrnlConf.Type, Unit: jrnlConf.Unit, Path: jrnlConf.Path, } if jrnl.Type == "file" { jrnl.Handler = logging.NewFile(jrnlConf.Path) } else if jrnl.Type == "systemd" { jrnl.Handler = logging.NewSystemd(jrnlConf.Unit) } else { continue } err = jrnl.Open() if err != nil { return } m.journals[jrnlConf.Key] = jrnl } for jrnlKeyInf := range activeJournals.Iter() { jrnlKey := jrnlKeyInf.(string) jrnl := m.journals[jrnlKey] if jrnl != nil { jrnl.Close() delete(m.journals, jrnlKey) } } return } func (m *Imds) SetInitialized() { m.initialized = true if m.engine != nil { m.engine.StartRunner() } } func (m *Imds) RunSync(fast bool) { m.waiter.Add(1) go func() { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in telemetry") } }() for { telemetry.Refresh() time.Sleep(1 * time.Minute) } }() go func() { defer m.waiter.Done() for { func() { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("agent: Panic in sync") time.Sleep(3 * time.Second) os.Exit(1) } }() _, err := m.Sync() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to sync") } }() if fast { time.Sleep(500 * time.Millisecond) } else { time.Sleep(1 * time.Second) } } }() } func (m *Imds) SyncStatus(status string) (err error) { SetStatus(status) _, err = m.Sync() if err != nil { logger.WithFields(logger.Fields{ "status": status, "error": err, }).Error("agent: Failed to sync status") } return } func (m *Imds) Wait() (err error) { m.waiter.Wait() return } func (m *Imds) Init(eng *engine.Engine) (err error) { m.engine = eng confData, err := utils.Read(constants.ImdsConfPath) if err != nil { return } err = json.Unmarshal([]byte(confData), m) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Failed to unmarshal imds conf"), } return } return } func (m *Imds) OpenLog() (err error) { m.logger = &logging.Redirect{} err = m.logger.Open() if err != nil { return } return } func (m *Imds) Close() { if m.logger != nil { m.logger.Close() } } ================================================ FILE: agent/imds/journal.go ================================================ package imds import ( "github.com/pritunl/pritunl-cloud/agent/logging" "github.com/pritunl/tools/logger" ) type Journal struct { Index int32 `json:"-"` Key string `json:"-"` Type string `json:"-"` Unit string `json:"-"` Path string `json:"-"` Handler logging.Handler `json:"-"` } func (j *Journal) Open() (err error) { logger.WithFields(logger.Fields{ "index": j.Index, "key": j.Key, "type": j.Type, "unit": j.Unit, "path": j.Path, }).Info("agent: Starting journal") err = j.Handler.Open() if err != nil { return } return } func (j *Journal) Close() { logger.WithFields(logger.Fields{ "index": j.Index, "key": j.Key, "type": j.Type, "unit": j.Unit, "path": j.Path, }).Info("agent: Stopping journal") err := j.Handler.Close() if err != nil { logger.WithFields(logger.Fields{ "index": j.Index, "key": j.Key, "type": j.Type, "unit": j.Unit, "path": j.Path, "error": err, }).Error("agent: Error stopping journal") } return } ================================================ FILE: agent/imds/sync.go ================================================ package imds import ( "sync" "time" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/telemetry" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/logger" ) var ( curStatus = types.Initializing curStatusLock sync.Mutex ) type StateData struct { *types.State } func (m *Imds) GetState(curHash uint32) (data *StateData, err error) { data = &StateData{ &types.State{}, } data.Hash = curHash curStatusLock.Lock() data.Status = curStatus curStatusLock.Unlock() mem, err := utils.GetMemInfo() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Limit(30 * time.Minute).Error("imds: Failed to get memory") } else { data.Memory = utils.ToFixed(mem.UsedPercent, 2) data.HugePages = utils.ToFixed(mem.HugePagesUsedPercent, 2) } load, err := utils.LoadAverage() if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Limit(30 * time.Minute).Error("imds: Failed to get load") } else { data.Load1 = load.Load1 data.Load5 = load.Load5 data.Load15 = load.Load15 } updates, ok := telemetry.Updates.Get() if ok { data.Updates = updates } else { data.Updates = nil } return } func SetStatus(status string) { curStatusLock.Lock() curStatus = status curStatusLock.Unlock() } ================================================ FILE: agent/imds/utils.go ================================================ package imds import ( "encoding/json" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/agent/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) func GetState() string { confData, err := utils.Read(constants.ImdsConfPath) if err != nil { return "" } conf := &Imds{} err = json.Unmarshal([]byte(confData), conf) if err != nil { return "" } return conf.State } func SetState(state string) (err error) { confData, err := utils.Read(constants.ImdsConfPath) if err != nil { return } conf := &Imds{} err = json.Unmarshal([]byte(confData), conf) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Failed to unmarshal imds conf"), } return } conf.State = state dataByt, err := json.Marshal(conf) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Failed to unmarshal imds conf"), } return } err = utils.Write(constants.ImdsConfPath, string(dataByt), 0600) if err != nil { return } return } ================================================ FILE: agent/logging/file.go ================================================ package logging import ( "bufio" "context" "os/exec" "runtime/debug" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/tools/logger" ) type File struct { path string output chan *types.Entry cmd *exec.Cmd ctx context.Context cancel context.CancelFunc } func (f *File) GetOutput() (entries []*types.Entry) { for { select { case entry := <-f.output: entries = append(entries, entry) default: return } } } func (f *File) followJournal() (err error) { defer func() { rec := recover() if rec != nil { logger.WithFields(logger.Fields{ "path": f.path, "panic": rec, }).Error("agent: File follower panic") } }() f.cmd = exec.CommandContext(f.ctx, "tail", "-F", f.path, ) stdout, err := f.cmd.StdoutPipe() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "agent: Error creating stdout pipe"), } return } err = f.cmd.Start() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "agent: Error starting tail"), } return } scanner := bufio.NewScanner(stdout) buf := make([]byte, maxCapacity) scanner.Buffer(buf, maxCapacity) for scanner.Scan() { select { case <-f.ctx.Done(): return default: } line := scanner.Text() timestamp := time.Now() level := int32(5) if strings.Contains(strings.ToLower(line), "error") { level = 3 } select { case f.output <- &types.Entry{ Timestamp: timestamp, Level: level, Message: line, }: default: } continue } err = scanner.Err() if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Error reading tail"), } return } f.cmd.Wait() return } func (f *File) Open() (err error) { f.output = make(chan *types.Entry, 10000) f.ctx, f.cancel = context.WithCancel(context.Background()) go func() { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in tail open") } }() for { select { case <-f.ctx.Done(): return default: } e := f.followJournal() select { case <-f.ctx.Done(): return default: } if e != nil { logger.WithFields(logger.Fields{ "path": f.path, "error": e, }).Error("agent: Journal follower error, restarting") } else { logger.WithFields(logger.Fields{ "path": f.path, }).Info("agent: Journal follower exited, restarting") } select { case <-time.After(3 * time.Second): case <-f.ctx.Done(): return } } }() return } func (f *File) Close() (err error) { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in journal close") } }() if f.cancel != nil { f.cancel() } if f.cmd != nil && f.cmd.Process != nil { f.cmd.Process.Kill() } if f.output != nil { close(f.output) } return } func NewFile(path string) *File { return &File{ path: path, } } ================================================ FILE: agent/logging/handler.go ================================================ package logging import ( "github.com/pritunl/pritunl-cloud/imds/types" ) type Handler interface { Open() (err error) Close() (err error) GetOutput() (entries []*types.Entry) } ================================================ FILE: agent/logging/logging.go ================================================ package logging import ( "bufio" "fmt" "io" "os" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/agent/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/types" ) const maxCapacity = 128 * 1024 type Redirect struct { file *os.File writer io.Writer origStout *os.File origStderr *os.File output chan *types.Entry stdoutReader *os.File stdoutWriter *os.File stderrReader *os.File stderrWriter *os.File } func (r *Redirect) GetOutput() (entries []*types.Entry) { for { select { case entry := <-r.output: entries = append(entries, entry) default: return } } } func (r *Redirect) handleOutput(reader *os.File, level int32) { scanner := bufio.NewScanner(reader) buf := make([]byte, maxCapacity) scanner.Buffer(buf, maxCapacity) for scanner.Scan() { line := scanner.Text() timestamp := time.Now() fmt.Fprintf( r.writer, "[%s] %s\n", timestamp.Format("2006-01-02 15:04:05"), line, ) select { case r.output <- &types.Entry{ Timestamp: timestamp, Level: level, Message: line, }: default: } } } func (r *Redirect) Open() (err error) { r.file, err = os.OpenFile( constants.ImdsLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600, ) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "agent: Failed to create log file"), } return } r.output = make(chan *types.Entry, 10000) r.writer = io.MultiWriter(r.file, os.Stdout) r.origStout = os.Stdout r.origStderr = os.Stderr r.stdoutReader, r.stdoutWriter, err = os.Pipe() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "agent: Failed to create stdout pipe"), } return } r.stderrReader, r.stderrWriter, err = os.Pipe() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "agent: Failed to create stderr pipe"), } return } os.Stdout = r.stdoutWriter os.Stderr = r.stderrWriter go r.handleOutput(r.stdoutReader, types.Info) go r.handleOutput(r.stderrReader, types.Error) return } func (r *Redirect) Close() (err error) { os.Stdout = r.origStout os.Stderr = r.origStderr if r.stdoutWriter != nil { r.stdoutWriter.Close() } if r.stdoutReader != nil { r.stdoutReader.Close() } if r.stderrWriter != nil { r.stderrWriter.Close() } if r.stderrReader != nil { r.stderrReader.Close() } err = r.file.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "agent: Failed to close log pipe"), } return } return } ================================================ FILE: agent/logging/systemd.go ================================================ package logging import ( "bufio" "context" "encoding/json" "os/exec" "runtime/debug" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/tools/commander" "github.com/pritunl/tools/logger" ) type Systemd struct { unit string output chan *types.Entry cmd *exec.Cmd ctx context.Context cancel context.CancelFunc } type journalEntry struct { Message string `json:"MESSAGE"` Priority string `json:"PRIORITY"` Timestamp string `json:"__REALTIME_TIMESTAMP"` } func (s *Systemd) GetOutput() (entries []*types.Entry) { for { select { case entry := <-s.output: entries = append(entries, entry) default: return } } } func (s *Systemd) followJournal() (err error) { defer func() { rec := recover() if rec != nil { logger.WithFields(logger.Fields{ "unit": s.unit, "panic": rec, }).Error("agent: Journal follower panic") } }() existsTime := time.Time{} for { resp, _ := commander.Exec(&commander.Opt{ Name: "systemctl", Args: []string{ "status", s.unit, }, PipeOut: true, PipeErr: true, }) if resp.ExitCode != 4 { if existsTime.IsZero() { existsTime = time.Now() } if resp.ExitCode == 0 || time.Since(existsTime) > 10*time.Second { break } } time.Sleep(800 * time.Millisecond) } s.cmd = exec.CommandContext(s.ctx, "journalctl", "-f", "-b", "-n", "20", "-o", "json", "-u", s.unit, ) stdout, err := s.cmd.StdoutPipe() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "agent: Error creating stdout pipe"), } return } err = s.cmd.Start() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "agent: Error starting journalctl"), } return } scanner := bufio.NewScanner(stdout) buf := make([]byte, maxCapacity) scanner.Buffer(buf, maxCapacity) for scanner.Scan() { select { case <-s.ctx.Done(): return default: } line := scanner.Bytes() var entry journalEntry e := json.Unmarshal(line, &entry) if e != nil { continue } var timestamp time.Time ts, e := strconv.ParseInt(entry.Timestamp, 10, 64) if e == nil { timestamp = time.Unix(0, ts*1000) } else { timestamp = time.Now() } level := int32(5) if entry.Priority != "" { switch entry.Priority { case "0": level = 1 case "1", "2": level = 2 case "3": level = 3 case "4": level = 4 case "5", "6": level = 5 case "7": level = 6 } } select { case s.output <- &types.Entry{ Timestamp: timestamp, Level: level, Message: strings.TrimSuffix(entry.Message, "\n"), }: default: } } err = scanner.Err() if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Error reading journal"), } return } s.cmd.Wait() return } func (s *Systemd) Open() (err error) { s.output = make(chan *types.Entry, 10000) s.ctx, s.cancel = context.WithCancel(context.Background()) go func() { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in journal open") } }() for { select { case <-s.ctx.Done(): return default: } e := s.followJournal() select { case <-s.ctx.Done(): return default: } if e != nil { logger.WithFields(logger.Fields{ "unit": s.unit, "error": e, }).Error("agent: Journal follower error, restarting") } else { logger.WithFields(logger.Fields{ "unit": s.unit, }).Info("agent: Journal follower exited, restarting") } select { case <-time.After(3 * time.Second): case <-s.ctx.Done(): return } } }() return } func (s *Systemd) Close() (err error) { defer func() { panc := recover() if panc != nil { logger.WithFields(logger.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in journal close") } }() if s.cancel != nil { s.cancel() } if s.cmd != nil && s.cmd.Process != nil { s.cmd.Process.Kill() } if s.output != nil { close(s.output) } return } func NewSystemd(unit string) *Systemd { return &Systemd{ unit: unit, } } ================================================ FILE: agent/utils/sanitize.go ================================================ package utils import ( "strings" "github.com/pritunl/pritunl-cloud/agent/constants" "github.com/pritunl/tools/commander" ) type fileOp struct { cmd string args []string path string } type findOp struct { path string pattern string action string } func Sanitize() (err error) { operations := []fileOp{ {"rm", []string{"-f"}, "/var/lib/systemd/random-seed"}, {"rm", []string{"-f"}, "/etc/machine-id"}, {"rm", []string{"-rf"}, "/root/.cache"}, {"shred", []string{"-u"}, "/root/.ssh/authorized_keys"}, {"shred", []string{"-u"}, "/root/.bash_history"}, {"shred", []string{"-u"}, "/var/log/lastlog"}, {"shred", []string{"-u"}, "/var/log/secure"}, {"shred", []string{"-u"}, "/var/log/utmp"}, {"shred", []string{"-u"}, "/var/log/wtmp"}, {"shred", []string{"-u"}, "/var/log/btmp"}, {"shred", []string{"-u"}, "/var/log/dmesg"}, {"shred", []string{"-u"}, "/var/log/dmesg.old"}, {"shred", []string{"-u"}, "/var/lib/systemd/random-seed"}, } findOps := []findOp{ {"/var/tmp", "-name dnf-*", "rm -rf"}, {"/home", "-type d -name .cache", "rm -rf"}, {"/home", "-type f -name .bash_history", "rm -f"}, {"/var/log", "-type f -name *.gz", "rm -f"}, {"/var/log", "-type f -name *.[0-9]", "rm -f"}, {"/var/log", "-type f -name *-????????", "rm -f"}, {"/var/lib/cloud/instances", "-mindepth 1", "rm -rf"}, {"/etc/ssh", "-type f -name *_key", "shred -u"}, {"/etc/ssh", "-type f -name *_key.pub", "shred -u"}, {"/var/log", "-mtime -1 -type f", "truncate -s 0"}, } _, _ = commander.Exec(&commander.Opt{ Name: "sync", Args: []string{}, PipeOut: true, PipeErr: true, }) for _, op := range operations { _, _ = commander.Exec(&commander.Opt{ Name: op.cmd, Args: append(op.args, op.path), PipeOut: true, PipeErr: true, }) } for _, op := range findOps { args := []string{op.path} if op.pattern != "" { args = append(args, strings.Split(op.pattern, " ")...) } args = append(args, "-exec") args = append(args, strings.Split(op.action, " ")...) args = append(args, "{}", ";") _, _ = commander.Exec(&commander.Opt{ Name: "find", Args: args, PipeOut: true, PipeErr: true, }) } _, _ = commander.Exec(&commander.Opt{ Name: "touch", Args: []string{"/etc/machine-id"}, PipeOut: true, PipeErr: true, }) _, _ = commander.Exec(&commander.Opt{ Name: "sync", Args: []string{}, PipeOut: true, PipeErr: true, }) _, _ = commander.Exec(&commander.Opt{ Name: "history", Args: []string{"-c"}, PipeOut: true, PipeErr: true, }) _, _ = commander.Exec(&commander.Opt{ Name: "fstrim", Args: []string{"-av"}, PipeOut: true, PipeErr: true, }) return } func SanitizeImds() (err error) { _, _ = commander.Exec(&commander.Opt{ Name: "shred", Args: []string{"-u", constants.ImdsLogPath}, PipeOut: true, PipeErr: true, }) _, _ = commander.Exec(&commander.Opt{ Name: "shred", Args: []string{"-u", constants.ImdsConfPath}, PipeOut: true, PipeErr: true, }) return } ================================================ FILE: agent/utils/sys.go ================================================ package utils import ( "io/ioutil" "os" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) func Read(path string) (data string, err error) { dataByt, err := ioutil.ReadFile(path) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", path), } return } data = string(dataByt) return } func DelayExit(code int, delay time.Duration) { time.Sleep(delay) os.Exit(code) } ================================================ FILE: aggregate/block.go ================================================ package aggregate import ( "sync" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" ) type BlockPipe struct { block.Block `bson:",inline"` IpCount int64 `bson:"ip_count"` } type BlocksPipe struct { Metadata []*Metadata `bson:"meta"` Blocks []*BlockPipe `bson:"blocks"` } type BlockAggregate struct { block.Block Available int64 `json:"available"` Capacity int64 `json:"capacity"` } func GetBlockPaged(db *database.Database, query *bson.M, page, pageCount int64) (blocks []*BlockAggregate, count int64, err error) { coll := db.Blocks() blocks = []*BlockAggregate{} if pageCount == 0 { pageCount = 20 } skip := page * pageCount addBlock := func(doc *BlockPipe) error { total, e := doc.GetIpCount() if e != nil { return e } blck := &BlockAggregate{ Block: doc.Block, Available: total - doc.IpCount, Capacity: total, } blocks = append(blocks, blck) return nil } var cursor *mongo.Cursor if len(*query) == 0 { waiter := &sync.WaitGroup{} var countErr error waiter.Add(1) go func() { defer waiter.Done() count, countErr = coll.EstimatedDocumentCount(db) if countErr != nil { countErr = database.ParseError(countErr) return } }() cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "blocks_ip", "localField": "_id", "foreignField": "block", "as": "ips", }, }, &bson.M{ "$addFields": &bson.M{ "ip_count": &bson.M{ "$size": "$ips", }, }, }, &bson.M{ "$project": &bson.M{ "ips": 0, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { doc := &BlockPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } err = addBlock(doc) if err != nil { return } } waiter.Wait() if countErr != nil { err = countErr return } } else { cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$facet": &bson.M{ "meta": []*bson.M{ &bson.M{ "$count": "count", }, }, "blocks": []*bson.M{ &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "blocks_ip", "localField": "_id", "foreignField": "block", "as": "ips", }, }, &bson.M{ "$addFields": &bson.M{ "ip_count": &bson.M{ "$size": "$ips", }, }, }, &bson.M{ "$project": &bson.M{ "ips": 0, }, }, }, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Not found"), } return } doc := &BlocksPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } if len(doc.Metadata) > 0 { count = doc.Metadata[0].Count } for _, blockDoc := range doc.Blocks { err = addBlock(blockDoc) if err != nil { return } } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: aggregate/deployment.go ================================================ package aggregate import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/zone" ) type DeploymentPipe struct { Deployment `bson:",inline"` SpecDocs []*spec.Spec `bson:"spec_docs"` InstanceDocs []*instance.Instance `bson:"instance_docs"` ZoneDocs []*zone.Zone `bson:"zone_docs"` NodeDocs []*node.Node `bson:"node_docs"` ImageDocs []*image.Image `bson:"image_docs"` } type Deployment struct { Id bson.ObjectID `bson:"_id" json:"id"` Pod bson.ObjectID `bson:"pod" json:"pod"` Unit bson.ObjectID `bson:"unit" json:"unit"` Spec bson.ObjectID `bson:"spec" json:"spec"` SpecOffset int `bson:"spec_offset" json:"spec_offset"` SpecIndex int `bson:"spec_index" json:"spec_index"` SpecTimestamp time.Time `bson:"spec_timestamp" json:"spec_timestamp"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Tags []string `bson:"tags" json:"tags"` Kind string `bson:"kind" json:"kind"` State string `bson:"state" json:"state"` Action string `bson:"action" json:"action"` Status string `bson:"status" json:"status"` Node bson.ObjectID `bson:"node" json:"node"` Instance bson.ObjectID `bson:"instance" json:"instance"` InstanceData *deployment.InstanceData `bson:"instance_data" json:"instance_data"` DomainData *deployment.DomainData `bson:"domain_data" json:"domain_data"` Journals []*deployment.Journal `bson:"journals" json:"journals"` ImageId bson.ObjectID `bson:"image_id" json:"image_id"` ImageName string `bson:"image_name" json:"image_name"` ZoneName string `bson:"-" json:"zone_name"` NodeName string `bson:"-" json:"node_name"` InstanceName string `bson:"-" json:"instance_name"` InstanceRoles []string `bson:"-" json:"instance_roles"` InstanceMemory int `bson:"-" json:"instance_memory"` InstanceProcessors int `bson:"-" json:"instance_processors"` InstanceStatus string `bson:"-" json:"instance_status"` InstanceUptime string `bson:"-" json:"instance_uptime"` InstanceState string `bson:"-" json:"instance_state"` InstanceAction string `bson:"-" json:"instance_action"` InstanceGuestStatus string `bson:"-" json:"instance_guest_status"` InstanceTimestamp time.Time `bson:"-" json:"instance_timestamp"` InstanceHeartbeat time.Time `bson:"-" json:"instance_heartbeat"` InstanceMemoryUsage float64 `bson:"-" json:"instance_memory_usage"` InstanceHugePages float64 `bson:"-" json:"instance_hugepages"` InstanceLoad1 float64 `bson:"-" json:"instance_load1"` InstanceLoad5 float64 `bson:"-" json:"instance_load5"` InstanceLoad15 float64 `bson:"-" json:"instance_load15"` } func GetDeployments(db *database.Database, unt *unit.Unit) ( deplys []*Deployment, err error) { coll := db.Deployments() deplys = []*Deployment{} cursor, err := coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": &bson.M{ "unit": unt.Id, }, }, &bson.M{ "$sort": &bson.M{ "timestamp": -1, }, }, &bson.M{ "$lookup": &bson.M{ "from": "specs", "localField": "spec", "foreignField": "_id", "as": "spec_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "instances", "localField": "instance", "foreignField": "_id", "as": "instance_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "zones", "localField": "zone", "foreignField": "_id", "as": "zone_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "nodes", "localField": "node", "foreignField": "_id", "as": "node_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "images", "localField": "_id", "foreignField": "deployment", "as": "image_docs", }, }, &bson.M{ "$project": &bson.D{ {"_id", 1}, {"pod", 1}, {"unit", 1}, {"timestamp", 1}, {"tags", 1}, {"spec", 1}, {"kind", 1}, {"state", 1}, {"action", 1}, {"status", 1}, {"node", 1}, {"instance", 1}, {"instance_data", 1}, {"domain_data", 1}, {"journals", 1}, {"spec_docs.index", 1}, {"spec_docs.timestamp", 1}, {"instance_docs.name", 1}, {"instance_docs.roles", 1}, {"instance_docs.memory", 1}, {"instance_docs.processors", 1}, {"instance_docs.state", 1}, {"instance_docs.action", 1}, {"instance_docs.timestamp", 1}, {"instance_docs.restart", 1}, {"instance_docs.restart_block_ip", 1}, {"instance_docs.guest", 1}, {"zone_docs.name", 1}, {"node_docs.name", 1}, {"image_docs._id", 1}, {"image_docs.name", 1}, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) latest := true for cursor.Next(db) { doc := &DeploymentPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } deply := &doc.Deployment if deply.Tags == nil { deply.Tags = []string{} } if latest { latest = false if doc.Kind == deployment.Image { deply.Tags = append([]string{"latest"}, deply.Tags...) } } deply.Journals = append([]*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, deply.Journals...) if len(doc.ZoneDocs) > 0 { zne := doc.ZoneDocs[0] deply.ZoneName = zne.Name } if len(doc.NodeDocs) > 0 { nde := doc.NodeDocs[0] deply.NodeName = nde.Name } if len(doc.SpecDocs) > 0 { spc := doc.SpecDocs[0] deply.SpecOffset = spc.Index - unt.SpecIndex deply.SpecIndex = spc.Index deply.SpecTimestamp = spc.Timestamp } if len(doc.InstanceDocs) > 0 { inst := doc.InstanceDocs[0] inst.Json(true) deply.InstanceName = inst.Name deply.InstanceRoles = inst.Roles deply.InstanceMemory = inst.Memory deply.InstanceProcessors = inst.Processors deply.InstanceStatus = inst.Status deply.InstanceUptime = inst.Uptime deply.InstanceState = inst.State deply.InstanceAction = inst.Action if inst.Guest != nil { deply.InstanceGuestStatus = inst.Guest.Status deply.InstanceTimestamp = inst.Guest.Timestamp deply.InstanceHeartbeat = inst.Guest.Heartbeat if inst.IsActive() { deply.InstanceMemoryUsage = inst.Guest.Memory deply.InstanceHugePages = inst.Guest.HugePages deply.InstanceLoad1 = inst.Guest.Load1 deply.InstanceLoad5 = inst.Guest.Load5 deply.InstanceLoad15 = inst.Guest.Load15 } else { deply.InstanceGuestStatus = types.Offline } } } if len(doc.ImageDocs) > 0 { img := doc.ImageDocs[0] deply.ImageId = img.Id deply.ImageName = img.Name } deplys = append(deplys, deply) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: aggregate/disk.go ================================================ package aggregate import ( "sync" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/node" ) type DiskPipe struct { disk.Disk `bson:",inline"` ImageDocs []*node.Node `bson:"image_docs"` } type DisksPipe struct { Metadata []*Metadata `bson:"meta"` Disks []*DiskPipe `bson:"disks"` } type DiskBackup struct { Image bson.ObjectID `json:"image"` Name string `json:"name"` } type DiskAggregate struct { disk.Disk Backups []*DiskBackup `json:"backups"` } func GetDiskPaged(db *database.Database, query *bson.M, page, pageCount int64) (disks []*DiskAggregate, count int64, err error) { coll := db.Disks() disks = []*DiskAggregate{} if pageCount == 0 { pageCount = 20 } skip := page * pageCount addDisk := func(doc *DiskPipe) { backups := []*DiskBackup{} for _, img := range doc.ImageDocs { backup := &DiskBackup{ Image: img.Id, Name: img.Name, } backups = append(backups, backup) } dsk := &DiskAggregate{ Disk: doc.Disk, Backups: backups, } disks = append(disks, dsk) } var cursor *mongo.Cursor if len(*query) == 0 { waiter := &sync.WaitGroup{} var countErr error waiter.Add(1) go func() { defer waiter.Done() count, countErr = coll.EstimatedDocumentCount(db) if countErr != nil { countErr = database.ParseError(countErr) return } }() cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "images", "localField": "_id", "foreignField": "disk", "as": "image_docs", }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { doc := &DiskPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } addDisk(doc) } waiter.Wait() if countErr != nil { err = countErr return } } else { cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$facet": &bson.M{ "meta": []*bson.M{ &bson.M{ "$count": "count", }, }, "disks": []*bson.M{ &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "images", "localField": "_id", "foreignField": "disk", "as": "image_docs", }, }, }, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Not found"), } return } doc := &DisksPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } if len(doc.Metadata) > 0 { count = doc.Metadata[0].Count } for _, diskDoc := range doc.Disks { addDisk(diskDoc) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: aggregate/domain.go ================================================ package aggregate import ( "sync" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/domain" ) type Domain struct { domain.Domain `bson:",inline"` Records []*domain.Record `bson:"records" json:"records"` } type DomainsPipe struct { Metadata []*Metadata `bson:"meta"` Domains []*Domain `bson:"domains"` } func GetDomainPaged(db *database.Database, query *bson.M, page, pageCount int64) (domains []*domain.Domain, count int64, err error) { coll := db.Domains() domains = []*domain.Domain{} if pageCount == 0 { pageCount = 20 } skip := page * pageCount addDomain := func(domn *Domain) { domn.Domain.Records = domn.Records domn.Json() domains = append(domains, &domn.Domain) } var cursor *mongo.Cursor if len(*query) == 0 { waiter := &sync.WaitGroup{} var countErr error waiter.Add(1) go func() { defer waiter.Done() count, countErr = coll.EstimatedDocumentCount(db) if countErr != nil { countErr = database.ParseError(countErr) return } }() cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "domains_records", "localField": "_id", "foreignField": "domain", "as": "records", }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { domn := &Domain{} err = cursor.Decode(domn) if err != nil { err = database.ParseError(err) return } addDomain(domn) } waiter.Wait() if countErr != nil { err = countErr return } } else { cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$facet": &bson.M{ "meta": []*bson.M{ &bson.M{ "$count": "count", }, }, "domains": []*bson.M{ &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "domains_records", "localField": "_id", "foreignField": "domain", "as": "records", }, }, }, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Not found"), } return } doc := &DomainsPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } if len(doc.Metadata) > 0 { count = doc.Metadata[0].Count } for _, domn := range doc.Domains { addDomain(domn) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: aggregate/instance.go ================================================ package aggregate import ( "fmt" "sort" "strings" "sync" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/iso" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/vm" ) type InstancePipe struct { instance.Instance `bson:",inline"` NodeDocs []*node.Node `bson:"node_docs"` DatacenterDocs []*datacenter.Datacenter `bson:"datacenter_docs"` DiskDocs []*disk.Disk `bson:"disk_docs"` } type InstancesPipe struct { Metadata []*Metadata `bson:"meta"` Instances []*InstancePipe `bson:"instances"` } type InstanceInfo struct { Node string `json:"node"` NodePublicIp string `json:"node_public_ip"` Mtu int `json:"mtu"` Iscsi bool `json:"iscsi"` Disks []string `json:"disks"` FirewallRules map[string]string `json:"firewall_rules"` Authorities []string `json:"authorities"` Isos []*iso.Iso `json:"isos"` UsbDevices []*usb.Device `json:"usb_devices"` PciDevices []*pci.Device `json:"pci_devices"` DriveDevices []*drive.Device `json:"drive_devices"` CloudSubnets []*node.CloudSubnet `json:"cloud_subnets"` } type InstanceAggregate struct { instance.Instance Info *InstanceInfo `json:"info"` } func GetInstancePaged(db *database.Database, query *bson.M, page, pageCount int64) (insts []*InstanceAggregate, count int64, err error) { coll := db.Instances() insts = []*InstanceAggregate{} if pageCount == 0 { pageCount = 20 } skip := page * pageCount firesOrg := map[bson.ObjectID]map[string][]*firewall.Firewall{} firesRoles := map[bson.ObjectID]set.Set{} authrsOrg := map[bson.ObjectID]map[string][]*authority.Authority{} authrsRoles := map[bson.ObjectID]set.Set{} addInstance := func(doc *InstancePipe) error { info := &InstanceInfo{ Node: "Unknown", Disks: []string{}, FirewallRules: map[string]string{}, Authorities: []string{}, CloudSubnets: []*node.CloudSubnet{}, } var nde *node.Node var dc *datacenter.Datacenter if len(doc.NodeDocs) > 0 { nde = doc.NodeDocs[0] } if len(doc.DatacenterDocs) > 0 { dc = doc.DatacenterDocs[0] } if nde != nil { info.Node = nde.Name if len(nde.PublicIps) > 0 { info.NodePublicIp = nde.PublicIps[0] } info.Iscsi = nde.Iscsi info.Isos = nde.LocalIsos info.CloudSubnets = nde.GetCloudSubnetsName() if nde.UsbPassthrough { info.UsbDevices = nde.UsbDevices } if nde.PciDevices != nil { info.PciDevices = nde.PciDevices } if nde.InstanceDrives != nil { info.DriveDevices = nde.InstanceDrives } } if nde != nil && dc != nil { info.Mtu = dc.GetInstanceMtu() } for _, dsk := range doc.DiskDocs { info.Disks = append( info.Disks, fmt.Sprintf("%s: %s", dsk.Index, dsk.Name), ) } fires := firesOrg[doc.Organization] if fires == nil { var e error fires, e = firewall.GetOrgMapRoles(db, doc.Organization) if e != nil { return e } for _, roleFires := range fires { for _, fire := range roleFires { if _, ok := firesRoles[fire.Id]; ok { continue } roles := set.NewSet() for _, role := range fire.Roles { roles.Add(role) } firesRoles[fire.Id] = roles } } firesOrg[doc.Organization] = fires } authrs := authrsOrg[doc.Organization] if authrs == nil { var e error authrs, e = authority.GetOrgMapRoles(db, doc.Organization) if e != nil { return e } for _, roleAuthrs := range authrs { for _, authr := range roleAuthrs { if _, ok := authrsRoles[authr.Id]; ok { continue } roles := set.NewSet() for _, role := range authr.Roles { roles.Add(role) } authrsRoles[authr.Id] = roles } } authrsOrg[doc.Organization] = authrs } curFires := set.NewSet() firewallRules := map[string]set.Set{} firewallRulesKeys := []string{} authrNames := set.NewSet() for _, role := range doc.Roles { roleFires := fires[role] for _, fire := range roleFires { if curFires.Contains(fire.Id) { continue } curFires.Add(fire.Id) for _, rule := range fire.Ingress { key := rule.Protocol if rule.Port != "" { key += ":" + rule.Port } rules := firewallRules[key] if rules == nil { rules = set.NewSet() firewallRules[key] = rules firewallRulesKeys = append( firewallRulesKeys, key, ) } for _, sourceIp := range rule.SourceIps { rules.Add(sourceIp) } } } roleAuthrs := authrs[role] for _, authr := range roleAuthrs { authrNames.Add(authr.Name) } } if !doc.Instance.Deployment.IsZero() { doc.Instance.LoadVirt(nil, nil) specRules, _, e := firewall.GetSpecRulesSlow( db, doc.Instance.Node, []*instance.Instance{&doc.Instance}) if e != nil { return e } instNamespace := vm.GetNamespace(doc.Instance.Id, 0) for namespace, rules := range specRules { if namespace != instNamespace { continue } for _, rule := range rules { key := rule.Protocol if rule.Port != "" { key += ":" + rule.Port } rules := firewallRules[key] if rules == nil { rules = set.NewSet() firewallRules[key] = rules firewallRulesKeys = append( firewallRulesKeys, key, ) } for _, sourceIp := range rule.SourceIps { rules.Add(sourceIp) } } } } sort.Strings(firewallRulesKeys) for _, key := range firewallRulesKeys { rules := firewallRules[key] vals := []string{} for rule := range rules.Iter() { vals = append(vals, rule.(string)) } sort.Strings(vals) info.FirewallRules[key] = strings.Join(vals, ", ") } for authr := range authrNames.Iter() { info.Authorities = append(info.Authorities, authr.(string)) } sort.Strings(info.Authorities) inst := &InstanceAggregate{ Instance: doc.Instance, Info: info, } insts = append(insts, inst) return nil } var cursor *mongo.Cursor if len(*query) == 0 { waiter := &sync.WaitGroup{} var countErr error waiter.Add(1) go func() { defer waiter.Done() count, countErr = coll.EstimatedDocumentCount(db) if countErr != nil { countErr = database.ParseError(countErr) return } }() cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "nodes", "localField": "node", "foreignField": "_id", "as": "node_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "datacenters", "localField": "datacenter", "foreignField": "_id", "as": "datacenter_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "disks", "localField": "_id", "foreignField": "instance", "as": "disk_docs", }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { doc := &InstancePipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } err = addInstance(doc) if err != nil { return } } waiter.Wait() if countErr != nil { err = countErr return } } else { cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$facet": &bson.M{ "meta": []*bson.M{ &bson.M{ "$count": "count", }, }, "instances": []*bson.M{ &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "nodes", "localField": "node", "foreignField": "_id", "as": "node_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "datacenters", "localField": "datacenter", "foreignField": "_id", "as": "datacenter_docs", }, }, &bson.M{ "$lookup": &bson.M{ "from": "disks", "localField": "_id", "foreignField": "instance", "as": "disk_docs", }, }, }, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Not found"), } return } doc := &InstancesPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } if len(doc.Metadata) > 0 { count = doc.Metadata[0].Count } for _, instDoc := range doc.Instances { err = addInstance(instDoc) if err != nil { return } } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: aggregate/pod.go ================================================ package aggregate import ( "sort" "sync" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/unit" ) func sortUnits(units []*unit.Unit) { sort.SliceStable(units, func(i, j int) bool { return units[i].Name < units[j].Name }) } type PodPipe struct { pod.Pod `bson:",inline"` UnitDocs []*unit.Unit `bson:"unit_docs"` } type Metadata struct { Count int64 `bson:"count"` } type PodsPipe struct { Metadata []*Metadata `bson:"meta"` Pods []*PodPipe `bson:"pods"` } type PodAggregate struct { pod.Pod Units []*unit.Unit `json:"units"` } func GetPod(db *database.Database, usrId bson.ObjectID, query *bson.M) ( pd *PodAggregate, err error) { coll := db.Pods() cursor, err := coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$lookup": &bson.M{ "from": "units", "localField": "_id", "foreignField": "pod", "as": "unit_docs", }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Pod not found"), } return } doc := &PodPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } sortUnits(doc.UnitDocs) pd = &PodAggregate{ Pod: doc.Pod, Units: doc.UnitDocs, } pd.Json(usrId) err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetPodsPaged(db *database.Database, usrId bson.ObjectID, query *bson.M, page, pageCount int64) (pods []*PodAggregate, count int64, err error) { coll := db.Pods() pods = []*PodAggregate{} if pageCount == 0 { pageCount = 20 } skip := page * pageCount var cursor *mongo.Cursor if len(*query) == 0 { waiter := &sync.WaitGroup{} var countErr error waiter.Add(1) go func() { defer waiter.Done() count, countErr = coll.EstimatedDocumentCount(db) if countErr != nil { countErr = database.ParseError(countErr) return } }() cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "units", "localField": "_id", "foreignField": "pod", "as": "unit_docs", }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { doc := &PodPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } sortUnits(doc.UnitDocs) pd := &PodAggregate{ Pod: doc.Pod, Units: doc.UnitDocs, } pd.Json(usrId) pods = append(pods, pd) } waiter.Wait() if countErr != nil { err = countErr return } } else { cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$facet": &bson.M{ "meta": []*bson.M{ &bson.M{ "$count": "count", }, }, "pods": []*bson.M{ &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, &bson.M{ "$lookup": &bson.M{ "from": "units", "localField": "_id", "foreignField": "pod", "as": "unit_docs", }, }, }, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Not found"), } return } doc := &PodsPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } if len(doc.Metadata) > 0 { count = doc.Metadata[0].Count } for _, podDoc := range doc.Pods { sortUnits(podDoc.UnitDocs) pd := &PodAggregate{ Pod: podDoc.Pod, Units: podDoc.UnitDocs, } pd.Json(usrId) pods = append(pods, pd) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: aggregate/shape.go ================================================ package aggregate import ( "sync" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/shape" ) type ShapePipe struct { shape.Shape `bson:",inline"` NodeDocs []*node.Node `bson:"node_docs"` } type ShapesPipe struct { Metadata []*Metadata `bson:"meta"` Shapes []*ShapePipe `bson:"shapes"` } func GetShapePaged(db *database.Database, query *bson.M, page, pageCount int64) (shapes []*shape.Shape, count int64, err error) { coll := db.Shapes() shapes = []*shape.Shape{} if pageCount == 0 { pageCount = 20 } skip := page * pageCount sizeQuery := &bson.M{ "$ifNull": bson.A{ &bson.M{ "$setIntersection": bson.A{ &bson.M{"$ifNull": bson.A{"$roles", bson.A{}}}, &bson.M{"$ifNull": bson.A{"$$shape_roles", bson.A{}}}, }, }, bson.A{}, }, } nodeLookup := &bson.M{ "$lookup": &bson.M{ "from": "nodes", "let": &bson.M{ "shape_roles": "$roles", }, "pipeline": []*bson.M{ { "$match": &bson.M{ "$expr": &bson.M{ "$gt": bson.A{ &bson.M{ "$size": sizeQuery, }, 0, }, }, }, }, }, "as": "node_docs", }, } addShape := func(doc *ShapePipe) { shpe := &doc.Shape shpe.NodeCount = len(doc.NodeDocs) shapes = append(shapes, shpe) } var cursor *mongo.Cursor if len(*query) == 0 { waiter := &sync.WaitGroup{} var countErr error waiter.Add(1) go func() { defer waiter.Done() count, countErr = coll.EstimatedDocumentCount(db) if countErr != nil { countErr = database.ParseError(countErr) return } }() cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, nodeLookup, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { doc := &ShapePipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } addShape(doc) } waiter.Wait() if countErr != nil { err = countErr return } } else { cursor, err = coll.Aggregate(db, []*bson.M{ &bson.M{ "$match": query, }, &bson.M{ "$sort": &bson.M{ "name": 1, }, }, &bson.M{ "$facet": &bson.M{ "meta": []*bson.M{ &bson.M{ "$count": "count", }, }, "shapes": []*bson.M{ &bson.M{ "$skip": skip, }, &bson.M{ "$limit": pageCount, }, nodeLookup, }, }, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) if !cursor.Next(db) { err = &database.NotFoundError{ errors.New("aggregate: Not found"), } return } doc := &ShapesPipe{} err = cursor.Decode(doc) if err != nil { err = database.ParseError(err) return } if len(doc.Metadata) > 0 { count = doc.Metadata[0].Count } for _, shapeDoc := range doc.Shapes { addShape(shapeDoc) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: ahandlers/alert.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/alert" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type alertData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Roles []string `json:"roles"` Resource string `json:"resource"` Level int `json:"level"` Frequency int `json:"frequency"` Ignores []string `json:"ignores"` ValueInt int `json:"value_int"` ValueStr string `json:"value_str"` } type alertsData struct { Alerts []*alert.Alert `json:"alerts"` Count int64 `json:"count"` } func alertPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &alertData{} alertId, ok := utils.ParseObjectId(c.Param("alert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } alrt, err := alert.Get(db, alertId) if err != nil { utils.AbortWithError(c, 500, err) return } alrt.Name = data.Name alrt.Comment = data.Comment alrt.Organization = data.Organization alrt.Roles = data.Roles alrt.Resource = data.Resource alrt.Level = data.Level alrt.Frequency = data.Frequency alrt.Ignores = data.Ignores alrt.ValueInt = data.ValueInt alrt.ValueStr = data.ValueStr fields := set.NewSet( "name", "comment", "organization", "roles", "resource", "level", "frequency", "ignores", "value_int", "value_str", ) errData, err := alrt.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = alrt.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, alrt) } func alertPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &alertData{ Name: "new-alert", Resource: alert.InstanceOffline, Level: alert.Medium, } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } alrt := &alert.Alert{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, Roles: data.Roles, Resource: data.Resource, Level: data.Level, Frequency: data.Frequency, Ignores: data.Ignores, ValueInt: data.ValueInt, ValueStr: data.ValueStr, } errData, err := alrt.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = alrt.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, alrt) } func alertDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) alertId, ok := utils.ParseObjectId(c.Param("alert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := alert.Remove(db, alertId) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, nil) } func alertsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { utils.AbortWithError(c, 500, err) return } err = alert.RemoveMulti(db, dta) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, nil) } func alertGet(c *gin.Context) { if demo.IsDemo() { alrt := demo.Alerts[0] c.JSON(200, alrt) return } db := c.MustGet("db").(*database.Database) alertId, ok := utils.ParseObjectId(c.Query("id")) if !ok { utils.AbortWithStatus(c, 400) return } alrt, err := alert.Get(db, alertId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, alrt) } func alertsGet(c *gin.Context) { if demo.IsDemo() { data := &alertsData{ Alerts: demo.Alerts, Count: int64(len(demo.Alerts)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} alertId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = alertId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["$or"] = []*bson.M{ &bson.M{ "name": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", }, }, } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } alerts, count, err := alert.GetAllPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } dta := &alertsData{ Alerts: alerts, Count: count, } c.JSON(200, dta) } ================================================ FILE: ahandlers/audit.go ================================================ package ahandlers import ( "strconv" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/utils" ) type auditsData struct { Audits []*audit.Audit `json:"audits"` Count int64 `json:"count"` } func auditsGet(c *gin.Context) { if demo.IsDemo() { data := &auditsData{ Audits: demo.Audits, Count: int64(len(demo.Audits)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) userId, ok := utils.ParseObjectId(c.Param("user_id")) if !ok { utils.AbortWithStatus(c, 400) return } audits, count, err := audit.GetAll(db, userId, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &auditsData{ Audits: audits, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/auth.go ================================================ package ahandlers import ( "strings" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/auth" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/cookie" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/secondary" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/validator" ) func authStateGet(c *gin.Context) { data := auth.GetState() if demo.IsDemo() { provider := &auth.StateProvider{ Id: "demo", Type: "demo", Label: "demo", } data.Providers = append(data.Providers, provider) } c.JSON(200, data) } type authData struct { Username string `json:"username"` Password string `json:"password"` } func authSessionPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) data := &authData{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usr, errData, err := auth.Local(db, data.Username, data.Password) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.AdminPrimaryApprove, audit.Fields{ "method": "local", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } devAuth, secProviderId, errAudit, errData, err := validator.ValidateAdmin( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "local" err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } if devAuth { deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } secType := "" var secProvider bson.ObjectID if deviceCount == 0 { if secProviderId.IsZero() { secType = secondary.AdminDeviceRegister secProvider = secondary.DeviceProvider } else { secType = secondary.Admin secProvider = secProviderId } } else { secType = secondary.AdminDevice secProvider = secondary.DeviceProvider } secd, err := secondary.New(db, usr.Id, secType, secProvider) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } else if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.Admin, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } err = audit.New( db, c.Request, usr.Id, audit.AdminLogin, audit.Fields{ "method": "local", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewAdmin(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.Admin) if err != nil { utils.AbortWithError(c, 500, err) return } c.Status(200) } type secondaryData struct { Token string `json:"token"` Factor string `json:"factor"` Passcode string `json:"passcode"` } func authSecondaryPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) data := &secondaryData{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.Admin) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := secd.Handle(db, c.Request, data.Factor, data.Passcode) if err != nil { if _, ok := err.(*secondary.IncompleteError); ok { c.Status(201) } else { utils.AbortWithError(c, 500, err) } return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, audit.Fields{ "method": "secondary", "provider_id": secd.ProviderId, "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.AdminSecondaryApprove, audit.Fields{ "provider_id": secd.ProviderId, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } deviceAuth, _, errAudit, errData, err := validator.ValidateAdmin( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "secondary" errAudit["provider_id"] = secd.ProviderId err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } if deviceAuth { deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } if deviceCount == 0 { secd, err := secondary.New(db, usr.Id, secondary.AdminDeviceRegister, secondary.DeviceProvider) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } } err = audit.New( db, c.Request, usr.Id, audit.AdminLogin, audit.Fields{ "method": "secondary", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewAdmin(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.Admin) if err != nil { utils.AbortWithError(c, 500, err) return } c.Status(200) } func logoutGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if authr.IsValid() { err := authr.Remove(db) if err != nil { utils.AbortWithError(c, 500, err) return } } usr, _ := authr.GetUser(db) if usr != nil { err := audit.New( db, c.Request, usr.Id, audit.AdminLogout, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } } c.Redirect(302, "/login") } func authRequestGet(c *gin.Context) { auth.Request(c, auth.Admin) } func authCallbackGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) sig := c.Query("sig") query := strings.Split(c.Request.URL.RawQuery, "&sig=")[0] usr, _, errAudit, errData, err := auth.Callback(db, sig, query) if err != nil { switch err.(type) { case *auth.InvalidState: c.Redirect(302, "/") break default: utils.AbortWithError(c, 500, err) } return } if errData != nil { if usr != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "callback" err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.AdminPrimaryApprove, audit.Fields{ "method": "callback", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } devAuth, secProviderId, errAudit, errData, err := validator.ValidateAdmin( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "callback" err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } if devAuth { deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } secType := "" var secProvider bson.ObjectID if deviceCount == 0 { if secProviderId.IsZero() { secType = secondary.AdminDeviceRegister secProvider = secondary.DeviceProvider } else { secType = secondary.Admin secProvider = secProviderId } } else { secType = secondary.AdminDevice secProvider = secondary.DeviceProvider } secd, err := secondary.New(db, usr.Id, secType, secProvider) if err != nil { utils.AbortWithError(c, 500, err) return } urlQuery, err := secd.GetQuery() if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, "/login?"+urlQuery) return } else if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.Admin, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } urlQuery, err := secd.GetQuery() if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, "/login?"+urlQuery) return } err = audit.New( db, c.Request, usr.Id, audit.AdminLogin, audit.Fields{ "method": "callback", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewAdmin(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.Admin) if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, "/") } func authWanRegisterGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) token := c.Query("token") if node.Self.WebauthnDomain == "" { errData := &errortypes.ErrorData{ Error: "webauthn_domain_unavailable", Message: "WebAuthn domain must be configured", } c.JSON(400, errData) return } secd, err := secondary.Get(db, token, secondary.AdminDeviceRegister) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = audit.New( db, c.Request, usr.Id, audit.AdminDeviceRegisterRequest, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } resp, errData, err := secd.DeviceRegisterRequest(db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, audit.Fields{ "method": "device_register", "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } c.JSON(200, resp) } type authWanRegisterData struct { Token string `json:"token"` Name string `json:"name"` } func authWanRegisterPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &authWanRegisterData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.AdminDeviceRegister) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } _, _, errAudit, errData, err := validator.ValidateAdmin( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "device_register" err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } devc, errData, err := secd.DeviceRegisterResponse( db, utils.GetOrigin(c.Request), body, data.Name) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.DeviceRegisterFailed, audit.Fields{ "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.AdminDeviceRegister, audit.Fields{ "device_id": devc.Id, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") err = audit.New( db, c.Request, usr.Id, audit.AdminLogin, audit.Fields{ "method": "device_register", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewAdmin(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.Admin) if err != nil { utils.AbortWithError(c, 500, err) return } c.Status(200) } func authWanRequestGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) token := c.Query("token") secd, err := secondary.Get(db, token, secondary.AdminDevice) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } resp, errData, err := secd.DeviceRequest( db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, audit.Fields{ "method": "device", "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } c.JSON(200, resp) } type authWanRespondData struct { Token string `json:"token"` } func authWanRespondPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) data := &authWanRespondData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.AdminDevice) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } _, secProviderId, errAudit, errData, err := validator.ValidateAdmin( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "device" err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } errData, err = secd.DeviceRespond( db, utils.GetOrigin(c.Request), body) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.AdminLoginFailed, audit.Fields{ "method": "device", "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.AdminDeviceApprove, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.Admin, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } err = audit.New( db, c.Request, usr.Id, audit.AdminLogin, audit.Fields{ "method": "device", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewAdmin(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.Admin) if err != nil { utils.AbortWithError(c, 500, err) return } c.Status(200) } ================================================ FILE: ahandlers/authority.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type authorityData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` Organization bson.ObjectID `json:"organization"` Roles []string `json:"roles"` Key string `json:"key"` Principals []string `json:"principals"` Certificate string `json:"certificate"` } type authoritiesData struct { Authorities []*authority.Authority `json:"authorities"` Count int64 `json:"count"` } func authorityPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &authorityData{} authorityId, ok := utils.ParseObjectId(c.Param("authority_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } authr, err := authority.Get(db, authorityId) if err != nil { utils.AbortWithError(c, 500, err) return } authr.Name = data.Name authr.Comment = data.Comment authr.Type = data.Type authr.Organization = data.Organization authr.Roles = data.Roles authr.Key = data.Key authr.Principals = data.Principals authr.Certificate = data.Certificate fields := set.NewSet( "name", "comment", "type", "organization", "roles", "key", "principals", "certificate", ) errData, err := authr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = authr.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, authr) } func authorityPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &authorityData{ Name: "new-authority", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } authr := &authority.Authority{ Name: data.Name, Comment: data.Comment, Type: data.Type, Organization: data.Organization, Roles: data.Roles, Key: data.Key, Principals: data.Principals, Certificate: data.Certificate, } errData, err := authr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = authr.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, authr) } func authorityDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authorityId, ok := utils.ParseObjectId(c.Param("authority_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := authority.Remove(db, authorityId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, nil) } func authoritiesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } err = authority.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, nil) } func authorityGet(c *gin.Context) { if demo.IsDemo() { authr := demo.Authorities[0] c.JSON(200, authr) return } db := c.MustGet("db").(*database.Database) authorityId, ok := utils.ParseObjectId(c.Param("authority_id")) if !ok { utils.AbortWithStatus(c, 400) return } authr, err := authority.Get(db, authorityId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, authr) } func authoritiesGet(c *gin.Context) { if demo.IsDemo() { data := &authoritiesData{ Authorities: demo.Authorities, Count: int64(len(demo.Authorities)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} authrId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = authrId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } principal := strings.TrimSpace(c.Query("principal")) if principal != "" { query["principals"] = principal } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } authorities, count, err := authority.GetAllPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &authoritiesData{ Authorities: authorities, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/balancer.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type balancerData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` State bool `json:"state"` Type string `json:"type"` Organization bson.ObjectID `json:"organization"` Datacenter bson.ObjectID `json:"datacenter"` Certificates []bson.ObjectID `json:"certificates"` WebSockets bool `json:"websockets"` Domains []*balancer.Domain `json:"domains"` Backends []*balancer.Backend `json:"backends"` CheckPath string `json:"check_path"` } type balancersData struct { Balancers []*balancer.Balancer `json:"balancers"` Count int64 `json:"count"` } func balancerPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &balancerData{} balancerId, ok := utils.ParseObjectId(c.Param("balancer_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } balnc, err := balancer.Get(db, balancerId) if err != nil { utils.AbortWithError(c, 500, err) return } balnc.Name = data.Name balnc.Comment = data.Comment balnc.State = data.State balnc.Type = data.Type balnc.Organization = data.Organization balnc.Datacenter = data.Datacenter balnc.Certificates = data.Certificates balnc.WebSockets = data.WebSockets balnc.Domains = data.Domains balnc.Backends = data.Backends balnc.CheckPath = data.CheckPath fields := set.NewSet( "name", "comment", "state", "type", "organization", "datacenter", "certificates", "websockets", "domains", "backends", "check_path", ) errData, err := balnc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = balnc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") balnc.Json() c.JSON(200, balnc) } func balancerPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &balancerData{ Name: "new-balancer", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } balnc := &balancer.Balancer{ Name: data.Name, Comment: data.Comment, State: data.State, Type: data.Type, Organization: data.Organization, Datacenter: data.Datacenter, Certificates: data.Certificates, WebSockets: data.WebSockets, Domains: data.Domains, Backends: data.Backends, CheckPath: data.CheckPath, } errData, err := balnc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = balnc.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") balnc.Json() c.JSON(200, balnc) } func balancerDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) balancerId, ok := utils.ParseObjectId(c.Param("balancer_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := balancer.Remove(db, balancerId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") c.JSON(200, nil) } func balancersDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } err = balancer.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") c.JSON(200, nil) } func balancerGet(c *gin.Context) { if demo.IsDemo() { balnc := demo.Balancers[0] c.JSON(200, balnc) return } db := c.MustGet("db").(*database.Database) balancerId, ok := utils.ParseObjectId(c.Param("balancer_id")) if !ok { utils.AbortWithStatus(c, 400) return } balnc, err := balancer.Get(db, balancerId) if err != nil { utils.AbortWithError(c, 500, err) return } balnc.Json() c.JSON(200, balnc) } func balancersGet(c *gin.Context) { if demo.IsDemo() { data := &balancersData{ Balancers: demo.Balancers, Count: int64(len(demo.Balancers)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} balancerId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = balancerId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } datacenter, ok := utils.ParseObjectId(c.Query("datacenter")) if ok { query["datacenter"] = datacenter } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } balncs, count, err := balancer.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, balnc := range balncs { balnc.Json() } data := &balancersData{ Balancers: balncs, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/block.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type blockData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Vlan int `json:"vlan"` Type string `json:"type"` Subnets []string `json:"subnets"` Subnets6 []string `json:"subnets6"` Excludes []string `json:"excludes"` Netmask string `json:"netmask"` Gateway string `json:"gateway"` Gateway6 string `json:"gateway6"` } type blocksData struct { Blocks []*aggregate.BlockAggregate `json:"blocks"` Count int64 `json:"count"` } func blockPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &blockData{} blckId, ok := utils.ParseObjectId(c.Param("block_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } blck, err := block.Get(db, blckId) if err != nil { utils.AbortWithError(c, 500, err) return } blck.Name = dta.Name blck.Comment = dta.Comment blck.Vlan = dta.Vlan blck.Subnets = dta.Subnets blck.Subnets6 = dta.Subnets6 blck.Excludes = dta.Excludes blck.Netmask = dta.Netmask blck.Gateway = dta.Gateway blck.Gateway6 = dta.Gateway6 fields := set.NewSet( "name", "comment", "vlan", "subnets", "subnets6", "excludes", "netmask", "gateway", "gateway6", ) errData, err := blck.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = blck.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "block.change") c.JSON(200, blck) } func blockPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &blockData{ Name: "new-block", } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } blck := &block.Block{ Name: dta.Name, Comment: dta.Comment, Vlan: dta.Vlan, Type: dta.Type, Subnets: dta.Subnets, Subnets6: dta.Subnets6, Excludes: dta.Excludes, Netmask: dta.Netmask, Gateway: dta.Gateway, Gateway6: dta.Gateway6, } errData, err := blck.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = blck.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "block.change") c.JSON(200, blck) } func blockDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) blckId, ok := utils.ParseObjectId(c.Param("block_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "block", blckId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = block.Remove(db, blckId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "block.change") c.JSON(200, nil) } func blocksDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "block", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = block.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "block.change") c.JSON(200, nil) } func blockGet(c *gin.Context) { if demo.IsDemo() { blck := demo.Blocks[0] c.JSON(200, blck) return } db := c.MustGet("db").(*database.Database) blckId, ok := utils.ParseObjectId(c.Param("block_id")) if !ok { utils.AbortWithStatus(c, 400) return } blck, err := block.Get(db, blckId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, blck) } func blocksGet(c *gin.Context) { if demo.IsDemo() { data := &blocksData{ Blocks: demo.Blocks, Count: int64(len(demo.Blocks)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} blockId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = blockId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } blcks, count, err := aggregate.GetBlockPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &blocksData{ Blocks: blcks, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/certificate.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/acme" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type certificateData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Type string `json:"type"` Key string `json:"key"` Certificate string `json:"certificate"` AcmeDomains []string `json:"acme_domains"` AcmeType string `json:"acme_type"` AcmeAuth string `json:"acme_auth"` AcmeSecret bson.ObjectID `json:"acme_secret"` Refresh bool `json:"refresh"` } type certificatesData struct { Certificates []*certificate.Certificate `json:"certificates"` Count int64 `json:"count"` } func certificatePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &certificateData{} certId, ok := utils.ParseObjectId(c.Param("cert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } cert, err := certificate.Get(db, certId) if err != nil { utils.AbortWithError(c, 500, err) return } cert.Name = data.Name cert.Comment = data.Comment cert.Organization = data.Organization cert.Type = data.Type cert.AcmeDomains = data.AcmeDomains cert.AcmeType = data.AcmeType cert.AcmeAuth = data.AcmeAuth cert.AcmeSecret = data.AcmeSecret fields := set.NewSet( "name", "comment", "organization", "type", "acme_domains", "acme_type", "acme_auth", "acme_secret", "info", ) if cert.Type != certificate.LetsEncrypt { cert.Key = data.Key fields.Add("key") cert.Certificate = data.Certificate fields.Add("certificate") } errData, err := cert.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = cert.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } if cert.Type == certificate.LetsEncrypt { acme.RenewBackground(cert, data.Refresh) } event.PublishDispatch(db, "certificate.change") c.JSON(200, cert) } func certificatePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &certificateData{ Name: "new-certificate", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } cert := &certificate.Certificate{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, Type: data.Type, AcmeDomains: data.AcmeDomains, AcmeType: data.AcmeType, AcmeAuth: data.AcmeAuth, AcmeSecret: data.AcmeSecret, } if cert.Type != certificate.LetsEncrypt { cert.Key = data.Key cert.Certificate = data.Certificate } errData, err := cert.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = cert.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } if cert.Type == certificate.LetsEncrypt { acme.RenewBackground(cert, false) } event.PublishDispatch(db, "certificate.change") c.JSON(200, cert) } func certificateDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) certId, ok := utils.ParseObjectId(c.Param("cert_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "certificate", certId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = certificate.Remove(db, certId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "certificate.change") c.JSON(200, nil) } func certificatesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "certificate", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = certificate.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "certificate.change") c.JSON(200, nil) } func certificateGet(c *gin.Context) { if demo.IsDemo() { cert := demo.Certificates[0] c.JSON(200, cert) return } db := c.MustGet("db").(*database.Database) certId, ok := utils.ParseObjectId(c.Param("cert_id")) if !ok { utils.AbortWithStatus(c, 400) return } cert, err := certificate.Get(db, certId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { cert.Key = "demo" cert.AcmeAccount = "demo" } c.JSON(200, cert) } func certificatesGet(c *gin.Context) { if demo.IsDemo() { data := &certificatesData{ Certificates: demo.Certificates, Count: int64(len(demo.Certificates)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { certs, err := certificate.GetAllNames(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, certs) return } page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} certificateId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = certificateId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } certs, count, err := certificate.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &certificatesData{ Certificates: certs, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/check.go ================================================ package ahandlers import ( "github.com/gin-gonic/gin" ) func checkGet(c *gin.Context) { c.String(200, "ok") } ================================================ FILE: ahandlers/completion.go ================================================ package ahandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/completion" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) func completionGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) if demo.IsDemo() { data := &completion.Completion{} for _, item := range demo.Organizations { data.Organizations = append(data.Organizations, &database.Named{ Id: item.Id, Name: item.Name, }) } for _, item := range demo.Authorities { data.Authorities = append(data.Authorities, &database.Named{ Id: item.Id, Name: item.Name, }) } for _, item := range demo.Policies { data.Policies = append(data.Policies, &database.Named{ Id: item.Id, Name: item.Name, }) } for _, item := range demo.Domains { data.Domains = append(data.Domains, &domain.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, }) } for _, item := range demo.Vpcs { data.Vpcs = append(data.Vpcs, &vpc.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, VpcId: item.VpcId, Network: item.Network, Subnets: item.Subnets, Datacenter: item.Datacenter, }) } for _, item := range demo.Datacenters { data.Datacenters = append(data.Datacenters, &datacenter.Completion{ Id: item.Id, Name: item.Name, NetworkMode: item.NetworkMode, }) } for _, item := range demo.Blocks { data.Blocks = append(data.Blocks, &block.Completion{ Id: item.Id, Name: item.Name, Type: item.Type, }) } for _, item := range demo.Nodes { data.Nodes = append(data.Nodes, &node.Completion{ Id: item.Id, Name: item.Name, Zone: item.Zone, Types: item.Types, }) } for _, item := range demo.Pools { data.Pools = append(data.Pools, &pool.Completion{ Id: item.Id, Name: item.Name, Zone: item.Zone, }) } for _, item := range demo.Zones { data.Zones = append(data.Zones, &zone.Completion{ Id: item.Id, Datacenter: item.Datacenter, Name: item.Name, }) } for _, item := range demo.Shapes { data.Shapes = append(data.Shapes, &shape.Completion{ Id: item.Id, Name: item.Name, Datacenter: item.Datacenter, Flexible: item.Flexible, Memory: item.Memory, Processors: item.Processors, }) } imgs, err := image.GetAllCompletion(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } data.Images = imgs for _, item := range demo.Storages { data.Storages = append(data.Storages, &storage.Completion{ Id: item.Id, Name: item.Name, Type: item.Type, }) } for _, item := range demo.Instances { data.Instances = append(data.Instances, &instance.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, Zone: item.Zone, Vpc: item.Vpc, Subnet: item.Subnet, Node: item.Node, }) } for _, item := range demo.Plans { data.Plans = append(data.Plans, &plan.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, }) } for _, item := range demo.Certificates { data.Certificates = append( data.Certificates, &certificate.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, Type: item.Type, }, ) } for _, item := range demo.Secrets { data.Secrets = append(data.Secrets, &secret.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, Type: item.Type, }) } for _, item := range demo.Pods { data.Pods = append(data.Pods, &pod.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, }) } for _, item := range demo.Units { data.Units = append(data.Units, &unit.Completion{ Id: item.Id, Pod: item.Pod, Organization: item.Organization, Name: item.Name, Kind: item.Kind, }) } c.JSON(200, data) return } cmpl, err := completion.GetCompletion(db, bson.NilObjectID, nil) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, cmpl) } ================================================ FILE: ahandlers/csrf.go ================================================ package ahandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/csrf" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/utils" ) type csrfData struct { Token string `json:"token"` Theme string `json:"theme"` EditorTheme string `json:"editor_theme"` OracleLicense bool `json:"oracle_license"` } func csrfGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } token, err := csrf.NewToken(db, authr.SessionId()) if err != nil { utils.AbortWithError(c, 500, err) return } oracleLicense := usr.OracleLicense if demo.IsDemo() { oracleLicense = true } data := &csrfData{ Token: token, Theme: usr.Theme, EditorTheme: usr.EditorTheme, OracleLicense: oracleLicense, } c.JSON(200, data) } ================================================ FILE: ahandlers/datacenter.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type datacenterData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` NetworkMode string `json:"network_mode"` MatchOrganizations bool `json:"match_organizations"` Organizations []bson.ObjectID `json:"organizations"` JumboMtu int `json:"jumbo_mtu"` PublicStorages []bson.ObjectID `json:"public_storages"` PrivateStorage bson.ObjectID `json:"private_storage"` PrivateStorageClass string `json:"private_storage_class"` BackupStorage bson.ObjectID `json:"backup_storage"` BackupStorageClass string `json:"backup_storage_class"` } type datacentersData struct { Datacenters []*datacenter.Datacenter `json:"datacenters"` Count int64 `json:"count"` } func datacenterPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &datacenterData{} dcId, ok := utils.ParseObjectId(c.Param("dc_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } dc, err := datacenter.Get(db, dcId) if err != nil { utils.AbortWithError(c, 500, err) return } dc.Name = data.Name dc.Comment = data.Comment dc.NetworkMode = data.NetworkMode dc.MatchOrganizations = data.MatchOrganizations dc.Organizations = data.Organizations dc.JumboMtu = data.JumboMtu dc.PublicStorages = data.PublicStorages dc.PrivateStorage = data.PrivateStorage dc.PrivateStorageClass = data.PrivateStorageClass dc.BackupStorage = data.BackupStorage dc.BackupStorageClass = data.BackupStorageClass fields := set.NewSet( "name", "comment", "network_mode", "match_organizations", "organizations", "jumbo_mtu", "public_storages", "private_storage", "private_storage_class", "backup_storage", "backup_storage_class", ) errData, err := dc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = dc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "datacenter.change") c.JSON(200, dc) } func datacenterPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &datacenterData{ Name: "new-datacenter", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } dc := &datacenter.Datacenter{ Name: data.Name, Comment: data.Comment, NetworkMode: data.NetworkMode, MatchOrganizations: data.MatchOrganizations, Organizations: data.Organizations, JumboMtu: data.JumboMtu, PublicStorages: data.PublicStorages, PrivateStorage: data.PrivateStorage, PrivateStorageClass: data.PrivateStorageClass, BackupStorage: data.BackupStorage, BackupStorageClass: data.BackupStorageClass, } errData, err := dc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = dc.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "datacenter.change") c.JSON(200, dc) } func datacenterDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dcId, ok := utils.ParseObjectId(c.Param("dc_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "datacenter", dcId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = datacenter.Remove(db, dcId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "datacenter.change") c.JSON(200, nil) } func datacentersDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "datacenter", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = datacenter.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "datacenter.change") c.JSON(200, nil) } func datacenterGet(c *gin.Context) { if demo.IsDemo() { dc := demo.Datacenters[0] c.JSON(200, dc) return } db := c.MustGet("db").(*database.Database) dcId, ok := utils.ParseObjectId(c.Param("dc_id")) if !ok { utils.AbortWithStatus(c, 400) return } dc, err := datacenter.Get(db, dcId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, dc) } func datacentersGet(c *gin.Context) { if demo.IsDemo() { data := &datacentersData{ Datacenters: demo.Datacenters, Count: int64(len(demo.Datacenters)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { dcs, err := datacenter.GetAllNames(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, dcs) return } page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} datacenterId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = datacenterId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } dc, count, err := datacenter.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &datacentersData{ Datacenters: dc, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/devices.go ================================================ package ahandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/alertevent" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/secondary" "github.com/pritunl/pritunl-cloud/utils" ) type deviceData struct { User bson.ObjectID `json:"user"` Name string `json:"name"` Type string `json:"type"` Mode string `json:"mode"` Number string `json:"number"` } func devicePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &deviceData{} devcId, ok := utils.ParseObjectId(c.Param("device_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } devc, err := device.Get(db, devcId) if err != nil { utils.AbortWithError(c, 500, err) return } devc.Name = data.Name fields := set.NewSet( "name", ) errData, err := devc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = devc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, devc) } func devicePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &deviceData{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } devc := device.New(data.User, data.Type, data.Mode) devc.Name = data.Name devc.Number = data.Number errData, err := devc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = devc.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, devc) } func deviceDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) devcId, ok := utils.ParseObjectId(c.Param("device_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := device.Remove(db, devcId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, nil) } func devicesGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) usrId, ok := utils.ParseObjectId(c.Param("user_id")) if !ok { utils.AbortWithStatus(c, 400) return } devices, err := device.GetAllSorted(db, usrId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, devices) } func deviceAlertPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &deviceData{} devcId, ok := utils.ParseObjectId(c.Param("resource_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } devc, err := device.Get(db, devcId) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := alertevent.SendTest(db, devc) if errData != nil { c.JSON(400, errData) return } if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, devc) } func deviceMethodPost(c *gin.Context) { switch c.Param("method") { case "alert": deviceAlertPost(c) return default: utils.AbortWithStatus(c, 404) return } return } type devicesWanRegisterRespData struct { Token string `json:"token"` Options interface{} `json:"options"` } func deviceWanRegisterGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) if node.Self.WebauthnDomain == "" { errData := &errortypes.ErrorData{ Error: "webauthn_domain_unavailable", Message: "WebAuthn domain must be configured", } c.JSON(400, errData) return } usrId, ok := utils.ParseObjectId(c.Param("user_id")) if !ok { utils.AbortWithStatus(c, 400) return } secd, err := secondary.New(db, usrId, secondary.AdminDeviceRegister, secondary.DeviceProvider) if err != nil { utils.AbortWithError(c, 500, err) return } jsonResp, errData, err := secd.DeviceRegisterRequest(db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } resp := &devicesWanRegisterRespData{ Token: secd.Id, Options: jsonResp, } c.JSON(200, resp) } type devicesWanRegisterData struct { Token string `json:"token"` Name string `json:"name"` } func deviceWanRegisterPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &devicesWanRegisterData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usrId, ok := utils.ParseObjectId(c.Param("resource_id")) if !ok { utils.AbortWithStatus(c, 400) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.AdminDeviceRegister) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } devc, errData, err := secd.DeviceRegisterResponse( db, utils.GetOrigin(c.Request), body, data.Name) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = audit.New( db, c.Request, usrId, audit.AdminDeviceRegister, audit.Fields{ "admin_id": usr.Id, "device_id": devc.Id, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, nil) } ================================================ FILE: ahandlers/disk.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" ) type diskData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Instance bson.ObjectID `json:"instance"` Index string `json:"index"` Type string `json:"type"` Node bson.ObjectID `json:"node"` Pool bson.ObjectID `json:"pool"` DeleteProtection bool `json:"delete_protection"` FileSystem string `json:"file_system"` Image bson.ObjectID `json:"image"` RestoreImage bson.ObjectID `json:"restore_image"` Backing bool `json:"backing"` Action string `json:"action"` Size int `json:"size"` LvSize int `json:"lv_size"` NewSize int `json:"new_size"` Backup bool `json:"backup"` } type disksMultiData struct { Ids []bson.ObjectID `json:"ids"` Action string `json:"action"` } type disksData struct { Disks []*aggregate.DiskAggregate `json:"disks"` Count int64 `json:"count"` } func diskPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &diskData{} diskId, ok := utils.ParseObjectId(c.Param("disk_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } dsk, err := disk.Get(db, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } fields := set.NewSet( "name", "comment", "type", "instance", "delete_protection", "index", "backup", "new_size", ) dsk.PreCommit() dsk.Name = dta.Name dsk.Comment = dta.Comment dsk.Instance = dta.Instance dsk.DeleteProtection = dta.DeleteProtection dsk.Index = dta.Index dsk.Backup = dta.Backup if dta.Action != "" && dsk.Action != "" { errData := &errortypes.ErrorData{ Error: "disk_actin_active", Message: "Disk action already active", } c.JSON(400, errData) return } if dsk.IsActive() && dta.Action == disk.Snapshot { dsk.Action = disk.Snapshot fields.Add("action") } else if dsk.IsActive() && dta.Action == disk.Backup { dsk.Action = disk.Backup fields.Add("action") } else if dsk.IsActive() && dta.Action == disk.Expand { dsk.Action = disk.Expand dsk.NewSize = dta.NewSize fields.Add("action") } else if dsk.IsActive() && dta.Action == disk.Restore { img, err := image.Get(db, dta.RestoreImage) if err != nil { utils.AbortWithError(c, 500, err) return } if img.Disk != dsk.Id { errData := &errortypes.ErrorData{ Error: "invalid_restore_image", Message: "Invalid restore image", } c.JSON(400, errData) return } dsk.Action = disk.Restore dsk.RestoreImage = img.Id fields.Add("action") fields.Add("restore_image") } errData, err := dsk.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = dsk.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, dsk) } func diskPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &diskData{ Name: "new-disk", } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } nde, err := node.Get(db, dta.Node) if err != nil { utils.AbortWithError(c, 500, err) return } imgSystemType := "" imgSystemKind := "" if !dta.Image.IsZero() { img, err := image.GetOrgPublic(db, dta.Organization, dta.Image) if err != nil { utils.AbortWithError(c, 500, err) return } imgSystemType = img.GetSystemType() imgSystemKind = img.GetSystemKind() store, err := storage.Get(db, img.Storage) if err != nil { utils.AbortWithError(c, 500, err) return } available, err := data.ImageAvailable(store, img) if err != nil { utils.AbortWithError(c, 500, err) return } if !available { if store.IsOracle() { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from archive", } c.JSON(400, errData) } else { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from glacier", } c.JSON(400, errData) } return } } dsk := &disk.Disk{ Name: dta.Name, Comment: dta.Comment, Organization: dta.Organization, Instance: dta.Instance, Datacenter: nde.Datacenter, Zone: nde.Zone, Index: dta.Index, Type: dta.Type, SystemType: imgSystemType, SystemKind: imgSystemKind, Node: dta.Node, Pool: dta.Pool, Image: dta.Image, DeleteProtection: dta.DeleteProtection, FileSystem: dta.FileSystem, Backing: dta.Backing, Size: dta.Size, LvSize: dta.LvSize, Backup: dta.Backup, } errData, err := dsk.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = dsk.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, dsk) } func disksPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &disksMultiData{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } if data.Action != disk.Snapshot && data.Action != disk.Backup { errData := &errortypes.ErrorData{ Error: "invalid_action", Message: "Invalid disk action", } c.JSON(400, errData) return } doc := bson.M{ "action": data.Action, } err = disk.UpdateMulti(db, data.Ids, &doc) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, nil) } func diskDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) diskId, ok := utils.ParseObjectId(c.Param("disk_id")) if !ok { utils.AbortWithStatus(c, 400) return } dsk, err := disk.Get(db, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } if dsk.DeleteProtection { errData := &errortypes.ErrorData{ Error: "delete_protection", Message: "Cannot delete disk with delete protection", } c.JSON(400, errData) return } if !dsk.Instance.IsZero() { inst, e := instance.Get(db, dsk.Instance) if e != nil { err = e return } if inst.DeleteProtection { errData := &errortypes.ErrorData{ Error: "instance_delete_protection", Message: "Cannot delete disk attached to " + "instance with delete protection", } c.JSON(400, errData) return } } err = disk.Delete(db, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, nil) } func disksDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } force := c.Query("force") if force == "true" { for _, diskId := range dta { err = disk.Remove(db, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } } } else { err = disk.DeleteMulti(db, dta) if err != nil { utils.AbortWithError(c, 500, err) return } } event.PublishDispatch(db, "disk.change") c.JSON(200, nil) } func diskGet(c *gin.Context) { if demo.IsDemo() { dsk := demo.Disks[0] c.JSON(200, dsk) return } db := c.MustGet("db").(*database.Database) diskId, ok := utils.ParseObjectId(c.Param("disk_id")) if !ok { utils.AbortWithStatus(c, 400) return } dsk, err := disk.Get(db, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, dsk) } func disksGet(c *gin.Context) { if demo.IsDemo() { data := &disksData{ Disks: demo.Disks, Count: int64(len(demo.Disks)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} diskId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = diskId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } inst, ok := utils.ParseObjectId(c.Query("instance")) if ok { query["instance"] = inst } nodeId, ok := utils.ParseObjectId(c.Query("node")) if ok { query["node"] = nodeId } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } disks, count, err := aggregate.GetDiskPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } dta := &disksData{ Disks: disks, Count: count, } c.JSON(200, dta) } ================================================ FILE: ahandlers/domain.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type domainData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Type string `json:"type"` Secret bson.ObjectID `json:"secret"` RootDomain string `json:"root_domain"` Records []*domain.Record `json:"records"` } type domainsData struct { Domains []*domain.Domain `json:"domains"` Count int64 `json:"count"` } func domainPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &domainData{} domainId, ok := utils.ParseObjectId(c.Param("domain_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } domn, err := domain.Get(db, domainId) if err != nil { utils.AbortWithError(c, 500, err) return } err = domn.LoadRecords(db, true) if err != nil { return } domn.PreCommit() domn.Name = data.Name domn.Comment = data.Comment domn.Organization = data.Organization domn.Type = data.Type domn.Secret = data.Secret domn.RootDomain = data.RootDomain domn.Records = data.Records fields := set.NewSet( "name", "comment", "organization", "type", "secret", "root_domain", ) errData, err := domn.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = domn.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } err = domn.CommitRecords(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, domn) } func domainPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &domainData{ Name: "new.domain", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } domn := &domain.Domain{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, Type: data.Type, Secret: data.Secret, RootDomain: data.RootDomain, } errData, err := domn.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = domn.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, domn) } func domainDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) domainId, ok := utils.ParseObjectId(c.Param("domain_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := domain.Remove(db, domainId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, nil) } func domainsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = domain.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, nil) } func domainGet(c *gin.Context) { if demo.IsDemo() { domn := demo.Domains[0] c.JSON(200, domn) return } db := c.MustGet("db").(*database.Database) domainId, ok := utils.ParseObjectId(c.Param("domain_id")) if !ok { utils.AbortWithStatus(c, 400) return } domn, err := domain.Get(db, domainId) if err != nil { utils.AbortWithError(c, 500, err) return } err = domn.LoadRecords(db, true) if err != nil { return } domn.Json() c.JSON(200, domn) } func domainsGet(c *gin.Context) { if demo.IsDemo() { data := &domainsData{ Domains: demo.Domains, Count: int64(len(demo.Domains)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { query := &bson.M{} domns, err := domain.GetAllName(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, domns) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} domainId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = domainId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } domains, count, err := aggregate.GetDomainPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &domainsData{ Domains: domains, Count: count, } c.JSON(200, data) } } ================================================ FILE: ahandlers/event.go ================================================ package ahandlers import ( "context" "fmt" "time" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) const ( writeTimeout = 10 * time.Second pingInterval = 30 * time.Second pingWait = 40 * time.Second ) func eventGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) socket := &event.WebSocket{} defer func() { socket.Close() event.WebSocketsLock.Lock() event.WebSockets.Remove(socket) event.WebSocketsLock.Unlock() }() event.WebSocketsLock.Lock() event.WebSockets.Add(socket) event.WebSocketsLock.Unlock() ctx, cancel := context.WithCancel(context.Background()) socket.Cancel = cancel conn, err := event.Upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to upgrade request"), } utils.AbortWithError(c, 500, err) return } socket.Conn = conn err = conn.SetReadDeadline(time.Now().Add(pingWait)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to set read deadline"), } utils.AbortWithError(c, 500, err) return } conn.SetPongHandler(func(x string) (err error) { err = conn.SetReadDeadline(time.Now().Add(pingWait)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to set read deadline"), } utils.AbortWithError(c, 500, err) return } return }) lst, err := event.SubscribeListener(db, []string{"dispatch"}) if err != nil { utils.AbortWithError(c, 500, err) return } socket.Listener = lst ticker := time.NewTicker(pingInterval) socket.Ticker = ticker sub := lst.Listen() defer lst.Close() go func() { defer func() { r := recover() if r != nil && !socket.Closed { logrus.WithFields(logrus.Fields{ "error": errors.New(fmt.Sprintf("%s", r)), }).Error("mhandlers: Event panic") } }() for { _, _, err := conn.NextReader() if err != nil { conn.Close() break } } }() for { select { case <-ctx.Done(): return case msg, ok := <-sub: if !ok { err = conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(writeTimeout)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to set write control"), } return } return } err = conn.SetWriteDeadline(time.Now().Add(writeTimeout)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to set write deadline"), } return } err = conn.WriteJSON(msg) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to set write json"), } return } case <-ticker.C: err = conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeTimeout)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "mhandlers: Failed to set write control"), } return } } } } ================================================ FILE: ahandlers/firewall.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type firewallData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Roles []string `json:"roles"` Ingress []*firewall.Rule `json:"ingress"` } type firewallsData struct { Firewalls []*firewall.Firewall `json:"firewalls"` Count int64 `json:"count"` } func firewallPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &firewallData{} firewallId, ok := utils.ParseObjectId(c.Param("firewall_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } fire, err := firewall.Get(db, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } fire.Name = data.Name fire.Comment = data.Comment fire.Organization = data.Organization fire.Roles = data.Roles fire.Ingress = data.Ingress fields := set.NewSet( "name", "comment", "organization", "roles", "ingress", ) errData, err := fire.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = fire.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, fire) } func firewallPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &firewallData{ Name: "new-firewall", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } fire := &firewall.Firewall{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, Roles: data.Roles, Ingress: data.Ingress, } errData, err := fire.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = fire.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, fire) } func firewallDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) firewallId, ok := utils.ParseObjectId(c.Param("firewall_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "firewall", firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = firewall.Remove(db, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, nil) } func firewallsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "firewall", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = firewall.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, nil) } func firewallGet(c *gin.Context) { if demo.IsDemo() { fire := demo.Firewalls[0] c.JSON(200, fire) return } db := c.MustGet("db").(*database.Database) firewallId, ok := utils.ParseObjectId(c.Param("firewall_id")) if !ok { utils.AbortWithStatus(c, 400) return } fire, err := firewall.Get(db, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, fire) } func firewallsGet(c *gin.Context) { if demo.IsDemo() { data := &firewallsData{ Firewalls: demo.Firewalls, Count: int64(len(demo.Firewalls)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} firewallId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = firewallId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } firewalls, count, err := firewall.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &firewallsData{ Firewalls: firewalls, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/handlers.go ================================================ package ahandlers import ( "net/http" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/middlewear" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/static" ) var ( store *static.Store fileServer http.Handler ) func Register(engine *gin.Engine) { engine.Use(middlewear.Limiter) engine.Use(middlewear.Counter) engine.Use(middlewear.Recovery) engine.Use(middlewear.Headers) dbGroup := engine.Group("") dbGroup.Use(middlewear.Database) sessGroup := dbGroup.Group("") sessGroup.Use(middlewear.SessionAdmin) authGroup := sessGroup.Group("") authGroup.Use(middlewear.AuthAdmin) csrfGroup := authGroup.Group("") csrfGroup.Use(middlewear.CsrfToken) engine.NoRoute(middlewear.NotFound) csrfGroup.GET("/audit/:user_id", auditsGet) csrfGroup.GET("/alert", alertsGet) csrfGroup.GET("/alert/:alert_id", alertGet) csrfGroup.PUT("/alert/:alert_id", alertPut) csrfGroup.POST("/alert", alertPost) csrfGroup.DELETE("/alert", alertsDelete) csrfGroup.DELETE("/alert/:alert_id", alertDelete) engine.GET("/auth/state", authStateGet) dbGroup.POST("/auth/session", authSessionPost) dbGroup.POST("/auth/secondary", authSecondaryPost) dbGroup.GET("/auth/request", authRequestGet) dbGroup.GET("/auth/callback", authCallbackGet) dbGroup.GET("/auth/webauthn/request", authWanRequestGet) dbGroup.POST("/auth/webauthn/respond", authWanRespondPost) dbGroup.GET("/auth/webauthn/register", authWanRegisterGet) dbGroup.POST("/auth/webauthn/register", authWanRegisterPost) sessGroup.GET("/logout", logoutGet) csrfGroup.GET("/authority", authoritiesGet) csrfGroup.GET("/authority/:authority_id", authorityGet) csrfGroup.PUT("/authority/:authority_id", authorityPut) csrfGroup.POST("/authority", authorityPost) csrfGroup.DELETE("/authority", authoritiesDelete) csrfGroup.DELETE("/authority/:authority_id", authorityDelete) csrfGroup.GET("/balancer", balancersGet) csrfGroup.GET("/balancer/:balancer_id", balancerGet) csrfGroup.PUT("/balancer/:balancer_id", balancerPut) csrfGroup.POST("/balancer", balancerPost) csrfGroup.DELETE("/balancer", balancersDelete) csrfGroup.DELETE("/balancer/:balancer_id", balancerDelete) csrfGroup.GET("/block", blocksGet) csrfGroup.GET("/block/:block_id", blockGet) csrfGroup.PUT("/block/:block_id", blockPut) csrfGroup.POST("/block", blockPost) csrfGroup.DELETE("/block", blocksDelete) csrfGroup.DELETE("/block/:block_id", blockDelete) csrfGroup.GET("/certificate", certificatesGet) csrfGroup.GET("/certificate/:cert_id", certificateGet) csrfGroup.PUT("/certificate/:cert_id", certificatePut) csrfGroup.POST("/certificate", certificatePost) csrfGroup.DELETE("/certificate", certificatesDelete) csrfGroup.DELETE("/certificate/:cert_id", certificateDelete) engine.GET("/check", checkGet) authGroup.GET("/csrf", csrfGet) csrfGroup.GET("/completion", completionGet) csrfGroup.GET("/datacenter", datacentersGet) csrfGroup.GET("/datacenter/:dc_id", datacenterGet) csrfGroup.PUT("/datacenter/:dc_id", datacenterPut) csrfGroup.POST("/datacenter", datacenterPost) csrfGroup.DELETE("/datacenter", datacentersDelete) csrfGroup.DELETE("/datacenter/:dc_id", datacenterDelete) csrfGroup.GET("/device/:user_id", devicesGet) csrfGroup.PUT("/device/:device_id", devicePut) csrfGroup.POST("/device", devicePost) csrfGroup.DELETE("/device/:device_id", deviceDelete) csrfGroup.POST("/device/:resource_id/:method", deviceMethodPost) csrfGroup.GET("/device/:user_id/webauthn/register", deviceWanRegisterGet) csrfGroup.POST("/device/:resource_id/webauthn/register", deviceWanRegisterPost) csrfGroup.GET("/disk", disksGet) csrfGroup.GET("/disk/:disk_id", diskGet) csrfGroup.PUT("/disk", disksPut) csrfGroup.PUT("/disk/:disk_id", diskPut) csrfGroup.POST("/disk", diskPost) csrfGroup.DELETE("/disk", disksDelete) csrfGroup.DELETE("/disk/:disk_id", diskDelete) csrfGroup.GET("/domain", domainsGet) csrfGroup.GET("/domain/:domain_id", domainGet) csrfGroup.PUT("/domain/:domain_id", domainPut) csrfGroup.POST("/domain", domainPost) csrfGroup.DELETE("/domain", domainsDelete) csrfGroup.DELETE("/domain/:domain_id", domainDelete) csrfGroup.GET("/event", eventGet) csrfGroup.GET("/firewall", firewallsGet) csrfGroup.GET("/firewall/:firewall_id", firewallGet) csrfGroup.PUT("/firewall/:firewall_id", firewallPut) csrfGroup.POST("/firewall", firewallPost) csrfGroup.DELETE("/firewall", firewallsDelete) csrfGroup.DELETE("/firewall/:firewall_id", firewallDelete) csrfGroup.GET("/image", imagesGet) csrfGroup.GET("/image/:image_id", imageGet) csrfGroup.PUT("/image/:image_id", imagePut) csrfGroup.DELETE("/image", imagesDelete) csrfGroup.DELETE("/image/:image_id", imageDelete) csrfGroup.GET("/instance", instancesGet) csrfGroup.PUT("/instance", instancesPut) csrfGroup.GET("/instance/:instance_id", instanceGet) csrfGroup.GET("/instance/:instance_id/vnc", instanceVncGet) csrfGroup.PUT("/instance/:instance_id", instancePut) csrfGroup.POST("/instance", instancePost) csrfGroup.DELETE("/instance", instancesDelete) csrfGroup.DELETE("/instance/:instance_id", instanceDelete) csrfGroup.PUT("/license", licensePut) csrfGroup.GET("/log", logsGet) csrfGroup.GET("/log/:log_id", logGet) csrfGroup.GET("/node", nodesGet) csrfGroup.GET("/node/:node_id", nodeGet) csrfGroup.PUT("/node/:node_id", nodePut) csrfGroup.PUT("/node/:node_id/:operation", nodeOperationPut) csrfGroup.POST("/node/:node_id/init", nodeInitPost) csrfGroup.DELETE("/node/:node_id", nodeDelete) csrfGroup.GET("/organization", organizationsGet) csrfGroup.GET("/organization/:org_id", organizationGet) csrfGroup.PUT("/organization/:org_id", organizationPut) csrfGroup.POST("/organization", organizationPost) csrfGroup.DELETE("/organization/:org_id", organizationDelete) csrfGroup.GET("/plan", plansGet) csrfGroup.GET("/plan/:plan_id", planGet) csrfGroup.PUT("/plan/:plan_id", planPut) csrfGroup.POST("/plan", planPost) csrfGroup.DELETE("/plan", plansDelete) csrfGroup.DELETE("/plan/:plan_id", planDelete) csrfGroup.GET("/policy", policiesGet) csrfGroup.GET("/policy/:policy_id", policyGet) csrfGroup.PUT("/policy/:policy_id", policyPut) csrfGroup.POST("/policy", policyPost) csrfGroup.DELETE("/policy", policiesDelete) csrfGroup.DELETE("/policy/:policy_id", policyDelete) csrfGroup.GET("/pool", poolsGet) csrfGroup.GET("/pool/:pool_id", poolGet) csrfGroup.PUT("/pool/:pool_id", poolPut) csrfGroup.POST("/pool", poolPost) csrfGroup.DELETE("/pool", poolsDelete) csrfGroup.DELETE("/pool/:pool_id", poolDelete) csrfGroup.GET("/relations/:kind/:id", relationsGet) csrfGroup.GET("/secret", secretsGet) csrfGroup.GET("/secret/:secr_id", secretGet) csrfGroup.PUT("/secret/:secr_id", secretPut) csrfGroup.POST("/secret", secretPost) csrfGroup.DELETE("/secret", secretsDelete) csrfGroup.DELETE("/secret/:secr_id", secretDelete) csrfGroup.GET("/session/:user_id", sessionsGet) csrfGroup.DELETE("/session/:session_id", sessionDelete) csrfGroup.GET("/settings", settingsGet) csrfGroup.PUT("/settings", settingsPut) csrfGroup.GET("/pod", podsGet) csrfGroup.GET("/pod/:pod_id", podGet) csrfGroup.PUT("/pod/:pod_id", podPut) csrfGroup.PUT("/pod/:pod_id/drafts", podDraftsPut) csrfGroup.PUT("/pod/:pod_id/deploy", podDeployPut) csrfGroup.POST("/pod", podPost) csrfGroup.DELETE("/pod", podsDelete) csrfGroup.DELETE("/pod/:pod_id", podDelete) csrfGroup.GET("/pod/:pod_id/unit/:unit_id", podUnitGet) csrfGroup.PUT("/pod/:pod_id/unit/:unit_id/deployment", podUnitDeploymentsPut) csrfGroup.POST("/pod/:pod_id/unit/:unit_id/deployment", podUnitDeploymentPost) csrfGroup.PUT("/pod/:pod_id/unit/:unit_id/deployment/:deployment_id", podUnitDeploymentPut) csrfGroup.GET( "/pod/:pod_id/unit/:unit_id/deployment/:deployment_id/log", podUnitDeploymentLogGet, ) csrfGroup.GET("/pod/:pod_id/unit/:unit_id/spec", podUnitSpecsGet) csrfGroup.GET("/pod/:pod_id/unit/:unit_id/spec/:spec_id", podUnitSpecGet) csrfGroup.GET("/shape", shapesGet) csrfGroup.GET("/shape/:shape_id", shapeGet) csrfGroup.PUT("/shape/:shape_id", shapePut) csrfGroup.POST("/shape", shapePost) csrfGroup.DELETE("/shape", shapesDelete) csrfGroup.DELETE("/shape/:shape_id", shapeDelete) csrfGroup.GET("/storage", storagesGet) csrfGroup.GET("/storage/:store_id", storageGet) csrfGroup.PUT("/storage/:store_id", storagePut) csrfGroup.POST("/storage", storagePost) csrfGroup.DELETE("/storage", storagesDelete) csrfGroup.DELETE("/storage/:store_id", storageDelete) csrfGroup.GET("/subscription", subscriptionGet) csrfGroup.GET("/subscription/update", subscriptionUpdateGet) csrfGroup.POST("/subscription", subscriptionPost) csrfGroup.PUT("/theme", themePut) csrfGroup.GET("/user", usersGet) csrfGroup.GET("/user/:user_id", userGet) csrfGroup.PUT("/user/:user_id", userPut) csrfGroup.POST("/user", userPost) csrfGroup.DELETE("/user", usersDelete) csrfGroup.GET("/vpc", vpcsGet) csrfGroup.GET("/vpc/:vpc_id", vpcGet) csrfGroup.PUT("/vpc/:vpc_id", vpcPut) csrfGroup.GET("/vpc/:vpc_id/routes", vpcRoutesGet) csrfGroup.PUT("/vpc/:vpc_id/routes", vpcRoutesPut) csrfGroup.POST("/vpc", vpcPost) csrfGroup.DELETE("/vpc", vpcsDelete) csrfGroup.DELETE("/vpc/:vpc_id", vpcDelete) csrfGroup.GET("/zone", zonesGet) csrfGroup.GET("/zone/:zone_id", zoneGet) csrfGroup.PUT("/zone/:zone_id", zonePut) csrfGroup.POST("/zone", zonePost) csrfGroup.DELETE("/zone", zonesDelete) csrfGroup.DELETE("/zone/:zone_id", zoneDelete) engine.GET("/robots.txt", middlewear.RobotsGet) if constants.Production { sessGroup.GET("/", staticIndexGet) engine.GET("/login", staticLoginGet) engine.GET("/logo.png", staticLogoGet) authGroup.GET("/static/*path", staticGet) } else { fs := gin.Dir(config.StaticTestingRoot, false) fileServer = http.FileServer(fs) sessGroup.GET("/", staticTestingGet) engine.GET("/login", staticTestingGet) engine.GET("/logo.png", staticTestingGet) authGroup.GET("/static/*path", staticTestingGet) } } func init() { module := requires.New("ahandlers") module.After("settings") module.Handler = func() (err error) { if constants.Production { store, err = static.NewStore(config.StaticRoot) if err != nil { return } } return } } ================================================ FILE: ahandlers/image.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/utils" ) type imageData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` } type imagesData struct { Images []*image.Image `json:"images"` Count int64 `json:"count"` } func imagePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &imageData{} imageId, ok := utils.ParseObjectId(c.Param("image_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { utils.AbortWithError(c, 500, err) return } img, err := image.Get(db, imageId) if err != nil { utils.AbortWithError(c, 500, err) return } img.Name = dta.Name img.Comment = dta.Comment img.Organization = dta.Organization fields := set.NewSet( "name", "comment", "organization", ) errData, err := img.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = img.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "image.change") c.JSON(200, img) } func imageDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) imageId, ok := utils.ParseObjectId(c.Param("image_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := data.DeleteImage(db, imageId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "image.change") event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func imagesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { utils.AbortWithError(c, 500, err) return } err = data.DeleteImages(db, dta) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "image.change") event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func imageGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) imageId, ok := utils.ParseObjectId(c.Param("image_id")) if !ok { utils.AbortWithStatus(c, 400) return } img, err := image.Get(db, imageId) if err != nil { utils.AbortWithError(c, 500, err) return } img.Json() c.JSON(200, img) } func imagesGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) dcId, _ := utils.ParseObjectId(c.Query("datacenter")) if !dcId.IsZero() { dc, err := datacenter.Get(db, dcId) if err != nil { return } storages := dc.PublicStorages if storages == nil { storages = []bson.ObjectID{} } if len(storages) == 0 { c.JSON(200, []bson.ObjectID{}) return } query := &bson.M{ "storage": &bson.M{ "$in": storages, }, } if demo.IsDemo() { query = &bson.M{} } images, err := image.GetAllNames(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } for _, img := range images { img.Json() } if !dc.PrivateStorage.IsZero() { query = &bson.M{ "storage": dc.PrivateStorage, } images2, err := image.GetAllNames(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } for _, img := range images2 { img.Json() images = append(images, img) } } c.JSON(200, images) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} imageId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = imageId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["$or"] = []*bson.M{ &bson.M{ "name": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", }, }, &bson.M{ "key": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", }, }, } } typ := strings.TrimSpace(c.Query("type")) if typ != "" { query["type"] = typ } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } images, count, err := image.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, img := range images { img.Json() } dta := &imagesData{ Images: images, Count: count, } c.JSON(200, dta) } } ================================================ FILE: ahandlers/instance.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/iscsi" "github.com/pritunl/pritunl-cloud/iso" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/zone" ) type instanceData struct { Id bson.ObjectID `json:"id"` Organization bson.ObjectID `json:"organization"` Zone bson.ObjectID `json:"zone"` Vpc bson.ObjectID `json:"vpc"` Subnet bson.ObjectID `json:"subnet"` CloudSubnet string `json:"cloud_subnet"` Shape bson.ObjectID `json:"shape"` Node bson.ObjectID `json:"node"` DiskType string `json:"disk_type"` DiskPool bson.ObjectID `json:"disk_pool"` Image bson.ObjectID `json:"image"` ImageBacking bool `json:"image_backing"` Name string `json:"name"` Comment string `json:"comment"` Action string `json:"action"` RootEnabled bool `json:"root_enabled"` Uefi bool `json:"uefi"` SecureBoot bool `json:"secure_boot"` Tpm bool `json:"tpm"` DhcpServer bool `json:"dhcp_server"` CloudType string `json:"cloud_type"` CloudScript string `json:"cloud_script"` DeleteProtection bool `json:"delete_protection"` SkipSourceDestCheck bool `json:"skip_source_dest_check"` InitDiskSize int `json:"init_disk_size"` Memory int `json:"memory"` Processors int `json:"processors"` Roles []string `json:"roles"` Isos []*iso.Iso `json:"isos"` UsbDevices []*usb.Device `json:"usb_devices"` PciDevices []*pci.Device `json:"pci_devices"` DriveDevices []*drive.Device `json:"drive_devices"` IscsiDevices []*iscsi.Device `json:"iscsi_devices"` Mounts []*instance.Mount `json:"mounts"` Vnc bool `json:"vnc"` Spice bool `json:"spice"` Gui bool `json:"gui"` NodePorts []*nodeport.Mapping `json:"node_ports"` NoPublicAddress bool `json:"no_public_address"` NoPublicAddress6 bool `json:"no_public_address6"` NoHostAddress bool `json:"no_host_address"` Count int `json:"count"` } type instanceMultiData struct { Ids []bson.ObjectID `json:"ids"` Action string `json:"action"` } type instancesData struct { Instances []*instance.Instance `json:"instances"` Count int64 `json:"count"` } func instancePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &instanceData{} instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { utils.AbortWithError(c, 500, err) return } inst, err := instance.Get(db, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } inst.PreCommit() inst.Name = dta.Name inst.Comment = dta.Comment inst.Vpc = dta.Vpc inst.Subnet = dta.Subnet inst.CloudSubnet = dta.CloudSubnet if dta.Action != "" { inst.Action = dta.Action } inst.Uefi = dta.Uefi inst.SecureBoot = dta.SecureBoot inst.Tpm = dta.Tpm inst.DhcpServer = dta.DhcpServer inst.CloudType = dta.CloudType inst.CloudScript = dta.CloudScript inst.DeleteProtection = dta.DeleteProtection inst.SkipSourceDestCheck = dta.SkipSourceDestCheck inst.Memory = dta.Memory inst.Processors = dta.Processors inst.Roles = dta.Roles inst.Isos = dta.Isos inst.UsbDevices = dta.UsbDevices inst.PciDevices = dta.PciDevices inst.DriveDevices = dta.DriveDevices inst.IscsiDevices = dta.IscsiDevices inst.Mounts = dta.Mounts inst.RootEnabled = dta.RootEnabled inst.Vnc = dta.Vnc inst.Spice = dta.Spice inst.Gui = dta.Gui inst.NodePorts = dta.NodePorts inst.NoPublicAddress = dta.NoPublicAddress inst.NoPublicAddress6 = dta.NoPublicAddress6 inst.NoHostAddress = dta.NoHostAddress fields := set.NewSet( "unix_id", "name", "comment", "datacenter", "vpc", "subnet", "dhcp_ip", "dhcp_ip6", "cloud_subnet", "state", "restart", "restart_block_ip", "uefi", "secure_boot", "tpm", "tpm_secret", "dhcp_server", "cloud_type", "cloud_script", "delete_protection", "skip_source_dest_check", "memory", "processors", "roles", "isos", "usb_devices", "pci_devices", "drive_devices", "iscsi_devices", "mounts", "root_enabled", "root_passwd", "vnc", "vnc_display", "vnc_password", "spice", "spice_port", "spice_password", "gui", "node_ports", "no_public_address", "no_public_address6", "no_host_address", ) errData, err := inst.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } dskChange, err := inst.PostCommit(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = inst.CommitFields(db, fields) if err != nil { _ = inst.Cleanup(db) utils.AbortWithError(c, 500, err) return } err = inst.Cleanup(db) if err != nil { return } event.PublishDispatch(db, "instance.change") if dskChange { event.PublishDispatch(db, "disk.change") } c.JSON(200, inst) } func instancePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &instanceData{ Name: "new-instance", } err := c.Bind(dta) if err != nil { utils.AbortWithError(c, 500, err) return } zne, err := zone.Get(db, dta.Zone) if err != nil { utils.AbortWithError(c, 500, err) return } if !dta.Shape.IsZero() { dta.Node = bson.NilObjectID dta.DiskType = "" dta.DiskPool = bson.NilObjectID } else { nde, err := node.Get(db, dta.Node) if err != nil { utils.AbortWithError(c, 500, err) return } if nde.Zone != zne.Id { utils.AbortWithStatus(c, 405) return } if dta.DiskType == disk.Lvm { poolMatch := false for _, plId := range nde.Pools { if plId == dta.DiskPool { poolMatch = true } } if !poolMatch { errData := &errortypes.ErrorData{ Error: "pool_not_found", Message: "Pool not found", } c.JSON(400, errData) return } } } if !dta.Image.IsZero() { img, err := image.GetOrgPublic(db, dta.Organization, dta.Image) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "image_not_found", Message: "Image not found", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } stre, err := storage.Get(db, img.Storage) if err != nil { utils.AbortWithError(c, 500, err) return } available, err := data.ImageAvailable(stre, img) if err != nil { utils.AbortWithError(c, 500, err) return } if !available { if stre.IsOracle() { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from archive", } c.JSON(400, errData) } else { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from glacier", } c.JSON(400, errData) } return } } insts := []*instance.Instance{} if dta.Count == 0 { dta.Count = 1 } for i := 0; i < dta.Count; i++ { name := "" if strings.Contains(dta.Name, "%") { name = fmt.Sprintf(dta.Name, i+1) } else { name = dta.Name } inst := &instance.Instance{ Action: dta.Action, Organization: dta.Organization, Zone: dta.Zone, Vpc: dta.Vpc, Subnet: dta.Subnet, CloudSubnet: dta.CloudSubnet, Shape: dta.Shape, Node: dta.Node, DiskType: dta.DiskType, DiskPool: dta.DiskPool, Image: dta.Image, ImageBacking: dta.ImageBacking, Uefi: dta.Uefi, SecureBoot: dta.SecureBoot, Tpm: dta.Tpm, DhcpServer: dta.DhcpServer, CloudType: dta.CloudType, CloudScript: dta.CloudScript, DeleteProtection: dta.DeleteProtection, SkipSourceDestCheck: dta.SkipSourceDestCheck, Name: name, Comment: dta.Comment, InitDiskSize: dta.InitDiskSize, Memory: dta.Memory, Processors: dta.Processors, Roles: dta.Roles, Isos: dta.Isos, UsbDevices: dta.UsbDevices, PciDevices: dta.PciDevices, DriveDevices: dta.DriveDevices, IscsiDevices: dta.IscsiDevices, Mounts: dta.Mounts, RootEnabled: dta.RootEnabled, Vnc: dta.Vnc, Spice: dta.Spice, Gui: dta.Gui, NodePorts: dta.NodePorts, NoPublicAddress: dta.NoPublicAddress, NoPublicAddress6: dta.NoPublicAddress6, NoHostAddress: dta.NoHostAddress, } errData, err := inst.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = inst.SyncNodePorts(db) if err != nil { return } err = inst.Insert(db) if err != nil { _ = inst.Cleanup(db) utils.AbortWithError(c, 500, err) return } err = inst.Cleanup(db) if err != nil { return } insts = append(insts, inst) } event.PublishDispatch(db, "instance.change") if len(insts) == 1 { c.JSON(200, insts[0]) } else { c.JSON(200, insts) } } func instancesPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &instanceMultiData{} err := c.Bind(dta) if err != nil { utils.AbortWithError(c, 500, err) return } if !instance.ValidActions.Contains(dta.Action) { errData := &errortypes.ErrorData{ Error: "invalid_action", Message: "Invalid instance action", } c.JSON(400, errData) return } doc := bson.M{ "action": dta.Action, } if dta.Action != instance.Start { doc["restart"] = false doc["restart_block_ip"] = false } err = instance.UpdateMulti(db, dta.Ids, &doc) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "instance.change") c.JSON(200, nil) } func instanceDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } inst, err := instance.Get(db, instanceId) if err != nil { return } if inst.DeleteProtection { errData := &errortypes.ErrorData{ Error: "delete_protection", Message: "Cannot delete instance with delete protection", } c.JSON(400, errData) return } err = instance.Delete(db, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "instance.change") c.JSON(200, nil) } func instancesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { utils.AbortWithError(c, 500, err) return } force := c.Query("force") if force == "true" { for _, instId := range dta { err = instance.Remove(db, instId) if err != nil { utils.AbortWithError(c, 500, err) return } } } else { err = instance.DeleteMulti(db, dta) if err != nil { utils.AbortWithError(c, 500, err) return } } event.PublishDispatch(db, "instance.change") c.JSON(200, nil) } func instanceGet(c *gin.Context) { if demo.IsDemo() { inst := demo.Instances[0] inst.Guest.Timestamp = time.Now() inst.Guest.Heartbeat = time.Now() c.JSON(200, inst) return } db := c.MustGet("db").(*database.Database) instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } inst, err := instance.Get(db, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { inst.State = vm.Running inst.Action = instance.Start inst.Status = "Running" inst.PublicIps = []string{ demo.RandIp(inst.Id), } inst.PublicIps6 = []string{ demo.RandIp6(inst.Id), } inst.PrivateIps = []string{ demo.RandPrivateIp(inst.Id), } inst.PrivateIps6 = []string{ demo.RandPrivateIp6(inst.Id), } inst.NetworkNamespace = vm.GetNamespace(inst.Id, 0) } c.JSON(200, inst) } func instancesGet(c *gin.Context) { if demo.IsDemo() { for _, inst := range demo.Instances { inst.Guest.Timestamp = time.Now() inst.Guest.Heartbeat = time.Now() } data := &instancesData{ Instances: demo.Instances, Count: int64(len(demo.Instances)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) ndeId, _ := utils.ParseObjectId(c.Query("node_names")) plId, _ := utils.ParseObjectId(c.Query("pool_names")) if !ndeId.IsZero() { query := &bson.M{ "node": ndeId, } insts, err := instance.GetAllName(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, insts) } else if !plId.IsZero() { nodes, err := node.GetAllPool(db, plId) if err != nil { return } ndeIds := []bson.ObjectID{} for _, nde := range nodes { ndeIds = append(ndeIds, nde.Id) } query := &bson.M{ "node": &bson.M{ "$in": ndeIds, }, } insts, err := instance.GetAllName(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, insts) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} instId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = instId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } networkNamespace := strings.TrimSpace(c.Query("network_namespace")) if networkNamespace != "" { query["network_namespace"] = networkNamespace } nodeId, ok := utils.ParseObjectId(c.Query("node")) if ok { query["node"] = nodeId } zoneId, ok := utils.ParseObjectId(c.Query("zone")) if ok { query["zone"] = zoneId } vpcId, ok := utils.ParseObjectId(c.Query("vpc")) if ok { query["vpc"] = vpcId } subnetId, ok := utils.ParseObjectId(c.Query("subnet")) if ok { query["subnet"] = subnetId } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } instances, count, err := instance.GetAllPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, inst := range instances { inst.Json(false) } dta := &instancesData{ Instances: instances, Count: count, } c.JSON(200, dta) } } func instanceVncGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } inst, err := instance.Get(db, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } err = inst.VncConnect(db, c.Writer, c.Request) if err != nil { if _, ok := err.(*instance.VncDialError); ok { utils.AbortWithStatus(c, 504) } else { utils.AbortWithError(c, 500, err) } return } } ================================================ FILE: ahandlers/license.go ================================================ package ahandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/utils" ) type licenseData struct { Oracle bool `json:"oracle"` } func licensePut(c *gin.Context) { if demo.IsDemo() { c.JSON(200, nil) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &licenseData{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } usr.OracleLicense = data.Oracle err = usr.CommitFields(db, set.NewSet("oracle_licese")) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, data) return } ================================================ FILE: ahandlers/log.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/log" "github.com/pritunl/pritunl-cloud/utils" ) type logsData struct { Logs []*log.Entry `json:"logs"` Count int64 `json:"count"` } func logGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.Logs[1]) return } db := c.MustGet("db").(*database.Database) logId, ok := utils.ParseObjectId(c.Param("log_id")) if !ok { utils.AbortWithStatus(c, 400) return } usr, err := log.Get(db, logId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, usr) } func logsGet(c *gin.Context) { if demo.IsDemo() { data := &logsData{ Logs: demo.Logs, Count: int64(len(demo.Logs)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) pageStr := c.Query("page") page, _ := strconv.ParseInt(pageStr, 10, 0) pageCountStr := c.Query("page_count") pageCount, _ := strconv.ParseInt(pageCountStr, 10, 0) query := bson.M{} message := strings.TrimSpace(c.Query("message")) if message != "" { query["message"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(message)), "$options": "i", } } level := strings.TrimSpace(c.Query("level")) if level != "" { query["level"] = level } logs, count, err := log.GetAll(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &logsData{ Logs: logs, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/node.go ================================================ package ahandlers import ( "fmt" "net" "regexp" "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/subscription" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" ) type nodeData struct { Id bson.ObjectID `json:"id"` Zone bson.ObjectID `json:"zone"` Name string `json:"name"` Comment string `json:"comment"` Types []string `json:"types"` Port int `json:"port"` Http2 bool `json:"http2"` NoRedirectServer bool `json:"no_redirect_server"` Protocol string `json:"protocol"` Hypervisor string `json:"hypervisor"` Vga string `json:"vga"` VgaRender string `json:"vga_render"` Gui bool `json:"gui"` GuiUser string `json:"gui_user"` GuiMode string `json:"gui_mode"` Certificates []bson.ObjectID `json:"certificates"` AdminDomain string `json:"admin_domain"` UserDomain string `json:"user_domain"` WebauthnDomain string `json:"webauthn_domain"` ExternalInterfaces []string `json:"external_interfaces"` ExternalInterfaces6 []string `json:"external_interfaces6"` InternalInterfaces []string `json:"internal_interfaces"` CloudSubnets []string `json:"cloud_subnets"` NetworkMode string `json:"network_mode"` NetworkMode6 string `json:"network_mode6"` Blocks []*node.BlockAttachment `json:"blocks"` Blocks6 []*node.BlockAttachment `json:"blocks6"` Shares []*node.Share `json:"shares"` InstanceDrives []*drive.Device `json:"instance_drives"` NoHostNetwork bool `json:"no_host_network"` NoNodePortNetwork bool `json:"no_node_port_network"` HostNat bool `json:"host_nat"` DefaultNoPublicAddress bool `json:"default_no_public_address"` DefaultNoPublicAddress6 bool `json:"default_no_public_address6"` JumboFrames bool `json:"jumbo_frames"` JumboFramesInternal bool `json:"jumbo_frames_internal"` Iscsi bool `json:"iscsi"` UsbPassthrough bool `json:"usb_passthrough"` PciPassthrough bool `json:"pci_passthrough"` Hugepages bool `json:"hugepages"` HugepagesSize int `json:"hugepages_size"` ForwardedForHeader string `json:"forwarded_for_header"` ForwardedProtoHeader string `json:"forwarded_proto_header"` Firewall bool `json:"firewall"` Roles []string `json:"roles"` OracleUser string `json:"oracle_user"` OracleTenancy string `json:"oracle_tenancy"` } type nodesData struct { Nodes []*node.Node `json:"nodes"` Count int64 `json:"count"` } type nodeInitData struct { Provider string `json:"provider"` Zone bson.ObjectID `json:"zone"` Firewall bool `json:"firewall"` InternalInterface string `json:"internal_interface"` ExternalInterface string `json:"external_interface"` BlockGateway string `json:"block_gateway"` BlockNetmask string `json:"block_netmask"` BlockSubnets []string `json:"block_subnets"` } func nodePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &nodeData{} nodeId, ok := utils.ParseObjectId(c.Param("node_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } nde, err := node.Get(db, nodeId) if err != nil { utils.AbortWithError(c, 500, err) return } nodeTypes := set.NewSet() for _, typ := range nde.Types { nodeTypes.Add(typ) } for _, typ := range data.Types { if typ == node.User && !nodeTypes.Contains(typ) { if !subscription.Sub.Active { errData := &errortypes.ErrorData{ Error: "subscription_required", Message: "Subscription required for multi-tenant", } c.JSON(400, errData) return } } } nde.Name = data.Name nde.Comment = data.Comment nde.Types = data.Types nde.Port = data.Port nde.Http2 = data.Http2 nde.NoRedirectServer = data.NoRedirectServer nde.Protocol = data.Protocol nde.Hypervisor = data.Hypervisor nde.Vga = data.Vga nde.VgaRender = data.VgaRender nde.Gui = data.Gui nde.GuiUser = data.GuiUser nde.GuiMode = data.GuiMode nde.Certificates = data.Certificates nde.AdminDomain = data.AdminDomain nde.UserDomain = data.UserDomain nde.WebauthnDomain = data.WebauthnDomain nde.ExternalInterfaces = data.ExternalInterfaces nde.ExternalInterfaces6 = data.ExternalInterfaces6 nde.InternalInterfaces = data.InternalInterfaces nde.CloudSubnets = data.CloudSubnets nde.NetworkMode = data.NetworkMode nde.NetworkMode6 = data.NetworkMode6 nde.Blocks = data.Blocks nde.Blocks6 = data.Blocks6 nde.Shares = data.Shares nde.InstanceDrives = data.InstanceDrives nde.NoHostNetwork = data.NoHostNetwork nde.NoNodePortNetwork = data.NoNodePortNetwork nde.HostNat = data.HostNat nde.DefaultNoPublicAddress = data.DefaultNoPublicAddress nde.DefaultNoPublicAddress6 = data.DefaultNoPublicAddress6 nde.JumboFrames = data.JumboFrames nde.JumboFramesInternal = data.JumboFramesInternal nde.Iscsi = data.Iscsi nde.UsbPassthrough = data.UsbPassthrough nde.PciPassthrough = data.PciPassthrough nde.Hugepages = data.Hugepages nde.HugepagesSize = data.HugepagesSize nde.ForwardedForHeader = data.ForwardedForHeader nde.ForwardedProtoHeader = data.ForwardedProtoHeader nde.Firewall = data.Firewall nde.Roles = data.Roles nde.OracleUser = data.OracleUser nde.OracleTenancy = data.OracleTenancy fields := set.NewSet( "name", "comment", "zone", "types", "port", "http2", "no_redirect_server", "protocol", "hypervisor", "vga", "vga_render", "gui", "gui_user", "gui_mode", "certificates", "admin_domain", "user_domain", "webauthn_domain", "external_interfaces", "external_interfaces6", "internal_interfaces", "cloud_subnets", "network_mode", "network_mode6", "blocks", "blocks6", "shares", "instance_drives", "no_host_network", "no_node_port_network", "host_nat", "default_no_public_address", "default_no_public_address6", "jumbo_frames", "jumbo_frames_internal", "iscsi", "usb_passthrough", "pci_passthrough", "hugepages", "hugepages_size", "forwarded_for_header", "forwarded_proto_header", "firewall", "roles", "oracle_user", "oracle_tenancy", ) if !data.Zone.IsZero() && data.Zone != nde.Zone { if !nde.Zone.IsZero() { errData := &errortypes.ErrorData{ Error: "zone_modified", Message: "Cannot modify zone once set", } c.JSON(400, errData) return } nde.Zone = data.Zone zne, e := zone.Get(db, nde.Zone) if e != nil { utils.AbortWithError(c, 500, e) return } fields.Add("datacenter") nde.Datacenter = zne.Datacenter } errData, err := nde.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = nde.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "node.change") c.JSON(200, nde) } func nodeOperationPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) nodeId, ok := utils.ParseObjectId(c.Param("node_id")) if !ok { utils.AbortWithStatus(c, 400) return } operation := c.Param("operation") if operation != node.Restart { utils.AbortWithStatus(c, 400) return } nde, err := node.Get(db, nodeId) if err != nil { utils.AbortWithError(c, 500, err) return } nde.Operation = node.Restart errData, err := nde.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = nde.CommitFields(db, set.NewSet("operation")) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "node.change") c.JSON(200, nde) } func nodeInitPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &nodeInitData{} err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } nodeId, ok := utils.ParseObjectId(c.Param("node_id")) if !ok { utils.AbortWithStatus(c, 400) return } nde, err := node.Get(db, nodeId) if err != nil { utils.AbortWithError(c, 500, err) return } fields := set.NewSet( "host_nat", "zone", "network_mode", "network_mode6", "internal_interfaces", "external_interfaces", ) nde.Zone = data.Zone nde.HostNat = true if data.Provider == "phoenixnap" { fields.Add("default_no_public_address") nde.DefaultNoPublicAddress = true nde.NetworkMode = node.Static nde.NetworkMode6 = node.Disabled nde.InternalInterfaces = []string{ data.InternalInterface, } } else if data.Provider == "vultr" { fields.Add("default_no_public_address") nde.DefaultNoPublicAddress = true nde.NetworkMode = node.Disabled nde.NetworkMode6 = node.Dhcp nde.InternalInterfaces = []string{ settings.Hypervisor.HostNetworkName, } nde.ExternalInterfaces = []string{ data.ExternalInterface, } } else { nde.NetworkMode = node.Disabled nde.NetworkMode6 = node.Dhcp nde.InternalInterfaces = []string{ settings.Hypervisor.HostNetworkName, } nde.ExternalInterfaces = []string{ data.ExternalInterface, } } dc, err := datacenter.Get(db, nde.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } dc.NetworkMode = datacenter.Default err = dc.CommitFields(db, set.NewSet("network_mode")) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "zone.change") if data.Provider == "phoenixnap" { publicBlck := &block.Block{ Name: nde.Name + "-public", Type: block.IPv4, Subnets: data.BlockSubnets, } if data.BlockNetmask == "" { _, gateway, err := net.ParseCIDR(data.BlockGateway) if err != nil { errData := &errortypes.ErrorData{ Error: "invalid_block_gateway", Message: "Invalid public gateway", } c.JSON(400, errData) return } publicBlck.Netmask = fmt.Sprintf( "%d.%d.%d.%d", gateway.Mask[0], gateway.Mask[1], gateway.Mask[2], gateway.Mask[3], ) publicBlck.Gateway = strings.Split(data.BlockGateway, "/")[0] } else { publicBlck.Netmask = data.BlockNetmask publicBlck.Gateway = data.BlockGateway } errData, err := publicBlck.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = publicBlck.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } nde.Blocks = []*node.BlockAttachment{ &node.BlockAttachment{ Interface: data.ExternalInterface, Block: publicBlck.Id, }, } fields.Add("blocks") } errData, err := nde.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } if data.Firewall { nde.Firewall = true fields.Add("firewall") if nde.Roles == nil { nde.Roles = []string{} } hasRole := false for _, role := range nde.Roles { if role == "firewall" { hasRole = true } } if !hasRole { nde.Roles = append(nde.Roles, "firewall") fields.Add("roles") } fires, err := firewall.GetAll(db, &bson.M{ "organization": firewall.Global, "roles": "firewall", }) if err != nil { utils.AbortWithError(c, 500, err) return } if len(fires) == 0 { fire := &firewall.Firewall{ Name: "node-firewall", Comment: "", Roles: []string{ "firewall", }, Ingress: []*firewall.Rule{ &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Icmp, }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "22", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "80", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "443", }, }, } errData, err = fire.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = fire.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } } } err = nde.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "node.change") event.PublishDispatch(db, "block.change") if data.Firewall { event.PublishDispatch(db, "firewall.change") } c.JSON(200, nde) } func nodeDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) nodeId, ok := utils.ParseObjectId(c.Param("node_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "node", nodeId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = node.Remove(db, nodeId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "node.change") c.JSON(200, nil) } func nodeGet(c *gin.Context) { if demo.IsDemo() { nde := demo.Nodes[0] nde.Timestamp = time.Now() c.JSON(200, nde) return } db := c.MustGet("db").(*database.Database) nodeId, ok := utils.ParseObjectId(c.Param("node_id")) if !ok { utils.AbortWithStatus(c, 400) return } nde, err := node.Get(db, nodeId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, nde) } func nodesGet(c *gin.Context) { if demo.IsDemo() { for _, nde := range demo.Nodes { nde.Timestamp = time.Now() } data := &nodesData{ Nodes: demo.Nodes, Count: int64(len(demo.Nodes)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { zone, _ := utils.ParseObjectId(c.Query("zone")) query := &bson.M{ "zone": zone, } nodes, err := node.GetAllHypervisors(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, nodes) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} nodeId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = nodeId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } zone, _ := utils.ParseObjectId(c.Query("zone")) if !zone.IsZero() { query["zone"] = zone } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } types := []string{} notTypes := []string{} adminType := c.Query(node.Admin) switch adminType { case "true": types = append(types, node.Admin) break case "false": notTypes = append(notTypes, node.Admin) break } userType := c.Query(node.User) switch userType { case "true": types = append(types, node.User) break case "false": notTypes = append(notTypes, node.User) break } hypervisorType := c.Query(node.Hypervisor) switch hypervisorType { case "true": types = append(types, node.Hypervisor) break case "false": notTypes = append(notTypes, node.Hypervisor) break } typesQuery := bson.M{} if len(types) > 0 { typesQuery["$all"] = types } if len(notTypes) > 0 { typesQuery["$nin"] = notTypes } if len(types) > 0 || len(notTypes) > 0 { query["types"] = &typesQuery } nodes, count, err := node.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &nodesData{ Nodes: nodes, Count: count, } c.JSON(200, data) } } ================================================ FILE: ahandlers/organization.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/subscription" "github.com/pritunl/pritunl-cloud/utils" ) type organizationData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Roles []string `json:"roles"` } type organizationsData struct { Organizations []*organization.Organization `json:"organizations"` Count int64 `json:"count"` } func organizationPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &organizationData{} orgId, ok := utils.ParseObjectId(c.Param("org_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } org, err := organization.Get(db, orgId) if err != nil { utils.AbortWithError(c, 500, err) return } org.Name = data.Name org.Comment = data.Comment org.Roles = data.Roles fields := set.NewSet( "name", "comment", "roles", ) errData, err := org.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = org.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "organization.change") c.JSON(200, org) } func organizationPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &organizationData{ Name: "new-organization", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } if !subscription.Sub.Active { count, e := organization.Count(db) if e != nil { utils.AbortWithError(c, 500, e) return } if count > 0 { errData := &errortypes.ErrorData{ Error: "subscription_required", Message: "Subscription required for multiple organizations", } c.JSON(400, errData) return } } org := &organization.Organization{ Name: data.Name, Comment: data.Comment, Roles: data.Roles, } errData, err := org.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = org.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "organization.change") c.JSON(200, org) } func organizationDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) orgId, ok := utils.ParseObjectId(c.Param("org_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "organization", orgId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = organization.Remove(db, orgId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "organization.change") c.JSON(200, nil) } func organizationGet(c *gin.Context) { if demo.IsDemo() { org := demo.Organizations[0] c.JSON(200, org) return } db := c.MustGet("db").(*database.Database) orgId, ok := utils.ParseObjectId(c.Param("org_id")) if !ok { utils.AbortWithStatus(c, 400) return } org, err := organization.Get(db, orgId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, org) } func organizationsGet(c *gin.Context) { if demo.IsDemo() { data := &organizationsData{ Organizations: demo.Organizations, Count: int64(len(demo.Organizations)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} organizationId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = organizationId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } secrs, count, err := organization.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &organizationsData{ Organizations: secrs, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/plan.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/utils" ) type planData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Statements []*plan.Statement `json:"statements"` } type plansData struct { Plans []*plan.Plan `json:"plans"` Count int64 `json:"count"` } func planPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &planData{} planId, ok := utils.ParseObjectId(c.Param("plan_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } pln, err := plan.Get(db, planId) if err != nil { utils.AbortWithError(c, 500, err) return } pln.Name = data.Name pln.Comment = data.Comment pln.Organization = data.Organization err = pln.UpdateStatements(data.Statements) if err != nil { utils.AbortWithError(c, 500, err) return } fields := set.NewSet( "name", "comment", "organization", "statements", ) errData, err := pln.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pln.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, pln) } func planPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &planData{ Name: "new-plan", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } pln := &plan.Plan{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, } err = pln.UpdateStatements(data.Statements) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := pln.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pln.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, pln) } func planDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) planId, ok := utils.ParseObjectId(c.Param("plan_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := plan.Remove(db, planId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, nil) } func plansDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = plan.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, nil) } func planGet(c *gin.Context) { if demo.IsDemo() { pln := demo.Plans[0] c.JSON(200, pln) return } db := c.MustGet("db").(*database.Database) planId, ok := utils.ParseObjectId(c.Param("plan_id")) if !ok { utils.AbortWithStatus(c, 400) return } pln, err := plan.Get(db, planId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, pln) } func plansGet(c *gin.Context) { if demo.IsDemo() { data := &plansData{ Plans: demo.Plans, Count: int64(len(demo.Plans)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { query := &bson.M{} plns, err := plan.GetAllName(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, plns) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} planId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = planId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } plans, count, err := plan.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &plansData{ Plans: plans, Count: count, } c.JSON(200, data) } } ================================================ FILE: ahandlers/pod.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/scheduler" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" ) type podData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` DeleteProtection bool `json:"delete_protection"` Units []*unit.UnitInput `json:"units"` Drafts []*pod.UnitDraft `json:"drafts"` Count int `json:"count"` } type podsData struct { Pods []*aggregate.PodAggregate `json:"pods"` Count int64 `json:"count"` } type podsDeployData struct { Count int `json:"count"` Spec bson.ObjectID `json:"spec"` } type deploymentData struct { Id bson.ObjectID `json:"id"` Tags []string `json:"tags"` } type specsData struct { Specs []*spec.Named `json:"specs"` Count int64 `json:"count"` } func podPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &podData{} podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } pd, err := pod.Get(db, podId) if err != nil { utils.AbortWithError(c, 500, err) return } pd.Name = data.Name pd.Comment = data.Comment pd.Organization = data.Organization pd.DeleteProtection = data.DeleteProtection fields := set.NewSet( "name", "comment", "organization", "delete_protection", ) errData, err := pd.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } errData, err = pd.CommitFieldsUnits(db, data.Units, fields) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = pod.UpdateDrafts(db, podId, usr.Id, []*pod.UnitDraft{}) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, pd) } func podDraftsPut(c *gin.Context) { if demo.BlockedSilent(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &podData{} podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = pod.UpdateDrafts(db, podId, usr.Id, data.Drafts) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, nil) } func podDeployPut(c *gin.Context) { if demo.BlockedSilent(c) { return } db := c.MustGet("db").(*database.Database) data := &podData{} podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } units, err := unit.GetAll(db, &bson.M{ "pod": podId, }) if err != nil { return } unitsDataMap := map[bson.ObjectID]*unit.UnitInput{} for _, unitData := range data.Units { unitsDataMap[unitData.Id] = unitData } for _, unt := range units { unitData := unitsDataMap[unt.Id] if unitData == nil || unitData.DeploySpec.IsZero() { continue } deploySpec, e := spec.Get(db, unitData.DeploySpec) if e != nil || deploySpec.Unit != unt.Id { errData := &errortypes.ErrorData{ Error: "unit_deploy_spec_invalid", Message: "Invalid unit deployment commit", } c.JSON(400, errData) return } unt.DeploySpec = unitData.DeploySpec err = unt.CommitFields(db, set.NewSet("deploy_spec")) if err != nil { utils.AbortWithError(c, 500, err) return } } event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func podPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &podData{ Name: "new-pod", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } pd := &pod.Pod{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, DeleteProtection: data.DeleteProtection, } errData, err := pd.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pd.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err = pd.InitUnits(db, data.Units) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, pd) } func podDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "pod", podId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pod.Remove(db, podId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "pod", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pod.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podGet(c *gin.Context) { if demo.IsDemo() { pd := demo.Pods[0] c.JSON(200, pd) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } pd, err := aggregate.GetPod(db, usr.Id, &bson.M{ "_id": podId, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, pd) } func podsGet(c *gin.Context) { if demo.IsDemo() { data := &podsData{ Pods: demo.Pods, Count: int64(len(demo.Pods)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} podId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = podId } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } pods, count, err := aggregate.GetPodsPaged(db, usr.Id, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &podsData{ Pods: pods, Count: count, } c.JSON(200, data) } type PodUnit struct { Id bson.ObjectID `json:"id"` Pod bson.ObjectID `json:"pod"` Kind string `json:"kind"` Deployments []*aggregate.Deployment `json:"deployments"` } func podUnitGet(c *gin.Context) { if demo.IsDemo() { unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } var unit *unit.Unit for _, unt := range demo.Units { if unt.Id == unitId { unit = unt break } } deplys := []*aggregate.Deployment{} for _, deply := range demo.Deployments { if deply.Unit == unit.Id { deplys = append(deplys, deply) } } data := &PodUnit{ Id: unit.Id, Pod: demo.Pods[0].Id, Kind: unit.Kind, Deployments: deplys, } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } unt, err := unit.Get(db, unitId) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } deploys, err := aggregate.GetDeployments(db, unt) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } pdUnit := &PodUnit{ Id: unt.Id, Pod: unt.Pod, Kind: unt.Kind, Deployments: deploys, } c.JSON(200, pdUnit) } func podUnitDeploymentsPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } unt, err := unit.Get(db, unitId) if err != nil { utils.AbortWithError(c, 500, err) return } action := c.Query("action") switch action { case deployment.Archive: err = deployment.ArchiveMulti(db, unt.Id, data) if err != nil { utils.AbortWithError(c, 500, err) return } break case deployment.Restore: err = deployment.RestoreMulti(db, unt.Id, data) if err != nil { utils.AbortWithError(c, 500, err) return } break case deployment.Destroy: err = deployment.RemoveMulti(db, unt.Id, data) if err != nil { utils.AbortWithError(c, 500, err) return } break case deployment.Migrate: commitId, ok := utils.ParseObjectId(c.Query("commit")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := unt.MigrateDeployements(db, commitId, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } break } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podUnitDeploymentPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &podsDeployData{} unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } unt, err := unit.Get(db, unitId) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := scheduler.ManualSchedule(db, unt, data.Spec, data.Count) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podUnitDeploymentPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &deploymentData{} unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } deplyId, ok := utils.ParseObjectId(c.Param("deployment_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } deply, err := deployment.GetUnit(db, unitId, deplyId) if err != nil { utils.AbortWithError(c, 500, err) return } deply.Tags = data.Tags fields := set.NewSet( "tags", ) errData, err := deply.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = deply.CommitFields(db, fields) if err != nil { return } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func podUnitDeploymentLogGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.DeploymentLogs) return } db := c.MustGet("db").(*database.Database) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } deplyId, ok := utils.ParseObjectId(c.Param("deployment_id")) if !ok { utils.AbortWithStatus(c, 400) return } deply, err := deployment.GetUnit(db, unitId, deplyId) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } kind := int32(0) resource := c.Query("resource") if resource == "agent" { kind = journal.DeploymentAgent } for _, jrnl := range deply.Journals { if jrnl.Key == resource { kind = jrnl.Index } } if kind == 0 { utils.AbortWithStatus(c, 404) return } data, err := journal.GetOutput(c, db, deply.Id, kind) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, data) } func podUnitSpecsGet(c *gin.Context) { if demo.IsDemo() { unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } specs := []*spec.Named{} for _, spc := range demo.SpecsNamed { if spc.Unit == unitId { specs = append(specs, spc) } } data := &specsData{ Specs: specs, Count: int64(len(specs)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } specs, count, err := spec.GetAllPaged(db, &bson.M{ "unit": unitId, }, page, pageCount) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } data := &specsData{ Specs: specs, Count: count, } c.JSON(200, data) } func podUnitSpecGet(c *gin.Context) { if demo.IsDemo() { specId, ok := utils.ParseObjectId(c.Param("spec_id")) if !ok { utils.AbortWithStatus(c, 400) return } for _, spc := range demo.Specs { if spc.Id == specId { c.JSON(200, spc) return } } c.AbortWithStatus(404) return } db := c.MustGet("db").(*database.Database) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } specId, ok := utils.ParseObjectId(c.Param("spec_id")) if !ok { utils.AbortWithStatus(c, 400) return } spec, err := spec.GetOne(db, &bson.M{ "_id": specId, "unit": unitId, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, spec) } ================================================ FILE: ahandlers/policy.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/policy" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type policyData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Disabled bool `json:"disabled"` Authorities []bson.ObjectID `json:"authorities"` Roles []string `json:"roles"` Rules map[string]*policy.Rule `json:"rules"` AdminSecondary bson.ObjectID `json:"admin_secondary"` UserSecondary bson.ObjectID `json:"user_secondary"` ProxySecondary bson.ObjectID `json:"proxy_secondary"` AuthoritySecondary bson.ObjectID `json:"authority_secondary"` AdminDeviceSecondary bool `json:"admin_device_secondary"` UserDeviceSecondary bool `json:"user_device_secondary"` } type policiesData struct { Policies []*policy.Policy `json:"policies"` Count int64 `json:"count"` } func policyPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &policyData{} polcyId, ok := utils.ParseObjectId(c.Param("policy_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } polcy, err := policy.Get(db, polcyId) if err != nil { utils.AbortWithError(c, 500, err) return } polcy.Name = data.Name polcy.Comment = data.Comment polcy.Disabled = data.Disabled polcy.Roles = data.Roles polcy.Rules = data.Rules polcy.AdminSecondary = data.AdminSecondary polcy.UserSecondary = data.UserSecondary polcy.AdminDeviceSecondary = data.AdminDeviceSecondary polcy.UserDeviceSecondary = data.UserDeviceSecondary fields := set.NewSet( "name", "comment", "disabled", "roles", "rules", "admin_secondary", "user_secondary", "admin_device_secondary", "user_device_secondary", ) errData, err := polcy.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = polcy.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "policy.change") c.JSON(200, polcy) } func policyPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &policyData{ Name: "new-policy", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } polcy := &policy.Policy{ Name: data.Name, Comment: data.Comment, Disabled: data.Disabled, Roles: data.Roles, Rules: data.Rules, AdminSecondary: data.AdminSecondary, UserSecondary: data.UserSecondary, AdminDeviceSecondary: data.AdminDeviceSecondary, UserDeviceSecondary: data.UserDeviceSecondary, } errData, err := polcy.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = polcy.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "policy.change") c.JSON(200, polcy) } func policyDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) polcyId, ok := utils.ParseObjectId(c.Param("policy_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "policy", polcyId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = policy.Remove(db, polcyId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "policy.change") c.JSON(200, nil) } func policiesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "policy", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = policy.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "policy.change") c.JSON(200, nil) } func policyGet(c *gin.Context) { if demo.IsDemo() { polcy := demo.Policies[0] c.JSON(200, polcy) return } db := c.MustGet("db").(*database.Database) polcyId, ok := utils.ParseObjectId(c.Param("policy_id")) if !ok { utils.AbortWithStatus(c, 400) return } polcy, err := policy.Get(db, polcyId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, polcy) } func policiesGet(c *gin.Context) { if demo.IsDemo() { data := &policiesData{ Policies: demo.Policies, Count: int64(len(demo.Policies)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} policyId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = policyId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } polcies, count, err := policy.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &policiesData{ Policies: polcies, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/pool.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/zone" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" ) type poolData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` DeleteProtection bool `json:"delete_protection"` Zone bson.ObjectID `json:"zone"` Type string `json:"type"` VgName string `json:"vg_name"` } type poolsData struct { Pools []*pool.Pool `json:"pools"` Count int64 `json:"count"` } func poolPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &poolData{} poolId, ok := utils.ParseObjectId(c.Param("pool_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } pl, err := pool.Get(db, poolId) if err != nil { utils.AbortWithError(c, 500, err) return } pl.Name = data.Name pl.Comment = data.Comment pl.DeleteProtection = data.DeleteProtection pl.Type = data.Type fields := set.NewSet( "name", "comment", "delete_protection", "type", ) errData, err := pl.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pl.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pool.change") c.JSON(200, pl) } func poolPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &poolData{ Name: "new-pool", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } zne, err := zone.Get(db, data.Zone) if err != nil { return } pl := &pool.Pool{ Name: data.Name, Comment: data.Comment, DeleteProtection: data.DeleteProtection, Datacenter: zne.Datacenter, Zone: data.Zone, Type: data.Type, VgName: data.VgName, } errData, err := pl.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pl.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pool.change") c.JSON(200, pl) } func poolDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) poolId, ok := utils.ParseObjectId(c.Param("pool_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := pool.Remove(db, poolId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pool.change") c.JSON(200, nil) } func poolsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = pool.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pool.change") c.JSON(200, nil) } func poolGet(c *gin.Context) { if demo.IsDemo() { pl := demo.Pools[0] c.JSON(200, pl) return } db := c.MustGet("db").(*database.Database) poolId, ok := utils.ParseObjectId(c.Param("pool_id")) if !ok { utils.AbortWithStatus(c, 400) return } pl, err := pool.Get(db, poolId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, pl) } func poolsGet(c *gin.Context) { if demo.IsDemo() { data := &poolsData{ Pools: demo.Pools, Count: int64(len(demo.Pools)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) nodeNames, err := node.GetAllNamesMap(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} poolId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = poolId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } pools, count, err := pool.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, pl := range pools { pl.Json(nodeNames) } data := &poolsData{ Pools: pools, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/relations.go ================================================ package ahandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type relationsData struct { Id any `json:"id"` Kind string `json:"kind"` Data string `json:"data"` } func relationsGet(c *gin.Context) { if demo.IsDemo() { kind := c.Param("kind") resourceId, ok := utils.ParseObjectId(c.Param("id")) if !ok { utils.AbortWithStatus(c, 400) return } data := &relationsData{ Id: resourceId, Kind: kind, Data: "demo", } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) kind := c.Param("kind") resourceId, ok := utils.ParseObjectId(c.Param("id")) if !ok { utils.AbortWithStatus(c, 400) return } resp, err := relations.Aggregate(db, kind, resourceId) if err != nil { utils.AbortWithError(c, 500, err) return } if resp == nil { utils.AbortWithStatus(c, 404) return } data := &relationsData{ Id: resp.Id, Kind: kind, Data: resp.Yaml(), } c.JSON(200, data) } ================================================ FILE: ahandlers/secret.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/utils" ) type secretData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` Type string `json:"type"` Key string `json:"key"` Value string `json:"value"` Data string `json:"data"` Region string `json:"region"` } type secretsData struct { Secrets []*secret.Secret `json:"secrets"` Count int64 `json:"count"` } func secretPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &secretData{} secrId, ok := utils.ParseObjectId(c.Param("secr_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secr, err := secret.Get(db, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } secr.Name = data.Name secr.Comment = data.Comment secr.Organization = data.Organization secr.Type = data.Type secr.Key = data.Key secr.Value = data.Value secr.Data = data.Data secr.Region = data.Region fields := set.NewSet( "name", "comment", "organization", "type", "key", "value", "data", "region", "public_key", "private_key", ) errData, err := secr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secr.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, secr) } func secretPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &secretData{ Name: "new-secret", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secr := &secret.Secret{ Name: data.Name, Comment: data.Comment, Organization: data.Organization, Type: data.Type, Key: data.Key, Value: data.Value, Data: data.Data, Region: data.Region, } errData, err := secr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secr.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, secr) } func secretDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) secrId, ok := utils.ParseObjectId(c.Param("secr_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "secret", secrId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secret.Remove(db, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, nil) } func secretsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "secret", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secret.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, nil) } func secretGet(c *gin.Context) { if demo.IsDemo() { secr := demo.Secrets[0] c.JSON(200, secr) return } db := c.MustGet("db").(*database.Database) secrId, ok := utils.ParseObjectId(c.Param("secr_id")) if !ok { utils.AbortWithStatus(c, 400) return } secr, err := secret.Get(db, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { secr.Key = "demo" secr.Value = "demo" } c.JSON(200, secr) } func secretsGet(c *gin.Context) { if demo.IsDemo() { data := &secretsData{ Secrets: demo.Secrets, Count: int64(len(demo.Secrets)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { secrs, err := secret.GetAllNames(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, secrs) return } page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} secretId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = secretId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } secrs, count, err := secret.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &secretsData{ Secrets: secrs, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/session.go ================================================ package ahandlers import ( "strconv" "time" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/utils" ) func sessionsGet(c *gin.Context) { if demo.IsDemo() { demo.Sessions[0].LastActive = time.Now() c.JSON(200, demo.Sessions) return } db := c.MustGet("db").(*database.Database) showRemoved, _ := strconv.ParseBool(c.Query("show_removed")) userId, ok := utils.ParseObjectId(c.Param("user_id")) if !ok { utils.AbortWithStatus(c, 400) return } sessions, err := session.GetAll(db, userId, showRemoved) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, sessions) } func sessionDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) sessionId := c.Param("session_id") if sessionId == "" { utils.AbortWithStatus(c, 400) return } err := session.Remove(db, sessionId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "session.change") c.JSON(200, nil) } ================================================ FILE: ahandlers/settings.go ================================================ package ahandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) type settingsData struct { AuthProviders []*settings.Provider `json:"auth_providers"` AuthSecondaryProviders []*settings.SecondaryProvider `json:"auth_secondary_providers"` AuthAdminExpire int `json:"auth_admin_expire"` AuthAdminMaxDuration int `json:"auth_admin_max_duration"` AuthProxyExpire int `json:"auth_proxy_expire"` AuthProxyMaxDuration int `json:"auth_proxy_max_duration"` AuthUserExpire int `json:"auth_user_expire"` AuthUserMaxDuration int `json:"auth_user_max_duration"` AuthFastLogin bool `json:"auth_fast_login"` AuthForceFastUserLogin bool `json:"auth_force_fast_user_login"` AuthForceFastServiceLogin bool `json:"auth_force_fast_service_login"` TwilioAccount string `json:"twilio_account"` TwilioSecret string `json:"twilio_secret"` TwilioNumber string `json:"twilio_number"` NvdApiKey string `json:"nvd_api_key"` } func getSettingsData() *settingsData { data := &settingsData{ AuthProviders: settings.Auth.Providers, AuthSecondaryProviders: settings.Auth.SecondaryProviders, AuthAdminExpire: settings.Auth.AdminExpire, AuthAdminMaxDuration: settings.Auth.AdminMaxDuration, AuthUserExpire: settings.Auth.UserExpire, AuthUserMaxDuration: settings.Auth.UserMaxDuration, AuthFastLogin: settings.Auth.FastLogin, AuthForceFastUserLogin: settings.Auth.ForceFastUserLogin, TwilioAccount: settings.System.TwilioAccount, TwilioSecret: settings.System.TwilioSecret, TwilioNumber: settings.System.TwilioNumber, NvdApiKey: settings.Telemetry.NvdApiKey, } return data } func settingsGet(c *gin.Context) { data := getSettingsData() c.JSON(200, data) } func settingsPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &settingsData{} err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } fields := set.NewSet() if settings.System.TwilioAccount != data.TwilioAccount { settings.System.TwilioAccount = data.TwilioAccount fields.Add("twilio_account") } if settings.System.TwilioSecret != data.TwilioSecret { settings.System.TwilioSecret = data.TwilioSecret fields.Add("twilio_secret") } if settings.System.TwilioNumber != data.TwilioNumber { settings.System.TwilioNumber = data.TwilioNumber fields.Add("twilio_number") } if fields.Len() != 0 { err = settings.Commit(db, settings.System, fields) if err != nil { utils.AbortWithError(c, 500, err) return } } if settings.Telemetry.NvdApiKey != data.NvdApiKey { settings.Telemetry.NvdApiKey = data.NvdApiKey err = settings.Commit( db, settings.Telemetry, set.NewSet("nvd_api_key"), ) if err != nil { utils.AbortWithError(c, 500, err) return } } fields = set.NewSet( "providers", "secondary_providers", ) if settings.Auth.AdminExpire != data.AuthAdminExpire { settings.Auth.AdminExpire = data.AuthAdminExpire fields.Add("admin_expire") } if settings.Auth.AdminMaxDuration != data.AuthAdminMaxDuration { settings.Auth.AdminMaxDuration = data.AuthAdminMaxDuration fields.Add("admin_max_duration") } if settings.Auth.UserExpire != data.AuthUserExpire { settings.Auth.UserExpire = data.AuthUserExpire fields.Add("user_expire") } if settings.Auth.UserMaxDuration != data.AuthUserMaxDuration { settings.Auth.UserMaxDuration = data.AuthUserMaxDuration fields.Add("user_max_duration") } if settings.Auth.FastLogin != data.AuthFastLogin { settings.Auth.FastLogin = data.AuthFastLogin fields.Add("fast_login") } if settings.Auth.ForceFastUserLogin != data.AuthForceFastUserLogin { settings.Auth.ForceFastUserLogin = data.AuthForceFastUserLogin fields.Add("force_fast_user_login") } for _, provider := range data.AuthProviders { errData, err := provider.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } } settings.Auth.Providers = data.AuthProviders for _, provider := range data.AuthSecondaryProviders { errData, err := provider.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } } settings.Auth.SecondaryProviders = data.AuthSecondaryProviders err = settings.Commit(db, settings.Auth, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "settings.change") data = getSettingsData() c.JSON(200, data) } ================================================ FILE: ahandlers/shape.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/utils" ) type shapeData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` DeleteProtection bool `json:"delete_protection"` Datacenter bson.ObjectID `json:"datacenter"` Roles []string `json:"roles"` Flexible bool `json:"flexible"` DiskType string `json:"disk_type"` DiskPool bson.ObjectID `json:"disk_pool"` Memory int `json:"memory"` Processors int `json:"processors"` } type shapesData struct { Shapes []*shape.Shape `json:"shapes"` Count int64 `json:"count"` } func shapePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &shapeData{} shapeId, ok := utils.ParseObjectId(c.Param("shape_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } shpe, err := shape.Get(db, shapeId) if err != nil { utils.AbortWithError(c, 500, err) return } shpe.Name = data.Name shpe.Type = data.Type shpe.Comment = data.Comment shpe.DeleteProtection = data.DeleteProtection shpe.Datacenter = data.Datacenter shpe.Roles = data.Roles shpe.Flexible = data.Flexible shpe.DiskType = data.DiskType shpe.DiskPool = data.DiskPool shpe.Memory = data.Memory shpe.Processors = data.Processors fields := set.NewSet( "name", "type", "comment", "delete_protection", "datacenter", "roles", "flexible", "disk_type", "disk_pool", "memory", "processors", ) errData, err := shpe.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = shpe.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "shape.change") c.JSON(200, shpe) } func shapePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &shapeData{ Name: "new-shape", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } shpe := &shape.Shape{ Name: data.Name, Comment: data.Comment, DeleteProtection: data.DeleteProtection, Datacenter: data.Datacenter, Roles: data.Roles, Flexible: data.Flexible, DiskType: data.DiskType, DiskPool: data.DiskPool, Memory: data.Memory, Processors: data.Processors, } errData, err := shpe.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = shpe.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "shape.change") c.JSON(200, shpe) } func shapeDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) shapeId, ok := utils.ParseObjectId(c.Param("shape_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "shape", shapeId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = shape.Remove(db, shapeId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "shape.change") c.JSON(200, nil) } func shapesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "shape", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = shape.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "shape.change") c.JSON(200, nil) } func shapeGet(c *gin.Context) { if demo.IsDemo() { shpe := demo.Shapes[0] c.JSON(200, shpe) return } db := c.MustGet("db").(*database.Database) shapeId, ok := utils.ParseObjectId(c.Param("shape_id")) if !ok { utils.AbortWithStatus(c, 400) return } shpe, err := shape.Get(db, shapeId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, shpe) } func shapesGet(c *gin.Context) { if demo.IsDemo() { data := &shapesData{ Shapes: demo.Shapes, Count: int64(len(demo.Shapes)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} shapeId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = shapeId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } shapes, count, err := aggregate.GetShapePaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &shapesData{ Shapes: shapes, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/static.go ================================================ package ahandlers import ( "strings" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/auth" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/middlewear" "github.com/pritunl/pritunl-cloud/static" "github.com/pritunl/pritunl-cloud/utils" ) func staticPath(c *gin.Context, pth string, cache bool) { pth = config.StaticRoot + pth file, ok := store.Files[pth] if !ok { utils.AbortWithStatus(c, 404) return } if constants.StaticCache && cache { c.Writer.Header().Add("Cache-Control", "public, max-age=86400") c.Writer.Header().Add("ETag", file.Hash) } else { c.Writer.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") c.Writer.Header().Add("Pragma", "no-cache") c.Writer.Header().Add("Expires", "0") } if strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") { c.Writer.Header().Add("Content-Encoding", "gzip") c.Data(200, file.Type, file.GzipData) } else { c.Data(200, file.Type, file.Data) } } func staticIndexGet(c *gin.Context) { authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { fastPth := auth.GetFastAdminPath() if fastPth != "" { c.Redirect(302, fastPth) return } c.Redirect(302, "/login") return } staticPath(c, "/index.html", false) } func staticLoginGet(c *gin.Context) { staticPath(c, "/login.html", false) } func staticLogoGet(c *gin.Context) { staticPath(c, "/logo.png", true) } func staticGet(c *gin.Context) { staticPath(c, "/static"+c.Params.ByName("path"), true) } func staticTestingGet(c *gin.Context) { pth := c.Params.ByName("path") if pth == "" { if c.Request.URL.Path == "/config.js" { pth = "config.js" } else if c.Request.URL.Path == "/logo.png" { pth = "logo.png" } else if c.Request.URL.Path == "/build.js" { pth = "build.js" } else if c.Request.URL.Path == "/login" { fastPth := auth.GetFastAdminPath() if fastPth != "" { c.Redirect(302, fastPth) return } c.Request.URL.Path = "/login.html" pth = "login.html" } else { authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { fastPth := auth.GetFastAdminPath() if fastPth != "" { c.Redirect(302, fastPth) return } c.Redirect(302, "/login") return } pth = "index.html" } } if strings.HasPrefix(c.Request.URL.Path, "/node_modules/") || strings.HasPrefix(c.Request.URL.Path, "/jspm_packages/") { c.Writer.Header().Add("Cache-Control", "public, max-age=86400") } else { c.Writer.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") c.Writer.Header().Add("Pragma", "no-cache") c.Writer.Header().Add("Expires", "0") } c.Writer.Header().Add("Content-Type", static.GetMimeType(pth)) gzipWriter := middlewear.NewGzipWriter(c) defer gzipWriter.Close() fileServer.ServeHTTP(gzipWriter, c.Request) } ================================================ FILE: ahandlers/storage.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type storageData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` Endpoint string `json:"endpoint"` Bucket string `json:"bucket"` AccessKey string `json:"access_key"` SecretKey string `json:"secret_key"` Insecure bool `json:"insecure"` } type storagesData struct { Storages []*storage.Storage `json:"storages"` Count int64 `json:"count"` } func storagePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &storageData{} storeId, ok := utils.ParseObjectId(c.Param("store_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { utils.AbortWithError(c, 500, err) return } store, err := storage.Get(db, storeId) if err != nil { utils.AbortWithError(c, 500, err) return } store.Name = dta.Name store.Comment = dta.Comment store.Type = dta.Type store.Endpoint = dta.Endpoint store.Bucket = dta.Bucket store.AccessKey = dta.AccessKey store.SecretKey = dta.SecretKey store.Insecure = dta.Insecure fields := set.NewSet( "name", "comment", "type", "endpoint", "bucket", "access_key", "secret_key", "insecure", ) errData, err := store.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = store.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } go func() { db := database.GetDatabase() defer db.Close() err = data.Sync(db, store) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("storage: Failed to sync storage") } event.PublishDispatch(db, "image.change") }() event.PublishDispatch(db, "storage.change") c.JSON(200, store) } func storagePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) dta := &storageData{ Name: "new-storage", } err := c.Bind(dta) if err != nil { utils.AbortWithError(c, 500, err) return } store := &storage.Storage{ Name: dta.Name, Comment: dta.Comment, Type: dta.Type, Endpoint: dta.Endpoint, Bucket: dta.Bucket, AccessKey: dta.AccessKey, SecretKey: dta.SecretKey, Insecure: dta.Insecure, } errData, err := store.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = store.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } go func() { db := database.GetDatabase() defer db.Close() err = data.Sync(db, store) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("storage: Failed to sync storage") } event.PublishDispatch(db, "image.change") }() event.PublishDispatch(db, "storage.change") c.JSON(200, store) } func storageDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) storeId, ok := utils.ParseObjectId(c.Param("store_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := storage.Remove(db, storeId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "storage.change") c.JSON(200, nil) } func storagesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } err = storage.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "storage.change") c.JSON(200, nil) } func storageGet(c *gin.Context) { if demo.IsDemo() { store := demo.Storages[0] c.JSON(200, store) return } db := c.MustGet("db").(*database.Database) storeId, ok := utils.ParseObjectId(c.Param("store_id")) if !ok { utils.AbortWithStatus(c, 400) return } store, err := storage.Get(db, storeId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { if store.AccessKey != "" { store.AccessKey = "demo" } if store.SecretKey != "" { store.SecretKey = "demo" } } c.JSON(200, store) } func storagesGet(c *gin.Context) { if demo.IsDemo() { data := &storagesData{ Storages: demo.Storages, Count: int64(len(demo.Storages)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} storageId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = storageId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } datacenter, ok := utils.ParseObjectId(c.Query("datacenter")) if ok { query["datacenter"] = datacenter } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } stores, count, err := storage.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { for _, store := range stores { if store.AccessKey != "" { store.AccessKey = "demo" } if store.SecretKey != "" { store.SecretKey = "demo" } } } data := &storagesData{ Storages: stores, Count: count, } c.JSON(200, data) } ================================================ FILE: ahandlers/subscription.go ================================================ package ahandlers import ( "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/subscription" "github.com/pritunl/pritunl-cloud/utils" ) type subscriptionPostData struct { License string `json:"license"` } func subscriptionGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.Subscription) return } c.JSON(200, subscription.Sub) } func subscriptionUpdateGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.Subscription) return } errData, err := subscription.Update() if err != nil { if errData != nil { c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, subscription.Sub) } func subscriptionPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &subscriptionPostData{} err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } license := strings.TrimSpace(data.License) license = strings.Replace(license, "BEGIN LICENSE", "", 1) license = strings.Replace(license, "END LICENSE", "", 1) license = strings.Replace(license, "-", "", -1) license = strings.Replace(license, " ", "", -1) license = strings.Replace(license, "\n", "", -1) settings.System.License = license errData, err := subscription.Update() if err != nil { settings.System.License = "" if errData != nil { c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } err = settings.Commit(db, settings.System, set.NewSet( "license", )) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "subscription.change") event.PublishDispatch(db, "settings.change") c.JSON(200, subscription.Sub) } ================================================ FILE: ahandlers/theme.go ================================================ package ahandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/utils" ) type themeData struct { Theme string `json:"theme"` EditorTheme string `json:"editor_theme"` } func themePut(c *gin.Context) { if demo.IsDemo() { c.JSON(200, nil) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &themeData{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } usr.Theme = data.Theme usr.EditorTheme = data.EditorTheme err = usr.CommitFields(db, set.NewSet("theme", "editor_theme")) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, data) return } ================================================ FILE: ahandlers/user.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" ) type userData struct { Id bson.ObjectID `json:"id"` Type string `json:"type"` Username string `json:"username"` Password string `json:"password"` Comment string `json:"comment"` Roles []string `json:"roles"` Administrator string `json:"administrator"` Permissions []string `json:"permissions"` GenerateSecret bool `json:"generate_secret"` Disabled bool `json:"disabled"` ActiveUntil time.Time `json:"active_until"` } type usersData struct { Users []*user.User `json:"users"` Count int64 `json:"count"` } func userGet(c *gin.Context) { if demo.IsDemo() { usr := demo.Users[0] usr.LastActive = time.Now() c.JSON(200, usr) return } db := c.MustGet("db").(*database.Database) userId, ok := utils.ParseObjectId(c.Param("user_id")) if !ok { utils.AbortWithStatus(c, 400) return } usr, err := user.Get(db, userId) if err != nil { utils.AbortWithError(c, 500, err) return } usr.Secret = "" c.JSON(200, usr) } func userPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &userData{} userId, ok := utils.ParseObjectId(c.Param("user_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } usr, err := user.Get(db, userId) if err != nil { utils.AbortWithError(c, 500, err) return } showSecret := false if usr.Type != data.Type { if data.Type == user.Api { usr.GenerateToken() showSecret = true } else { usr.Token = "" usr.Secret = "" } } usr.Type = data.Type usr.Username = data.Username usr.Comment = data.Comment usr.Roles = data.Roles usr.Administrator = data.Administrator usr.Permissions = data.Permissions usr.Disabled = data.Disabled usr.ActiveUntil = data.ActiveUntil if usr.Disabled { usr.ActiveUntil = time.Time{} } if usr.Type == user.Api && data.GenerateSecret { usr.GenerateToken() showSecret = true } fields := set.NewSet( "type", "token", "secret", "username", "comment", "roles", "administrator", "permissions", "disabled", "active_until", ) if usr.Type == user.Local && data.Password != "" { err = usr.SetPassword(data.Password) if err != nil { utils.AbortWithError(c, 500, err) return } fields.Add("password") } else if usr.Type != user.Local && usr.Password != "" { usr.Password = "" fields.Add("password") } errData, err := usr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } errData, err = usr.SuperExists(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = usr.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "user.change") if !showSecret { usr.Secret = "" } c.JSON(200, usr) } func userPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &userData{} err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } usr := &user.User{ Type: data.Type, Username: data.Username, Comment: data.Comment, Roles: data.Roles, Administrator: data.Administrator, Permissions: data.Permissions, Disabled: data.Disabled, ActiveUntil: data.ActiveUntil, } if usr.Disabled { usr.ActiveUntil = time.Time{} } if usr.Type == user.Local && data.Password != "" { err = usr.SetPassword(data.Password) if err != nil { utils.AbortWithError(c, 500, err) return } } if usr.Type == user.Api { usr.GenerateToken() } errData, err := usr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = usr.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "user.change") c.JSON(200, usr) } func usersGet(c *gin.Context) { if demo.IsDemo() { for _, usr := range demo.Users { usr.LastActive = time.Now() } data := &usersData{ Users: demo.Users, Count: int64(len(demo.Users)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} userId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = userId } username := strings.TrimSpace(c.Query("username")) if username != "" { query["username"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(username)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } typ := strings.TrimSpace(c.Query("type")) if typ != "" { query["type"] = typ } administrator := c.Query("administrator") switch administrator { case "true": query["administrator"] = "super" break case "false": query["administrator"] = "" break } disabled := c.Query("disabled") switch disabled { case "true": query["disabled"] = true break case "false": query["disabled"] = false break } users, count, err := user.GetAll(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, usr := range users { usr.Secret = "" } data := &usersData{ Users: users, Count: count, } c.JSON(200, data) } func usersDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := user.Remove(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } event.PublishDispatch(db, "user.change") c.JSON(200, nil) } ================================================ FILE: ahandlers/vpc.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" ) type vpcData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Network string `json:"network"` IcmpRedirects bool `json:"icmp_redirects"` Subnets []*vpc.Subnet `json:"subnets"` Organization bson.ObjectID `json:"organization"` Datacenter bson.ObjectID `json:"datacenter"` Routes []*vpc.Route `json:"routes"` Maps []*vpc.Map `json:"maps"` Arps []*vpc.Arp `json:"arps"` } type vpcsData struct { Vpcs []*vpc.Vpc `json:"vpcs"` Count int64 `json:"count"` } func vpcPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &vpcData{} vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } vc, err := vpc.Get(db, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } vc.PreCommit() vc.Name = data.Name vc.Comment = data.Comment vc.IcmpRedirects = data.IcmpRedirects vc.Routes = data.Routes vc.Maps = data.Maps vc.Arps = data.Arps vc.Subnets = data.Subnets fields := set.NewSet( "name", "comment", "icmp_redirects", "routes", "maps", "arps", "subnets", ) errData, err := vc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } errData, err = vc.PostCommit(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") vc.Json() c.JSON(200, vc) } func vpcPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &vpcData{ Name: "new-vpc", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "ahandler: Failed to bind"), } utils.AbortWithError(c, 500, err) return } vc := &vpc.Vpc{ Name: data.Name, Comment: data.Comment, Network: data.Network, Subnets: data.Subnets, Organization: data.Organization, Datacenter: data.Datacenter, IcmpRedirects: data.IcmpRedirects, Routes: data.Routes, Maps: data.Maps, Arps: data.Arps, } vc.InitVpc() errData, err := vc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vc.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") vc.Json() c.JSON(200, vc) } func vpcDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "vpc", vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vpc.Remove(db, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") c.JSON(200, nil) } func vpcsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "vpc", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vpc.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") c.JSON(200, nil) } func vpcGet(c *gin.Context) { if demo.IsDemo() { vc := demo.Vpcs[0] c.JSON(200, vc) return } db := c.MustGet("db").(*database.Database) vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } vc, err := vpc.Get(db, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } vc.Json() c.JSON(200, vc) } func vpcRoutesGet(c *gin.Context) { if demo.IsDemo() { vc := demo.Vpcs[0] c.JSON(200, vc.Routes) return } db := c.MustGet("db").(*database.Database) vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } vc, err := vpc.Get(db, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, vc.Routes) } func vpcRoutesPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []*vpc.Route{} vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } vc, err := vpc.Get(db, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } vc.Routes = data fields := set.NewSet( "routes", ) errData, err := vc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") vc.Json() c.JSON(200, vc) } func vpcsGet(c *gin.Context) { if demo.IsDemo() { data := &vpcsData{ Vpcs: demo.Vpcs, Count: int64(len(demo.Vpcs)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { query := &bson.M{} vpcs, err := vpc.GetAllNames(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, vpcs) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} vpcId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = vpcId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } network := strings.TrimSpace(c.Query("network")) if network != "" { query["network"] = network } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } dc, ok := utils.ParseObjectId(c.Query("datacenter")) if ok { query["datacenter"] = dc } vpcs, count, err := vpc.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, vc := range vpcs { vc.Json() } data := &vpcsData{ Vpcs: vpcs, Count: count, } c.JSON(200, data) } } ================================================ FILE: ahandlers/zone.go ================================================ package ahandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" ) type zoneData struct { Id bson.ObjectID `json:"id"` Datacenter bson.ObjectID `json:"datacenter"` Name string `json:"name"` Comment string `json:"comment"` } type zonesData struct { Zones []*zone.Zone `json:"zones"` Count int64 `json:"count"` } func zonePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &zoneData{} zoneId, ok := utils.ParseObjectId(c.Param("zone_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } zne, err := zone.Get(db, zoneId) if err != nil { utils.AbortWithError(c, 500, err) return } zne.Name = data.Name zne.Comment = data.Comment fields := set.NewSet( "name", "comment", ) errData, err := zne.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = zne.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "zone.change") c.JSON(200, zne) } func zonePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &zoneData{ Name: "new-zone", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } zne := &zone.Zone{ Datacenter: data.Datacenter, Name: data.Name, Comment: data.Comment, } errData, err := zne.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = zne.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "zone.change") c.JSON(200, zne) } func zoneDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) zoneId, ok := utils.ParseObjectId(c.Param("zone_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDelete(db, "zone", zoneId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = zone.Remove(db, zoneId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "zone.change") c.JSON(200, nil) } func zonesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteAll(db, "zone", data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = zone.RemoveMulti(db, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "zone.change") c.JSON(200, nil) } func zoneGet(c *gin.Context) { if demo.IsDemo() { zne := demo.Zones[0] c.JSON(200, zne) return } db := c.MustGet("db").(*database.Database) zoneId, ok := utils.ParseObjectId(c.Param("zone_id")) if !ok { utils.AbortWithStatus(c, 400) return } zne, err := zone.Get(db, zoneId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, zne) } func zonesGet(c *gin.Context) { if demo.IsDemo() { data := &zonesData{ Zones: demo.Zones, Count: int64(len(demo.Zones)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) if c.Query("names") == "true" { dcs, err := zone.GetAllNames(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, dcs) return } page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{} zoneId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = zoneId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } organization, ok := utils.ParseObjectId(c.Query("organization")) if ok { query["organization"] = organization } datacenter, ok := utils.ParseObjectId(c.Query("datacenter")) if ok { query["datacenter"] = datacenter } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } znes, count, err := zone.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &zonesData{ Zones: znes, Count: count, } c.JSON(200, data) } ================================================ FILE: alert/alert.go ================================================ package alert import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Alert struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Roles []string `bson:"roles" json:"roles"` Resource string `bson:"resource" json:"resource"` Level int `bson:"level" json:"level"` Frequency int `bson:"frequency" json:"frequency"` Ignores []string `bson:"ignores" json:"ignores"` ValueInt int `bson:"value_int" json:"value_int"` ValueStr string `bson:"value_str" json:"value_str"` } func (a *Alert) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { if a.Id.IsZero() { a.Id, err = utils.RandObjectId() if err != nil { return } } a.Name = utils.FilterName(a.Name) if a.Roles == nil { a.Roles = []string{} } if a.Frequency == 0 { a.Frequency = 300 } if a.Frequency < 300 { errData = &errortypes.ErrorData{ Error: "alert_frequency_invalid", Message: "Alert frequency cannot be less then 300 seconds", } return } if a.Frequency > 604800 { errData = &errortypes.ErrorData{ Error: "alert_frequency_invalid", Message: "Alert frequency too large", } return } if a.Ignores != nil { a.Ignores = []string{} } switch a.Resource { case InstanceOffline: a.ValueInt = 0 a.ValueStr = "" break default: errData = &errortypes.ErrorData{ Error: "alert_resource_name_invalid", Message: "Alert resource name is invalid", } return } switch a.Level { case Low, Medium, High: break default: errData = &errortypes.ErrorData{ Error: "alert_resource_level_invalid", Message: "Alert resource level is invalid", } return } return } func (a *Alert) Commit(db *database.Database) (err error) { coll := db.Alerts() err = coll.Commit(a.Id, a) if err != nil { return } return } func (a *Alert) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Alerts() err = coll.CommitFields(a.Id, a, fields) if err != nil { return } return } func (a *Alert) Insert(db *database.Database) (err error) { coll := db.Alerts() _, err = coll.InsertOne(db, a) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: alert/constants.go ================================================ package alert const ( Low = 1 Medium = 5 High = 10 ) const ( InstanceOffline = "instance_offline" ) ================================================ FILE: alert/utils.go ================================================ package alert import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, alertId bson.ObjectID) ( alrt *Alert, err error) { coll := db.Alerts() alrt = &Alert{} err = coll.FindOneId(alertId, alrt) if err != nil { return } return } func GetOrg(db *database.Database, orgId, alertId bson.ObjectID) ( alrt *Alert, err error) { coll := db.Alerts() alrt = &Alert{} err = coll.FindOne(db, &bson.M{ "_id": alertId, "organization": orgId, }).Decode(alrt) if err != nil { err = database.ParseError(err) return } return } func GetMulti(db *database.Database, alertIds []bson.ObjectID) ( alerts []*Alert, err error) { coll := db.Alerts() alerts = []*Alert{} cursor, err := coll.Find( db, &bson.M{ "_id": &bson.M{ "$in": alertIds, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { alrt := &Alert{} err = cursor.Decode(alrt) if err != nil { err = database.ParseError(err) return } alerts = append(alerts, alrt) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database) (alerts []*Alert, err error) { coll := db.Alerts() alerts = []*Alert{} cursor, err := coll.Find( db, &bson.M{}, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { alrt := &Alert{} err = cursor.Decode(alrt) if err != nil { err = database.ParseError(err) return } alerts = append(alerts, alrt) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (alerts []*Alert, count int64, err error) { coll := db.Alerts() alerts = []*Alert{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) defer cursor.Close(db) for cursor.Next(db) { alrt := &Alert{} err = cursor.Decode(alrt) if err != nil { err = database.ParseError(err) return } alerts = append(alerts, alrt) alrt = &Alert{} } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetRoles(db *database.Database, roles []string) ( alerts []*Alert, err error) { coll := db.Alerts() alerts = []*Alert{} if roles == nil { roles = []string{} } cursor, err := coll.Find( db, &bson.M{ "roles": &bson.M{ "$in": roles, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { polcy := &Alert{} err = cursor.Decode(polcy) if err != nil { err = database.ParseError(err) return } alerts = append(alerts, polcy) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetRolesMapped(db *database.Database, rolesSet set.Set) ( alertsMap map[string][]*Alert, err error) { alertsMap = map[string][]*Alert{} roles := []string{} for role := range rolesSet.Iter() { roles = append(roles, role.(string)) } alerts, err := GetRoles(db, roles) if err != nil { return } for _, alrt := range alerts { for _, role := range alrt.Roles { roleAlrts := alertsMap[role] if roleAlrts == nil { roleAlrts = []*Alert{} } alertsMap[role] = append(roleAlrts, alrt) } } return } func Remove(db *database.Database, alertId bson.ObjectID) (err error) { coll := db.Alerts() _, err = coll.DeleteMany(db, &bson.M{ "_id": alertId, }) if err != nil { err = database.ParseError(err) return } return } func RemoveOrg(db *database.Database, orgId, alertId bson.ObjectID) ( err error) { coll := db.Alerts() _, err = coll.DeleteOne(db, &bson.M{ "_id": alertId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, alertIds []bson.ObjectID) ( err error) { coll := db.Alerts() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": alertIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, alertIds []bson.ObjectID) (err error) { coll := db.Alerts() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": alertIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: alertevent/alertevent.go ================================================ package alertevent import ( "fmt" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/user" "github.com/sirupsen/logrus" ) type Alert struct { Id string `bson:"_id" json:"_id"` Name string `bson:"name" json:"name"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Roles []string `bson:"roles" json:"roles"` Source bson.ObjectID `bson:"source" json:"source"` SourceName string `bson:"source_name" json:"source_name"` Level int `bson:"level" json:"level"` Resource string `bson:"resource" json:"resource"` Message string `bson:"message" json:"message"` Frequency time.Duration `bson:"frequency" json:"frequency"` } func (a *Alert) DocId() string { timestamp := a.Timestamp.Unix() timekey := timestamp - (timestamp % int64(a.Frequency.Seconds())) return fmt.Sprintf( "%s-%s-%d", a.Source.Hex(), a.Resource, timekey, ) } func (a *Alert) Key(devc *device.Device) string { timestamp := a.Timestamp.Unix() timekey := timestamp - (timestamp % int64(a.Frequency.Seconds())) return fmt.Sprintf( "%s-%s-%s-%d", a.Source.Hex(), a.Resource, devc.Id.Hex(), timekey, ) } func (a *Alert) Lock(db *database.Database, devc *device.Device) ( success bool, err error) { coll := db.AlertsEventLock() _, err = coll.InsertOne(db, &bson.M{ "_id": a.Key(devc), "timestamp": time.Now(), }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { err = nil } return } success = true return } func (a *Alert) FormattedTextMessage() string { return fmt.Sprintf("%s:%s == %s", a.Name, a.SourceName, a.Message) } func (a *Alert) FormattedCallMessage() string { return fmt.Sprintf("%s. %s", a.SourceName, a.Message) } func (a *Alert) Send(db *database.Database, roles []string) (err error) { coll := db.AlertsEvent() alrt := &Alert{} err = coll.FindOneId(a.Id, alrt) if err != nil { if _, ok := err.(*database.NotFoundError); ok { alrt = nil err = nil } else { return } } if alrt != nil && time.Since(alrt.Timestamp) < alrt.Frequency { return } users, _, err := user.GetAll(db, &bson.M{ "roles": &bson.D{ {"$in", roles}, }, }, 0, 0) if err != nil { return } for _, usr := range users { devices, e := usr.GetDevices(db) if e != nil { err = e return } for _, devc := range devices { if devc.Mode != device.Phone || !devc.CheckLevel(a.Level) { continue } success, e := a.Lock(db, devc) if e != nil { err = e return } if !success { continue } msg := "" if devc.Type == device.Call { msg = a.FormattedCallMessage() } else { msg = a.FormattedTextMessage() } errData, e := Send(devc.Number, msg, devc.Type) if e != nil { if errData != nil { logrus.WithFields(logrus.Fields{ "server_error": errData.Error, "server_message": errData.Message, "error": e, }).Error("alert: Failed to send alert") } else { logrus.WithFields(logrus.Fields{ "error": e, }).Error("alert: Failed to send alert") } } } } _, err = coll.InsertOne(db, a) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { err = nil } return } return } func New(roles []string, source bson.ObjectID, name, sourceName, resource, message string, level int, frequency time.Duration) { db := database.GetDatabase() defer db.Close() alrt := &Alert{ Name: name, Timestamp: time.Now(), Roles: roles, Source: source, SourceName: sourceName, Level: level, Resource: resource, Message: message, Frequency: frequency, } alrt.Id = alrt.DocId() err := alrt.Send(db, roles) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("alert: Failed to process alert") } return } ================================================ FILE: alertevent/utils.go ================================================ package alertevent import ( "bytes" "encoding/json" "io/ioutil" "net/http" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/twilio" ) var ( client = &http.Client{ Timeout: 10 * time.Second, } ) type AlertParams struct { License string `json:"license"` Number string `json:"number"` Type string `json:"type"` Message string `json:"message"` } func SendTest(db *database.Database, devc *device.Device) ( errData *errortypes.ErrorData, err error) { err = devc.SetActive(db) if err != nil { return } errData, err = Send(devc.Number, "Test alert message", devc.Type) if err != nil { return } return } func Send(number, message, alertType string) ( errData *errortypes.ErrorData, err error) { if settings.System.TwilioAccount != "" { if alertType == device.Call { err = twilio.PhoneCall(number, message) if err != nil { return } } else if alertType == device.Message { err = twilio.TextMessage(number, message) if err != nil { return } } else { err = &errortypes.ParseError{ errors.Wrap( err, "alert: Unknown alert type"), } return } } else { params := &AlertParams{ License: settings.System.License, Number: number, Type: alertType, Message: message, } alertBody, e := json.Marshal(params) if e != nil { err = &errortypes.ParseError{ errors.Wrap( e, "alert: Failed to parse alert params"), } return } req, e := http.NewRequest( "POST", "https://app.pritunl.com/alert", bytes.NewBuffer(alertBody), ) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "alert: Failed to create alert request"), } return } req.Header.Set("User-Agent", "pritunl-cloud") req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "alert: Alert request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData = &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } err = &errortypes.RequestError{ errors.Newf( "alert: Alert server error %d - %s", resp.StatusCode, body), } return } } return } ================================================ FILE: arp/arp.go ================================================ package arp import ( "encoding/json" "net" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" ) type Record struct { Ip string Mac string } type entry struct { Dst string `json:"dst"` Dev string `json:"dev"` Lladdr string `json:"lladdr,omitempty"` State []string `json:"state"` Router string `json:"router,omitempty"` } func GetRecords(namespace string) (records set.Set, err error) { records = set.NewSet() output, _ := utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ip", "--json", "neighbor", ) if output == "" { return } var entries []*entry err = json.Unmarshal([]byte(output), &entries) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "arp: Failed to process arp output"), } return } for _, ent := range entries { if ent.Dev != settings.Hypervisor.BridgeIfaceName { continue } mac := "" if ent.State != nil { for _, state := range ent.State { if state == "PERMANENT" { mac = ent.Lladdr } } } ip := net.ParseIP(ent.Dst) if ip != nil { records.Add(Record{ Ip: ip.String(), Mac: mac, }) } } return } func BuildState(instances []*instance.Instance, vpcsMap map[bson.ObjectID]*vpc.Vpc, vpcIpsMap map[bson.ObjectID][]*vpc.VpcIp) ( recrds map[string]set.Set) { recrds = map[string]set.Set{} for _, inst := range instances { if !inst.IsActive() { continue } for i, adapter := range inst.Virt.NetworkAdapters { namespace := vm.GetNamespace(inst.Id, i) vc := vpcsMap[adapter.Vpc] vpcIps := vpcIpsMap[adapter.Vpc] newRecrds := set.NewSet() for _, vpcIp := range vpcIps { if vpcIp.Instance.IsZero() { continue } addr := vpcIp.GetIp() newRecrds.Add(Record{ Ip: vpc.GetIp6(vpcIp.Vpc, vpcIp.Instance).String(), Mac: vm.GetMacAddr(vpcIp.Instance, adapter.Vpc), }) newRecrds.Add(Record{ Ip: addr.String(), Mac: vm.GetMacAddr(vpcIp.Instance, adapter.Vpc), }) } if vc != nil && vc.Arps != nil { for _, ap := range vc.Arps { newRecrds.Add(Record{ Ip: ap.Ip, Mac: ap.Mac, }) } } recrds[namespace] = newRecrds } } return } func ApplyState(namespace string, oldState, newState set.Set) ( changed bool, err error) { addRecords := newState.Copy() remRecords := oldState.Copy() addRecords.Subtract(oldState) remRecords.Subtract(newState) for recordInf := range remRecords.Iter() { recrd := recordInf.(Record) changed = true utils.ExecCombinedOutputLogged( []string{ "No such file", }, "ip", "netns", "exec", namespace, "ip", "neighbor", "del", recrd.Ip, "dev", settings.Hypervisor.BridgeIfaceName, ) } for recordInf := range addRecords.Iter() { recrd := recordInf.(Record) changed = true utils.ExecCombinedOutputLogged( []string{ "No such file", }, "ip", "netns", "exec", namespace, "ip", "neighbor", "del", recrd.Ip, "dev", settings.Hypervisor.BridgeIfaceName, ) _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "netns", "exec", namespace, "ip", "neighbor", "replace", recrd.Ip, "lladdr", recrd.Mac, "dev", settings.Hypervisor.BridgeIfaceName, "nud", "permanent", ) if err != nil { return } } return } ================================================ FILE: audit/audit.go ================================================ package audit import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/useragent" ) type Fields map[string]interface{} type Audit struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` User bson.ObjectID `bson:"u" json:"user"` Timestamp time.Time `bson:"t" json:"timestamp"` Type string `bson:"y" json:"type"` Fields Fields `bson:"f" json:"fields"` Agent *useragent.Agent `bson:"a" json:"agent"` } func (a *Audit) Insert(db *database.Database) (err error) { coll := db.Audits() if !a.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("audit: Entry already exists"), } return } _, err = coll.InsertOne(db, a) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: audit/constants.go ================================================ package audit const ( AdminLogin = "admin_login" AdminLoginFailed = "admin_login_failed" AdminAuthFailed = "admin_auth_failed" AdminLogout = "admin_logout" AdminPrimaryApprove = "admin_primary_approve" AdminSecondaryApprove = "admin_secondary_approve" AdminDeviceApprove = "admin_device_approve" AdminDeviceRegisterRequest = "admin_device_register_request" AdminDeviceRegister = "admin_device_register" ProxyLogin = "proxy_login" ProxyLoginFailed = "proxy_login_failed" ProxyAuthFailed = "proxy_auth_failed" ProxyLogout = "proxy_logout" ProxyPrimaryApprove = "proxy_primary_approve" ProxySecondaryApprove = "proxy_secondary_approve" ProxyDeviceApprove = "proxy_device_approve" ProxyDeviceRegisterRequest = "proxy_device_register_request" ProxyDeviceRegister = "proxy_device_register" UserLogin = "user_login" UserLoginFailed = "user_login_failed" UserAuthFailed = "user_auth_failed" UserLogout = "user_logout" UserLogoutAll = "user_logout_all" UserPrimaryApprove = "user_primary_approve" UserSecondaryApprove = "user_secondary_approve" UserDeviceApprove = "user_device_approve" UserDeviceRegisterRequest = "user_device_register_request" UserDeviceRegister = "user_device_register" UserAccountDisable = "user_account_disable" DeviceRegister = "device_register" DeviceRegisterFailed = "device_register_failed" DuoApprove = "duo_approve" DuoDeny = "duo_deny" OneLoginApprove = "one_login_approve" OneLoginDeny = "one_login_deny" OktaApprove = "okta_approve" OktaDeny = "okta_deny" ) ================================================ FILE: audit/utils.go ================================================ package audit import ( "net/http" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/useragent" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, adtId string) ( adt *Audit, err error) { coll := db.Audits() adt = &Audit{} err = coll.FindOneId(adtId, adt) if err != nil { return } return } func GetAll(db *database.Database, userId bson.ObjectID, page, pageCount int64) (audits []*Audit, count int64, err error) { coll := db.Audits() audits = []*Audit{} count, err = coll.CountDocuments(db, &bson.M{ "u": userId, }) if err != nil { err = database.ParseError(err) return } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find(db, &bson.M{ "u": userId, }, options.Find(). SetSort(&bson.D{ {"$natural", -1}, }). SetSkip(skip). SetLimit(pageCount)) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { adt := &Audit{} err = cursor.Decode(adt) if err != nil { err = database.ParseError(err) return } audits = append(audits, adt) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func New(db *database.Database, r *http.Request, userId bson.ObjectID, typ string, fields Fields) ( err error) { if settings.System.Demo { return } agnt, err := useragent.Parse(db, r) if err != nil { return } adt := &Audit{ User: userId, Timestamp: time.Now(), Type: typ, Fields: fields, Agent: agnt, } err = adt.Insert(db) if err != nil { return } return } ================================================ FILE: auth/auth.go ================================================ package auth import ( "net/http" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) var ( client = &http.Client{ Timeout: 20 * time.Second, } ) type authData struct { Url string `json:"url"` } type Token struct { Id string `bson:"_id"` Type string `bson:"type"` Secret string `bson:"secret"` Timestamp time.Time `bson:"timestamp"` Provider bson.ObjectID `bson:"provider,omitempty"` Query string `bson:"query"` } func (t *Token) Remove(db *database.Database) (err error) { coll := db.Tokens() _, err = coll.DeleteOne(db, &bson.M{ "_id": t.Id, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } return } ================================================ FILE: auth/authzero.go ================================================ package auth import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" ) const ( AuthZero = "authzero" ) func AuthZeroRequest(db *database.Database, location, query string, provider *settings.Provider) (redirect string, err error) { coll := db.Tokens() state, err := utils.RandStr(64) if err != nil { return } secret, err := utils.RandStr(64) if err != nil { return } data, err := json.Marshal(struct { License string `json:"license"` Callback string `json:"callback"` State string `json:"state"` Secret string `json:"secret"` AppDomain string `json:"app_domain"` AppId string `json:"app_id"` AppSecret string `json:"app_secret"` }{ License: settings.System.License, Callback: location + "/auth/callback", State: state, Secret: secret, AppDomain: provider.Domain, AppId: provider.ClientId, AppSecret: provider.ClientSecret, }) if err != nil { return } req, err := http.NewRequest( "POST", settings.Auth.Server+"/v1/request/authzero", bytes.NewBuffer(data), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } authData := &authData{} err = json.NewDecoder(resp.Body).Decode(authData) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "auth: Failed to parse auth response", ), } return } tokn := &Token{ Id: state, Type: AuthZero, Secret: secret, Timestamp: time.Now(), Provider: provider.Id, Query: query, } _, err = coll.InsertOne(db, tokn) if err != nil { err = database.ParseError(err) return } redirect = authData.Url return } type authZeroJwks struct { Keys []json.RawMessage `json:"keys"` } type authZeroTokenReq struct { GrantType string `json:"grant_type"` ClientId string `json:"client_id"` ClientSecret string `json:"client_secret"` Audience string `json:"audience"` } type authZeroTokenData struct { AccessToken string `json:"access_token"` TokenType string `json:"token_type"` ExpiresIn int `json:"expires_in"` } type authZeroToken struct { Iss string `json:"iss"` Sub string `json:"sub"` Aud string `json:"aud"` Exp int `json:"exp"` Iat int `json:"iat"` Scope string `json:"scope"` } type authZeroAppAuthorization struct { Groups []string `json:"groups"` Roles []string `json:"roles"` } type authZeroAppMetadata struct { Authorization authZeroAppAuthorization `json:"authorization"` } type authZeroUser struct { UserId string `json:"user_id"` Email string `json:"email"` AppMetadata authZeroAppMetadata `json:"app_metadata"` } //func authZeroGetJwk(provider *settings.Provider) ( // jwk *jose.JSONWebKey, err error) { // // req, err := http.NewRequest( // "GET", // fmt.Sprintf( // "https://%s.auth0.com/.well-known/jwks.json", // provider.Domain, // ), // nil, // ) // if err != nil { // err = &errortypes.RequestError{ // errors.Wrap(err, "auth: Failed to create auth0 request"), // } // return // } // // resp, err := client.Do(req) // if err != nil { // err = &errortypes.RequestError{ // errors.Wrap(err, "auth: auth0 request failed"), // } // return // } // defer resp.Body.Close() // // data := &authZeroJwks{} // err = json.NewDecoder(resp.Body).Decode(data) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to parse response"), // } // return // } // // if len(data.Keys) < 1 { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: No JWK keys available"), // } // return // } // // jwk = &jose.JSONWebKey{} // // err = jwk.UnmarshalJSON(data.Keys[0]) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to parse jwt key"), // } // return // } // // return //} // //func authZeroGetJwkToken(provider *settings.Provider) ( // accessToken string, token *authZeroToken, err error) { // // reqData := &authZeroTokenReq{ // GrantType: "client_credentials", // ClientId: provider.ClientId, // ClientSecret: provider.ClientSecret, // Audience: fmt.Sprintf( // "https://%s.auth0.com/api/v2/", provider.Domain), // } // // reqDataBuf := &bytes.Buffer{} // err = json.NewEncoder(reqDataBuf).Encode(reqData) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to parse request data"), // } // return // } // // req, err := http.NewRequest( // "POST", // fmt.Sprintf("https://%s.auth0.com/oauth/token", provider.Domain), // reqDataBuf, // ) // if err != nil { // err = &errortypes.RequestError{ // errors.Wrap(err, "auth: Failed to create auth0 request"), // } // return // } // // req.Header.Add("Content-Type", "application/json") // // resp, err := client.Do(req) // if err != nil { // err = &errortypes.RequestError{ // errors.Wrap(err, "auth: auth0 request failed"), // } // return // } // defer resp.Body.Close() // // tokenData := &authZeroTokenData{} // err = json.NewDecoder(resp.Body).Decode(tokenData) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to parse response"), // } // return // } // // accessToken = tokenData.AccessToken // // object, err := jose.ParseSigned(tokenData.AccessToken) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to parse jwt data"), // } // return // } // // jwt, err := authZeroGetJwk(provider) // if err != nil { // return // } // // data, err := object.Verify(jwt) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to verify jwt data"), // } // return // } // // token = &authZeroToken{} // err = json.Unmarshal(data, token) // if err != nil { // err = &errortypes.ParseError{ // errors.Wrap(err, "auth: Failed to parse jwt token"), // } // return // } // // return //} func authZeroGetToken(provider *settings.Provider) (token string, err error) { reqData := &authZeroTokenReq{ GrantType: "client_credentials", ClientId: provider.ClientId, ClientSecret: provider.ClientSecret, Audience: fmt.Sprintf( "https://%s.auth0.com/api/v2/", provider.Domain), } reqDataBuf := &bytes.Buffer{} err = json.NewEncoder(reqDataBuf).Encode(reqData) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse request data"), } return } req, err := http.NewRequest( "POST", fmt.Sprintf("https://%s.auth0.com/oauth/token", provider.Domain), reqDataBuf, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create auth0 request"), } return } req.Header.Add("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: auth0 request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } tokenData := &authZeroTokenData{} err = json.NewDecoder(resp.Body).Decode(tokenData) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse response"), } return } token = tokenData.AccessToken return } func AuthZeroRoles(provider *settings.Provider, username string) ( roles []string, err error) { roles = []string{} token, err := authZeroGetToken(provider) if err != nil { return } reqUrl, err := url.Parse(fmt.Sprintf( "https://%s.auth0.com/api/v2/users", provider.Domain, )) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse auth0 url"), } return } query := reqUrl.Query() query.Set("search_engine", "v3") query.Set("email", username) reqUrl.RawQuery = query.Encode() req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create auth0 request"), } return } req.Header.Add("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: auth0 request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } data := []*authZeroUser{} err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse response"), } return } userId := "" for _, usr := range data { if usr.Email != username { continue } userId = usr.UserId if usr.AppMetadata.Authorization.Roles != nil { roles = usr.AppMetadata.Authorization.Roles } break } if userId == "" { err = &errortypes.NotFoundError{ errors.Wrap(err, "auth: Failed to find auth0 user"), } return } return } func AuthZeroSync(db *database.Database, usr *user.User, provider *settings.Provider) (active bool, err error) { token, err := authZeroGetToken(provider) if err != nil { return } reqUrl, err := url.Parse(fmt.Sprintf( "https://%s.auth0.com/api/v2/users", provider.Domain, )) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse auth0 url"), } return } query := reqUrl.Query() query.Set("search_engine", "v3") query.Set("email", usr.Username) reqUrl.RawQuery = query.Encode() req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create auth0 request"), } return } req.Header.Add("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: auth0 request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } data := []*authZeroUser{} err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse response"), } return } userId := "" for _, authUser := range data { if authUser.Email != usr.Username { continue } userId = authUser.UserId break } if userId == "" { err = &errortypes.NotFoundError{ errors.Wrap(err, "auth: Failed to find auth0 user"), } return } active = true return } ================================================ FILE: auth/azure.go ================================================ package auth import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) const ( Azure = "azure" ) func azureGetUrls(provider *settings.Provider) (loginUrl, graphUrl string) { switch provider.Region { case "us-gov", "us-gov2": loginUrl = "https://login.microsoftonline.us" graphUrl = "https://graph.microsoft.us" case "us-dod", "us-dod2": loginUrl = "https://login.microsoftonline.us" graphUrl = "https://dod-graph.microsoft.us" case "china", "china2": loginUrl = "https://login.partner.microsoftonline.cn" graphUrl = "https://microsoftgraph.chinacloudapi.cn" default: loginUrl = "https://login.microsoftonline.com" graphUrl = "https://graph.microsoft.com" } return } func AzureRequest(db *database.Database, location, query string, provider *settings.Provider) (redirect string, err error) { coll := db.Tokens() state, err := utils.RandStr(64) if err != nil { return } secret, err := utils.RandStr(64) if err != nil { return } data, err := json.Marshal(struct { License string `json:"license"` Callback string `json:"callback"` State string `json:"state"` Secret string `json:"secret"` Region string `json:"region"` DirectoryId string `json:"directory_id"` AppId string `json:"app_id"` AppSecret string `json:"app_secret"` }{ License: settings.System.License, Callback: location + "/auth/callback", State: state, Secret: secret, Region: provider.Region, DirectoryId: provider.Tenant, AppId: provider.ClientId, AppSecret: provider.ClientSecret, }) if err != nil { return } req, err := http.NewRequest( "POST", settings.Auth.Server+"/v1/request/azure", bytes.NewBuffer(data), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Azure server error") if err != nil { return } athData := &authData{} err = json.NewDecoder(resp.Body).Decode(athData) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "auth: Failed to parse auth response", ), } return } tokn := &Token{ Id: state, Type: Azure, Secret: secret, Timestamp: time.Now(), Provider: provider.Id, Query: query, } _, err = coll.InsertOne(db, tokn) if err != nil { err = database.ParseError(err) return } redirect = athData.Url return } type azureTokenData struct { AccessToken string `json:"access_token"` Resource string `json:"resource"` TokenType string `json:"token_type"` } type azureMemberData struct { NextLink string `json:"@odata.nextLink"` Value []azureGroupData `json:"value"` } type azureUserData struct { Id string `json:"id"` UserPrincipalName string `json:"userPrincipalName"` AccountEnabled bool `json:"accountEnabled"` } type azureGroupData struct { DisplayName string `json:"displayName"` } func azureGetToken(provider *settings.Provider) (token string, err error) { loginUrl, graphUrl := azureGetUrls(provider) reqForm := url.Values{} reqForm.Add("grant_type", "client_credentials") reqForm.Add("client_id", provider.ClientId) reqForm.Add("client_secret", provider.ClientSecret) reqForm.Add("resource", graphUrl) req, err := http.NewRequest( "POST", fmt.Sprintf( "%s/%s/oauth2/token", loginUrl, provider.Tenant, ), strings.NewReader(reqForm.Encode()), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create azure request"), } return } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Azure request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Azure server error") if err != nil { return } tokenData := &azureTokenData{} err = json.NewDecoder(resp.Body).Decode(tokenData) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse response"), } return } token = tokenData.AccessToken return } func AzureRoles(provider *settings.Provider, username string) ( roles []string, err error) { _, graphUrl := azureGetUrls(provider) userId, active, err := AzureSync(provider, username) if err != nil { return } if !active { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Azure sync user disabled"), } return } if userId == "" { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Azure sync missing user ID"), } return } roles = []string{} token, err := azureGetToken(provider) if err != nil { return } reqUrlStr := fmt.Sprintf( "%s/v1.0/users/%s/memberOf", graphUrl, userId, ) start := time.Now() for { reqUrl, e := url.Parse(reqUrlStr) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "auth: Failed to parse azure url"), } return } reqData, e := json.Marshal(struct { SecurityEnabledOnly string `json:"securityEnabledOnly"` }{ SecurityEnabledOnly: "false", }) if e != nil { err = e return } req, e := http.NewRequest( "GET", reqUrl.String(), bytes.NewBuffer(reqData), ) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "auth: Failed to create azure request"), } return } req.Header.Add("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "auth: Azure request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Azure server error") if err != nil { return } data := &azureMemberData{} err = json.NewDecoder(resp.Body).Decode(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse response"), } return } for _, groupData := range data.Value { groupName := groupData.DisplayName if groupName == "" { continue } roles = append(roles, groupName) } if data.NextLink != "" { reqUrlStr = data.NextLink } else { break } if time.Since(start) > 45*time.Second { err = &errortypes.RequestError{ errors.New("auth: Azure group paging timeout"), } return } } return } func AzureSync(provider *settings.Provider, username string) ( userId string, active bool, err error) { _, graphUrl := azureGetUrls(provider) token, err := azureGetToken(provider) if err != nil { return } reqUrl, err := url.Parse(fmt.Sprintf( "%s/v1.0/%s/users/%s", graphUrl, provider.Tenant, url.QueryEscape(username), )) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse azure url"), } return } query := reqUrl.Query() query.Set("$select", "id,userPrincipalName,accountEnabled") reqUrl.RawQuery = query.Encode() req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create azure request"), } return } req.Header.Add("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Azure request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Azure server error") if err != nil { return } data := &azureUserData{} err = json.NewDecoder(resp.Body).Decode(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse response"), } return } if strings.ToLower(username) != strings.ToLower( data.UserPrincipalName) { err = &errortypes.ApiError{ errors.Wrapf( err, "auth: Azure principal name '%s' does not match user '%s'", data.UserPrincipalName, username, ), } return } userId = data.Id active = data.AccountEnabled return } ================================================ FILE: auth/constants.go ================================================ package auth const ( Admin = "admin" User = "user" ) ================================================ FILE: auth/errortypes.go ================================================ package auth import ( "github.com/dropbox/godropbox/errors" ) type InvalidState struct { errors.DropboxError } ================================================ FILE: auth/google.go ================================================ package auth import ( "bytes" "encoding/json" "net/http" "net/url" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" "golang.org/x/oauth2" "golang.org/x/oauth2/google" admin "google.golang.org/api/admin/directory/v1" ) const ( Google = "google" ) func GoogleRequest(db *database.Database, location, query string) ( redirect string, err error) { coll := db.Tokens() state, err := utils.RandStr(64) if err != nil { return } secret, err := utils.RandStr(64) if err != nil { return } data, err := json.Marshal(struct { License string `json:"license"` Callback string `json:"callback"` State string `json:"state"` Secret string `json:"secret"` }{ License: settings.System.License, Callback: location + "/auth/callback", State: state, Secret: secret, }) if err != nil { return } req, err := http.NewRequest( "POST", settings.Auth.Server+"/v1/request/google", bytes.NewBuffer(data), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } authData := &authData{} err = json.NewDecoder(resp.Body).Decode(authData) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "auth: Failed to parse auth response", ), } return } tokn := &Token{ Id: state, Type: Google, Secret: secret, Timestamp: time.Now(), Query: query, } _, err = coll.InsertOne(db, tokn) if err != nil { err = database.ParseError(err) return } redirect = authData.Url return } func GoogleRoles(provider *settings.Provider, username string) ( roles []string, err error) { roles = []string{} if provider.GoogleKey == "" && provider.GoogleEmail == "" { return } conf, err := google.JWTConfigFromJSON( []byte(provider.GoogleKey), "https://www.googleapis.com/auth/admin.directory.group.readonly", ) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "auth: Failed to parse google key", ), } return } conf.Subject = provider.GoogleEmail client := conf.Client(oauth2.NoContext) service, err := admin.New(client) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "auth: Failed to parse google client", ), } return } results, err := service.Groups.List().UserKey(username).Do() if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "auth: Google api error getting user groups", ), } return } for _, group := range results.Groups { roles = append(roles, group.Name) } return } func GoogleSync(db *database.Database, usr *user.User) ( active bool, err error) { reqVals := url.Values{} reqVals.Set("user", usr.Username) reqVals.Set("license", settings.System.License) reqUrl, _ := url.Parse(settings.Auth.Server + "/update/google") reqUrl.RawQuery = reqVals.Encode() req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Google request failed"), } return } resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Google request failed"), } return } defer resp.Body.Close() if resp.StatusCode == 200 { active = true } else { err = &errortypes.RequestError{ errors.Newf("auth: Google request bad status %d", resp.StatusCode), } return } return } ================================================ FILE: auth/handler.go ================================================ package auth import ( "crypto/hmac" "crypto/sha512" "crypto/subtle" "encoding/base64" "net/url" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" ) func Local(db *database.Database, username, password string) ( usr *user.User, errData *errortypes.ErrorData, err error) { username = strings.ToLower(username) if username == "" { errData = &errortypes.ErrorData{ Error: "auth_invalid", Message: "Authentication credentials are invalid", } return } usr, err = user.GetUsername(db, user.Local, username) if err != nil { switch err.(type) { case *database.NotFoundError: usr = nil err = nil errData = &errortypes.ErrorData{ Error: "auth_invalid", Message: "Authentication credentials are invalid", } break } return } valid := usr.CheckPassword(password) if !valid { errData = &errortypes.ErrorData{ Error: "auth_invalid", Message: "Authentication credentials are invalid", } return } return } func Request(c *gin.Context, typ string) { db := c.MustGet("db").(*database.Database) domains := []string{} switch typ { case Admin: domains = append(domains, node.Self.AdminDomain) case User: domains = append(domains, node.Self.UserDomain) } loc := utils.GetLocation(c.Request, domains) if loc == "" { err := &errortypes.ParseError{ errors.New("auth: Missing domains in node settings"), } utils.AbortWithError(c, 500, err) return } id := c.Query("id") vals := c.Request.URL.Query() vals.Del("id") query := vals.Encode() if id == Google { redirect, err := GoogleRequest(db, loc, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, redirect) return } else { providerId, err := bson.ObjectIDFromHex(id) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: ObjectId parse error"), } return } var provider *settings.Provider for _, prvidr := range settings.Auth.Providers { if prvidr.Id == providerId { provider = prvidr break } } if provider == nil { utils.AbortWithStatus(c, 404) return } switch provider.Type { case Azure: redirect, err := AzureRequest(db, loc, query, provider) if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, redirect) return case AuthZero: redirect, err := AuthZeroRequest(db, loc, query, provider) if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, redirect) return case OneLogin, Okta, JumpCloud: body, err := SamlRequest(db, loc, query, provider) if err != nil { utils.AbortWithError(c, 500, err) return } c.Data(200, "text/html;charset=utf-8", body) return } } utils.AbortWithStatus(c, 404) } func Callback(db *database.Database, sig, query string) ( usr *user.User, tokn *Token, errAudit audit.Fields, errData *errortypes.ErrorData, err error) { params, err := url.ParseQuery(query) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse query"), } return } state := params.Get("state") tokn, err = Get(db, state) if err != nil { switch err.(type) { case *database.NotFoundError: err = &InvalidState{ errors.Wrap(err, "auth: Invalid state"), } break } return } if tokn.Secret == "" { err = &errortypes.ReadError{ errors.Wrap(err, "auth: Empty secret"), } return } hashFunc := hmac.New(sha512.New, []byte(tokn.Secret)) hashFunc.Write([]byte(query)) rawSignature := hashFunc.Sum(nil) testSig := base64.URLEncoding.EncodeToString(rawSignature) if subtle.ConstantTimeCompare([]byte(sig), []byte(testSig)) != 1 { errAudit = audit.Fields{ "error": "signature_mismatch", "message": "Signature hash does not match", } errData = &errortypes.ErrorData{ Error: "authentication_error", Message: "Authentication error occurred", } return } username := strings.ToLower(params.Get("username")) if username == "" { errAudit = audit.Fields{ "error": "invalid_username", "message": "Invalid username", } errData = &errortypes.ErrorData{ Error: "invalid_username", Message: "Invalid username", } return } var provider *settings.Provider if tokn.Type == Google { domainSpl := strings.SplitN(username, "@", 2) if len(domainSpl) == 2 { domain := domainSpl[1] if domain != "" { for _, prv := range settings.Auth.Providers { if prv.Type == Google && prv.Domain == domain { provider = prv break } } } } if provider == nil { errAudit = audit.Fields{ "error": "provider_unavailable", "message": "Google provider is unavailable", } errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } return } } else { provider = settings.Auth.GetProvider(tokn.Provider) if provider == nil { err = &errortypes.NotFoundError{ errors.New("auth: Auth provider not found"), } return } } if provider.Type == Azure { usernameSpl := strings.SplitN(username, "/", 2) if len(usernameSpl) != 2 { errAudit = audit.Fields{ "error": "invalid_username", "message": "Azure username missing tenant", } errData = &errortypes.ErrorData{ Error: "invalid_username", Message: "Invalid username", } return } tenant := usernameSpl[0] username = usernameSpl[1] if tenant != provider.Tenant { errAudit = audit.Fields{ "error": "invalid_tenant", "message": "Azure tenant mismatch", } errData = &errortypes.ErrorData{ Error: "invalid_tenant", Message: "Invalid tenant", } return } } err = tokn.Remove(db) if err != nil { return } roles := []string{} roles = append(roles, provider.DefaultRoles...) roleParam := params.Get("roles") if roleParam == "" { roleParam = params.Get("groups") } splitChar := "," if strings.Contains(roleParam, ";") { splitChar = ";" } for _, role := range strings.Split(roleParam, splitChar) { if role != "" { roles = append(roles, role) } } switch provider.Type { case Google: googleRoles, e := GoogleRoles(provider, username) if e != nil { err = e return } for _, role := range googleRoles { roles = append(roles, role) } break case AuthZero: authZeroRoles, e := AuthZeroRoles(provider, username) if e != nil { err = e return } for _, role := range authZeroRoles { roles = append(roles, role) } break case Azure: azureRoles, e := AzureRoles(provider, username) if e != nil { err = e return } for _, role := range azureRoles { roles = append(roles, role) } break } usr, err = user.GetUsername(db, provider.Type, username) if err != nil { switch err.(type) { case *database.NotFoundError: usr = nil err = nil break default: return } } if usr == nil { if provider.AutoCreate { usr = &user.User{ Type: provider.Type, Username: username, Provider: provider.Id, Roles: roles, } err = usr.Upsert(db) if err != nil { return } event.PublishDispatch(db, "user.change") errData, err = usr.Validate(db) if err != nil { return } if errData != nil { return } } else { errAudit = audit.Fields{ "error": "user_unavailable", "message": "User does not exist with auto create false", } errData = &errortypes.ErrorData{ Error: "user_unavailable", Message: "Not authorized", } return } } else { fields := set.NewSet("provider") usr.Provider = provider.Id switch provider.RoleManagement { case settings.Merge: changed := usr.RolesMerge(roles) if changed { errData, err = usr.Validate(db) if err != nil { return } if errData != nil { return } fields.Add("roles") } break case settings.Overwrite: changed := usr.RolesOverwrite(roles) if changed { errData, err = usr.Validate(db) if err != nil { return } if errData != nil { return } fields.Add("roles") } break } err = usr.CommitFields(db, fields) if err != nil { return } event.PublishDispatch(db, "user.change") } return } ================================================ FILE: auth/jumpcloud.go ================================================ package auth import ( "encoding/json" "fmt" "net/http" "net/url" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) const ( JumpCloud = "jumpcloud" ) type jumpcloudResponse struct { Results []*jumpcloudUser `json:"results"` TotalCount int `json:"totalCount"` } type jumpcloudUser struct { Id string `json:"id"` IdAlt string `json:"_id"` Email string `json:"email"` AccountLocked bool `json:"account_locked"` Suspended bool `json:"suspended"` Activated bool `json:"activated"` } type jumpcloudApp struct { Id string `json:"id"` } func jumpcloudCheckApp(provider *settings.Provider, userId string) ( attached bool, err error) { reqUrl := &url.URL{ Scheme: "https", Host: "console.jumpcloud.com", Path: fmt.Sprintf("/api/v2/users/%s/applications", userId), } req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create jumpcloud request"), } return } req.Header.Add("Accept", "application/json") req.Header.Add("X-Api-Key", provider.JumpCloudSecret) resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Jumpcloud request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } data := []*jumpcloudApp{} err = json.NewDecoder(resp.Body).Decode(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse jumpcloud response"), } return } for _, app := range data { if app.Id == provider.JumpCloudAppId { attached = true return } } return } func JumpcloudSync(db *database.Database, usr *user.User, provider *settings.Provider) (active bool, err error) { reqUrl := &url.URL{ Scheme: "https", Host: "console.jumpcloud.com", Path: "/api/systemusers", } query := reqUrl.Query() query.Set("filter", fmt.Sprintf("email:$eq:%s", usr.Username)) reqUrl.RawQuery = query.Encode() req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Failed to create jumpcloud request"), } return } req.Header.Add("Accept", "application/json") req.Header.Add("X-Api-Key", provider.JumpCloudSecret) resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Jumpcloud request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } data := &jumpcloudResponse{} err = json.NewDecoder(resp.Body).Decode(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "auth: Failed to parse jumpcloud response"), } return } userId := "" if data.TotalCount > 0 && data.Results != nil { for _, authUser := range data.Results { if authUser.Email != usr.Username { continue } if authUser.AccountLocked || authUser.Suspended || !authUser.Activated { logrus.WithFields(logrus.Fields{ "user_id": usr.Id.Hex(), "username": usr.Username, }).Info("auth: Jumpcloud user disabled") return } else { if authUser.Id != "" { userId = authUser.Id } else { userId = authUser.IdAlt } break } } } if userId == "" { err = &errortypes.NotFoundError{ errors.Wrap(err, "auth: Jumpcloud user not found"), } return } if provider.JumpCloudAppId != "" { attached, e := jumpcloudCheckApp(provider, userId) if e != nil { err = e return } if attached { active = true } else { logrus.WithFields(logrus.Fields{ "user_id": usr.Id.Hex(), "username": usr.Username, }).Info("auth: Jumpcloud user not bound to application") return } } else { active = true } return } ================================================ FILE: auth/saml.go ================================================ package auth import ( "bytes" "encoding/json" "io/ioutil" "net/http" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) const ( OneLogin = "onelogin" Okta = "okta" ) func SamlRequest(db *database.Database, location, query string, provider *settings.Provider) (body []byte, err error) { if provider.Type != OneLogin && provider.Type != Okta && provider.Type != JumpCloud { err = &errortypes.ParseError{ errors.New("auth: Invalid provider type"), } return } coll := db.Tokens() state, err := utils.RandStr(64) if err != nil { return } secret, err := utils.RandStr(64) if err != nil { return } data, err := json.Marshal(struct { License string `json:"license"` Callback string `json:"callback"` State string `json:"state"` Secret string `json:"secret"` SsoUrl string `json:"sso_url"` IssuerUrl string `json:"issuer_url"` Cert string `json:"cert"` }{ License: settings.System.License, Callback: location + "/auth/callback", State: state, Secret: secret, SsoUrl: provider.SamlUrl, IssuerUrl: provider.IssuerUrl, Cert: provider.SamlCert, }) req, err := http.NewRequest( "POST", settings.Auth.Server+"/v1/request/saml", bytes.NewBuffer(data), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "auth: Auth request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "auth: Auth server error") if err != nil { return } body, err = ioutil.ReadAll(resp.Body) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "auth: Failed to parse auth response", ), } return } tokn := &Token{ Id: state, Type: provider.Type, Secret: secret, Timestamp: time.Now(), Provider: provider.Id, Query: query, } _, err = coll.InsertOne(db, tokn) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: auth/state.go ================================================ package auth import ( "fmt" "sort" "github.com/pritunl/pritunl-cloud/settings" ) type StateProvider struct { Id interface{} `json:"id"` Type string `json:"type"` Label string `json:"label"` } type StateProviders []*StateProvider func (s StateProviders) Len() int { return len(s) } func (s StateProviders) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s StateProviders) Less(i, j int) bool { return s[i].Label < s[j].Label } type State struct { Providers StateProviders `json:"providers"` } func GetState() (state *State) { state = &State{ Providers: StateProviders{}, } if !settings.Local.NoLocalAuth { prv := &StateProvider{ Type: "local", } state.Providers = append(state.Providers, prv) } google := false for _, provider := range settings.Auth.Providers { prv := &StateProvider{ Type: provider.Type, Label: provider.Label, } if provider.Type == Google { if google { continue } google = true prv.Id = Google } else { prv.Id = provider.Id } state.Providers = append(state.Providers, prv) } sort.Sort(state.Providers) return } func GetFastAdminPath() (path string) { if !settings.Local.NoLocalAuth || !settings.Auth.FastLogin || len(settings.Auth.Providers) == 0 { return } if len(settings.Auth.Providers) > 1 { googleOnly := true for _, provider := range settings.Auth.Providers { if provider.Type != Google { googleOnly = false break } } if !googleOnly { return } } for _, provider := range settings.Auth.Providers { if provider.Type == Google { path = fmt.Sprintf("/auth/request?id=%s", Google) } else { path = fmt.Sprintf("/auth/request?id=%s", provider.Id.Hex()) } return } return } func GetFastUserPath() (path string) { if settings.Auth.FastLogin && settings.Auth.ForceFastUserLogin && len(settings.Auth.Providers) != 0 { } else if !settings.Local.NoLocalAuth || !settings.Auth.FastLogin || len(settings.Auth.Providers) == 0 { return } if len(settings.Auth.Providers) > 1 { googleOnly := true for _, provider := range settings.Auth.Providers { if provider.Type != Google { googleOnly = false break } } if !googleOnly { return } } for _, provider := range settings.Auth.Providers { if provider.Type == Google { path = fmt.Sprintf("/auth/request?id=%s", Google) } else { path = fmt.Sprintf("/auth/request?id=%s", provider.Id.Hex()) } return } return } ================================================ FILE: auth/sync.go ================================================ package auth import ( "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" ) func SyncUser(db *database.Database, usr *user.User) ( active bool, err error) { if time.Since(usr.LastSync) < time.Duration( settings.Auth.Sync)*time.Second { active = true return } provider := settings.Auth.GetProvider(usr.Provider) if usr.Type == user.AuthZero && provider != nil && provider.Type == user.AuthZero { active, err = AuthZeroSync(db, usr, provider) if err != nil { return } } else if usr.Type == user.Azure && provider != nil && provider.Type == user.Azure { _, active, err = AzureSync(provider, usr.Username) if err != nil { return } } else if usr.Type == user.Google { active, err = GoogleSync(db, usr) if err != nil { return } } else if usr.Type == user.JumpCloud { active, err = JumpcloudSync(db, usr, provider) if err != nil { return } } else { active = true } if active { usr.LastSync = time.Now() err = usr.CommitFields(db, set.NewSet("last_sync")) if err != nil { return } } return } ================================================ FILE: auth/utils.go ================================================ package auth import ( "fmt" "net/http" "net/url" "github.com/pritunl/pritunl-cloud/cookie" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, state string) (tokn *Token, err error) { coll := db.Tokens() tokn = &Token{} err = coll.FindOneId(state, tokn) if err != nil { return } return } func CookieSessionAdmin(db *database.Database, w http.ResponseWriter, r *http.Request) ( cook *cookie.Cookie, sess *session.Session, err error) { cook, err = cookie.GetAdmin(w, r) if err != nil { sess = nil err = nil return } sess, err = cook.GetSession(db, r, session.Admin) if err != nil { switch err.(type) { case *errortypes.NotFoundError: sess = nil err = nil break } return } return } func CookieSessionUser(db *database.Database, w http.ResponseWriter, r *http.Request) (cook *cookie.Cookie, sess *session.Session, err error) { cook, err = cookie.GetUser(w, r) if err != nil { sess = nil err = nil return } sess, err = cook.GetSession(db, r, session.User) if err != nil { switch err.(type) { case *errortypes.NotFoundError: sess = nil err = nil break } return } return } func CsrfCheck(w http.ResponseWriter, r *http.Request, domain string) bool { port := "" if node.Self.Protocol == "http" { if node.Self.Port != 80 { port += fmt.Sprintf(":%d", node.Self.Port) } } else { if node.Self.Port != 443 { port += fmt.Sprintf(":%d", node.Self.Port) } } match := fmt.Sprintf("http://%s%s", domain, port) matchSec := fmt.Sprintf("https://%s%s", domain, port) origin := r.Header.Get("Origin") if origin != "" { u, err := url.Parse(origin) if err != nil { utils.WriteUnauthorized(w, "CSRF origin invalid") return false } origin = fmt.Sprintf("%s://%s", u.Scheme, u.Host) } if origin != "" && origin != match && origin != matchSec { utils.WriteUnauthorized(w, "CSRF origin error") return false } return true } ================================================ FILE: authority/authority.go ================================================ package authority import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Authority struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Type string `bson:"type" json:"type"` Organization bson.ObjectID `bson:"organization" json:"organization"` Roles []string `bson:"roles" json:"roles"` Key string `bson:"key" json:"key"` Principals []string `bson:"principals" json:"principals"` Certificate string `bson:"certificate" json:"certificate"` } func (f *Authority) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { f.Name = utils.FilterName(f.Name) if f.Roles == nil { f.Roles = []string{} } if f.Principals == nil { f.Principals = []string{} } if f.Type == "" { f.Type = SshKey } switch f.Type { case SshKey: f.Principals = []string{} f.Certificate = "" break case SshCertificate: f.Key = "" break } return } func (f *Authority) Commit(db *database.Database) (err error) { coll := db.Authorities() err = coll.Commit(f.Id, f) if err != nil { return } return } func (f *Authority) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Authorities() err = coll.CommitFields(f.Id, f, fields) if err != nil { return } return } func (f *Authority) Insert(db *database.Database) (err error) { coll := db.Authorities() if !f.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("authority: Authority already exists"), } return } resp, err := coll.InsertOne(db, f) if err != nil { err = database.ParseError(err) return } f.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: authority/constants.go ================================================ package authority import "github.com/pritunl/mongo-go-driver/v2/bson" const ( SshKey = "ssh_key" SshCertificate = "ssh_certificate" ) var ( Global = bson.NilObjectID ) ================================================ FILE: authority/utils.go ================================================ package authority import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, authrId bson.ObjectID) ( authr *Authority, err error) { coll := db.Authorities() authr = &Authority{} err = coll.FindOneId(authrId, authr) if err != nil { return } return } func GetOrg(db *database.Database, orgId, authrId bson.ObjectID) ( authr *Authority, err error) { coll := db.Authorities() authr = &Authority{} err = coll.FindOne(db, &bson.M{ "_id": authrId, "organization": orgId, }).Decode(authr) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( authrs []*Authority, err error) { coll := db.Authorities() authrs = []*Authority{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { authr := &Authority{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } authrs = append(authrs, authr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetRoles(db *database.Database, roles []string) ( authrs []*Authority, err error) { coll := db.Authorities() authrs = []*Authority{} cursor, err := coll.Find(db, &bson.M{ "organization": Global, "roles": &bson.M{ "$in": roles, }, }, options.Find().SetSort(&bson.D{ {"_id", 1}, })) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { authr := &Authority{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } authrs = append(authrs, authr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetMapRoles(db *database.Database, query *bson.M) ( authrs map[string][]*Authority, err error) { coll := db.Authorities() authrs = map[string][]*Authority{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { authr := &Authority{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } for _, role := range authr.Roles { roleAuthrs := authrs[role] if roleAuthrs == nil { roleAuthrs = []*Authority{} } authrs[role] = append(roleAuthrs, authr) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOrgMapRoles(db *database.Database, orgId bson.ObjectID) ( authrs map[string][]*Authority, err error) { coll := db.Authorities() authrs = map[string][]*Authority{} cursor, err := coll.Find(db, &bson.M{ "organization": orgId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { authr := &Authority{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } for _, role := range authr.Roles { roleAuthrs := authrs[role] if roleAuthrs == nil { roleAuthrs = []*Authority{} } authrs[role] = append(roleAuthrs, authr) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOrgRoles(db *database.Database, orgId bson.ObjectID, roles []string) (authrs []*Authority, err error) { coll := db.Authorities() authrs = []*Authority{} cursor, err := coll.Find(db, &bson.M{ "organization": orgId, "roles": &bson.M{ "$in": roles, }, }, options.Find().SetSort(&bson.D{ {"_id", 1}, })) defer cursor.Close(db) for cursor.Next(db) { authr := &Authority{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } authrs = append(authrs, authr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( authrs []*database.Named, err error) { coll := db.Authorities() authrs = []*database.Named{} cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetProjection(&bson.D{ {"name", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { authr := &database.Named{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } authrs = append(authrs, authr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (authrs []*Authority, count int64, err error) { coll := db.Authorities() authrs = []*Authority{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { authr := &Authority{} err = cursor.Decode(authr) if err != nil { err = database.ParseError(err) return } authrs = append(authrs, authr) authr = &Authority{} } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, authrId bson.ObjectID) (err error) { coll := db.Authorities() _, err = coll.DeleteOne(db, &bson.M{ "_id": authrId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, authrId bson.ObjectID) ( err error) { coll := db.Authorities() _, err = coll.DeleteOne(db, &bson.M{ "_id": authrId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, authrIds []bson.ObjectID) (err error) { coll := db.Authorities() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": authrIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, authrIds []bson.ObjectID) (err error) { coll := db.Authorities() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": authrIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: authorizer/authorizer.go ================================================ package authorizer import ( "net/http" "github.com/pritunl/pritunl-cloud/cookie" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/signature" "github.com/pritunl/pritunl-cloud/user" ) type Authorizer struct { typ string cook *cookie.Cookie sess *session.Session sig *signature.Signature usr *user.User } func (a *Authorizer) IsApi() bool { return a.sig != nil } func (a *Authorizer) IsValid() bool { return a.sess != nil || a.sig != nil } func (a *Authorizer) AddSignature(db *database.Database, sig *signature.Signature) (err error) { err = sig.Validate(db) if err != nil { return } a.sig = sig return } func (a *Authorizer) AddCookie(cook *cookie.Cookie, sess *session.Session) (err error) { a.cook = cook a.sess = sess return } func (a *Authorizer) Clear(db *database.Database, w http.ResponseWriter, r *http.Request) (err error) { a.sess = nil a.sig = nil if a.cook != nil { err = a.cook.Remove(db) if err != nil { return } } switch a.typ { case Admin: cookie.CleanAdmin(w, r) break case User: cookie.CleanUser(w, r) break } return } func (a *Authorizer) Remove(db *database.Database) error { if a.sess == nil { return nil } return a.sess.Remove(db) } func (a *Authorizer) GetUser(db *database.Database) ( usr *user.User, err error) { if a.sess != nil { if a.usr != nil { usr = a.usr return } if db != nil { usr, err = a.sess.GetUser(db) if err != nil { switch err.(type) { case *database.NotFoundError: usr = nil err = nil break default: return } } } if usr == nil { a.sess = nil } else { a.usr = usr } } else if a.sig != nil { if a.usr != nil { usr = a.usr return } if db != nil { usr, err = a.sig.GetUser(db) if err != nil { switch err.(type) { case *database.NotFoundError: usr = nil err = nil break default: return } } } if usr == nil { a.sig = nil } else { a.usr = usr } } return } func (a *Authorizer) GetSession() *session.Session { return a.sess } func (a *Authorizer) SessionId() string { if a.sess != nil { return a.sess.Id } return "" } ================================================ FILE: authorizer/constants.go ================================================ package authorizer const ( Admin = "admin" User = "user" ) ================================================ FILE: authorizer/utils.go ================================================ package authorizer import ( "net/http" "github.com/pritunl/pritunl-cloud/auth" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/signature" ) func AuthorizeAdmin(db *database.Database, w http.ResponseWriter, r *http.Request) (authr *Authorizer, err error) { authr = NewAdmin() token := r.Header.Get("Auth-Token") sigStr := r.Header.Get("Auth-Signature") if token != "" && sigStr != "" { timestamp := r.Header.Get("Auth-Timestamp") nonce := r.Header.Get("Auth-Nonce") sig, e := signature.Parse( token, sigStr, timestamp, nonce, r.Method, r.URL.Path, ) if e != nil { err = e return } err = authr.AddSignature(db, sig) if err != nil { return } } else { cook, sess, e := auth.CookieSessionAdmin(db, w, r) if e != nil { err = e return } err = authr.AddCookie(cook, sess) if err != nil { return } } return } func AuthorizeUser(db *database.Database, w http.ResponseWriter, r *http.Request) (authr *Authorizer, err error) { authr = NewUser() token := r.Header.Get("Auth-Token") sigStr := r.Header.Get("Auth-Signature") if token != "" && sigStr != "" { timestamp := r.Header.Get("Auth-Timestamp") nonce := r.Header.Get("Auth-Nonce") sig, e := signature.Parse( token, sigStr, timestamp, nonce, r.Method, r.URL.Path, ) if e != nil { err = e return } err = authr.AddSignature(db, sig) if err != nil { return } } else { cook, sess, e := auth.CookieSessionUser(db, w, r) if e != nil { err = e return } err = authr.AddCookie(cook, sess) if err != nil { return } } return } func NewAdmin() (authr *Authorizer) { authr = &Authorizer{ typ: Admin, } return } func NewUser() (authr *Authorizer) { authr = &Authorizer{ typ: User, } return } ================================================ FILE: backup/backup.go ================================================ package backup import ( "fmt" "io/ioutil" "os" "path" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) type Backup struct { Destination string node *node.Node virtPath string errorCount int } func (b *Backup) backupDisk(db *database.Database, dsk *disk.Disk, dest string) (err error) { online := false if !dsk.Instance.IsZero() { inst, e := instance.Get(db, dsk.Instance) if e != nil { err = e return } if inst != nil { if inst.State == vm.Starting { time.Sleep(5 * time.Second) online = true } if inst.State == vm.Running { online = true } } } _ = os.Remove(dest) if online { err = qmp.BackupDisk(dsk.Instance, dsk, dest) if err != nil { if _, ok := err.(*qmp.DiskNotFound); ok { online = false err = nil } else { return } } } if !online { dskPth := path.Join(b.virtPath, "disks", fmt.Sprintf("%s.qcow2", dsk.Id.Hex())) err = utils.Exec("", "cp", dskPth, dest) if err != nil { return } } return } func (b *Backup) backupDisks(db *database.Database) (err error) { logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), }).Info("backup: Exporting disks") trashDir := path.Join(b.Destination, "trash") disksDir := path.Join(b.Destination, "disks") err = utils.ExistsMkdir(disksDir, 0755) if err != nil { return } disks, err := disk.GetAll(db, &bson.M{ "node": b.node.Id, }) diskFilenames := set.NewSet() for _, dsk := range disks { filename := fmt.Sprintf("%s.qcow2", dsk.Id.Hex()) diskFilenames.Add(filename) destPath := path.Join(disksDir, filename) err = b.backupDisk(db, dsk, destPath) if err != nil { b.errorCount += 1 logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "error": err, }).Error("qemu: Failed to backup disk") } else { logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), "disk_id": dsk.Id.Hex(), }).Info("backup: Disk exported") } } exportedDisks, err := ioutil.ReadDir(disksDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "backup: Failed to read disks directory"), } return } trashDiskDir := path.Join(trashDir, "disks") for _, item := range exportedDisks { filename := item.Name() diskPath := path.Join(disksDir, filename) newDiskPath := path.Join(trashDiskDir, filename) idStr := strings.Split(path.Base(filename), ".")[0] if !diskFilenames.Contains(filename) { err = utils.ExistsMkdir(trashDiskDir, 0755) if err != nil { return } err = os.Rename(diskPath, newDiskPath) if err != nil { b.errorCount += 1 logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), "disk_id": idStr, "error": err, }).Error("backup: Failed to move disk to trash") } else { logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), "disk_id": idStr, }).Info("backup: Disk moved to trash") } } } return } func (b *Backup) backupBackingDisks(db *database.Database) (err error) { logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), }).Info("backup: Exporting backing disks") trashDir := path.Join(b.Destination, "trash") backingDisksDir := path.Join(b.Destination, "backing") curBackingDisksDir := path.Join(b.virtPath, "backing") err = utils.ExistsMkdir(backingDisksDir, 0755) if err != nil { return } exists, err := utils.Exists(curBackingDisksDir) if err != nil { return } curBackingDisks := []os.FileInfo{} if exists { curBackingDisks, err = ioutil.ReadDir(curBackingDisksDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap( err, "backup: Failed to read backing disks directory", ), } return } } backingFilenames := set.NewSet() for _, item := range curBackingDisks { filename := item.Name() backingFilenames.Add(filename) backingPath := path.Join(curBackingDisksDir, filename) newBackingPath := path.Join(backingDisksDir, filename) _ = os.Remove(newBackingPath) err = utils.Exec("", "cp", backingPath, newBackingPath) if err != nil { return } logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), "backing_disk": filename, }).Info("backup: Backing disk exported") } exportedBackingDisks, err := ioutil.ReadDir(backingDisksDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap( err, "backup: Failed to read backing disks directory", ), } return } trashBackingDir := path.Join(trashDir, "backing") for _, item := range exportedBackingDisks { filename := item.Name() backingPath := path.Join(backingDisksDir, filename) newBackingPath := path.Join(trashBackingDir, filename) if !backingFilenames.Contains(filename) { err = utils.ExistsMkdir(trashBackingDir, 0755) if err != nil { return } err = os.Rename(backingPath, newBackingPath) if err != nil { b.errorCount += 1 logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), "backing_disk": filename, "error": err, }).Error("backup: Failed to move backing disk to trash") } else { logrus.WithFields(logrus.Fields{ "node_id": b.node.Id.Hex(), "backing_disk": filename, }).Info("backup: Backing disk moved to trash") } } } return } func (b *Backup) Run() (err error) { db := database.GetDatabase() defer db.Close() ndeId, err := bson.ObjectIDFromHex(config.Config.NodeId) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "backup: Failed to parse ObjectId"), } return } nde, err := node.Get(db, ndeId) if err != nil { return } b.node = nde b.virtPath = nde.GetVirtPath() err = b.backupDisks(db) if err != nil { return } err = b.backupBackingDisks(db) if err != nil { return } if b.errorCount > 0 { err = &errortypes.ExecError{ errors.Wrap(err, "backup: Backup encountered errors"), } return } return } func New(dest string) *Backup { return &Backup{ Destination: dest, } } ================================================ FILE: balancer/balancer.go ================================================ package balancer import ( "fmt" "net" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" ) type Domain struct { Domain string `bson:"domain" json:"domain"` Host string `bson:"host" json:"host"` } type Backend struct { Protocol string `bson:"protocol" json:"protocol"` Hostname string `bson:"hostname" json:"hostname"` Port int `bson:"port" json:"port"` } type State struct { Timestamp time.Time `bson:"timestamp" json:"timestamp"` Requests int `bson:"requests" json:"requests"` Retries int `bson:"retries" json:"retries"` WebSockets int `bson:"websockets" json:"websockets"` Online []string `bson:"online" json:"online"` UnknownHigh []string `bson:"unknown_high" json:"unknown_high"` UnknownMid []string `bson:"unknown_mid" json:"unknown_mid"` UnknownLow []string `bson:"unknown_low" json:"unknown_low"` Offline []string `bson:"offline" json:"offline"` } type Balancer struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Type string `bson:"type" json:"type"` State bool `bson:"state" json:"state"` Organization bson.ObjectID `bson:"organization" json:"organization"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Certificates []bson.ObjectID `bson:"certificates" json:"certificates"` ClientAuthority bson.ObjectID `bson:"client_authority" json:"client_authority"` WebSockets bool `bson:"websockets" json:"websockets"` Domains []*Domain `bson:"domains" json:"domains"` Backends []*Backend `bson:"backends" json:"backends"` States map[string]*State `bson:"states" json:"states"` CheckPath string `bson:"check_path" json:"check_path"` } func (b *Balancer) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { b.Name = utils.FilterName(b.Name) if b.Type == "" { b.Type = Http } if b.Domains == nil { b.Domains = []*Domain{} } domains := []*Domain{} for _, domain := range b.Domains { domain.Domain = utils.FilterDomain(domain.Domain) domain.Host = utils.FilterDomain(domain.Host) if domain.Domain == "" { continue } domains = append(domains, domain) } b.Domains = domains if b.Backends == nil { b.Backends = []*Backend{} } if b.Certificates == nil { b.Certificates = []bson.ObjectID{} } if b.States == nil { b.States = map[string]*State{} } for _, backend := range b.Backends { if backend.Protocol != "http" && backend.Protocol != "https" { errData = &errortypes.ErrorData{ Error: "balancer_protocol_invalid", Message: "Invalid balancer backend protocol", } return } if backend.Hostname == "" { errData = &errortypes.ErrorData{ Error: "balancer_hostname_invalid", Message: "Invalid balancer backend hostname", } return } if backend.Port < 1 || backend.Port > 65535 { errData = &errortypes.ErrorData{ Error: "balancer_port_invalid", Message: "Invalid balancer backend port", } return } ip := net.ParseIP(backend.Hostname) if ip == nil { errData = &errortypes.ErrorData{ Error: "balancer_hostname_invalid", Message: fmt.Sprintf("Balancer hostname '%s' must "+ "match existing instance address", backend.Hostname), } return } backend.Hostname = ip.String() exists, e := instance.ExistsIp(db, backend.Hostname) if e != nil { err = e return } if !exists { errData = &errortypes.ErrorData{ Error: "balancer_hostname_not_found", Message: fmt.Sprintf("Balancer hostname '%s' must "+ "match existing instance address", backend.Hostname), } return } } if b.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "organization_required", Message: "Missing required organization", } return } if b.Datacenter.IsZero() { errData = &errortypes.ErrorData{ Error: "datacenter_required", Message: "Missing required datacenter", } return } if b.State { if len(b.Domains) == 0 { errData = &errortypes.ErrorData{ Error: "domain_required", Message: "Missing required domain", } return } if b.CheckPath == "" { errData = &errortypes.ErrorData{ Error: "check_path_required", Message: "Missing required health check path", } return } if len(b.Backends) == 0 { errData = &errortypes.ErrorData{ Error: "backend_required", Message: "Missing required backend", } return } domains := []string{} for _, domain := range b.Domains { domains = append(domains, domain.Domain) } coll := db.Balancers() count, e := coll.CountDocuments(db, &bson.M{ "_id": &bson.M{ "$ne": b.Id, }, "state": true, "domains.domain": &bson.M{ "$in": domains, }, }) if e != nil { err = database.ParseError(e) return } if count > 0 { errData = &errortypes.ErrorData{ Error: "domain_conflict", Message: "External domain conflicts with another " + "load balancer in same datacenter", } return } } return } func (b *Balancer) Json() { if b.States == nil || len(b.States) == 0 { return } for key, state := range b.States { if time.Since(state.Timestamp) > 1*time.Minute { delete(b.States, key) } } return } func (b *Balancer) Clean(db *database.Database) (err error) { if b.States == nil || len(b.States) == 0 { return } changed := false for key, state := range b.States { if time.Since(state.Timestamp) > 1*time.Minute { changed = true delete(b.States, key) } } if changed { err = b.CommitFields(db, set.NewSet("states")) if err != nil { return } } return } func (b *Balancer) CommitState(db *database.Database, state *State) ( err error) { coll := db.Balancers() _, err = coll.UpdateOne(db, &bson.M{ "_id": b.Id, }, &bson.M{ "$set": &bson.M{ "states." + node.Self.Id.Hex(): state, }, }) if err != nil { err = database.ParseError(err) return } return } func (b *Balancer) Commit(db *database.Database) (err error) { coll := db.Balancers() err = coll.Commit(b.Id, b) if err != nil { return } return } func (b *Balancer) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Balancers() if b.State && (fields.Contains("state") || fields.Contains("domains")) { domains := []string{} for _, domain := range b.Domains { domains = append(domains, domain.Domain) } coll := db.Balancers() count, e := coll.CountDocuments(db, &bson.M{ "_id": &bson.M{ "$ne": b.Id, }, "state": true, "domains.domain": &bson.M{ "$in": domains, }, }) if e != nil { err = database.ParseError(e) return } if count > 0 { err = &errortypes.ReadError{ errors.New("balancer: Datacenter domain conflict"), } return } } err = coll.CommitFields(b.Id, b, fields) if err != nil { return } return } func (b *Balancer) Insert(db *database.Database) (err error) { coll := db.Balancers() if !b.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("balancer: Balancer already exists"), } return } _, err = coll.InsertOne(db, b) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: balancer/constants.go ================================================ package balancer const ( Http = "http" ) ================================================ FILE: balancer/utils.go ================================================ package balancer import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, balncId bson.ObjectID) ( balnc *Balancer, err error) { coll := db.Balancers() balnc = &Balancer{} err = coll.FindOneId(balncId, balnc) if err != nil { return } return } func GetOrg(db *database.Database, orgId, balncId bson.ObjectID) ( balnc *Balancer, err error) { coll := db.Balancers() balnc = &Balancer{} err = coll.FindOne(db, &bson.M{ "_id": balncId, "organization": orgId, }).Decode(balnc) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( balncs []*Balancer, err error) { coll := db.Balancers() balncs = []*Balancer{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { balnc := &Balancer{} err = cursor.Decode(balnc) if err != nil { err = database.ParseError(err) return } balncs = append(balncs, balnc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (balncs []*Balancer, count int64, err error) { coll := db.Balancers() balncs = []*Balancer{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { balnc := &Balancer{} err = cursor.Decode(balnc) if err != nil { err = database.ParseError(err) return } balncs = append(balncs, balnc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, balncId bson.ObjectID) (err error) { coll := db.Balancers() _, err = coll.DeleteOne(db, &bson.M{ "_id": balncId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, balncId bson.ObjectID) ( err error) { coll := db.Balancers() _, err = coll.DeleteOne(db, &bson.M{ "_id": balncId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, balncIds []bson.ObjectID) ( err error) { coll := db.Balancers() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": balncIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, balncIds []bson.ObjectID) (err error) { coll := db.Balancers() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": balncIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: block/block.go ================================================ package block import ( "bytes" "crypto/md5" "fmt" "net" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Block struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Type string `bson:"type" json:"type"` Vlan int `bson:"vlan" json:"vlan"` Subnets []string `bson:"subnets" json:"subnets"` Subnets6 []string `bson:"subnets6" json:"subnets6"` Excludes []string `bson:"excludes" json:"excludes"` Netmask string `bson:"netmask" json:"netmask"` Gateway string `bson:"gateway" json:"gateway"` Gateway6 string `bson:"gateway6" json:"gateway6"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Type string `bson:"type" json:"type"` } func (b *Block) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { b.Name = utils.FilterName(b.Name) if b.Type == "" { b.Type = IPv4 } if b.Vlan < 0 || b.Vlan > 4095 { errData = &errortypes.ErrorData{ Error: "invalid_blan", Message: "VLan is invalid", } return } if b.Subnets == nil { b.Subnets = []string{} } if b.Subnets6 == nil { b.Subnets6 = []string{} } if b.Excludes == nil { b.Excludes = []string{} } if b.Type == IPv4 { b.Subnets6 = []string{} if b.Gateway != "" { gateway := net.ParseIP(b.Gateway) if gateway == nil || gateway.To4() == nil { errData = &errortypes.ErrorData{ Error: "invalid_gateway", Message: "Gateway address is invalid", } return } b.Gateway = gateway.String() } if b.Netmask != "" { netmask := utils.ParseIpMask(b.Netmask) if netmask == nil { errData = &errortypes.ErrorData{ Error: "invalid_netmask", Message: "Netmask is invalid", } return } } subnets := []string{} for _, subnet := range b.Subnets { if !strings.Contains(subnet, "/") { subnet += "/32" } _, subnetNet, e := net.ParseCIDR(subnet) if e != nil || subnetNet.IP.To4() == nil { errData = &errortypes.ErrorData{ Error: "invalid_subnet", Message: "Invalid subnet address", } return } subnets = append(subnets, subnetNet.String()) } b.Subnets = subnets excludes := []string{} for _, exclude := range b.Excludes { if !strings.Contains(exclude, "/") { exclude += "/32" } _, excludeNet, e := net.ParseCIDR(exclude) if e != nil || excludeNet.IP.To4() == nil { errData = &errortypes.ErrorData{ Error: "invalid_exclude", Message: "Invalid exclude address", } return } excludes = append(excludes, excludeNet.String()) } b.Excludes = excludes } else if b.Type == IPv6 { b.Subnets = []string{} b.Excludes = []string{} b.Netmask = "" b.Gateway = "" if b.Gateway6 != "" { gateway6 := net.ParseIP(b.Gateway6) if gateway6 == nil || gateway6.To4() != nil { errData = &errortypes.ErrorData{ Error: "invalid_gateway6", Message: "Gateway IPv6 address is invalid", } return } b.Gateway6 = gateway6.String() } subnets6 := []string{} for _, subnet6 := range b.Subnets6 { if !strings.Contains(subnet6, "/") { errData = &errortypes.ErrorData{ Error: "invalid_subnet6", Message: "Missing subnet6 cidr", } return } _, subnetNet, e := net.ParseCIDR(subnet6) if e != nil || subnetNet.IP.To4() != nil { errData = &errortypes.ErrorData{ Error: "invalid_subnet6", Message: "Invalid subnet6 address", } return } size, _ := subnetNet.Mask.Size() if size > 64 { errData = &errortypes.ErrorData{ Error: "invalid_subnet6_size", Message: "Minimum subnet6 size 64 is required", } return } subnets6 = append(subnets6, subnetNet.String()) } b.Subnets6 = subnets6 if len(b.Subnets6) > 1 { errData = &errortypes.ErrorData{ Error: "invalid_subnets6", Message: "Currently only one IPv6 subnet is supported", } return } } else { errData = &errortypes.ErrorData{ Error: "invalid_type", Message: "Block type is invalid", } return } return } func (b *Block) Contains(blckIp *BlockIp) (contains bool, err error) { ip := blckIp.GetIp() for _, exclude := range b.Excludes { _, network, e := net.ParseCIDR(exclude) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block exclude"), } return } if network.Contains(ip) { return } } for _, subnet := range b.Subnets { _, network, e := net.ParseCIDR(subnet) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block subnet"), } return } if network.Contains(ip) { contains = true return } } return } func (b *Block) GetGateway() net.IP { return net.ParseIP(b.Gateway) } func (b *Block) GetGateway6() net.IP { if b.Gateway6 == "" { return nil } return net.ParseIP(b.Gateway6) } func (b *Block) GetMask() net.IPMask { return utils.ParseIpMask(b.Netmask) } func (b *Block) GetGatewayCidr() string { staticGateway := net.ParseIP(b.Gateway) staticMask := utils.ParseIpMask(b.Netmask) if staticGateway == nil || staticMask == nil { return "" } staticSize, _ := staticMask.Size() return fmt.Sprintf("%s/%d", staticGateway.String(), staticSize) } func (b *Block) GetNetwork() (staticNet *net.IPNet, err error) { staticMask := utils.ParseIpMask(b.Netmask) if staticMask == nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Invalid netmask"), } return } staticSize, _ := staticMask.Size() _, staticNet, err = net.ParseCIDR( fmt.Sprintf("%s/%d", b.Gateway, staticSize)) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Failed to parse network cidr"), } return } return } func (b *Block) GetIps(db *database.Database) (blckIpsSet set.Set, err error) { coll := db.BlocksIp() blckIps := []int64{} err = coll.Distinct(db, "ip", bson.M{ "block": b.Id, }).Decode(&blckIps) if err != nil { err = database.ParseError(err) return } blckIpsSet = set.NewSet() for _, ipInt := range blckIps { if ipInt != 0 { blckIpsSet.Add(ipInt) } } return } func (b *Block) GetIpCount() (count int64, err error) { if b.Type != IPv4 { return } gatewayIp := net.ParseIP(b.Gateway) excludes := []*net.IPNet{} for _, exclude := range b.Excludes { _, network, e := net.ParseCIDR(exclude) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block exclude"), } return } excludes = append(excludes, network) } var totalCount int64 for _, subnet := range b.Subnets { _, network, e := net.ParseCIDR(subnet) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block subnet"), } return } ones, bits := network.Mask.Size() hostBits := bits - ones if hostBits <= 1 { if hostBits == 1 { totalCount += 2 } else { totalCount += 1 } } else { totalCount += (1 << hostBits) - 2 } } var excludedCount int64 for _, excludeNet := range excludes { ones, bits := excludeNet.Mask.Size() hostBits := bits - ones if hostBits <= 1 { if hostBits == 1 { excludedCount += 2 } else { excludedCount += 1 } } else { excludedCount += (1 << hostBits) - 2 } } gatewayInSubnet := false if gatewayIp != nil { for _, subnet := range b.Subnets { _, network, e := net.ParseCIDR(subnet) if e != nil { continue } if network.Contains(gatewayIp) { gatewayInSubnet = true break } } } count = totalCount - excludedCount if gatewayInSubnet { count-- } if count < 0 { count = 0 } return } func (b *Block) GetIp(db *database.Database, instId bson.ObjectID, typ string) (ip net.IP, err error) { blckIps, err := b.GetIps(db) if err != nil { return } coll := db.BlocksIp() gateway := net.ParseIP(b.Gateway) if gateway == nil { err = &errortypes.ParseError{ errors.New("block: Failed to parse block gateway"), } return } gatewaySize, _ := b.GetMask().Size() _, gatewayCidr, err := net.ParseCIDR(fmt.Sprintf("%s/%d", gateway.String(), gatewaySize)) if err != nil { err = &errortypes.ParseError{ errors.New("block: Failed to parse block gateway cidr"), } return } broadcast := utils.GetLastIpAddress(gatewayCidr) excludes := []*net.IPNet{} for _, exclude := range b.Excludes { _, network, e := net.ParseCIDR(exclude) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block exclude"), } return } excludes = append(excludes, network) } for _, subnet := range b.Subnets { _, network, e := net.ParseCIDR(subnet) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block subnet"), } return } first := true curIp := utils.CopyIpAddress(network.IP) for { if first { first = false } else { utils.IncIpAddress(curIp) } curIpInt := utils.IpAddress2Int(curIp) if !network.Contains(curIp) { break } if blckIps.Contains(curIpInt) || gatewayCidr.IP.Equal(curIp) || gateway.Equal(curIp) || broadcast.Equal(curIp) { continue } excluded := false for _, exclude := range excludes { if exclude.Contains(curIp) { excluded = true break } } if excluded { continue } blckIp := &BlockIp{ Block: b.Id, Ip: utils.IpAddress2Int(curIp), Instance: instId, Type: typ, } _, err = coll.InsertOne(db, blckIp) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { err = nil continue } return } ip = curIp break } if ip != nil { break } } if ip == nil { err = &BlockFull{ errors.New("block: Address pool full"), } return } return } func (b *Block) GetIp6(db *database.Database, instId bson.ObjectID, vlan int) (ip net.IP, cidr int, err error) { subnets6 := b.Subnets6 if subnets6 == nil || len(subnets6) < 1 { return } subnet6 := subnets6[0] if vlan == 0 || vlan > 4095 { err = &errortypes.ParseError{ errors.New("block: Failed to split subnet6"), } return } _, subnetNet, err := net.ParseCIDR(subnet6) if err != nil || subnetNet.IP.To4() != nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Invalid subnet6"), } return } cidr, _ = subnetNet.Mask.Size() subnet6 = subnetNet.String() subnet6spl := strings.Split(subnet6, ":/") if len(subnet6spl) != 2 { err = &errortypes.ParseError{ errors.New("block: Failed to split subnet6"), } return } addr6 := subnet6spl[0] if strings.Count(addr6, ":") < 4 { addr6 += ":" } addr6 += "0" + fmt.Sprintf("%03x", vlan) + ":" hash := md5.New() hash.Write([]byte(instId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:12] macBuf := bytes.Buffer{} for i, run := range macHash { if i != 0 && i%4 == 0 { macBuf.WriteRune(':') } macBuf.WriteRune(run) } addr6 += macBuf.String() + fmt.Sprintf("/%d", cidr) ip, _, err = net.ParseCIDR(addr6) if err != nil || subnetNet.IP.To4() != nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Failed to parse address6"), } return } return } func (b *Block) RemoveIp(db *database.Database, instId bson.ObjectID) (err error) { coll := db.BlocksIp() _, err = coll.DeleteMany(db, &bson.M{ "instance": instId, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } return } func (b *Block) ValidateAddresses(db *database.Database, commitFields set.Set) (err error) { coll := db.Blocks() ipColl := db.BlocksIp() instColl := db.Instances() gateway := net.ParseIP(b.Gateway) excludes := []*net.IPNet{} for _, exclude := range b.Excludes { _, network, e := net.ParseCIDR(exclude) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block exclude"), } return } excludes = append(excludes, network) } subnets := []*net.IPNet{} for _, subnet := range b.Subnets { _, network, e := net.ParseCIDR(subnet) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "block: Failed to parse block subnet"), } return } subnets = append(subnets, network) } if commitFields != nil { err = coll.CommitFields(b.Id, b, commitFields) if err != nil { return } } cursor, err := ipColl.Find(db, &bson.M{ "block": b.Id, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { blckIp := &BlockIp{} err = cursor.Decode(blckIp) if err != nil { err = database.ParseError(err) return } remove := false ip := utils.Int2IpAddress(blckIp.Ip) if gateway != nil && gateway.Equal(ip) { remove = true } if !remove { for _, exclude := range excludes { if exclude.Contains(ip) { remove = true break } } } if !remove { match := false for _, subnet := range subnets { if subnet.Contains(ip) { match = true break } } if !match { remove = true } } if remove { _, _ = instColl.UpdateOne(db, &bson.M{ "_id": blckIp.Instance, }, &bson.M{ "$set": &bson.M{ "restart_block_ip": true, }, }) _, err = ipColl.DeleteOne(db, &bson.M{ "_id": blckIp.Id, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func (b *Block) Commit(db *database.Database) (err error) { coll := db.Blocks() err = coll.Commit(b.Id, b) if err != nil { return } return } func (b *Block) CommitFields(db *database.Database, fields set.Set) ( err error) { err = b.ValidateAddresses(db, fields) if err != nil { return } return } func (b *Block) Insert(db *database.Database) (err error) { coll := db.Blocks() if !b.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("block: Block already exists"), } return } resp, err := coll.InsertOne(db, b) if err != nil { err = database.ParseError(err) return } b.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: block/constants.go ================================================ package block const ( External = "external" Host = "host" NodePort = "node_port" IPv4 = "ipv4" IPv6 = "ipv6" ) ================================================ FILE: block/errortypes.go ================================================ package block import ( "github.com/dropbox/godropbox/errors" ) type BlockFull struct { errors.DropboxError } ================================================ FILE: block/ip.go ================================================ package block import ( "net" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/utils" ) type BlockIp struct { Id bson.ObjectID `bson:"_id,omitempty"` Block bson.ObjectID `bson:"block"` Ip int64 `bson:"ip"` Instance bson.ObjectID `bson:"instance"` Type string `bson:"type"` } func (b *BlockIp) GetIp() net.IP { return utils.Int2IpAddress(b.Ip) } ================================================ FILE: block/utils.go ================================================ package block import ( "net" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func GetNodeBlock(ndeId bson.ObjectID) (blck *Block, err error) { hostNetwork := settings.Hypervisor.HostNetwork hostAddr, hostNet, err := net.ParseCIDR(hostNetwork) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Failed to parse host network"), } return } utils.IncIpAddress(hostAddr) blck = &Block{ Id: ndeId, Name: "host-block", Type: Host, Subnets: []string{ hostNetwork, }, Netmask: net.IP(hostNet.Mask).String(), Gateway: hostAddr.String(), } return } func GetNodePortBlock(ndeId bson.ObjectID) (blck *Block, err error) { portNetwork := settings.Hypervisor.NodePortNetwork portAddr, portNet, err := net.ParseCIDR(portNetwork) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Failed to parse node port network"), } return } utils.IncIpAddress(portAddr) blck = &Block{ Id: ndeId, Name: "node-port-block", Type: NodePort, Subnets: []string{ portNetwork, }, Netmask: net.IP(portNet.Mask).String(), Gateway: portAddr.String(), } return } func GetNodePortGateway() (gateway string, err error) { portAddr, _, err := net.ParseCIDR(settings.Hypervisor.NodePortNetwork) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "block: Failed to parse node port network"), } return } utils.IncIpAddress(portAddr) gateway = portAddr.String() return } func Get(db *database.Database, blockId bson.ObjectID) ( block *Block, err error) { coll := db.Blocks() block = &Block{} err = coll.FindOneId(blockId, block) if err != nil { return } return } func GetAll(db *database.Database) (blocks []*Block, err error) { coll := db.Blocks() blocks = []*Block{} opts := options.Find(). SetSort(bson.D{{"name", 1}}) cursor, err := coll.Find(db, bson.M{}, opts) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { blck := &Block{} err = cursor.Decode(blck) if err != nil { err = database.ParseError(err) return } blocks = append(blocks, blck) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetInstanceHostIp(db *database.Database, instId bson.ObjectID) (blckIp *BlockIp, err error) { coll := db.BlocksIp() blckIp = &BlockIp{} err = coll.FindOne(db, &bson.M{ "instance": instId, "type": Host, }).Decode(blckIp) if err != nil { err = database.ParseError(err) blckIp = nil if _, ok := err.(*database.NotFoundError); ok { err = nil } return } return } func GetInstanceNodePortIp(db *database.Database, instId bson.ObjectID) (blckIp *BlockIp, err error) { coll := db.BlocksIp() blckIp = &BlockIp{} err = coll.FindOne(db, &bson.M{ "instance": instId, "type": NodePort, }).Decode(blckIp) if err != nil { err = database.ParseError(err) blckIp = nil if _, ok := err.(*database.NotFoundError); ok { err = nil } return } return } func GetInstanceIp(db *database.Database, instId bson.ObjectID, typ string) (blck *Block, blckIp *BlockIp, err error) { coll := db.BlocksIp() blckIp = &BlockIp{} err = coll.FindOne(db, &bson.M{ "instance": instId, "type": typ, }).Decode(blckIp) if err != nil { err = database.ParseError(err) blckIp = nil if _, ok := err.(*database.NotFoundError); ok { err = nil } return } blck, err = Get(db, blckIp.Block) if err != nil { if _, ok := err.(*database.NotFoundError); ok { RemoveIp(db, blckIp.Id) } blckIp = nil return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (blcks []*Block, count int64, err error) { coll := db.Blocks() blcks = []*Block{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { blck := &Block{} err = cursor.Decode(blck) if err != nil { err = database.ParseError(err) return } blcks = append(blcks, blck) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, blockId bson.ObjectID) (err error) { coll := db.Blocks() ipColl := db.BlocksIp() instColl := db.Instances() nodeColl := db.Nodes() cursor, err := ipColl.Find(db, &bson.M{ "block": blockId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { blckIp := &BlockIp{} err = cursor.Decode(blckIp) if err != nil { err = database.ParseError(err) return } _, _ = instColl.UpdateOne(db, &bson.M{ "_id": blckIp.Instance, }, &bson.M{ "$set": &bson.M{ "restart_block_ip": true, }, }) } _, err = ipColl.DeleteMany(db, &bson.M{ "block": blockId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } _, err = nodeColl.UpdateMany(db, &bson.M{ "host_block": blockId, }, &bson.M{"$set": &bson.M{ "host_block": bson.NilObjectID, }}) if err != nil { err = database.ParseError(err) return } _, err = coll.DeleteOne(db, &bson.M{ "_id": blockId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveIp(db *database.Database, blockIpId bson.ObjectID) ( err error) { coll := db.BlocksIp() _, err = coll.DeleteOne(db, &bson.M{ "_id": blockIpId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveInstanceIps(db *database.Database, instId bson.ObjectID) ( err error) { coll := db.BlocksIp() _, err = coll.DeleteMany(db, &bson.M{ "instance": instId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveInstanceIpsType(db *database.Database, instId bson.ObjectID, typ string) (err error) { coll := db.BlocksIp() _, err = coll.DeleteMany(db, &bson.M{ "instance": instId, "type": typ, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, blckIds []bson.ObjectID) ( err error) { coll := db.Blocks() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": blckIds, }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: bridges/bridges.go ================================================ package bridges import ( "io/ioutil" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/ip" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/utils" ) var ( bridges = []ip.Interface{} lastBridgesSync time.Time curAddr = map[string]string{} curAddr6 = map[string]string{} lastAddrSync = map[string]time.Time{} ) func ClearCache() { lastBridgesSync = time.Time{} bridges = []ip.Interface{} } func GetBridges() (brdgs []ip.Interface, err error) { if time.Since(lastBridgesSync) < 300*time.Second { brdgs = bridges return } bridgesNew := []ip.Interface{} bridgesSet := set.NewSet() ifaces, err := iproute.IfaceGetBridges("") if err != nil { return } ifacesData, err := ip.GetIfacesCached("") if err != nil { return } for _, iface := range ifaces { if iface.Name == "" { continue } ifaceData := ifacesData[iface.Name] if ifaceData != nil { bridgesNew = append(bridgesNew, ip.Interface{ Name: iface.Name, Address: ifaceData.GetAddress(), }) } else { bridgesNew = append(bridgesNew, ip.Interface{ Name: iface.Name, }) } bridgesSet.Add(iface.Name) } exists, err := utils.ExistsDir("/etc/sysconfig/network-scripts") if err != nil { return } if exists { items, e := ioutil.ReadDir("/etc/sysconfig/network-scripts") if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "bridges: Failed to read network scripts"), } return } for _, item := range items { name := item.Name() if !strings.HasPrefix(name, "ifcfg-") || !strings.Contains(name, ":") { continue } name = name[6:] names := strings.Split(name, ":") if len(names) != 2 || names[0] == "" { continue } if bridgesSet.Contains(names[0]) && !bridgesSet.Contains(name) { bridgesNew = append(bridgesNew, ip.Interface{ Name: name, }) bridgesSet.Add(name) } } } bridges = bridgesNew lastBridgesSync = time.Now() brdgs = bridgesNew return } func GetIpAddrs(iface string) (addr string, addr6 string, err error) { if time.Since(lastAddrSync[iface]) < 600*time.Second { addr = curAddr[iface] addr6 = curAddr6[iface] return } if iface == "" { err = &errortypes.NotFoundError{ errors.New("bridges: Invalid external node interface"), } return } address, address6, err := iproute.AddressGetIface("", iface) if err != nil { return } if address != nil { addr = address.Local } if address6 != nil { addr6 = address6.Local } curAddr[iface] = addr curAddr6[iface] = addr6 lastAddrSync[iface] = time.Now() return } ================================================ FILE: certificate/certificate.go ================================================ package certificate import ( "crypto/md5" "crypto/x509" "encoding/pem" "fmt" "io" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Info struct { Hash string `bson:"hash" json:"hash"` SignatureAlg string `bson:"signature_alg" json:"signature_alg"` PublicKeyAlg string `bson:"public_key_alg" json:"public_key_alg"` Issuer string `bson:"issuer" json:"issuer"` IssuedOn time.Time `bson:"issued_on" json:"issued_on"` ExpiresOn time.Time `bson:"expires_on" json:"expires_on"` DnsNames []string `bson:"dns_names" json:"dns_names"` } type Certificate struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Type string `bson:"type" json:"type"` Key string `bson:"key" json:"key"` Certificate string `bson:"certificate" json:"certificate"` Info *Info `bson:"info" json:"info"` AcmeHash string `bson:"acme_hash" json:"-"` AcmeAccount string `bson:"acme_account" json:"-"` AcmeDomains []string `bson:"acme_domains" json:"acme_domains"` AcmeType string `bson:"acme_type" json:"acme_type"` AcmeAuth string `bson:"acme_auth" json:"acme_auth"` AcmeSecret bson.ObjectID `bson:"acme_secret" json:"acme_secret"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` Type string `bson:"type" json:"type"` } func (c *Certificate) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { c.Name = utils.FilterName(c.Name) if c.Type == "" { c.Type = Text } c.Key = strings.TrimSpace(c.Key) c.Certificate = strings.TrimSpace(c.Certificate) if c.Type == LetsEncrypt { switch c.AcmeType { case AcmeHTTP, "": c.AcmeType = AcmeHTTP break case AcmeDNS: if c.AcmeSecret.IsZero() { errData = &errortypes.ErrorData{ Error: "acme_secret_invalid", Message: "LetsEncrypt verification secret invalid", } return } break default: errData = &errortypes.ErrorData{ Error: "acme_type_invalid", Message: "LetsEncrypt verification type invalid", } return } switch c.AcmeAuth { case AcmeAWS, "": c.AcmeAuth = AcmeAWS break case AcmeCloudflare: break case AcmeOracleCloud: break case AcmeGoogleCloud: break default: errData = &errortypes.ErrorData{ Error: "acme_auth_invalid", Message: "LetsEncrypt verification provider invalid", } return } } else { c.AcmeAccount = "" c.AcmeDomains = []string{} c.AcmeType = "" c.AcmeAuth = "" c.AcmeSecret = bson.NilObjectID } if c.AcmeDomains == nil { c.AcmeDomains = []string{} } for i, domain := range c.AcmeDomains { if strings.HasSuffix(domain, ".") { c.AcmeDomains[i] = domain[:len(domain)-1] } } if c.Type == LetsEncrypt && len(c.AcmeDomains) == 0 { errData = &errortypes.ErrorData{ Error: "missing_acme_domains", Message: "Lets Encrypt domains required", } return } err = c.UpdateInfo() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("certificate: Failed to update certificate info") err = nil } return } func (c *Certificate) UpdateInfo() (err error) { hash := c.Hash() if c.Certificate == "" { c.Info = &Info{ DnsNames: []string{}, } return } if c.Info != nil && hash == c.Info.Hash { return } certBlock, _ := pem.Decode([]byte(c.Certificate)) if certBlock == nil { c.Info = nil err = &errortypes.ParseError{ errors.New("certificate: Failed to decode certificate"), } return } cert, err := x509.ParseCertificate(certBlock.Bytes) if err != nil { c.Info = nil err = &errortypes.ParseError{ errors.Wrap(err, "certificate: Failed to parse certificate"), } return } publicKeyAlg := "" switch cert.PublicKeyAlgorithm { case x509.RSA: publicKeyAlg = "RSA" break case x509.DSA: publicKeyAlg = "DSA" break case x509.ECDSA: publicKeyAlg = "ECDSA" break default: publicKeyAlg = "Unknown" } dnsNames := cert.DNSNames if len(dnsNames) == 0 && cert.Subject.CommonName != "" { dnsNames = append(dnsNames, cert.Subject.CommonName) } c.Info = &Info{ Hash: hash, SignatureAlg: cert.SignatureAlgorithm.String(), PublicKeyAlg: publicKeyAlg, Issuer: cert.Issuer.CommonName, IssuedOn: cert.NotBefore, ExpiresOn: cert.NotAfter, DnsNames: dnsNames, } return } func (c *Certificate) Commit(db *database.Database) (err error) { coll := db.Certificates() err = coll.Commit(c.Id, c) if err != nil { return } return } func (c *Certificate) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Certificates() err = coll.CommitFields(c.Id, c, fields) if err != nil { return } return } func (c *Certificate) Insert(db *database.Database) (err error) { coll := db.Certificates() if !c.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("certificate: Certificate already exists"), } return } resp, err := coll.InsertOne(db, c) if err != nil { err = database.ParseError(err) return } c.Id = resp.InsertedID.(bson.ObjectID) return } func (c *Certificate) Hash() string { hash := md5.New() hash.Write([]byte(c.Type)) hash.Write([]byte(c.Key)) hash.Write([]byte(c.Certificate)) hash.Write([]byte(c.AcmeAccount)) if c.AcmeDomains != nil { for _, domain := range c.AcmeDomains { io.WriteString(hash, domain) } } return fmt.Sprintf("%x", hash.Sum(nil)) } ================================================ FILE: certificate/constants.go ================================================ package certificate import "github.com/pritunl/mongo-go-driver/v2/bson" const ( Text = "text" LetsEncrypt = "lets_encrypt" AcmeHTTP = "acme_http" AcmeDNS = "acme_dns" AcmeAWS = "acme_aws" AcmeCloudflare = "acme_cloudflare" AcmeOracleCloud = "acme_oracle_cloud" AcmeGoogleCloud = "acme_google_cloud" ) var ( Global = bson.NilObjectID ) ================================================ FILE: certificate/utils.go ================================================ package certificate import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, certId bson.ObjectID) ( cert *Certificate, err error) { coll := db.Certificates() cert = &Certificate{} err = coll.FindOneId(certId, cert) if err != nil { return } return } func GetOrg(db *database.Database, orgId, certId bson.ObjectID) ( cert *Certificate, err error) { coll := db.Certificates() cert = &Certificate{} err = coll.FindOne(db, &bson.M{ "_id": certId, "organization": orgId, }).Decode(cert) if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (cert *Certificate, err error) { coll := db.Certificates() cert = &Certificate{} err = coll.FindOne(db, query).Decode(cert) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) (certs []*Certificate, err error) { coll := db.Certificates() certs = []*Certificate{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { cert := &Certificate{} err = cursor.Decode(cert) if err != nil { return } certs = append(certs, cert) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllOrg(db *database.Database, orgId bson.ObjectID) ( certs []*Certificate, err error) { coll := db.Certificates() certs = []*Certificate{} cursor, err := coll.Find(db, &bson.M{ "organization": orgId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { cert := &Certificate{} err = cursor.Decode(cert) if err != nil { return } certs = append(certs, cert) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( certs []*database.Named, err error) { coll := db.Certificates() certs = []*database.Named{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{{"name", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { crt := &database.Named{} err = cursor.Decode(crt) if err != nil { err = database.ParseError(err) return } certs = append(certs, crt) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (certs []*Certificate, count int64, err error) { coll := db.Certificates() certs = []*Certificate{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { cert := &Certificate{} err = cursor.Decode(cert) if err != nil { err = database.ParseError(err) return } certs = append(certs, cert) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, certId bson.ObjectID) (err error) { coll := db.Certificates() _, err = coll.DeleteMany(db, &bson.M{ "_id": certId, }) if err != nil { err = database.ParseError(err) return } return } func RemoveOrg(db *database.Database, orgId, certId bson.ObjectID) ( err error) { coll := db.Certificates() _, err = coll.DeleteOne(db, &bson.M{ "_id": certId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, certIds []bson.ObjectID) ( err error) { coll := db.Certificates() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": certIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, certIds []bson.ObjectID) (err error) { coll := db.Certificates() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": certIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: cloud/cloud.go ================================================ package cloud type Subnet struct { Id string `bson:"id" json:"id"` VpcId string `bson:"vpc_id" json:"vpc_id"` Name string `bson:"name" json:"name"` Network string `bson:"network" json:"network"` } type Vpc struct { Id string `bson:"id" json:"id"` Name string `bson:"name" json:"name"` Network string `bson:"network" json:"network"` Subnets []*Subnet `bson:"subnets" json:"subnets"` } ================================================ FILE: cloud/oracle.go ================================================ package cloud import ( "time" "github.com/pritunl/pritunl-cloud/oracle" ) var ( lastOracleSync time.Time oracleVpcs []*Vpc ) func GetOracleVpcs(authPv oracle.AuthProvider) (vpcs []*Vpc, err error) { if time.Since(lastOracleSync) < 30*time.Second { vpcs = oracleVpcs return } pv, err := oracle.NewProvider(authPv) if err != nil { return } vcns, err := oracle.GetVcns(pv) if err != nil { return } vpcs = []*Vpc{} for _, ociVcn := range vcns { vpc := &Vpc{ Id: ociVcn.Id, Name: ociVcn.Name, Network: ociVcn.Network, Subnets: []*Subnet{}, } for _, ociSubnet := range ociVcn.Subnets { subnet := &Subnet{ Id: ociSubnet.Id, VpcId: ociSubnet.VcnId, Name: ociSubnet.Name, Network: ociSubnet.Network, } vpc.Subnets = append(vpc.Subnets, subnet) } vpcs = append(vpcs, vpc) } lastOracleSync = time.Now() oracleVpcs = vpcs return } ================================================ FILE: cloudinit/cloudinit.go ================================================ package cloudinit import ( "bytes" "encoding/base64" "encoding/json" "fmt" "mime/multipart" "net" "net/textproto" "os" "os/exec" "path" "strings" "text/template" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) const metaDataTmpl = `instance-id: %s local-hostname: %s` const userDataTmpl = `Content-Type: multipart/mixed; boundary="%s" MIME-Version: 1.0 %s` const netConfigTmpl = `version: 1 config: - type: physical name: {{.Iface}} mac_address: {{.Mac}}{{.Mtu}} subnets: - type: static address: {{.Address}} netmask: {{.Netmask}} gateway: {{.Gateway}} dns_nameservers: - {{.Dns1}} - {{.Dns2}} - {{.Dns3}} - type: static6 address: {{.AddressCidr6}} gateway: {{.Gateway6}} ` const netConfigLegacyTmpl = `version: 1 config: - type: physical name: {{.Iface}} mac_address: {{.Mac}}{{.Mtu}} subnets: - type: static address: {{.Address}} netmask: {{.Netmask}} network: {{.Network}} gateway: {{.Gateway}} dns_nameservers: - {{.Dns1}} - {{.Dns2}} - {{.Dns3}} - type: static address: {{.AddressCidr6}} gateway: {{.Gateway6}} ` const netConfig2Tmpl = `version: 2 ethernets: {{.Iface}}: match: macaddress: {{.Mac}}{{.Mtu}} addresses: - {{.AddressCidr}} - {{.AddressCidr6}} gateway4: {{.Gateway}} gateway6: {{.Gateway6}} nameservers: addresses: - {{.Dns1}} - {{.Dns2}} - {{.Dns3}} ` const netMtu = ` mtu: %d` const cloudConfigTmpl = `#cloud-config hostname: {{.Hostname}} ssh_deletekeys: false {{if eq .RootPasswd ""}}disable_root: true{{else}}disable_root: false{{end}} ssh_pwauth: no write_files:{{.WriteFiles}} growpart: mode: auto devices: ["/"] ignore_growroot_disabled: false runcmd: - 'systemctl restart sshd || true' - [ {{.DeployRun}} ] users: - name: root {{if eq .RootPasswd ""}}lock-passwd: true{{else}}lock-passwd: false passwd: {{.RootPasswd}} hashed_passwd: {{.RootPasswd}}{{end}} - name: cloud groups: adm, video, wheel, systemd-journal selinux-user: staff_u sudo: ALL=(ALL) NOPASSWD:ALL lock-passwd: {{.LockPasswd}} ssh-authorized-keys: {{- range .Keys}} - {{.}} {{- end}} bootcmd: {{- range .Mounts}} - [ "mkdir", "-p", "{{.Path}}" ] {{- end}} - 'sysctl -w net.ipv4.conf.eth0.send_redirects=0 || true' - [ sh, -c, '{{.DeployBoot}}' ]{{if .RunScript}} - [ /etc/cloudinit-script ]{{end}} {{- if .HasMounts}} mounts: {{- range .Mounts}} - [ "{{.Tag}}", "{{.Path}}", {{.Type}}, "{{.Opts}}", "0", "{{.Fsck}}" ] {{- end}} {{- end}} ` const cloudBsdConfigTmpl = `#cloud-config hostname: {{.Hostname}} ssh_deletekeys: false {{if eq .RootPasswd ""}}disable_root: true{{else}}disable_root: false{{end}} ssh_pwauth: no write_files:{{.WriteFiles}} runcmd: - [ {{.DeployRun}} ] users: - name: root {{if eq .RootPasswd ""}}lock-passwd: true{{else}}lock-passwd: false passwd: {{.RootPasswd}} hashed_passwd: {{.RootPasswd}}{{end}} - name: cloud groups: cloud, wheel sudo: ALL=(ALL) NOPASSWD:ALL lock-passwd: {{.LockPasswd}} ssh-authorized-keys: {{- range .Keys}} - {{.}} {{- end}} bootcmd: {{- range .Mounts}} - [ "mkdir", "-p", "{{.Path}}" ] {{- end}} - [ sysctl, net.inet.ip.redirect=0 ] - [ sysctl, net.inet6.ip6.dad_count=0 ] - [ sh, -c, '{{.DeployBoot}}' ]{{if .RunScript}} - [ /etc/cloudinit-script ]{{end}} {{- if .HasMounts}} mounts: {{- range .Mounts}} - [ "{{.Tag}}", "{{.Path}}", {{.Type}}, "{{.Opts}}", "0", "{{.Fsck}}" ] {{- end}} {{- end}} ` const deploymentScriptTmpl = `#!/bin/sh set -e mkdir -p /iso mount /dev/sr0 /iso cp /iso/pci %s sync umount /iso rm -rf /iso rm -- "$0"%s ` const deploymentScriptBsdTmpl = `#!/bin/sh set -e mkdir -p /iso mount -t cd9660 /dev/cd0 /iso cp /iso/pci %s sync umount /iso rm -rf /iso rm -- "$0"%s ` var ( cloudConfig = template.Must(template.New( "cloud").Parse(cloudConfigTmpl)) cloudBsdConfig = template.Must(template.New( "cloud_bsd").Parse(cloudBsdConfigTmpl)) netConfig = template.Must(template.New( "net").Parse(netConfigTmpl)) netConfigLegacy = template.Must(template.New( "net").Parse(netConfigLegacyTmpl)) netConfig2 = template.Must(template.New( "net2").Parse(netConfig2Tmpl)) ) type netConfigData struct { Iface string Mac string Mtu string Address string AddressCidr string Netmask string Network string Gateway string Address6 string AddressCidr6 string Gateway6 string Dns1 string Dns2 string Dns3 string } type cloudConfigData struct { Hostname string RootPasswd string LockPasswd string WriteFiles string RunScript bool DeployRun string DeployBoot string Address6 string Gateway6 string Keys []string HasMounts bool Mounts []cloudMount } type cloudMount struct { Tag string Path string Type string Opts string Fsck string } type imdsConfig struct { Address string `json:"address"` Port int `json:"port"` Secret string `json:"secret"` State string `json:"state"` } func getUserData(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine, dc *datacenter.Datacenter, zne *zone.Zone, vc *vpc.Vpc, deply *deployment.Deployment, deployUnit *unit.Unit, deploySpec *spec.Spec, initial bool, addr6, gateway6 net.IP) ( usrData string, err error) { authrs, err := authority.GetOrgRoles(db, inst.Organization, inst.Roles) if err != nil { return } trusted := "" principals := "" authorizedKeys := "" writeFiles := []*fileData{} initGuestPath := utils.FilterPath(settings.Hypervisor.InitGuestPath) agentGuestPath := utils.FilterPath(settings.Hypervisor.AgentGuestPath) dnsCloud := strings.Split(settings.Hypervisor.ImdsAddress, "/")[0] data := cloudConfigData{ Keys: []string{}, Hostname: strings.Replace(inst.Name, " ", "_", -1), Address6: addr6.String(), Gateway6: gateway6.String(), DeployRun: initGuestPath, Mounts: []cloudMount{}, } if inst.RootEnabled { data.RootPasswd, err = utils.GenerateShadow(inst.RootPasswd) if err != nil { return } } if !initial || !settings.Hypervisor.LockCloudPass { data.LockPasswd = "false" } else { data.LockPasswd = "true" } owner := "" if virt.CloudType == instance.BSD { owner = "root:wheel" } else { owner = "root:root" } if virt.CloudType == instance.BSD || virt.SystemKind == instance.AlpineLinux { resolvConf := fmt.Sprintf("nameserver %s\n", dnsCloud) if inst.IsIpv6Only() { resolvConf += fmt.Sprintf("nameserver %s\n", zne.GetDnsServerPrimary6()) resolvConf += fmt.Sprintf("nameserver %s\n", zne.GetDnsServerSecondary6()) } else { resolvConf += fmt.Sprintf("nameserver %s\n", zne.GetDnsServerPrimary()) resolvConf += fmt.Sprintf("nameserver %s\n", zne.GetDnsServerSecondary()) } writeFiles = append(writeFiles, &fileData{ Content: resolvConf, Owner: owner, Path: "/etc/resolv.conf", Permissions: "0644", }) } if inst.CloudScript != "" { data.RunScript = true writeFiles = append(writeFiles, &fileData{ Content: inst.CloudScript, Owner: owner, Path: "/etc/cloudinit-script", Permissions: "0755", }) } stateObjId, err := utils.RandObjectId() if err != nil { return } stateId := stateObjId.Hex() imdsConf := &imdsConfig{ Address: strings.Split(settings.Hypervisor.ImdsAddress, "/")[0], Port: settings.Hypervisor.ImdsPort, Secret: virt.ImdsClientSecret, } imdsConfContent, err := json.Marshal(imdsConf) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cloudinit: Failed to marshal imds conf"), } return } writeFiles = append(writeFiles, &fileData{ Content: string(imdsConfContent), Owner: owner, Path: "/etc/pritunl-imds.json", Permissions: "0600", }) deployScript := "" deployScriptTmpl := "" if virt.CloudType == instance.BSD { deployScriptTmpl = deploymentScriptBsdTmpl } else { deployScriptTmpl = deploymentScriptTmpl } if deply != nil && deployUnit != nil && deploySpec != nil { if deply.Mounts != nil && len(deply.Mounts) > 0 { data.HasMounts = true for _, mnt := range deply.Mounts { data.Mounts = append(data.Mounts, cloudMount{ Tag: fmt.Sprintf("UUID=%s", mnt.Uuid), Path: utils.FilterPath(mnt.Path), Type: "auto", Opts: "defaults", Fsck: "2", }) } } if deployUnit.Kind == deployment.Image { deployScript = fmt.Sprintf( deployScriptTmpl, agentGuestPath, fmt.Sprintf( " && IMDS_STATE=\"run:%s\" %s --daemon engine image", stateId, agentGuestPath, ), ) } else if initial { deployScript = fmt.Sprintf( deployScriptTmpl, agentGuestPath, fmt.Sprintf( " && IMDS_STATE=\"run:%s\" %s --daemon engine initial", stateId, agentGuestPath, ), ) } else { deployScript = fmt.Sprintf( deployScriptTmpl, agentGuestPath, fmt.Sprintf( " && IMDS_STATE=\"run:%s\" %s --daemon engine post", stateId, agentGuestPath, ), ) } writeFiles = append(writeFiles, &fileData{ Content: deploySpec.Data + "\n", Owner: owner, Path: "/etc/pritunl-deploy.md", Permissions: "0600", }) if virt.CloudType == instance.BSD { data.DeployBoot = fmt.Sprintf( "[ -f %s ] && ! pgrep -f \"%s --daemon\" && "+ "IMDS_STATE=\"boot:%s\" %s --daemon engine post || true", agentGuestPath, agentGuestPath, stateId, agentGuestPath, ) } else { data.DeployBoot = fmt.Sprintf( "[ -f %s ] && ! pgrep -f \"^%s\" && "+ "IMDS_STATE=\"boot:%s\" %s --daemon engine post || true", agentGuestPath, agentGuestPath, stateId, agentGuestPath, ) } } else { deployScript = fmt.Sprintf( deployScriptTmpl, agentGuestPath, fmt.Sprintf( " && IMDS_STATE=\"run:%s\" %s --daemon agent", stateId, agentGuestPath, ), ) if virt.CloudType == instance.BSD { data.DeployBoot = fmt.Sprintf( "[ -f %s ] && ! pgrep -f \"%s --daemon\" && "+ "IMDS_STATE=\"boot:%s\" %s --daemon agent || true", agentGuestPath, agentGuestPath, stateId, agentGuestPath, ) } else { data.DeployBoot = fmt.Sprintf( "[ -f %s ] && ! pgrep -f \"^%s\" && "+ "IMDS_STATE=\"boot:%s\" %s --daemon agent || true", agentGuestPath, agentGuestPath, stateId, agentGuestPath, ) } } for _, mnt := range virt.Mounts { data.HasMounts = true pth := utils.FilterPath(mnt.Path) if pth == "" { continue } data.Mounts = append(data.Mounts, cloudMount{ Tag: mnt.Name, Path: utils.FilterPath(mnt.Path), Type: "virtiofs", Opts: "defaults,_netdev", Fsck: "0", }) } writeFiles = append(writeFiles, &fileData{ Content: deployScript, Owner: owner, Path: initGuestPath, Permissions: "0755", }) for _, authr := range authrs { switch authr.Type { case authority.SshKey: for _, key := range strings.Split(authr.Key, "\n") { data.Keys = append(data.Keys, key) authorizedKeys += key + "\n" } break case authority.SshCertificate: trusted += authr.Certificate + "\n" principals += strings.Join(authr.Principals, "\n") + "\n" break } } if trusted == "" { trusted = "\n" } if principals == "" { principals = "\n" } writeFiles = append(writeFiles, &fileData{ Content: trusted, Owner: owner, Path: "/etc/ssh/trusted", Permissions: "0644", }) writeFiles = append(writeFiles, &fileData{ Content: principals, Owner: owner, Path: "/etc/ssh/principals", Permissions: "0644", }) writeFiles = append(writeFiles, &fileData{ Content: authorizedKeys, Owner: "cloud:cloud", Path: "/home/cloud/.ssh/authorized_keys", Permissions: "0600", }) data.WriteFiles, err = generateWriteFiles(writeFiles) if err != nil { return } items := []string{} output := &bytes.Buffer{} var templ *template.Template if virt.CloudType == instance.BSD { templ = cloudBsdConfig } else { templ = cloudConfig } err = templ.Execute(output, data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cloudinit: Failed to exec cloud template"), } return } items = append(items, output.String()) buffer := &bytes.Buffer{} message := multipart.NewWriter(buffer) for _, item := range items { header := textproto.MIMEHeader{} header.Set("Content-Transfer-Encoding", "base64") header.Set("MIME-Version", "1.0") if strings.HasPrefix(item, "#!") { header.Set("Content-Type", "text/x-shellscript; charset=\"utf-8\"") } else { header.Set("Content-Type", "text/cloud-config; charset=\"utf-8\"") } part, e := message.CreatePart(header) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "cloudinit: Failed to create part"), } return } _, err = part.Write( []byte(base64.StdEncoding.EncodeToString([]byte(item)) + "\n")) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "cloudinit: Failed to write part"), } return } } err = message.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "cloudinit: Failed to close message"), } return } usrData = fmt.Sprintf( userDataTmpl, message.Boundary(), buffer.String(), ) return } func getNetData(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine, dc *datacenter.Datacenter, zne *zone.Zone, vc *vpc.Vpc) (netData string, addr6, gateway6 net.IP, err error) { if len(virt.NetworkAdapters) == 0 { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing network adapters"), } return } adapter := virt.NetworkAdapters[0] if adapter.Vpc.IsZero() { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing VPC"), } return } if adapter.Subnet.IsZero() { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing VPC subnet"), } return } vcNet, err := vc.GetNetwork() if err != nil { return } addr, gatewayAddr, err := vc.GetIp(db, inst.Subnet, inst.Id) if err != nil { return } cidr, _ := vcNet.Mask.Size() addr6 = vc.GetIp6(inst.Id) gateway6 = vc.GetGatewayIp6(inst.Id) dns1 := "" dns2 := "" dnsCloud := strings.Split(settings.Hypervisor.ImdsAddress, "/")[0] if inst.IsIpv6Only() { dns1 = utils.FilterIp(zne.GetDnsServerPrimary6()) dns2 = utils.FilterIp(zne.GetDnsServerSecondary6()) } else { dns1 = utils.FilterIp(zne.GetDnsServerPrimary()) dns2 = utils.FilterIp(zne.GetDnsServerSecondary()) } data := netConfigData{ Mac: adapter.MacAddress, Address: addr.String(), AddressCidr: fmt.Sprintf("%s/%d", addr.String(), cidr), Netmask: net.IP(vcNet.Mask).String(), Network: vcNet.IP.String(), Gateway: gatewayAddr.String(), Address6: addr6.String(), AddressCidr6: addr6.String() + "/64", Gateway6: gateway6.String(), Dns1: dnsCloud, Dns2: dns1, Dns3: dns2, } if virt.CloudType == instance.BSD { data.Iface = "vtnet0" } else { data.Iface = "eth0" } data.Mtu = fmt.Sprintf(netMtu, dc.GetInstanceMtu()) output := &bytes.Buffer{} if settings.Hypervisor.CloudInitNetVer == 2 { err = netConfig2.Execute(output, data) } else { if virt.CloudType == instance.LinuxLegacy { err = netConfigLegacy.Execute(output, data) } else { err = netConfig.Execute(output, data) } } if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cloudinit: Failed to exec cloud template"), } return } netData = output.String() return } func Write(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine, dc *datacenter.Datacenter, zne *zone.Zone, vc *vpc.Vpc, initial bool) (err error) { tempDir := paths.GetTempDir() metaPath := path.Join(tempDir, "meta-data") userPath := path.Join(tempDir, "user-data") netPath := path.Join(tempDir, "network-config") pciPath := path.Join(tempDir, "pci") initPath := paths.GetInitPath(inst.Id) defer os.RemoveAll(tempDir) var deply *deployment.Deployment var deployUnit *unit.Unit var deploySpec *spec.Spec if !virt.Deployment.IsZero() { deply, err = deployment.Get(db, virt.Deployment) if err != nil { return } deploySpec, err = spec.Get(db, deply.Spec) if err != nil { return } deployUnit, err = unit.Get(db, deply.Unit) if err != nil { return } } err = utils.ExistsMkdir(paths.GetInitsPath(), 0755) if err != nil { return } err = utils.ExistsMkdir(tempDir, 0700) if err != nil { return } netData, addr6, gateway6, err := getNetData(db, inst, virt, dc, zne, vc) if err != nil { return } usrData, err := getUserData(db, inst, virt, dc, zne, vc, deply, deployUnit, deploySpec, initial, addr6, gateway6) if err != nil { return } metaData := fmt.Sprintf(metaDataTmpl, bson.NewObjectID().Hex(), strings.Replace(inst.Name, " ", "_", -1), ) err = utils.CreateWrite(metaPath, metaData, 0644) if err != nil { return } if !virt.DhcpServer { err = utils.CreateWrite(netPath, netData, 0644) if err != nil { return } } err = utils.CreateWrite(userPath, usrData, 0644) if err != nil { return } if virt.CloudType == instance.BSD { err = utils.Exec("", "cp", settings.Hypervisor.AgentBsdHostPath, pciPath) if err != nil { return } } else { err = utils.Exec("", "cp", settings.Hypervisor.AgentHostPath, pciPath) if err != nil { return } } xorrisoPath, err := exec.LookPath("xorriso") if err != nil { xorrisoPath = "" err = nil } if xorrisoPath != "" { args := []string{ "-as", "mkisofs", "-output", initPath, "-volid", "cidata", "-joliet", "-rock", "user-data", "meta-data", } if !virt.DhcpServer { args = append(args, "network-config") } args = append(args, pciPath) _, err = utils.ExecCombinedOutputLoggedDir( nil, tempDir, "xorriso", args..., ) if err != nil { return } } else { args := []string{ "-output", initPath, "-volid", "cidata", "-joliet", "-rock", "user-data", "meta-data", } if !virt.DhcpServer { args = append(args, "network-config") } args = append(args, pciPath) _, err = utils.ExecCombinedOutputLoggedDir( nil, tempDir, "genisoimage", args..., ) if err != nil { return } } err = utils.Chmod(initPath, 0600) if err != nil { return } return } ================================================ FILE: cloudinit/query.go ================================================ package cloudinit import ( "encoding/json" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) type NetworkConfig struct { Config []NetworkInterface `json:"config"` Version int `json:"version"` } type NetworkInterface struct { Name string `json:"name"` Mtu int `json:"mtu,omitempty"` Type string `json:"type"` BondInterfaces []string `json:"bond_interfaces,omitempty"` Subnets []Subnet `json:"subnets,omitempty"` VlanId int `json:"vlan_id,omitempty"` VlanLink string `json:"vlan_link,omitempty"` } type Subnet struct { Address string `json:"address"` Gateway string `json:"gateway,omitempty"` Type string `json:"type"` } type CloudConfig struct { CombinedCloudConfig CombinedCloudConfig `json:"combined_cloud_config"` MergedSystemConfig MergedSystemConfig `json:"merged_system_cfg"` } type CombinedCloudConfig struct { Network NetworkConfig `json:"network"` } type MergedSystemConfig struct { Network NetworkConfig `json:"network"` } func GetCloudConfig() (data *CloudConfig, err error) { ret, err := commander.Exec(&commander.Opt{ Name: "cloud-init", Args: []string{ "query", "--all", }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { if ret != nil { logrus.WithFields(ret.Map()).Warn( "cloudinit: Cloud init query failed") } return } data = &CloudConfig{} err = json.Unmarshal(ret.Output, &data) if err != nil { err = &errortypes.ParseError{ errors.Wrapf(err, "cloudinit: Failed to parse cloudinit query"), } return } return } ================================================ FILE: cloudinit/utils.go ================================================ package cloudinit import ( "bytes" "encoding/base64" "text/template" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) type fileData struct { Content string Owner string Path string Permissions string } type writeFileData struct { Files []*fileData } const writeFileTmpl = `{{range .Files}} {{- if eq .Content ""}} - content: "" {{- else}} - encoding: base64 content: {{.Content}} {{- end}} owner: {{.Owner}} path: {{.Path}} permissions: "{{.Permissions}}" {{- end}}` var ( writeFile = template.Must(template.New("write_file").Parse(writeFileTmpl)) ) func generateWriteFiles(filesData []*fileData) (output string, err error) { for _, file := range filesData { file.Content = base64.StdEncoding.EncodeToString([]byte(file.Content)) } data := writeFileData{ Files: filesData, } outputBuf := &bytes.Buffer{} err = writeFile.Execute(outputBuf, data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cloudinit: Failed to exec write file template"), } return } output = outputBuf.String() return } ================================================ FILE: cmd/backup.go ================================================ package cmd import ( "flag" "fmt" "os" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/backup" "github.com/pritunl/pritunl-cloud/errortypes" ) func Backup() (err error) { dest := flag.Arg(1) if dest == "" { err = &errortypes.ParseError{ errors.New("cmd: Missing backup destination path"), } return } fmt.Println("Feature comming soon") os.Exit(1) back := backup.New(dest) err = back.Run() if err != nil { return } return } ================================================ FILE: cmd/dhcp.go ================================================ package cmd import ( "encoding/json" "os" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/dhcpc" "github.com/pritunl/pritunl-cloud/dhcps" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) func DhcpClient() (err error) { err = dhcpc.Main() if err != nil { return } return } func Dhcp4Server() (err error) { config := strings.Trim(os.Getenv("CONFIG"), "'") server4 := &dhcps.Server4{} err = json.Unmarshal([]byte(config), server4) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cmd: Failed to parse DHCP4 configuration"), } return } for { err = server4.Start() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("cmd: DHCP4 server error") } time.Sleep(3 * time.Second) } } func Dhcp6Server() (err error) { config := strings.Trim(os.Getenv("CONFIG"), "'") server6 := &dhcps.Server6{} err = json.Unmarshal([]byte(config), server6) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cmd: Failed to parse DHCP6 configuration"), } return } for { err = server6.Start() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("cmd: DHCP6 server error") } time.Sleep(3 * time.Second) } } func NdpServer() (err error) { config := strings.Trim(os.Getenv("CONFIG"), "'") serverNdp := &dhcps.ServerNdp{} err = json.Unmarshal([]byte(config), serverNdp) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cmd: Failed to parse NDP configuration"), } return } for { err = serverNdp.Start() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("cmd: NDP server error") } time.Sleep(3 * time.Second) } } ================================================ FILE: cmd/imds.go ================================================ package cmd import ( "github.com/pritunl/pritunl-cloud/imds/server" ) func ImdsServer() (err error) { err = server.Main() if err != nil { return } return } ================================================ FILE: cmd/instance.go ================================================ package cmd import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/instance" "github.com/sirupsen/logrus" ) func StartInstance(name string) (err error) { db := database.GetDatabase() defer db.Close() instances, err := instance.GetAll(db, &bson.M{ "name": name, }) for _, inst := range instances { if inst.Action != instance.Start { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), }).Info("cmd: Starting instance") inst.Action = instance.Start err = inst.CommitFields(db, set.NewSet("action")) if err != nil { return } } } return } func StopInstance(name string) (err error) { db := database.GetDatabase() defer db.Close() instances, err := instance.GetAll(db, &bson.M{ "name": name, }) for _, inst := range instances { if inst.Action != instance.Stop { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), }).Info("cmd: Stopping instance") inst.Action = instance.Stop err = inst.CommitFields(db, set.NewSet("action")) if err != nil { return } } } return } ================================================ FILE: cmd/log.go ================================================ package cmd import ( "github.com/sirupsen/logrus" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/log" ) func ClearLogs() (err error) { db := database.GetDatabase() defer db.Close() err = log.Clear(db) if err != nil { return } logrus.Info("cmd.log: Logs cleared") return } ================================================ FILE: cmd/mtu.go ================================================ package cmd import ( "github.com/pritunl/pritunl-cloud/mtu" ) func MtuCheck() (err error) { chk := mtu.NewCheck() err = chk.Run() if err != nil { return } return } ================================================ FILE: cmd/node.go ================================================ package cmd import ( "os" "os/signal" "syscall" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/defaults" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/relations/definitions" "github.com/pritunl/pritunl-cloud/router" "github.com/pritunl/pritunl-cloud/setup" "github.com/pritunl/pritunl-cloud/sync" "github.com/pritunl/pritunl-cloud/task" "github.com/pritunl/pritunl-cloud/upgrade" "github.com/sirupsen/logrus" ) func Node() (err error) { err = upgrade.Upgrade() if err != nil { return } objId, err := bson.ObjectIDFromHex(config.Config.NodeId) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cmd: Failed to parse ObjectId"), } return } nde := &node.Node{ Id: objId, } err = nde.Init() if err != nil { return } definitions.Init() err = setup.Iptables() if err != nil { return } err = defaults.Defaults() if err != nil { return } sync.Init() logrus.WithFields(logrus.Fields{ "production": constants.Production, "types": nde.Types, }).Info("router: Starting node") routr := &router.Router{} routr.Init() err = task.Init() if err != nil { return } go func() { err = routr.Run() if err != nil && !constants.Shutdown { panic(err) } }() sig := make(chan os.Signal, 2) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) <-sig logrus.Info("cmd.node: Shutting down") constants.Shutdown = true go routr.Shutdown() if constants.Production && !constants.FastExit { time.Sleep(20 * time.Second) } constants.Interrupt = true if !constants.Production || constants.FastExit { time.Sleep(300 * time.Millisecond) } else { time.Sleep(10 * time.Second) } return } ================================================ FILE: cmd/optimize.go ================================================ package cmd import ( "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) func Optimize() (err error) { err = optimizeNested() if err != nil { return } return } func optimizeNested() (err error) { resp, err := utils.ConfirmDefault( "Enable nested virtualization", true, ) if err != nil { return } pth := "/etc/modprobe.d/kvm-nested.conf" if resp { err = utils.CreateWrite( pth, "options kvm-intel nested=1\noptions kvm-amd nested=1\n", 0644, ) if err != nil { return } logrus.WithFields(logrus.Fields{ "path": pth, }).Info("sysctl: Nested virtualization enabled") } else { exists, e := utils.Exists(pth) if e != nil { err = e return } if exists { err = utils.Remove(pth) if err != nil { return } logrus.WithFields(logrus.Fields{ "path": pth, }).Info("sysctl: Nested virtualization disabled") } } return } ================================================ FILE: cmd/settings.go ================================================ package cmd import ( "encoding/json" "flag" "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/sirupsen/logrus" ) func Mongo() (err error) { mongodbUri := flag.Arg(1) err = config.Load() if err != nil { return } config.Config.MongoUri = mongodbUri err = config.Save() if err != nil { return } logrus.WithFields(logrus.Fields{ "mongo_uri": config.Config.MongoUri, }).Info("cmd: Set MongoDB URI") return } func ResetNodeWeb() (err error) { db := database.GetDatabase() defer db.Close() err = config.Load() if err != nil { return } ndeId, err := bson.ObjectIDFromHex(config.Config.NodeId) if err != nil || ndeId.IsZero() { err = nil logrus.Info("cmd: Node not initialized") return } coll := db.Nodes() _, err = coll.UpdateOne(db, &bson.M{ "_id": ndeId, }, &bson.M{ "$set": &bson.M{ "types": []string{"admin", "hypervisor"}, "port": 443, "protocol": "https", "no_redirect_server": false, "admin_domain": "", "user_domain": "", "webauthn_domain": "", }, }) if err != nil { err = database.ParseError(err) return } logrus.WithFields(logrus.Fields{ "node_id": config.Config.NodeId, }).Info("cmd: Node web server reset") return } func DefaultPassword() (err error) { db := database.GetDatabase() defer db.Close() usr, err := user.GetUsername(db, user.Local, "pritunl") if err != nil { return } if usr.DefaultPassword == "" { err = &errortypes.NotFoundError{ errors.New("cmd: No default password available"), } return } logrus.Info("cmd: Get default password") fmt.Println("Username: pritunl") fmt.Println("Password: " + usr.DefaultPassword) return } func ResetPassword() (err error) { db := database.GetDatabase() defer db.Close() coll := db.Users() _, err = coll.DeleteOne(db, &bson.M{ "username": "pritunl", }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } usr := user.User{ Type: user.Local, Username: "pritunl", Administrator: "super", } _, err = usr.Validate(db) if err != nil { return } err = usr.GenerateDefaultPassword() if err != nil { return } err = usr.Insert(db) if err != nil { return } logrus.Info("cmd: Password reset") fmt.Println("Username: pritunl") fmt.Println("Password: " + usr.DefaultPassword) return } func DisablePolicies() (err error) { db := database.GetDatabase() defer db.Close() coll := db.Policies() _, err = coll.UpdateMany(db, &bson.M{}, &bson.M{ "$set": &bson.M{ "disabled": true, }, }) if err != nil { err = database.ParseError(err) return } logrus.Info("cmd: Policies disabled") return } func DisableFirewall() (err error) { db := database.GetDatabase() defer db.Close() err = config.Load() if err != nil { return } ndeId, err := bson.ObjectIDFromHex(config.Config.NodeId) if err != nil || ndeId.IsZero() { err = nil logrus.Info("cmd: Node not initialized") return } coll := db.Nodes() _, err = coll.UpdateOne(db, &bson.M{ "_id": ndeId, }, &bson.M{ "$set": &bson.M{ "firewall": false, }, }) if err != nil { err = database.ParseError(err) return } logrus.WithFields(logrus.Fields{ "node_id": config.Config.NodeId, }).Info("cmd: Firewall disabled") return } func SettingsSet() (err error) { group := flag.Arg(1) key := flag.Arg(2) val := flag.Arg(3) db := database.GetDatabase() defer db.Close() var valParsed interface{} err = json.Unmarshal([]byte(val), &valParsed) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "cmd.settings: Failed to parse value"), } return } err = settings.Set(db, group, key, valParsed) if err != nil { return } return } func SettingsUnset() (err error) { group := flag.Arg(1) key := flag.Arg(2) db := database.GetDatabase() defer db.Close() err = settings.Unset(db, group, key) if err != nil { return } return } ================================================ FILE: colorize/colorize.go ================================================ package colorize type Color string const ( None = "" Bold = "\033[1m" Black = "\033[0;30m" BlackBold = "\033[1;30m" Red = "\033[0;31m" RedBold = "\033[1;31m" Green = "\033[0;32m" GreenBold = "\033[1;32m" Yellow = "\033[0;33m" YellowBold = "\033[1;33m" Blue = "\033[0;34m" BlueBold = "\033[1;34m" Purple = "\033[0;35m" PurpleBold = "\033[1;35m" Cyan = "\033[0;36m" CyanBold = "\033[1;36m" White = "\033[0;37m" WhiteBold = "\033[1;37m" BlackBg = "\033[40m" RedBg = "\033[41m" GreenBg = "\033[42m" YellowBg = "\033[43m" BlueBg = "\033[44m" PurpleBg = "\033[45m" CyanBg = "\033[46m" WhiteBg = "\033[47m" ) func ColorString(input string, fg Color, bg Color) (str string) { str = string(fg) + string(bg) + input + "\033[0m" return } ================================================ FILE: completion/completion.go ================================================ package completion import ( "sort" "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) type Completion struct { Organizations []*database.Named `json:"organizations"` Authorities []*database.Named `json:"authorities"` Policies []*database.Named `json:"policies"` Domains []*domain.Completion `json:"domains"` Vpcs []*vpc.Completion `json:"vpcs"` Datacenters []*datacenter.Completion `json:"datacenters"` Blocks []*block.Completion `json:"blocks"` Nodes []*node.Completion `json:"nodes"` Pools []*pool.Completion `json:"pools"` Zones []*zone.Completion `json:"zones"` Shapes []*shape.Completion `json:"shapes"` Images []*image.Completion `json:"images"` Storages []*storage.Completion `json:"storages"` Builds []*Build `json:"builds"` Instances []*instance.Completion `json:"instances"` Plans []*plan.Completion `json:"plans"` Certificates []*certificate.Completion `json:"certificates"` Secrets []*secret.Completion `json:"secrets"` Pods []*pod.Completion `json:"pods"` Units []*unit.Completion `json:"units"` } type Build struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Pod bson.ObjectID `json:"pod"` Organization bson.ObjectID `json:"organization"` Tags []*BuildTag `json:"tags"` } type BuildTag struct { Tag string `json:"tag"` Timestamp time.Time `json:"timestamp"` } func get(db *database.Database, coll *database.Collection, query bson.M, projection *bson.M, sort *bson.D, new func() interface{}, add func(interface{})) (err error) { opts := options.Find(). SetProjection(projection) if sort != nil { opts.SetSort(sort) } cursor, err := coll.Find(db, query, opts) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { item := new() err = cursor.Decode(item) if err != nil { err = database.ParseError(err) return } add(item) } return } func GetCompletion(db *database.Database, orgId bson.ObjectID, orgRoles []string) (cmpl *Completion, err error) { cmpl = &Completion{} query := bson.M{} if !orgId.IsZero() { query["organization"] = orgId } releaseImages := map[string][]*image.Completion{} otherImages := []*image.Completion{} unitsMap := map[bson.ObjectID]*unit.Completion{} buildsMap := map[bson.ObjectID]*Build{} deployments := []*deployment.Deployment{} var wg sync.WaitGroup errChan := make(chan error, 16) wg.Add(1) go func() { defer wg.Done() var orgQuery bson.M if !orgId.IsZero() { if orgRoles == nil { orgRoles = []string{} } orgQuery = bson.M{ "roles": bson.M{ "$in": orgRoles, }, } } else { orgQuery = bson.M{} } var orgs []*database.Named err := get( db, db.Organizations(), orgQuery, &bson.M{ "_id": 1, "name": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &database.Named{} }, func(item interface{}) { orgs = append( orgs, item.(*database.Named), ) }, ) if err != nil { errChan <- err return } cmpl.Organizations = orgs }() wg.Add(1) go func() { defer wg.Done() var authrs []*database.Named err := get( db, db.Authorities(), query, &bson.M{ "_id": 1, "name": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &database.Named{} }, func(item interface{}) { authrs = append( authrs, item.(*database.Named), ) }, ) if err != nil { errChan <- err return } cmpl.Authorities = authrs }() wg.Add(1) go func() { defer wg.Done() var domains []*domain.Completion err := get( db, db.Domains(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &domain.Completion{} }, func(item interface{}) { domains = append( domains, item.(*domain.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Domains = domains }() wg.Add(1) go func() { defer wg.Done() var vpcs []*vpc.Completion err := get( db, db.Vpcs(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, "vpc_id": 1, "network": 1, "subnets": 1, "datacenter": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &vpc.Completion{} }, func(item interface{}) { vpcs = append( vpcs, item.(*vpc.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Vpcs = vpcs }() wg.Add(1) go func() { defer wg.Done() var dcQuery bson.M if !orgId.IsZero() { dcQuery = bson.M{ "$or": []bson.M{ bson.M{ "match_organizations": false, }, bson.M{ "organizations": orgId, }, }, } } else { dcQuery = bson.M{} } var datacenters []*datacenter.Completion err := get( db, db.Datacenters(), dcQuery, &bson.M{ "_id": 1, "name": 1, "network_mode": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &datacenter.Completion{} }, func(item interface{}) { datacenters = append( datacenters, item.(*datacenter.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Datacenters = datacenters }() if orgId.IsZero() { wg.Add(1) go func() { defer wg.Done() var blocks []*block.Completion err := get( db, db.Blocks(), query, &bson.M{ "_id": 1, "name": 1, "type": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &block.Completion{} }, func(item interface{}) { blocks = append( blocks, item.(*block.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Blocks = blocks }() } wg.Add(1) go func() { defer wg.Done() var nodes []*node.Completion err := get( db, db.Nodes(), bson.M{}, &bson.M{ "_id": 1, "name": 1, "zone": 1, "types": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &node.Completion{} }, func(item interface{}) { nde := item.(*node.Completion) if !nde.IsHypervisor() { return } nodes = append( nodes, nde, ) }, ) if err != nil { errChan <- err return } cmpl.Nodes = nodes }() wg.Add(1) go func() { defer wg.Done() var pools []*pool.Completion err := get( db, db.Pools(), query, &bson.M{ "_id": 1, "name": 1, "zone": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &pool.Completion{} }, func(item interface{}) { pools = append( pools, item.(*pool.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Pools = pools }() wg.Add(1) go func() { defer wg.Done() var zones []*zone.Completion err := get( db, db.Zones(), bson.M{}, &bson.M{ "_id": 1, "name": 1, "datacenter": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &zone.Completion{} }, func(item interface{}) { zones = append( zones, item.(*zone.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Zones = zones }() wg.Add(1) go func() { defer wg.Done() var shapes []*shape.Completion err := get( db, db.Shapes(), bson.M{}, &bson.M{ "_id": 1, "name": 1, "datacenter": 1, "flexible": 1, "memory": 1, "processors": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &shape.Completion{} }, func(item interface{}) { shapes = append( shapes, item.(*shape.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Shapes = shapes }() wg.Add(1) go func() { defer wg.Done() err := get( db, db.Images(), query, &bson.M{ "_id": 1, "name": 1, "release": 1, "build": 1, "organization": 1, "deployment": 1, "type": 1, "firmware": 1, "key": 1, "storage": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &image.Completion{} }, func(item interface{}) { img := item.(*image.Completion) if img.Release != "" { releaseImages[img.Release] = append( releaseImages[img.Release], img, ) } else { otherImages = append(otherImages, img) } }, ) if err != nil { errChan <- err return } }() wg.Add(1) go func() { defer wg.Done() var storages []*storage.Completion if orgId.IsZero() { err := get( db, db.Storages(), bson.M{}, &bson.M{ "_id": 1, "name": 1, "type": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &storage.Completion{} }, func(item interface{}) { storages = append( storages, item.(*storage.Completion), ) }, ) if err != nil { errChan <- err return } } cmpl.Storages = storages }() wg.Add(1) go func() { defer wg.Done() var instances []*instance.Completion err := get( db, db.Instances(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, "zone": 1, "vpc": 1, "subnet": 1, "node": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &instance.Completion{} }, func(item interface{}) { instances = append( instances, item.(*instance.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Instances = instances }() wg.Add(1) go func() { defer wg.Done() var plans []*plan.Completion err := get( db, db.Plans(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &plan.Completion{} }, func(item interface{}) { plans = append( plans, item.(*plan.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Plans = plans }() wg.Add(1) go func() { defer wg.Done() var certificates []*certificate.Completion err := get( db, db.Certificates(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, "type": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &certificate.Completion{} }, func(item interface{}) { certificates = append( certificates, item.(*certificate.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Certificates = certificates }() wg.Add(1) go func() { defer wg.Done() var secrets []*secret.Completion err := get( db, db.Secrets(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, "type": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &secret.Completion{} }, func(item interface{}) { secrets = append( secrets, item.(*secret.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Secrets = secrets }() wg.Add(1) go func() { defer wg.Done() var pods []*pod.Completion err := get( db, db.Pods(), query, &bson.M{ "_id": 1, "name": 1, "organization": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &pod.Completion{} }, func(item interface{}) { pods = append( pods, item.(*pod.Completion), ) }, ) if err != nil { errChan <- err return } cmpl.Pods = pods }() wg.Add(1) go func() { defer wg.Done() var units []*unit.Completion err := get( db, db.Units(), query, &bson.M{ "_id": 1, "pod": 1, "organization": 1, "name": 1, "kind": 1, }, &bson.D{ {"name", 1}, }, func() interface{} { return &unit.Completion{} }, func(item interface{}) { unt := item.(*unit.Completion) units = append( units, unt, ) unitsMap[unt.Id] = unt }, ) if err != nil { errChan <- err return } cmpl.Units = units }() wg.Add(1) go func() { defer wg.Done() var deplyQuery bson.M if !orgId.IsZero() { deplyQuery = bson.M{ "organizations": orgId, "kind": "image", } } else { deplyQuery = bson.M{ "kind": "image", } } err = get( db, db.Deployments(), deplyQuery, &bson.M{ "_id": 1, "name": 1, "pod": 1, "unit": 1, "organization": 1, "kind": 1, "state": 1, "status": 1, "image": 1, "image_data": 1, "tags": 1, }, &bson.D{ {"timestamp", -1}, }, func() interface{} { return &deployment.Deployment{} }, func(item interface{}) { deployments = append(deployments, item.(*deployment.Deployment)) }, ) if err != nil { errChan <- err return } }() wg.Wait() close(errChan) for e := range errChan { if e != nil { err = e return } } for _, imgs := range releaseImages { tags := []string{"latest"} var latestImg *image.Completion for _, img := range imgs { tags = append(tags, img.Build) if latestImg == nil { latestImg = img } else if img.Build > latestImg.Build { latestImg = img } } latestImg.Name = latestImg.Release latestImg.Tags = tags cmpl.Images = append(cmpl.Images, latestImg) } sort.Sort(image.CompletionsSort(cmpl.Images)) cmpl.Images = append( cmpl.Images, otherImages..., ) for _, deply := range deployments { if !deply.ImageReady() { return } unt := unitsMap[deply.Unit] if unt == nil { return } build := buildsMap[deply.Unit] if build == nil { build = &Build{ Id: deply.Unit, Name: unt.Name, Pod: unt.Pod, Organization: unt.Organization, Tags: []*BuildTag{ &BuildTag{ Tag: "latest", Timestamp: deply.Timestamp, }, }, } buildsMap[deply.Unit] = build } for _, tag := range deply.Tags { build.Tags = append(build.Tags, &BuildTag{ Tag: tag, Timestamp: deply.Timestamp, }) } } for _, build := range buildsMap { cmpl.Builds = append(cmpl.Builds, build) } return } ================================================ FILE: compositor/compositor.go ================================================ package compositor import ( "fmt" "io/ioutil" "os/user" "path" "strconv" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) const ( ProcDir = "/proc" ) func GetEnv(username, driPath string, driPrime bool) ( envData string, err error) { desktopEnv := settings.Hypervisor.DesktopEnv files, err := ioutil.ReadDir(ProcDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "compositor: Failed to read proc directory"), } return } unixUser, err := user.Lookup(username) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "compositor: Failed to find GUI user"), } return } for _, file := range files { pidStr := file.Name() _, e := strconv.Atoi(pidStr) if e != nil { continue } cmdlinePath := path.Join(ProcDir, pidStr, "cmdline") environPath := path.Join(ProcDir, pidStr, "environ") loginuidPath := path.Join(ProcDir, pidStr, "loginuid") exists, e := utils.Exists(cmdlinePath) if e != nil { err = e return } if !exists { continue } exists, err = utils.Exists(environPath) if err != nil { return } if !exists { continue } exists, err = utils.Exists(loginuidPath) if err != nil { return } if !exists { continue } procUid, e := ioutil.ReadFile(loginuidPath) if e != nil { err = &errortypes.ReadError{ errors.Wrapf( e, "compositor: Failed to read proc '%s' loginuid", pidStr, ), } return } if strings.TrimSpace(string(procUid)) != unixUser.Uid { continue } procCmd, e := ioutil.ReadFile(cmdlinePath) if e != nil { err = &errortypes.ReadError{ errors.Wrapf( e, "compositor: Failed to read proc '%s' cmdline", pidStr, ), } return } if !strings.Contains(string(procCmd), desktopEnv) && !strings.Contains(string(procCmd), "xdg") { continue } procEnv, e := ioutil.ReadFile(environPath) if e != nil { err = &errortypes.ReadError{ errors.Wrapf( e, "compositor: Failed to read proc '%s' environ", pidStr, ), } return } displayEnv := "" waylandDisplayEnv := "" xauthEnv := "" environ := strings.ReplaceAll(string(procEnv), "\000", "\n") for _, env := range strings.Split(environ, "\n") { envSpl := strings.SplitN(env, "=", 2) if len(envSpl) != 2 { continue } if strings.ToLower(envSpl[0]) == "display" { displayEnv = envSpl[1] } else if strings.ToLower(envSpl[0]) == "wayland_display" { waylandDisplayEnv = envSpl[1] } else if strings.ToLower(envSpl[0]) == "xauthority" { xauthEnv = envSpl[1] } } if displayEnv == "" || xauthEnv == "" { continue } envData += fmt.Sprintf("\nEnvironment=\"DISPLAY=%s\"", displayEnv) if waylandDisplayEnv != "" { envData += fmt.Sprintf( "\nEnvironment=\"WAYLAND_DISPLAY=%s\"", waylandDisplayEnv) envData += "\nEnvironment=\"GDK_BACKEND=wayland\"" envData += "\nEnvironment=\"XDG_SESSION_TYPE=wayland\"" envData += fmt.Sprintf( "\nEnvironment=\"XDG_RUNTIME_DIR=/run/user/%s\"", unixUser.Uid) envData += "\nEnvironment=\"XDG_SESSION_CLASS=user\"" } envData += fmt.Sprintf("\nEnvironment=\"XAUTHORITY=%s\"", xauthEnv) if driPath != "" { envData += fmt.Sprintf( "\nEnvironment=\"DRI_RENDER_DEVICE=%s\"", driPath, ) } if driPrime { envData += "\nEnvironment=\"DRI_PRIME=1\"" envData += "\nEnvironment=\"__NV_PRIME_RENDER_OFFLOAD=1\"" envData += "\nEnvironment=\"__GLX_VENDOR_LIBRARY_NAME=nvidia\"" } return } err = &errortypes.ReadError{ errors.New("compositor: Failed to find X environment"), } return } ================================================ FILE: config/config.go ================================================ package config import ( "encoding/json" "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/utils" ) var ( Config = &ConfigData{} StaticRoot = "" StaticTestingRoot = "" DefaultMongoUri = "mongodb://localhost:27017/pritunl-cloud" ) type ConfigData struct { path string `json:"-"` loaded bool `json:"-"` MongoUri string `json:"mongo_uri"` NodeId string `json:"node_id"` } func (c *ConfigData) Save() (err error) { if !c.loaded { err = &errortypes.WriteError{ errors.New("config: Config file has not been loaded"), } return } data, err := json.MarshalIndent(c, "", "\t") if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "config: File marshal error"), } return } err = utils.ExistsMkdir(filepath.Dir(constants.ConfPath), 0755) if err != nil { return } err = ioutil.WriteFile(constants.ConfPath, data, 0600) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "config: File write error"), } return } return } func Load() (err error) { data := &ConfigData{} _, err = os.Stat("/cloud/pritunl-cloud.json") if err == nil { constants.ConfPath = "/cloud/pritunl-cloud.json" constants.DefaultRoot = "/cloud" constants.DefaultCache = "/cloud/cache" } _, err = os.Stat(constants.ConfPath) if err != nil { if os.IsNotExist(err) { err = nil data.loaded = true Config = data } else { err = &errortypes.ReadError{ errors.Wrap(err, "config: File stat error"), } } return } file, err := ioutil.ReadFile(constants.ConfPath) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "config: File read error"), } return } err = json.Unmarshal(file, data) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "config: File unmarshal error"), } return } data.loaded = true Config = data return } func Save() (err error) { err = Config.Save() if err != nil { return } return } func GetModTime() (mod time.Time, err error) { stat, err := os.Stat(constants.ConfPath) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "config: Failed to stat conf file"), } return } mod = stat.ModTime() return } func init() { module := requires.New("config") module.Handler = func() (err error) { for _, pth := range constants.StaticRoot { exists, _ := utils.ExistsDir(pth) if exists { StaticRoot = pth } } if StaticRoot == "" { StaticRoot = constants.StaticRoot[len(constants.StaticRoot)-1] } for _, pth := range constants.StaticTestingRoot { exists, _ := utils.ExistsDir(pth) if exists { StaticTestingRoot = pth } } if StaticTestingRoot == "" { StaticTestingRoot = constants.StaticTestingRoot[len( constants.StaticTestingRoot)-1] } err = Load() if err != nil { return } save := false if Config.NodeId == "" { save = true Config.NodeId = bson.NewObjectID().Hex() } if Config.MongoUri == "" { save = true data, err := utils.ReadExists("/var/lib/mongo/credentials.txt") if err != nil { err = nil } else { lines := strings.Split(string(data), "\n") for _, line := range lines { if strings.HasPrefix(strings.TrimSpace(line), "mongodb://pritunl-cloud") { Config.MongoUri = strings.TrimSpace(line) break } } } if Config.MongoUri == "" { Config.MongoUri = DefaultMongoUri } } if save { err = Save() if err != nil { return } } return } } ================================================ FILE: constants/constants.go ================================================ package constants import ( "time" ) const ( Version = "2.0.3665.99" DatabaseVersion = 1 LogPath = "/var/log/pritunl-cloud.log" LogPath2 = "/var/log/pritunl-cloud.log.1" StaticCache = true RetryDelay = 3 * time.Second ) var ( Production = true DebugWeb = false FastExit = false LockDebug = false Interrupt = false Shutdown = false ConfPath = "/etc/pritunl-cloud.json" DefaultRoot = "/var/lib/pritunl-cloud" DefaultCache = "/var/lib/pritunl-cloud/cache" DefaultTemp = "/var/lib/pritunl-cloud/temp" StaticRoot = []string{ "www/dist", "/usr/share/pritunl-cloud/www", } StaticTestingRoot = []string{ "/home/cloud/git/pritunl-cloud/www/dist-dev", "/home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/dist-dev", "/usr/share/pritunl-cloud/www", } ) var PritunlKeyring = `-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBFu4Ww4BCACq/6Tc4wMhOIMEM9nUtWOZNfVAPt9NEVQ3+PDdSC6+dAarM0z2 geUByo1Qie4sAc1KwJJZ6t+X8mpxBZdqVwCeI4MksUfEnrL01JbAwC8Gw5nw6R1g l9yuXC/NeZozI4aIjcg3etvX871G7oDZRfdlcRaP6BceqIZRVJXno9wGB4gns+zv 8RGtK+87YeCq3cFyuw1uvW3lPEcxdVdJMI6YLAiTjvM5RIpexjf6DYmb9uKpC9pu fbF8KNEi4C7usvSpdohkd747oQlL/JBJO8RBZi0bumbxVhjR1S0BPA+4Z//UyUEc dNvyxsTot8NeljWL8HBh/TROoCDq01URdf8nABEBAAG0HVByaXR1bmwgPGNvbnRh Y3RAcHJpdHVubC5jb20+iQE4BBMBAgAiBQJbuFsOAhsDBgsJCAcDAgYVCAIJCgsE FgIDAQIeAQIXgAAKCRAK21I+BVwIpOFfB/0ZrE+OOsbWxwdC6jR7jEH0kS1e+HSV bFZxqXgBl8zsxtWF5xpD9o4iSRSudtwfWKdRUvoliiL8VOYWMgyl4aHOq/oR11pR es1Cy70qDyj+SuzxZjnhhLZMAYZnynbWCB7e9MP0rcmIOZImE2UNbFXWV85vjAHp zMXnDKrvDlz5eyUT/dfT6HgkiaVq4SyfubYJwXMj+vF3+hppbovFIEWYFl/A24YI ql81EqfBXnzf2S9HDsJ2CAM5P33u+T7V0r8Q/HeX/1OlZGelmeyV3bumhg+PfTgg 3sQyOSiST/stczt2gyw7UfiTWgwW0oYP/68FCBOzHC/kQxpk+kpf3Z8JuQENBFu4 Ww4BCAC0d2fgGm+2WRjdrYxZpBzKS9z8XNBQ6feNmliQECdJcrB+VHj/PNXgAgCM aTM21eZCHm2t3pbwcEO4v3y2RIVbRl1PTGvULlzKK3ZvUcUINTqWFlERsSdQq2o1 v996WN6Crc+P6txu17S74XwsMZcbCSYPG9N80cEkvFajuYYjIIf2Zww/wcbgGr0S dZnGPTZScBIfyWsxMCnzVLwWkIig6gEFqLgP5gcPhAhT8Rfbw3SYYIVogXTw5tyl nZZE+LrHAIN2XABVH3ho3XZQIjWquKd6ipzSenKyZi+Gry8QpG+r17ppCZmigsDj y3rgCIhRCl46VGSh2a0R51s5npR3ABEBAAGJAR8EGAECAAkFAlu4Ww4CGyAACgkQ CttSPgVcCKTiKQgAiSXywu5m61uFyRkWxYURrKqR2R/DMQ1C5Q4bFTqR67BRlxTD V8zKBPPCAPLbdnWYxohXYNYyyoT/xbmH312829AL2GmAtgysKJpdlG+bbvd+JAmb wdgfyXGs4//mGUA7MDIvVBr/4Vd2qle3//AZLgKyErM3tuESlWYm40CmUp+pnEMt nDDPbo8ypt6X02dTjPZ81UVLWemU+v3fsFichpo66dlE5N1cXJg9nkvJbfRxQgKf jqqYVPMtU64wCwaZNFPuHXyWvU7G+WDWnw6RPzdONjN6QiyZSdSK34g86VsnduoW J4x+1Z/v6ycqqq+t+niEDGV9YyEbeSHlr7MGbrkBDQRbuFsOAQgAyraB3isfso3/ PivZnDGm7+Shmup9CbXD1JX6EL0AtKfWwSb9kPWTCw4Wr4aJmL5DNCxpEKjCz4yO HwZ4Qnn3OkclDem+lrEueXwvaGwvOPGBg0X44b2XNJkRGDCZQfFoePacp6SdhS3n Efd6HsRKMgG0Xo+gcYuqwFUJ4bvBi0dl6R1rPdbnRtbykCjrinNs56kiBH+Smzdh E1+wcivRuFOIIU6GZylVuTam+QNGZScKFxCB7FSp0QoxaQWmXZK7DH9vrKsNOC3y bMQcWRvir5SZ7GnoKl/H95FjX+3cgJoEIGMSc4EwCifnUqVNgEirKyfbzTOdGHVG zW8qaS7kewARAQABiQEfBBgBAgAJBQJbuFsOAhsMAAoJEArbUj4FXAikIjAH/jaL 6kFewz071THtll1E3+OwCK389UXVJyh7p1fbWftRR7AhH7Xte3MnPeFGvW9PzRx+ WY8VuQOMw3vDk2bGy4LEhZSMIFRLKYK2wzrrcom75cYSwqzopFVOukW8t0OjFThX WRJIk82EMo3wOsGlUXELAjOGNxvzJ8OIncH0hh/hbsxUMHRxJAHBWOEEmzdwc5po x8pCEDnXvIqL6mtQOQUmnVIXpMt1hui9dnE3JkyM/UY7rNvIcSU1A7pLmvoP4YlZ HHAgY88Wur0X2ksHdfQaISVxW0iZGnJIrGAbW1Ayw0UkRQGYjWglk4EVuvqW+Go7 3FxR7SpBSsF/SmguI/w= =ohOM -----END PGP PUBLIC KEY BLOCK-----` ================================================ FILE: cookie/cookie.go ================================================ package cookie import ( "net/http" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/settings" ) var ( AdminStore *sessions.CookieStore UserStore *sessions.CookieStore ) type Cookie struct { Id bson.ObjectID store *sessions.Session w http.ResponseWriter r *http.Request } func (c *Cookie) Get(key string) string { valInf := c.store.Values[key] if valInf == nil { return "" } return valInf.(string) } func (c *Cookie) Set(key string, val string) { c.store.Values[key] = val } func (c *Cookie) GetSession(db *database.Database, r *http.Request, typ string) (sess *session.Session, err error) { sessId := c.Get("id") if sessId == "" { err = &errortypes.NotFoundError{ errors.New("cookie: Session not found"), } return } sig := c.Get("signature") if sig == "" { err = &errortypes.NotFoundError{ errors.New("cookie: Session signature not found"), } return } sess, err = session.GetUpdate(db, sessId, r, typ, sig) if err != nil { switch err.(type) { case *database.NotFoundError: err = &errortypes.NotFoundError{ errors.New("cookie: Session not found"), } default: err = &errortypes.UnknownError{ errors.Wrap(err, "cookie: Unknown session error"), } } return } return } func (c *Cookie) NewSession(db *database.Database, r *http.Request, id bson.ObjectID, remember bool, typ string) ( sess *session.Session, err error) { sess, sig, err := session.New(db, r, id, typ) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "cookie: Unknown session error"), } return } c.Set("id", sess.Id) c.Set("signature", sig) maxAge := 0 if remember { maxAge = settings.Auth.CookieAge } c.store.Options.MaxAge = maxAge err = c.Save() if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "cookie: Unknown session error"), } return } return } func (c *Cookie) Remove(db *database.Database) (err error) { sessId := c.Get("id") if sessId == "" { return } err = session.Remove(db, sessId) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "cookie: Unknown session error"), } return } c.Set("id", "") c.Set("signature", "") err = c.Save() if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "cookie: Unknown session error"), } return } return } func (c *Cookie) Save() (err error) { err = c.store.Save(c.r, c.w) return } func init() { module := requires.New("cookie") module.After("settings") module.Handler = func() (err error) { db := database.GetDatabase() defer db.Close() adminCookieAuthKey := settings.System.AdminCookieAuthKey adminCookieCryptoKey := settings.System.AdminCookieCryptoKey userCookieAuthKey := settings.System.UserCookieAuthKey userCookieCryptoKey := settings.System.UserCookieCryptoKey if len(adminCookieAuthKey) == 0 || len(adminCookieCryptoKey) == 0 { adminCookieAuthKey = securecookie.GenerateRandomKey(64) adminCookieCryptoKey = securecookie.GenerateRandomKey(32) settings.System.AdminCookieAuthKey = adminCookieAuthKey settings.System.AdminCookieCryptoKey = adminCookieCryptoKey fields := set.NewSet( "admin_cookie_auth_key", "admin_cookie_crypto_key", ) err = settings.Commit(db, settings.System, fields) if err != nil { return } } if len(userCookieAuthKey) == 0 || len(userCookieCryptoKey) == 0 { userCookieAuthKey = securecookie.GenerateRandomKey(64) userCookieCryptoKey = securecookie.GenerateRandomKey(32) settings.System.UserCookieAuthKey = userCookieAuthKey settings.System.UserCookieCryptoKey = userCookieCryptoKey fields := set.NewSet( "user_cookie_auth_key", "user_cookie_crypto_key", ) err = settings.Commit(db, settings.System, fields) if err != nil { return } } AdminStore = sessions.NewCookieStore( adminCookieAuthKey, adminCookieCryptoKey) AdminStore.Options.Secure = true AdminStore.Options.HttpOnly = true UserStore = sessions.NewCookieStore( userCookieAuthKey, userCookieCryptoKey) UserStore.Options.Secure = true UserStore.Options.HttpOnly = true return } } ================================================ FILE: cookie/utils.go ================================================ package cookie import ( "net/http" "github.com/dropbox/godropbox/errors" "github.com/gorilla/securecookie" "github.com/pritunl/pritunl-cloud/errortypes" ) func GetAdmin(w http.ResponseWriter, r *http.Request) ( cook *Cookie, err error) { store, err := AdminStore.New(r, "pritunl-cloud-admin") if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err.(securecookie.MultiError)[0], "cookie: Unknown cookie error"), } return } cook = &Cookie{ store: store, w: w, r: r, } return } func NewAdmin(w http.ResponseWriter, r *http.Request) (cook *Cookie) { store, _ := AdminStore.New(r, "pritunl-cloud-admin") cook = &Cookie{ store: store, w: w, r: r, } return } func CleanAdmin(w http.ResponseWriter, r *http.Request) { cook := &http.Cookie{ Name: "pritunl-cloud-admin", Path: "/", Secure: true, HttpOnly: true, MaxAge: -1, } http.SetCookie(w, cook) return } func GetUser(w http.ResponseWriter, r *http.Request) ( cook *Cookie, err error) { store, err := UserStore.New(r, "pritunl-cloud-user") if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err.(securecookie.MultiError)[0], "cookie: Unknown cookie error"), } return } cook = &Cookie{ store: store, w: w, r: r, } return } func NewUser(w http.ResponseWriter, r *http.Request) (cook *Cookie) { store, _ := UserStore.New(r, "pritunl-cloud-user") cook = &Cookie{ store: store, w: w, r: r, } return } func CleanUser(w http.ResponseWriter, r *http.Request) { cook := &http.Cookie{ Name: "pritunl-cloud-user", Path: "/", Secure: true, HttpOnly: true, MaxAge: -1, } http.SetCookie(w, cook) return } ================================================ FILE: crypto/crypto.go ================================================ package crypto import ( "crypto/hmac" "crypto/rand" "crypto/sha512" "crypto/subtle" "encoding/base64" "encoding/json" "io" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/sign" ) type Message struct { Nonce string Message string Signature string } type AsymNaclHmacKey struct { Key string Secret string PublicKey string PrivateKey string } type AsymNaclHmac struct { key *[32]byte secret *[32]byte publicKey *[32]byte privateKey *[64]byte nonceHandler func(nonce []byte) error } func (a *AsymNaclHmac) RegisterNonce(handler func(nonce []byte) error) { a.nonceHandler = handler } func (a *AsymNaclHmac) Seal(input any) (msg *Message, err error) { if a.key == nil || a.secret == nil || a.privateKey == nil { err = &errortypes.AuthenticationError{ errors.New("crypto: Private key and secret not loaded"), } return } nonce := new([24]byte) _, err = io.ReadFull(rand.Reader, nonce[:]) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to generate nonce"), } return } nonceStr := base64.StdEncoding.EncodeToString(nonce[:]) data, err := json.Marshal(input) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to marshal json data"), } return } encByt := secretbox.Seal(nil, data, nonce, a.key) sigEncByt := sign.Sign(nil, encByt, a.privateKey) sigEncStr := base64.StdEncoding.EncodeToString(sigEncByt) hashFunc := hmac.New(sha512.New, a.secret[:]) hashFunc.Write([]byte(sigEncStr)) rawSignature := hashFunc.Sum(nil) sigStr := base64.StdEncoding.EncodeToString(rawSignature) msg = &Message{ Nonce: nonceStr, Message: sigEncStr, Signature: sigStr, } return } func (a *AsymNaclHmac) SealJson(input any) (output string, err error) { msg, err := a.Seal(input) if err != nil { return } outputByt, err := json.Marshal(msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to marshal message"), } return } output = string(outputByt) return } func (a *AsymNaclHmac) Unseal(msg *Message, output any) (err error) { if a.key == nil || a.secret == nil || a.publicKey == nil { err = &errortypes.AuthenticationError{ errors.New("crypto: Private key and secret not loaded"), } return } hashFunc := hmac.New(sha512.New, a.secret[:]) hashFunc.Write([]byte(msg.Message)) rawSignature := hashFunc.Sum(nil) sigStr := base64.StdEncoding.EncodeToString(rawSignature) if subtle.ConstantTimeCompare([]byte(sigStr), []byte(msg.Signature)) != 1 { err = &errortypes.AuthenticationError{ errors.New("crypto: Invalid message signature"), } return } nonceByt, err := base64.StdEncoding.DecodeString(msg.Nonce) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode nonce"), } return } if len(nonceByt) != 24 { err = &errortypes.ParseError{ errors.New("crypto: Invalid nonce length"), } return } if a.nonceHandler != nil { err = a.nonceHandler(nonceByt) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Nonce validate failed"), } return } } nonce := new([24]byte) copy(nonce[:], nonceByt) sigEncByt, err := base64.StdEncoding.DecodeString(msg.Message) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode message"), } return } encByt, valid := sign.Open(nil, sigEncByt, a.publicKey) if !valid { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to verify message signature"), } return } decByt, ok := secretbox.Open(nil, encByt, nonce, a.key) if !ok { err = &errortypes.AuthenticationError{ errors.New("crypto: Failed to decrypt message"), } return } err = json.Unmarshal(decByt, output) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to unmarshal data"), } return } return } func (a *AsymNaclHmac) UnsealJson(input string, output any) (err error) { msg := &Message{} err = json.Unmarshal([]byte(input), msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to unmarshal message"), } return } err = a.Unseal(msg, output) if err != nil { return } return } func (a *AsymNaclHmac) Export() AsymNaclHmacKey { return AsymNaclHmacKey{ Key: base64.StdEncoding.EncodeToString(a.key[:]), Secret: base64.StdEncoding.EncodeToString(a.secret[:]), PublicKey: base64.StdEncoding.EncodeToString(a.publicKey[:]), PrivateKey: base64.StdEncoding.EncodeToString(a.privateKey[:]), } } func (a *AsymNaclHmac) Import(key AsymNaclHmacKey) (err error) { keyByt, err := base64.StdEncoding.DecodeString(key.Key) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode key"), } return } if len(keyByt) != 32 { err = &errortypes.ParseError{ errors.New("crypto: Invalid key length"), } return } secrByt, err := base64.StdEncoding.DecodeString(key.Secret) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode secret key"), } return } if len(secrByt) != 32 { err = &errortypes.ParseError{ errors.New("crypto: Invalid secret key length"), } return } pubKeyByt, err := base64.StdEncoding.DecodeString( key.PublicKey) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode public key"), } return } if len(pubKeyByt) != 32 { err = &errortypes.ParseError{ errors.New("crypto: Invalid public key length"), } return } if key.PrivateKey != "" { privKeyByt, e := base64.StdEncoding.DecodeString( key.PrivateKey) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "crypto: Failed to decode private key"), } return } if len(privKeyByt) != 64 { err = &errortypes.ParseError{ errors.New("crypto: Invalid private key length"), } return } if a.privateKey == nil { a.privateKey = new([64]byte) } copy(a.privateKey[:], privKeyByt) } if a.key == nil { a.key = new([32]byte) } if a.secret == nil { a.secret = new([32]byte) } if a.publicKey == nil { a.publicKey = new([32]byte) } copy(a.key[:], keyByt) copy(a.secret[:], secrByt) copy(a.publicKey[:], pubKeyByt) return } func (a *AsymNaclHmac) Generate() (err error) { key := new([32]byte) _, err = io.ReadFull(rand.Reader, key[:]) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to generate key"), } return } secKey := new([32]byte) _, err = io.ReadFull(rand.Reader, secKey[:]) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to generate secret key"), } return } signPubKey, signPrivKey, err := sign.GenerateKey(rand.Reader) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "crypto: Failed to generate signing key"), } return } a.key = key a.secret = secKey a.publicKey = signPubKey a.privateKey = signPrivKey return } ================================================ FILE: csrf/csrf.go ================================================ package csrf import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) type CsrfToken struct { Id string `bson:"_id"` Session string `bson:"session"` Timestamp time.Time `bson:"timestamp"` } func NewToken(db *database.Database, sessionId string) ( token string, err error) { coll := db.CsrfTokens() tkn, err := utils.RandStr(48) if err != nil { return } doc := &CsrfToken{ Id: tkn, Session: sessionId, Timestamp: time.Now(), } _, err = coll.InsertOne(db, doc) if err != nil { err = database.ParseError(err) return } token = tkn return } func ValidateToken(db *database.Database, sessionId, token string) ( valid bool, err error) { coll := db.CsrfTokens() doc := &CsrfToken{} err = coll.FindOneId(token, doc) if err != nil { return } if doc.Session == sessionId { valid = true return } return } ================================================ FILE: data/disk.go ================================================ package data import ( "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/lvm" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" ) func createDiskQcow(db *database.Database, dsk *disk.Disk) ( newSize int, backingImage string, err error) { diskPath := paths.GetDiskPath(dsk.Id) if !dsk.Image.IsZero() { newSize, backingImage, err = writeImageQcow(db, dsk) if err != nil { return } } else if dsk.FileSystem != "" { err = writeFsQcow(db, dsk) if err != nil { return } } else { err = utils.Exec("", "qemu-img", "create", "-f", "qcow2", diskPath, fmt.Sprintf("%dG", dsk.Size)) if err != nil { return } err = utils.Chmod(diskPath, 0600) if err != nil { return } } return } func createDiskLvm(db *database.Database, dsk *disk.Disk) ( newSize int, err error) { pl, err := pool.Get(db, dsk.Pool) if err != nil { return } err = lvm.InitLock(pl.VgName) if err != nil { return } if !dsk.Image.IsZero() { newSize, err = writeImageLvm(db, dsk, pl) if err != nil { return } } else if dsk.FileSystem != "" { err = writeFsLvm(db, dsk, pl) if err != nil { return } } else { err = lvm.CreateLv(pl.VgName, dsk.Id.Hex(), dsk.Size) if err != nil { return } } return } func CreateDisk(db *database.Database, dsk *disk.Disk) ( newSize int, backingImage string, err error) { switch dsk.Type { case disk.Lvm: newSize, err = createDiskLvm(db, dsk) if err != nil { return } break case "", disk.Qcow2: newSize, backingImage, err = createDiskQcow(db, dsk) if err != nil { return } break default: err = &errortypes.ParseError{ errors.Newf("data: Unknown disk type %s", dsk.Type), } return } return } func ActivateDisk(db *database.Database, dsk *disk.Disk) (err error) { if dsk.Type != disk.Lvm { return } pl, err := pool.Get(db, dsk.Pool) if err != nil { return } vgName := pl.VgName lvName := dsk.Id.Hex() err = lvm.ActivateLv(vgName, lvName) if err != nil { return } return } func DeactivateDisk(db *database.Database, dsk *disk.Disk) (err error) { if dsk.Type != disk.Lvm { return } pl, err := pool.Get(db, dsk.Pool) if err != nil { return } vgName := pl.VgName lvName := dsk.Id.Hex() err = lvm.DeactivateLv(vgName, lvName) if err != nil { return } return } ================================================ FILE: data/image.go ================================================ package data import ( "context" "fmt" "io" "net/http" "os" "path" "path/filepath" "strconv" "strings" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/lock" "github.com/pritunl/pritunl-cloud/lvm" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" "golang.org/x/crypto/openpgp" ) var ( imageLock = utils.NewMultiTimeoutLock(10 * time.Minute) backingImageLock = utils.NewMultiTimeoutLock(5 * time.Minute) nbdLock = sync.Mutex{} ) func getImageS3(db *database.Database, store *storage.Storage, dsk *disk.Disk, img *image.Image) (tmpPth string, err error) { tmpPth = paths.GetImageTempPath() logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, "temp_path": tmpPth, }).Info("data: Downloading s3 image") client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4( store.AccessKey, store.SecretKey, "", ), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } stat, err := client.StatObject(context.Background(), store.Bucket, img.Key, minio.StatObjectOptions{}) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to stat s3 image"), } return } prog := NewProgressS3(db, dsk, img, tmpPth, stat.Size) prog.Start() defer prog.Stop() err = client.FGetObject(context.Background(), store.Bucket, img.Key, tmpPth, minio.GetObjectOptions{}) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to download s3 image"), } return } prog.Stop() logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, "temp_path": tmpPth, }).Info("data: Downloaded s3 image") return } type ProgressS3 struct { db *database.Database disk *disk.Disk img *image.Image done chan bool stopOnce sync.Once baseDir string outPrefix string Total int64 Wrote int64 LastWrote int64 LastReport int LastTime time.Time } func NewProgressS3(db *database.Database, dsk *disk.Disk, img *image.Image, outPath string, size int64) (prog *ProgressS3) { prog = &ProgressS3{ db: db, disk: dsk, img: img, done: make(chan bool), baseDir: filepath.Dir(outPath), outPrefix: filepath.Base(outPath), Total: size, LastTime: time.Now(), } return } func (p *ProgressS3) Start() { go func() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-p.done: return case <-ticker.C: p.calculateProgress() p.syncProgress() } } }() } func (p *ProgressS3) Stop() { p.stopOnce.Do(func() { p.done <- true close(p.done) }) } func (p *ProgressS3) calculateProgress() { var totalBytes int64 = 0 files, err := os.ReadDir(p.baseDir) if err != nil { return } for _, file := range files { if !file.IsDir() && strings.HasPrefix(file.Name(), p.outPrefix) { info, err := file.Info() if err != nil { continue } totalBytes += info.Size() } } p.Wrote = totalBytes } func (p *ProgressS3) syncProgress() { percent := int(float64(p.Wrote) / float64(p.Total) * 100) if percent > 100 { percent = 100 } if percent >= p.LastReport+10 { now := time.Now() elapsed := now.Sub(p.LastTime).Seconds() speed := float64(p.Wrote-p.LastWrote) / elapsed p.LastTime = now p.LastWrote = p.Wrote p.LastReport = percent - (percent % 10) if p.disk != nil && !p.disk.Instance.IsZero() { _ = instance.SetDownloadProgress( p.db, p.disk.Instance, p.LastReport, speed/1_000_000.0) } } return } type Progress struct { db *database.Database disk *disk.Disk img *image.Image Total int64 Wrote int64 LastWrote int64 LastReport int LastTime time.Time } func humanReadableSpeed(bytesPerSecond float64) string { switch { case bytesPerSecond >= 1_000_000_000: return fmt.Sprintf("%.2f GB/s", bytesPerSecond/1_000_000_000) case bytesPerSecond >= 1_000_000: return fmt.Sprintf("%.2f MB/s", bytesPerSecond/1_000_000) case bytesPerSecond >= 1_000: return fmt.Sprintf("%.2f KB/s", bytesPerSecond/1_000) default: return fmt.Sprintf("%.2f B/s", bytesPerSecond) } } func (p *Progress) Write(data []byte) (n int, err error) { n = len(data) p.Wrote += int64(n) percent := int(float64(p.Wrote) / float64(p.Total) * 100) if percent >= p.LastReport+10 { now := time.Now() elapsed := now.Sub(p.LastTime).Seconds() speed := float64(p.Wrote-p.LastWrote) / elapsed p.LastTime = now p.LastWrote = p.Wrote p.LastReport = percent - (percent % 10) if p.disk != nil && !p.disk.Instance.IsZero() { _ = instance.SetDownloadProgress( p.db, p.disk.Instance, p.LastReport, speed/1_000_000.0) } } return } func getImageWeb(db *database.Database, store *storage.Storage, dsk *disk.Disk, img *image.Image) (tmpPth string, err error) { tmpPth = paths.GetImageTempPath() logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, "temp_path": tmpPth, }).Info("data: Downloading web image") u := store.GetWebUrl() u.Path += "/" + img.Key req, err := http.NewRequest("GET", u.String(), nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: Failed to create file request"), } return } req.Header.Set("User-Agent", "pritunl-cloud") resp, err := clientLarge.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: File request error"), } return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = &errortypes.RequestError{ errors.Newf( "data: Bad status %d from file request", resp.StatusCode, ), } return } contentLen, err := strconv.ParseInt( resp.Header.Get("Content-Length"), 10, 64) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: Invalid content length from file request"), } return } if contentLen <= 0 { err = &errortypes.RequestError{ errors.Wrap(err, "data: Zero content length from file request"), } return } out, err := os.Create(tmpPth) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: Failed to create temporary file"), } return } defer out.Close() prog := &Progress{ db: db, disk: dsk, img: img, Total: contentLen, LastTime: time.Now(), } _, err = io.Copy(out, io.TeeReader(resp.Body, prog)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: Failed to download file"), } return } return } func checkImageSigS3(db *database.Database, store *storage.Storage, img *image.Image, tmpPth string) (err error) { sigPth := tmpPth + ".sig" defer os.Remove(sigPth) client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4( store.AccessKey, store.SecretKey, "", ), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } err = client.FGetObject(context.Background(), store.Bucket, img.Key+".sig", sigPth, minio.GetObjectOptions{}) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to download image signature"), } return } signature, err := os.Open(sigPth) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to open image signature"), } return } defer signature.Close() tmpImg, err := os.Open(tmpPth) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to open image"), } return } defer tmpImg.Close() keyring, err := openpgp.ReadArmoredKeyRing( strings.NewReader(constants.PritunlKeyring)) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "data: Failed to parse Pritunl keyring"), } return } entity, err := openpgp.CheckArmoredDetachedSignature( keyring, tmpImg, signature) if err != nil || entity == nil { err = &errortypes.VerificationError{ errors.Wrap(err, "data: Image signature verification failed"), } return } logrus.WithFields(logrus.Fields{ "id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, }).Info("data: Image signature successfully validated") return } func checkImageSigWeb(db *database.Database, store *storage.Storage, img *image.Image, tmpPth string) (err error) { sigPth := tmpPth + ".sig" defer os.Remove(sigPth) u := store.GetWebUrl() u.Path += "/" + img.Key + ".sig" req, err := http.NewRequest("GET", u.String(), nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: Failed to create file request"), } return } req.Header.Set("User-Agent", "pritunl-cloud") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "data: File request error"), } return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = &errortypes.RequestError{ errors.Newf( "data: Bad status %d from file request", resp.StatusCode, ), } return } tmpImg, err := os.Open(tmpPth) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to open image"), } return } defer tmpImg.Close() keyring, err := openpgp.ReadArmoredKeyRing( strings.NewReader(constants.PritunlKeyring)) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "data: Failed to parse Pritunl keyring"), } return } entity, err := openpgp.CheckArmoredDetachedSignature( keyring, tmpImg, resp.Body) if err != nil || entity == nil { err = &errortypes.VerificationError{ errors.Wrap(err, "data: Image signature verification failed"), } return } logrus.WithFields(logrus.Fields{ "id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, }).Info("data: Image signature successfully validated") return } func getImage(db *database.Database, dsk *disk.Disk, img *image.Image, pth string) (err error) { if imageLock.Locked(pth) { logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "key": img.Key, "path": pth, }).Info("data: Waiting for image") } lockId := imageLock.Lock(pth) defer imageLock.Unlock(pth, lockId) tmpPth := "" defer func() { if tmpPth != "" { utils.Remove(tmpPth) } }() exists, err := utils.Exists(pth) if err != nil { return } if exists { return } store, err := storage.Get(db, img.Storage) if err != nil { return } if img.Type == storage.Web { tmpPth, err = getImageWeb(db, store, dsk, img) if err != nil { return } } else { tmpPth, err = getImageS3(db, store, dsk, img) if err != nil { return } } if img.Signed || store.Endpoint == "images.pritunl.com" { if img.Type == storage.Web { err = checkImageSigWeb(db, store, img, tmpPth) if err != nil { return } } else { err = checkImageSigS3(db, store, img, tmpPth) if err != nil { return } } } hashed := false if img.Hash != "" { hash, e := utils.FileSha256(tmpPth) if e != nil { err = e return } if hash != img.Hash { err = &errortypes.VerificationError{ errors.Wrap(err, "data: Image hash verification failed"), } return } hashed = true } logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, "temp_path": tmpPth, "path": pth, "hashed": hashed, }).Info("data: Downloaded image") err = utils.Exec("", "mv", tmpPth, pth) if err != nil { return } tmpPth = "" return } func copyBackingImage(imagePth, backingImagePth string) (err error) { lockId := backingImageLock.Lock(backingImagePth) defer backingImageLock.Unlock(backingImagePth, lockId) exists, err := utils.Exists(backingImagePth) if err != nil { return } if exists { return } err = utils.Exec("", "cp", imagePth, backingImagePth) if err != nil { return } return } func writeFsQcow(db *database.Database, dsk *disk.Disk) (err error) { ndbPath := settings.Hypervisor.NbdPath nbdLock.Lock() defer func() { utils.Exec("", "sync") utils.Exec("", "qemu-nbd", "--disconnect", ndbPath) nbdLock.Unlock() }() diskPath := paths.GetDiskPath(dsk.Id) err = utils.Exec("", "qemu-img", "create", "-f", "qcow2", diskPath, fmt.Sprintf("%dG", dsk.Size)) if err != nil { return } err = utils.Chmod(diskPath, 0600) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "modprobe", "nbd") if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "qemu-nbd", "--disconnect", ndbPath) if err != nil { return } time.Sleep(300 * time.Millisecond) _, err = utils.ExecCombinedOutputLogged( nil, "qemu-nbd", "--connect", ndbPath, diskPath) if err != nil { return } time.Sleep(1 * time.Second) _, err = utils.ExecCombinedOutputLogged( nil, "parted", "--script", ndbPath, "mklabel", "gpt") if err != nil { return } time.Sleep(300 * time.Millisecond) _, err = utils.ExecCombinedOutputLogged( nil, "parted", "--script", ndbPath, "mkpart", "primary", "1MiB", "100%") if err != nil { return } time.Sleep(1 * time.Second) diskFs := "" diskLvm := false switch dsk.FileSystem { case disk.Xfs: diskFs = "xfs" diskLvm = false case disk.LvmXfs: diskFs = "xfs" diskLvm = true case disk.Ext4: diskFs = "ext4" diskLvm = false case disk.LvmExt4: diskFs = "ext4" diskLvm = true default: err = &errortypes.WriteError{ errors.Newf("data: Invalid disk filesystem %s", dsk.FileSystem), } return } if diskLvm { vgName := GetVgName(dsk.Id, 0) lvName := GetLvName(dsk.Id, 0) err = utils.Exec("", "pvcreate", ndbPath+"p1") if err != nil { return } err = utils.Exec("", "vgcreate", vgName, ndbPath+"p1") if err != nil { return } if dsk.LvSize == dsk.Size { err = utils.Exec("", "lvcreate", "-l", "100%", "-n", lvName, vgName) if err != nil { return } } else { err = utils.Exec("", "lvcreate", "-L", fmt.Sprintf("%dG", dsk.LvSize), "-n", lvName, vgName) if err != nil { return } } err = utils.Exec("", "mkfs", "-t", diskFs, fmt.Sprintf("/dev/%s/%s", vgName, lvName)) if err != nil { return } time.Sleep(100 * time.Millisecond) output, e := utils.ExecOutput("", "blkid", "-s", "UUID", "-o", "value", fmt.Sprintf("/dev/%s/%s", vgName, lvName)) if e != nil { err = e return } dsk.Uuid = strings.TrimSpace(output) err = utils.Exec("", "lvchange", "-an", fmt.Sprintf("/dev/%s/%s", vgName, lvName)) if err != nil { return } time.Sleep(50 * time.Millisecond) err = utils.Exec("", "vgchange", "-an", vgName) if err != nil { return } time.Sleep(50 * time.Millisecond) } else { err = utils.Exec("", "mkfs", "-t", diskFs, ndbPath+"p1") if err != nil { return } time.Sleep(100 * time.Millisecond) output, e := utils.ExecOutput("", "blkid", "-s", "UUID", "-o", "value", ndbPath+"p1") if e != nil { err = e return } dsk.Uuid = strings.TrimSpace(output) } err = dsk.CommitFields(db, set.NewSet("uuid")) if err != nil { return } return } func writeImageQcow(db *database.Database, dsk *disk.Disk) ( newSize int, backingImageName string, err error) { size := dsk.Size diskPath := paths.GetDiskPath(dsk.Id) diskTempPath := paths.GetDiskTempPath() disksPath := paths.GetDisksPath() backingPath := paths.GetBackingPath() err = utils.ExistsMkdir(disksPath, 0755) if err != nil { return } err = utils.ExistsMkdir(backingPath, 0755) if err != nil { return } err = utils.ExistsMkdir(paths.GetTempPath(), 0755) if err != nil { return } img, err := image.Get(db, dsk.Image) if err != nil { return } largeBase := strings.Contains(img.Key, "fedora") backingImagePth := path.Join( backingPath, fmt.Sprintf("image-%s-%s", img.Id.Hex(), img.Etag), ) backingImageExists := false if dsk.Backing { backingImageName = fmt.Sprintf("%s-%s", img.Id.Hex(), img.Etag) backingImageExists, err = utils.Exists(backingImagePth) if err != nil { return } } if img.Type == storage.Public || img.Type == storage.Web || !img.Deployment.IsZero() { cacheDir := node.Self.GetCachePath() imagePth := path.Join( cacheDir, fmt.Sprintf("image-%s-%s", img.Id.Hex(), img.Etag), ) err = utils.ExistsMkdir(cacheDir, 0755) if err != nil { return } if !backingImageExists { err = getImage(db, dsk, img, imagePth) if err != nil { return } } exists, e := utils.Exists(diskPath) if e != nil { err = e return } if exists { logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "image_type": img.Type, "disk_id": dsk.Id.Hex(), "key": img.Key, "path": diskPath, }).Error("data: Blocking disk image overwrite") err = &errortypes.WriteError{ errors.Wrap(err, "data: Image already exists"), } return } utils.Exec("", "touch", imagePth) if dsk.Backing { err = copyBackingImage(imagePth, backingImagePth) if err != nil { return } utils.Exec("", "touch", backingImagePth) err = utils.Chmod(backingImagePth, 0644) if err != nil { return } if largeBase && size < 16 { size = 16 newSize = 16 } else if !largeBase && size < 10 { size = 10 newSize = 10 } _, err = utils.ExecCombinedOutputLogged(nil, "qemu-img", "create", "-f", "qcow2", "-F", "qcow2", "-o", fmt.Sprintf("backing_file=%s", backingImagePth), diskTempPath, fmt.Sprintf("%dG", size)) if err != nil { return } } else { err = utils.Exec("", "cp", imagePth, diskTempPath) if err != nil { return } if largeBase && size < 16 { size = 16 newSize = 16 } if size > 8 { curSize, e := getQcowSize(diskTempPath) if e != nil { err = e return } if curSize > size { size = curSize newSize = curSize } if size > curSize { _, err = utils.ExecCombinedOutputLogged(nil, "qemu-img", "resize", diskTempPath, fmt.Sprintf("%dG", size)) if err != nil { return } } } } err = utils.Chmod(diskTempPath, 0600) if err != nil { return } err = utils.Exec("", "mv", diskTempPath, diskPath) if err != nil { return } } else { if dsk.Backing { err = getImage(db, dsk, img, backingImagePth) if err != nil { return } } else { err = getImage(db, dsk, img, diskTempPath) if err != nil { return } } exists, e := utils.Exists(diskPath) if e != nil { err = e return } if exists { logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "image_type": img.Type, "disk_id": dsk.Id.Hex(), "key": img.Key, "path": diskPath, }).Error("data: Blocking disk image overwrite") err = &errortypes.WriteError{ errors.Wrap(err, "data: Image already exists"), } return } if dsk.Backing { utils.Exec("", "touch", backingImagePth) err = utils.Chmod(backingImagePth, 0644) if err != nil { return } if largeBase && size < 16 { size = 16 newSize = 16 } else if !largeBase && size < 10 { size = 10 newSize = 10 } _, err = utils.ExecCombinedOutputLogged(nil, "qemu-img", "create", "-f", "qcow2", "-F", "qcow2", "-o", fmt.Sprintf("backing_file=%s", backingImagePth), diskTempPath, fmt.Sprintf("%dG", size)) if err != nil { return } } else { if largeBase && size < 16 { size = 16 newSize = 16 } if size > 8 { curSize, e := getQcowSize(diskTempPath) if e != nil { err = e return } if curSize > size { size = curSize newSize = curSize } if size > curSize { _, err = utils.ExecCombinedOutputLogged(nil, "qemu-img", "resize", diskTempPath, fmt.Sprintf("%dG", size)) if err != nil { return } } } } err = utils.Exec("", "mv", diskTempPath, diskPath) if err != nil { return } } return } func writeFsLvm(db *database.Database, dsk *disk.Disk, pl *pool.Pool) (err error) { size := dsk.Size vgName := pl.VgName lvName := dsk.Id.Hex() sourcePth := "" diskTempPath := paths.GetDiskTempPath() defer utils.Remove(diskTempPath) acquired, err := lock.LvmLock(db, vgName, lvName) if err != nil { return } if !acquired { err = &errortypes.WriteError{ errors.New("data: Failed to acquire LVM lock"), } return } defer func() { err2 := lock.LvmUnlock(db, vgName, lvName) if err2 != nil { logrus.WithFields(logrus.Fields{ "error": err2, }).Error("data: Failed to unlock lvm") } }() diskFs := "" diskLvm := false switch dsk.FileSystem { case disk.Xfs: diskFs = "xfs" diskLvm = false case disk.LvmXfs: diskFs = "xfs" diskLvm = true case disk.Ext4: diskFs = "ext4" diskLvm = false case disk.LvmExt4: diskFs = "ext4" diskLvm = true default: err = &errortypes.WriteError{ errors.Newf("data: Invalid disk filesystem %s", dsk.FileSystem), } return } err = lvm.CreateLv(vgName, lvName, size) if err != nil { return } err = lvm.ActivateLv(vgName, lvName) if err != nil { return } defer func() { err = lvm.DeactivateLv(vgName, lvName) if err != nil { return } }() err = lvm.WriteLv(vgName, lvName, sourcePth) if err != nil { return } diskPath := filepath.Join("/dev/mapper", fmt.Sprintf("%s-%s", vgName, lvName)) if diskLvm { vgName := GetVgName(dsk.Id, 0) lvName := GetLvName(dsk.Id, 0) err = utils.Exec("", "pvcreate", diskPath) if err != nil { return } err = utils.Exec("", "vgcreate", vgName, diskPath) if err != nil { return } if dsk.LvSize == dsk.Size { err = utils.Exec("", "lvcreate", "-l", "100%", "-n", lvName, vgName) if err != nil { return } } else { err = utils.Exec("", "lvcreate", "-L", fmt.Sprintf("%dG", dsk.LvSize), "-n", lvName, vgName) if err != nil { return } } err = utils.Exec("", "mkfs", "-t", diskFs, fmt.Sprintf("/dev/%s/%s", vgName, lvName)) if err != nil { return } time.Sleep(100 * time.Millisecond) output, e := utils.ExecOutput("", "blkid", "-s", "UUID", "-o", "value", fmt.Sprintf("/dev/%s/%s", vgName, lvName)) if e != nil { err = e return } dsk.Uuid = strings.TrimSpace(output) err = utils.Exec("", "lvchange", "-an", fmt.Sprintf("/dev/%s/%s", vgName, lvName)) if err != nil { return } time.Sleep(50 * time.Millisecond) err = utils.Exec("", "vgchange", "-an", vgName) if err != nil { return } time.Sleep(50 * time.Millisecond) } else { err = utils.Exec("", "mkfs", "-t", diskFs, diskPath) if err != nil { return } output, e := utils.ExecOutput("", "blkid", "-s", "UUID", "-o", "value", diskPath) if e != nil { err = e return } dsk.Uuid = strings.TrimSpace(output) } err = dsk.CommitFields(db, set.NewSet("uuid")) if err != nil { return } return } func writeImageLvm(db *database.Database, dsk *disk.Disk, pl *pool.Pool) (newSize int, err error) { size := dsk.Size vgName := pl.VgName lvName := dsk.Id.Hex() sourcePth := "" diskTempPath := paths.GetDiskTempPath() defer utils.Remove(diskTempPath) if dsk.Backing { err = &errortypes.ParseError{ errors.New("data: Cannot create LVM disk with linked image"), } return } img, err := image.Get(db, dsk.Image) if err != nil { return } largeBase := strings.Contains(img.Key, "fedora") if img.Type == storage.Public || img.Type == storage.Web || !img.Deployment.IsZero() { cacheDir := node.Self.GetCachePath() imagePth := path.Join( cacheDir, fmt.Sprintf("image-%s-%s", img.Id.Hex(), img.Etag), ) err = utils.ExistsMkdir(cacheDir, 0755) if err != nil { return } err = getImage(db, dsk, img, imagePth) if err != nil { return } sourcePth = imagePth if largeBase && size < 16 { size = 16 newSize = 16 } else if !largeBase && size < 10 { size = 10 newSize = 10 } } else { err = getImage(db, dsk, img, diskTempPath) if err != nil { return } sourcePth = diskTempPath if largeBase && size < 16 { size = 16 newSize = 16 } } acquired, err := lock.LvmLock(db, vgName, lvName) if err != nil { return } if !acquired { err = &errortypes.WriteError{ errors.New("data: Failed to acquire LVM lock"), } return } defer func() { err2 := lock.LvmUnlock(db, vgName, lvName) if err2 != nil { logrus.WithFields(logrus.Fields{ "error": err2, }).Error("data: Failed to unlock lvm") } }() err = lvm.CreateLv(vgName, lvName, size) if err != nil { return } err = lvm.ActivateLv(vgName, lvName) if err != nil { return } defer func() { err = lvm.DeactivateLv(vgName, lvName) if err != nil { return } }() err = lvm.WriteLv(vgName, lvName, sourcePth) if err != nil { return } return } func DeleteImage(db *database.Database, imgId bson.ObjectID) ( err error) { img, err := image.Get(db, imgId) if err != nil { return } if img.Type == storage.Public || img.Type == storage.Web { return } store, err := storage.Get(db, img.Storage) if err != nil { return } client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } err = client.RemoveObject(context.Background(), store.Bucket, img.Key, minio.RemoveObjectOptions{}) if err != nil { return } err = img.Remove(db) if err != nil { return } return } func WriteImage(db *database.Database, dsk *disk.Disk) ( newSize int, backingImageName string, err error) { switch dsk.Type { case disk.Lvm: pl, e := pool.Get(db, dsk.Pool) if e != nil { err = e return } err = lvm.InitLock(pl.VgName) if err != nil { return } newSize, err = writeImageLvm(db, dsk, pl) if err != nil { return } break case "", disk.Qcow2: newSize, backingImageName, err = writeImageQcow(db, dsk) if err != nil { return } break default: err = &errortypes.ParseError{ errors.Newf("data: Unknown disk type %s", dsk.Type), } return } return } func DeleteImages(db *database.Database, imgIds []bson.ObjectID) ( err error) { for _, imgId := range imgIds { err = DeleteImage(db, imgId) if err != nil { return } } return } func DeleteImageOrg(db *database.Database, orgId, imgId bson.ObjectID) ( err error) { img, err := image.GetOrg(db, orgId, imgId) if err != nil { return } if img.Type == storage.Public || img.Type == storage.Web { return } store, err := storage.Get(db, img.Storage) if err != nil { return } client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } err = client.RemoveObject(context.Background(), store.Bucket, img.Key, minio.RemoveObjectOptions{}) if err != nil { return } err = img.Remove(db) if err != nil { return } return } func DeleteImagesOrg(db *database.Database, orgId bson.ObjectID, imgIds []bson.ObjectID) (err error) { for _, imgId := range imgIds { err = DeleteImageOrg(db, orgId, imgId) if err != nil { return } } return } func CreateSnapshot(db *database.Database, dsk *disk.Disk, virt *vm.VirtualMachine) (err error) { dskPth := paths.GetDiskPath(dsk.Id) cacheDir := node.Self.GetCachePath() nde, err := node.Get(db, dsk.Node) if err != nil { return } zne, err := zone.Get(db, nde.Zone) if err != nil { return } dc, err := datacenter.Get(db, zne.Datacenter) if err != nil { return } if dc.PrivateStorage.IsZero() { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Error("data: Cannot snapshot disk without private storage") return } store, err := storage.Get(db, dc.PrivateStorage) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Error("data: Cannot snapshot disk without private storage") } return } if store.Type != storage.Private { err = &errortypes.ConnectionError{ errors.New("data: Cannot upload to non-private storage"), } return } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "storage_id": store.Id.Hex(), "disk_path": dskPth, }).Info("data: Creating disk snapshot") err = utils.ExistsMkdir(cacheDir, 0755) if err != nil { return } imgId := bson.NewObjectID() tmpPath := path.Join(cacheDir, fmt.Sprintf("snapshot-%s", imgId.Hex())) img := &image.Image{ Id: imgId, Name: fmt.Sprintf("%s-%s", dsk.Name, time.Now().Format("20060102-150405")), Organization: dsk.Organization, Deployment: dsk.Deployment, Type: storage.Private, SystemType: dsk.SystemType, SystemKind: dsk.SystemKind, Firmware: image.Unknown, Storage: store.Id, Key: fmt.Sprintf("snapshot/%s.qcow2", imgId.Hex()), } defer utils.Remove(tmpPath) available := false if virt != nil && virt.Running() { err = qmp.BackupDisk(virt.Id, dsk, tmpPath) if err != nil { if _, ok := err.(*qmp.DiskNotFound); ok { err = nil } else { return } } else { available = true } } if !available { err = utils.Exec("", "cp", dskPth, tmpPath) if err != nil { return } } err = utils.Chmod(tmpPath, 0600) if err != nil { return } hash, err := utils.FileSha256(tmpPath) if err != nil { return } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_path": dskPth, "storage_id": store.Id.Hex(), "object_key": img.Key, "hash": hash, }).Info("data: Uploading disk snapshot") client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } putOpts := minio.PutObjectOptions{} storageClass := storage.FormatStorageClass(dc.PrivateStorageClass) if storageClass != "" { putOpts.StorageClass = storageClass } _, err = client.FPutObject(context.Background(), store.Bucket, img.Key, tmpPath, putOpts) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "data: Failed to write object"), } return } time.Sleep(3 * time.Second) obj, err := client.StatObject(context.Background(), store.Bucket, img.Key, minio.StatObjectOptions{}) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to stat object"), } return } img.Hash = hash img.Etag = image.GetEtag(obj) img.LastModified = obj.LastModified if store.IsOracle() { img.StorageClass = storage.ParseStorageClass(obj) } else { img.StorageClass = dc.BackupStorageClass } err = img.Upsert(db) if err != nil { return } if !dsk.Deployment.IsZero() { deply, e := deployment.Get(db, dsk.Deployment) if e != nil { err = e return } deply.Image = img.Id deply.SetImageState(deployment.Complete) err = deply.CommitFields(db, set.NewSet( "image", "image_data.state")) if err != nil { return } err = instance.Delete(db, deply.Instance) if err != nil { return } } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_path": dskPth, "storage_id": store.Id.Hex(), "object_key": img.Key, }).Info("data: Uploaded disk snapshot") event.PublishDispatch(db, "image.change") return } func CreateBackup(db *database.Database, dsk *disk.Disk, virt *vm.VirtualMachine) (err error) { dskPth := paths.GetDiskPath(dsk.Id) cacheDir := node.Self.GetCachePath() nde, err := node.Get(db, dsk.Node) if err != nil { return } zne, err := zone.Get(db, nde.Zone) if err != nil { return } dc, err := datacenter.Get(db, zne.Datacenter) if err != nil { return } if dc.BackupStorage.IsZero() { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Error("data: Cannot backup disk without backup storage") return } if dsk.BackingImage != "" { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Error("data: Cannot backup disk with backing image") return } store, err := storage.Get(db, dc.BackupStorage) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Error("data: Cannot backup disk without backup storage") } return } if store.Type != storage.Private { err = &errortypes.ConnectionError{ errors.New("data: Cannot upload to non-private storage"), } return } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "storage_id": store.Id.Hex(), "disk_path": dskPth, }).Info("data: Creating disk backup") err = utils.ExistsMkdir(cacheDir, 0755) if err != nil { return } imgId := bson.NewObjectID() tmpPath := path.Join(cacheDir, fmt.Sprintf("backup-%s", imgId.Hex())) img := &image.Image{ Id: imgId, Disk: dsk.Id, Name: fmt.Sprintf("%s-%s", dsk.Name, time.Now().Format("20060102-150405")), Organization: dsk.Organization, Type: storage.Private, SystemType: dsk.SystemType, SystemKind: dsk.SystemKind, Firmware: image.Unknown, Storage: store.Id, Key: fmt.Sprintf("backup/%s.qcow2", imgId.Hex()), } defer utils.Remove(tmpPath) available := false if virt != nil && virt.Running() { err = qmp.BackupDisk(virt.Id, dsk, tmpPath) if err != nil { if _, ok := err.(*qmp.DiskNotFound); ok { err = nil } else { return } } else { available = true } } if !available { err = utils.Exec("", "cp", dskPth, tmpPath) if err != nil { return } } err = utils.Chmod(tmpPath, 0600) if err != nil { return } hash, err := utils.FileSha256(tmpPath) if err != nil { return } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_path": dskPth, "storage_id": store.Id.Hex(), "object_key": img.Key, "hash": hash, }).Info("data: Uploading disk backup") client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } putOpts := minio.PutObjectOptions{} storageClass := storage.FormatStorageClass(dc.BackupStorageClass) if storageClass != "" { putOpts.StorageClass = storageClass } _, err = client.FPutObject(context.Background(), store.Bucket, img.Key, tmpPath, putOpts) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "data: Failed to write object"), } return } time.Sleep(3 * time.Second) obj, err := client.StatObject(context.Background(), store.Bucket, img.Key, minio.StatObjectOptions{}) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to stat object"), } return } img.Hash = hash img.Etag = image.GetEtag(obj) img.LastModified = obj.LastModified if store.IsOracle() { img.StorageClass = storage.ParseStorageClass(obj) } else { img.StorageClass = dc.BackupStorageClass } err = img.Upsert(db) if err != nil { return } event.PublishDispatch(db, "image.change") return } func RestoreBackup(db *database.Database, dsk *disk.Disk) (err error) { dskPth := paths.GetDiskPath(dsk.Id) cacheDir := node.Self.GetCachePath() img, err := image.Get(db, dsk.RestoreImage) if err != nil { return } if img.Disk != dsk.Id { err = &errortypes.VerificationError{ errors.Wrap(err, "data: Restore image invalid"), } return } store, err := storage.Get(db, img.Storage) if err != nil { return } if store.Type != storage.Private { err = &errortypes.ConnectionError{ errors.New("data: Cannot restore from non-private storage"), } return } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "image_id": img.Id.Hex(), "storage_id": store.Id.Hex(), "disk_path": dskPth, }).Info("data: Restoring disk backup") err = utils.ExistsMkdir(cacheDir, 0755) if err != nil { return } client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "data: Failed to connect to storage"), } return } imgId := bson.NewObjectID() tmpPath := path.Join(cacheDir, fmt.Sprintf("restore-%s", imgId.Hex())) defer utils.Remove(tmpPath) err = client.FGetObject(context.Background(), store.Bucket, img.Key, tmpPath, minio.GetObjectOptions{}) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "data: Failed to download restore image"), } return } err = utils.Chmod(tmpPath, 0600) if err != nil { return } hashed := false if img.Hash != "" { hash, e := utils.FileSha256(tmpPath) if e != nil { err = e return } if hash != img.Hash { err = &errortypes.VerificationError{ errors.Wrap(err, "data: Image hash verification failed"), } return } hashed = true } logrus.WithFields(logrus.Fields{ "image_id": img.Id.Hex(), "storage_id": store.Id.Hex(), "key": img.Key, "temp_path": tmpPath, "disk_path": dskPth, "hashed": hashed, }).Info("data: Restored backup") err = utils.Exec("", "mv", "-f", tmpPath, dskPth) if err != nil { return } return } func ImageAvailable(store *storage.Storage, img *image.Image) ( available bool, err error) { if img.Type == storage.Web { available = true return } if strings.Contains(strings.ToLower(store.Endpoint), "oracle") { client, e := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if e != nil { err = &errortypes.ConnectionError{ errors.Wrap(e, "data: Failed to connect to storage"), } return } obj, e := client.StatObject(context.Background(), store.Bucket, img.Key, minio.StatObjectOptions{}) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "data: Failed to stat object"), } return } archivalState := strings.ToLower(obj.Metadata.Get("Archival-State")) if archivalState != "" && archivalState != "restored" { available = false return } available = true return } switch img.StorageClass { case storage.AwsStandard: available = true break case storage.AwsInfrequentAccess: available = true break case storage.AwsGlacier: client, e := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if e != nil { err = &errortypes.ConnectionError{ errors.Wrap(e, "data: Failed to connect to storage"), } return } obj, e := client.StatObject(context.Background(), store.Bucket, img.Key, minio.StatObjectOptions{}) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "data: Failed to stat object"), } return } restore := obj.Metadata.Get("x-amz-restore") if strings.Contains(restore, "ongoing-request=\"false\"") && strings.Contains(restore, "expiry-date") { available = true } else { available = false } break default: available = true break } return } ================================================ FILE: data/resize.go ================================================ package data import ( "encoding/json" "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/lock" "github.com/pritunl/pritunl-cloud/lvm" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type diskInfo struct { Filename string `json:"filename"` Format string `json:"format"` ActualSize int `json:"actual-size"` VirtualSize int `json:"virtual-size"` } func getQcowSize(pth string) (size int, err error) { output, err := utils.ExecOutput("", "qemu-img", "info", "--output=json", pth) if err != nil { return } diskInfo := &diskInfo{} err = json.Unmarshal([]byte(output), diskInfo) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "data: Failed to parse qemu disk info"), } return } size = diskInfo.VirtualSize / 1073741824 return } func getDiskSizeQcow(dsk *disk.Disk) (size int, err error) { return getQcowSize(paths.GetDiskPath(dsk.Id)) } func expandDiskQcow(db *database.Database, dsk *disk.Disk) (err error) { dskPth := paths.GetDiskPath(dsk.Id) logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_path": dskPth, "new_size": dsk.NewSize, }).Info("data: Expanding qcow disk") curSize, err := getDiskSizeQcow(dsk) if err != nil { return } if curSize >= dsk.NewSize { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_path": dskPth, "current_size": curSize, "new_size": dsk.NewSize, }).Warn("data: Disk size larger then new size") dsk.Size = curSize return } expandSize := dsk.NewSize - curSize _, err = utils.ExecCombinedOutputLogged(nil, "qemu-img", "resize", dskPth, fmt.Sprintf("+%dG", expandSize)) if err != nil { return } curSize, err = getDiskSizeQcow(dsk) if err != nil { return } dsk.Size = curSize return } func expandDiskLvm(db *database.Database, dsk *disk.Disk) (err error) { pl, err := pool.Get(db, dsk.Pool) if err != nil { return } vgName := pl.VgName lvName := dsk.Id.Hex() logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "vg_name": vgName, "lv_name": lvName, "new_size": dsk.NewSize, }).Info("data: Expanding lvm disk") curSize, err := lvm.GetSizeLv(vgName, lvName) if err != nil { return } if curSize >= dsk.NewSize { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "vg_name": vgName, "lv_name": lvName, "current_size": curSize, "new_size": dsk.NewSize, }).Warn("data: Disk size larger then new size") dsk.Size = curSize return } acquired, err := lock.LvmLock(db, vgName, lvName) if err != nil { return } if !acquired { err = &errortypes.WriteError{ errors.New("data: Failed to acquire LVM lock"), } return } defer func() { err2 := lock.LvmUnlock(db, vgName, lvName) if err2 != nil { logrus.WithFields(logrus.Fields{ "error": err2, }).Error("data: Failed to unlock lvm") } }() err = lvm.ActivateLv(vgName, lvName) if err != nil { return } defer func() { err = lvm.DeactivateLv(vgName, lvName) if err != nil { return } }() expandSize := dsk.NewSize - curSize err = lvm.ExtendLv(vgName, lvName, expandSize) if err != nil { return } curSize, err = lvm.GetSizeLv(vgName, lvName) if err != nil { return } dsk.Size = curSize return } func ExpandDisk(db *database.Database, dsk *disk.Disk) (err error) { switch dsk.Type { case disk.Lvm: err = expandDiskLvm(db, dsk) if err != nil { return } break case "", disk.Qcow2: err = expandDiskQcow(db, dsk) if err != nil { return } break default: err = &errortypes.ParseError{ errors.Newf("data: Unknown disk type %s", dsk.Type), } return } return } ================================================ FILE: data/sync.go ================================================ package data import ( "context" "crypto/tls" "encoding/json" "net/http" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var ( syncLock = utils.NewMultiTimeoutLock(1 * time.Minute) clientTransport = &http.Transport{ DisableKeepAlives: true, TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, }, } client = &http.Client{ Transport: clientTransport, Timeout: 30 * time.Second, } clientLarge = &http.Client{ Transport: clientTransport, Timeout: 30 * time.Minute, } ) func getImagesS3(db *database.Database, store *storage.Storage) ( images []*image.Image, err error) { client, err := minio.New(store.Endpoint, &minio.Options{ Creds: credentials.NewStaticV4(store.AccessKey, store.SecretKey, ""), Secure: !store.Insecure, }) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "storage: Failed to connect to storage"), } return } images = []*image.Image{} signedKeys := set.NewSet() for object := range client.ListObjects( context.Background(), store.Bucket, minio.ListObjectsOptions{ Recursive: true, }, ) { if object.Err != nil { err = &errortypes.RequestError{ errors.Wrap(object.Err, "storage: Failed to list objects"), } return } if strings.HasSuffix(object.Key, ".qcow2.sig") { signedKeys.Add(strings.TrimRight(object.Key, ".sig")) } else if strings.HasSuffix(object.Key, ".qcow2") { etag := image.GetEtag(object) img := &image.Image{ Storage: store.Id, Key: object.Key, Firmware: image.Uefi, Etag: etag, Type: store.Type, LastModified: object.LastModified, } if store.IsOracle() { obj, e := client.StatObject(context.Background(), store.Bucket, object.Key, minio.StatObjectOptions{}) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "storage: Failed to stat object"), } return } img.StorageClass = storage.ParseStorageClass(obj) } else { img.StorageClass = storage.ParseStorageClass(object) } images = append(images, img) } } for _, img := range images { img.Signed = signedKeys.Contains(img.Key) } return } type Files struct { Version int `json:"version"` Files []File } type File struct { Name string `json:"name"` Signed bool `json:"signed"` Hash string `json:"hash"` LastModified time.Time `json:"last_modified"` } func getImagesWeb(db *database.Database, store *storage.Storage) ( images []*image.Image, err error) { u := store.GetWebUrl() u.Path += "/files.json" req, e := http.NewRequest("GET", u.String(), nil) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "data: Failed to file listing request"), } return } req.Header.Set("User-Agent", "pritunl-cloud") resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "data: File listing request error"), } return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = &errortypes.RequestError{ errors.Newf( "data: Bad status %d from file listing request", resp.StatusCode, ), } return } filesData := &Files{} err = json.NewDecoder(resp.Body).Decode(filesData) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "data: Failed to unmarshal file listing", ), } return } images = []*image.Image{} for _, object := range filesData.Files { if strings.HasSuffix(object.Name, ".qcow2") { img := &image.Image{ Storage: store.Id, Key: object.Name, Firmware: image.Uefi, Etag: object.Hash, Type: storage.Web, Signed: object.Signed, LastModified: object.LastModified, } images = append(images, img) } } return } func Sync(db *database.Database, store *storage.Storage) (err error) { if store.Endpoint == "" { return } lockId := syncLock.Lock(store.Id.Hex()) defer syncLock.Unlock(store.Id.Hex(), lockId) var images []*image.Image if store.Type == storage.Web || store.Endpoint == "images.pritunl.com" { images, err = getImagesWeb(db, store) if err != nil { return } } else { images, err = getImagesS3(db, store) if err != nil { return } } remoteKeys := set.NewSet() for _, img := range images { remoteKeys.Add(img.Key) if img.Signed { if strings.Contains(img.Key, "_bios") { img.Firmware = image.Bios } else { img.Firmware = image.Uefi } } err = img.Sync(db) if err != nil { if _, ok := err.(*image.LostImageError); ok { logrus.WithFields(logrus.Fields{ "bucket": store.Bucket, "key": img.Key, }).Error("data: Ignoring lost image") } else { return } } } localKeys, err := image.Distinct(db, store.Id) if err != nil { return } removeKeysSet := set.NewSet() for _, key := range localKeys { removeKeysSet.Add(key) } removeKeysSet.Subtract(remoteKeys) for keyInf := range removeKeysSet.Iter() { key := keyInf.(string) img, e := image.GetKey(db, store.Id, key) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } logrus.WithFields(logrus.Fields{ "bucket": store.Bucket, "key": img.Key, }).Info("data: Remote image deleted, removing local") err = img.Remove(db) if err != nil { return } } return } ================================================ FILE: data/utils.go ================================================ package data import ( "crypto/md5" "encoding/base32" "fmt" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" ) func GetVgName(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:9] return fmt.Sprintf("cvg_%s%d", strings.ToLower(hashSum), n) } func GetLvName(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:9] return fmt.Sprintf("clv_%s%d", strings.ToLower(hashSum), n) } ================================================ FILE: database/base.go ================================================ package database import ( "github.com/pritunl/mongo-go-driver/v2/bson" ) type Named struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` } ================================================ FILE: database/client.go ================================================ package database import ( "context" "sync" "sync/atomic" "time" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/sirupsen/logrus" ) var ( globalClient atomic.Value globalClientLock sync.Mutex DefaultDatabase string ) func getClient() *mongo.Client { val := globalClient.Load() if val == nil { return nil } return val.(*mongo.Client) } func setClient(client *mongo.Client) { globalClientLock.Lock() curClientInf := globalClient.Load() if curClientInf != nil { curClient := curClientInf.(*mongo.Client) ctx, cancel := context.WithTimeout( context.Background(), 30*time.Second, ) defer cancel() err := curClient.Disconnect(ctx) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("database: Disconnect error") } } globalClient.Store(client) globalClientLock.Unlock() } ================================================ FILE: database/collection.go ================================================ package database import ( "fmt" "reflect" "strings" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/mongo-go-driver/v2/mongo/options" ) type Collection struct { db *Database *mongo.Collection } func (c *Collection) FindOneId(id interface{}, data interface{}) (err error) { err = c.FindOne(c.db, &bson.M{ "_id": id, }).Decode(data) if err != nil { err = ParseError(err) return } return } func (c *Collection) UpdateId(id interface{}, data interface{}) (err error) { _, err = c.UpdateOne(c.db, &bson.M{ "_id": id, }, data) if err != nil { err = ParseError(err) return } return } func (c *Collection) Commit(id interface{}, data interface{}) (err error) { _, err = c.UpdateOne(c.db, &bson.M{ "_id": id, }, &bson.M{ "$set": data, }) if err != nil { err = ParseError(err) return } return } func (c *Collection) CommitFields(id interface{}, data interface{}, fields set.Set) (err error) { _, err = c.UpdateOne(c.db, &bson.M{ "_id": id, }, SelectFieldsAll(data, fields)) if err != nil { err = ParseError(err) return } return } func (c *Collection) Upsert(query *bson.M, data interface{}) (err error) { _, err = c.UpdateOne(c.db, query, &bson.M{ "$set": data, }, options.UpdateOne().SetUpsert(true)) if err != nil { err = ParseError(err) return } return } func SelectFields(obj interface{}, fields set.Set) (data bson.M) { val := reflect.ValueOf(obj).Elem() data = bson.M{} n := val.NumField() for i := 0; i < n; i++ { field := val.Field(i) typ := val.Type().Field(i) if typ.PkgPath != "" { continue } tag := typ.Tag.Get("bson") if tag == "" || tag == "-" { continue } tag = strings.Split(tag, ",")[0] if fields.Contains(tag) { val := field.Interface() switch valTyp := val.(type) { case bson.ObjectID: if valTyp.IsZero() { data[tag] = nil } else { data[tag] = val } break default: data[tag] = val } } else if (field.Kind() == reflect.Struct) || (field.Kind() == reflect.Pointer && field.Elem().Kind() == reflect.Struct) { var val reflect.Value if field.Kind() == reflect.Struct { val = field } else { val = reflect.ValueOf(field.Interface()).Elem() } x := val.NumField() for j := 0; j < x; j++ { nestedField := val.Field(j) nestedTyp := val.Type().Field(j) if nestedTyp.PkgPath != "" { continue } nestedTag := nestedTyp.Tag.Get("bson") if nestedTag == "" || nestedTag == "-" { continue } nestedTag = strings.Split(nestedTag, ",")[0] nestedTag = tag + "." + nestedTag if fields.Contains(nestedTag) { nestedVal := nestedField.Interface() switch nestedValTyp := nestedVal.(type) { case bson.ObjectID: if nestedValTyp.IsZero() { data[nestedTag] = bson.NilObjectID } else { data[nestedTag] = nestedVal } break default: data[nestedTag] = nestedVal } } } } } return } func SelectFieldsAll(obj interface{}, fields set.Set) (data bson.M) { val := reflect.ValueOf(obj).Elem() dataSet := bson.M{} dataUnset := bson.M{} dataUnseted := false n := val.NumField() for i := 0; i < n; i++ { field := val.Field(i) typ := val.Type().Field(i) if typ.PkgPath != "" { continue } tag := typ.Tag.Get("bson") if tag == "" || tag == "-" { continue } omitempty := strings.Contains(tag, "omitempty") tag = strings.Split(tag, ",")[0] if fields.Contains(tag) { val := field.Interface() switch valTyp := val.(type) { case bson.ObjectID: if valTyp.IsZero() { if omitempty { dataUnset[tag] = 1 dataUnseted = true } else { dataSet[tag] = bson.NilObjectID } } else { dataSet[tag] = val } break default: dataSet[tag] = val } } else if (field.Kind() == reflect.Struct) || (field.Kind() == reflect.Pointer && field.Elem().Kind() == reflect.Struct) { var val reflect.Value if field.Kind() == reflect.Struct { val = field } else { val = reflect.ValueOf(field.Interface()).Elem() } x := val.NumField() for j := 0; j < x; j++ { nestedField := val.Field(j) nestedTyp := val.Type().Field(j) if nestedTyp.PkgPath != "" { continue } nestedTag := nestedTyp.Tag.Get("bson") if nestedTag == "" || nestedTag == "-" { continue } nestedOmitempty := strings.Contains(nestedTag, "omitempty") nestedTag = strings.Split(nestedTag, ",")[0] nestedTag = tag + "." + nestedTag if fields.Contains(nestedTag) { nestedVal := nestedField.Interface() switch nestedValTyp := nestedVal.(type) { case bson.ObjectID: if nestedValTyp.IsZero() { if nestedOmitempty { dataUnset[nestedTag] = 1 dataUnseted = true } else { dataSet[nestedTag] = bson.NilObjectID } } else { dataSet[nestedTag] = nestedVal } break default: dataSet[nestedTag] = nestedVal } } } } } data = bson.M{ "$set": dataSet, } if dataUnseted { data["$unset"] = dataUnset } return } type ArraySelectFields struct { count int setFields bson.M unsetFields bson.M filters []interface{} push []interface{} pull []bson.ObjectID rootKey string idKey string modified bool } func (a *ArraySelectFields) Modified() bool { return a.modified } func (a *ArraySelectFields) Update(docId bson.ObjectID, update bson.M) { a.modified = true matchKey := fmt.Sprintf("elem%d", a.count) a.count += 1 setStr := fmt.Sprintf("%s.$[%s].", a.rootKey, matchKey) for key, val := range update { a.setFields[setStr+key] = val } a.filters = append(a.filters, bson.M{ fmt.Sprintf("%s.%s", matchKey, a.idKey): docId, }) } func (a *ArraySelectFields) Push(doc interface{}) { a.modified = true a.push = append(a.push, doc) } func (a *ArraySelectFields) Delete(docId bson.ObjectID) { a.modified = true a.pull = append(a.pull, docId) } func (a *ArraySelectFields) GetQuery() (query bson.M, filters []interface{}) { query = bson.M{} if len(a.setFields) > 0 { query["$set"] = a.setFields } if len(a.unsetFields) > 0 { query["$unset"] = a.unsetFields } filters = a.filters if len(a.push) > 0 { query["$push"] = bson.M{ a.rootKey: bson.M{ "$each": &a.push, }, } } if len(a.pull) > 0 { query["$pull"] = bson.M{ a.rootKey: bson.M{ a.idKey: bson.M{ "$in": a.pull, }, }, } } return } func NewArraySelectFields(obj interface{}, rootKey string, fields set.Set) ( arraySel *ArraySelectFields) { selectFields := SelectFieldsAll(obj, fields) setFields := selectFields["$set"].(bson.M) var unsetFields bson.M if _, exists := selectFields["$unset"]; exists { unsetFields = selectFields["$unset"].(bson.M) } arraySel = &ArraySelectFields{ count: 1, setFields: setFields, unsetFields: unsetFields, filters: []interface{}{}, push: []interface{}{}, pull: []bson.ObjectID{}, rootKey: rootKey, idKey: "id", } return } ================================================ FILE: database/database.go ================================================ package database import ( "context" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/mongo-go-driver/v2/mongo/readconcern" "github.com/pritunl/mongo-go-driver/v2/mongo/writeconcern" "github.com/pritunl/mongo-go-driver/v2/x/mongo/driver/connstring" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/sirupsen/logrus" ) type Database struct { ctx context.Context client *mongo.Client database *mongo.Database } func (d *Database) Deadline() (time.Time, bool) { if d.ctx != nil { return d.ctx.Deadline() } return time.Time{}, false } func (d *Database) Done() <-chan struct{} { if d.ctx != nil { return d.ctx.Done() } return nil } func (d *Database) Err() error { if d.ctx != nil { return d.ctx.Err() } return nil } func (d *Database) Value(key interface{}) interface{} { if d.ctx != nil { return d.ctx.Value(key) } return nil } func (d *Database) String() string { return "context.database" } func (d *Database) Close() { } func (d *Database) GetCollection(name string) (coll *Collection) { coll = &Collection{ db: d, Collection: d.database.Collection(name), } return } func (d *Database) getCollectionWeak(name string) (coll *Collection) { opts := options.Collection() opts.SetWriteConcern(writeconcern.W1()) opts.SetReadConcern(readconcern.Local()) coll = &Collection{ db: d, Collection: d.database.Collection(name, opts), } return } func (d *Database) getCollectionStrong(name string) (coll *Collection) { opts := options.Collection() opts.SetWriteConcern(writeconcern.Majority()) opts.SetReadConcern(readconcern.Local()) coll = &Collection{ db: d, Collection: d.database.Collection(name, opts), } return } func (d *Database) Users() (coll *Collection) { coll = d.GetCollection("users") return } func (d *Database) Policies() (coll *Collection) { coll = d.GetCollection("policies") return } func (d *Database) Devices() (coll *Collection) { coll = d.GetCollection("devices") return } func (d *Database) Alerts() (coll *Collection) { coll = d.GetCollection("alerts") return } func (d *Database) AlertsEvent() (coll *Collection) { coll = d.GetCollection("alerts_event") return } func (d *Database) AlertsEventLock() (coll *Collection) { coll = d.GetCollection("alerts_event_lock") return } func (d *Database) Pods() (coll *Collection) { coll = d.GetCollection("pods") return } func (d *Database) Units() (coll *Collection) { coll = d.GetCollection("units") return } func (d *Database) Specs() (coll *Collection) { coll = d.GetCollection("specs") return } func (d *Database) Deployments() (coll *Collection) { coll = d.GetCollection("deployments") return } func (d *Database) Sessions() (coll *Collection) { coll = d.GetCollection("sessions") return } func (d *Database) Tasks() (coll *Collection) { coll = d.GetCollection("tasks") return } func (d *Database) Tokens() (coll *Collection) { coll = d.GetCollection("tokens") return } func (d *Database) CsrfTokens() (coll *Collection) { coll = d.GetCollection("csrf_tokens") return } func (d *Database) SecondaryTokens() (coll *Collection) { coll = d.GetCollection("secondary_tokens") return } func (d *Database) Nonces() (coll *Collection) { coll = d.GetCollection("nonces") return } func (d *Database) Rokeys() (coll *Collection) { coll = d.GetCollection("rokeys") return } func (d *Database) Schedulers() (coll *Collection) { coll = d.GetCollection("schedulers") return } func (d *Database) Settings() (coll *Collection) { coll = d.GetCollection("settings") return } func (d *Database) Events() (coll *Collection) { coll = d.getCollectionWeak("events") return } func (d *Database) Nodes() (coll *Collection) { coll = d.GetCollection("nodes") return } func (d *Database) NodePorts() (coll *Collection) { coll = d.GetCollection("node_ports") return } func (d *Database) Organizations() (coll *Collection) { coll = d.GetCollection("organizations") return } func (d *Database) Storages() (coll *Collection) { coll = d.GetCollection("storages") return } func (d *Database) Images() (coll *Collection) { coll = d.GetCollection("images") return } func (d *Database) Datacenters() (coll *Collection) { coll = d.GetCollection("datacenters") return } func (d *Database) Zones() (coll *Collection) { coll = d.GetCollection("zones") return } func (d *Database) Shapes() (coll *Collection) { coll = d.GetCollection("shapes") return } func (d *Database) Balancers() (coll *Collection) { coll = d.GetCollection("balancers") return } func (d *Database) Advisories() (coll *Collection) { coll = d.GetCollection("advisories") return } func (d *Database) Instances() (coll *Collection) { coll = d.GetCollection("instances") return } func (d *Database) Pools() (coll *Collection) { coll = d.GetCollection("pools") return } func (d *Database) Disks() (coll *Collection) { coll = d.GetCollection("disks") return } func (d *Database) Blocks() (coll *Collection) { coll = d.GetCollection("blocks") return } func (d *Database) BlocksIp() (coll *Collection) { coll = d.GetCollection("blocks_ip") return } func (d *Database) LvmLock() (coll *Collection) { coll = d.GetCollection("lvm_lock") return } func (d *Database) Journal() (coll *Collection) { coll = d.getCollectionWeak("journal") return } func (d *Database) Firewalls() (coll *Collection) { coll = d.GetCollection("firewalls") return } func (d *Database) Versions() (coll *Collection) { coll = d.GetCollection("versions") return } func (d *Database) Plans() (coll *Collection) { coll = d.GetCollection("plans") return } func (d *Database) Vpcs() (coll *Collection) { coll = d.GetCollection("vpcs") return } func (d *Database) VpcsIp() (coll *Collection) { coll = d.GetCollection("vpcs_ip") return } func (d *Database) Authorities() (coll *Collection) { coll = d.GetCollection("authorities") return } func (d *Database) Certificates() (coll *Collection) { coll = d.GetCollection("certificates") return } func (d *Database) Secrets() (coll *Collection) { coll = d.GetCollection("secrets") return } func (d *Database) Domains() (coll *Collection) { coll = d.GetCollection("domains") return } func (d *Database) DomainsRecords() (coll *Collection) { coll = d.GetCollection("domains_records") return } func (d *Database) AcmeChallenges() (coll *Collection) { coll = d.GetCollection("acme_challenges") return } func (d *Database) Logs() (coll *Collection) { coll = d.getCollectionWeak("logs") return } func (d *Database) Audits() (coll *Collection) { coll = d.GetCollection("audits") return } func (d *Database) Geo() (coll *Collection) { coll = d.getCollectionWeak("geo") return } func Connect() (err error) { mongoUrl, err := connstring.ParseAndValidate(config.Config.MongoUri) if err != nil { err = &ConnectionError{ errors.Wrap(err, "database: Failed to parse mongo uri"), } return } logrus.WithFields(logrus.Fields{ "mongodb_hosts": mongoUrl.Hosts, }).Info("database: Connecting to MongoDB server") if mongoUrl.Database != "" { DefaultDatabase = mongoUrl.Database } opts := options.Client().ApplyURI(config.Config.MongoUri) opts.SetRetryReads(true) opts.SetRetryWrites(true) opts.SetWriteConcern(writeconcern.Majority()) opts.SetReadConcern(readconcern.Local()) client, err := mongo.Connect(opts) if err != nil { err = &ConnectionError{ errors.Wrap(err, "database: Connection error"), } return } setClient(client) version, err := ValidateDatabase() if err != nil { return } logrus.WithFields(logrus.Fields{ "mongodb_hosts": mongoUrl.Hosts, "mongodb_version": version, }).Info("database: Connected to MongoDB server") err = addCollections() if err != nil { return } err = addIndexes() if err != nil { return } cleanDb := GetDatabase() defer cleanDb.Close() err = CleanIndexes(cleanDb) if err != nil { return } return } func ValidateDatabase() (version string, err error) { db := GetDatabase() defer db.Close() buildInfo := bson.M{} err = db.database.RunCommand( db, bson.D{{"buildInfo", 1}}, ).Decode(&buildInfo) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("database: Failed to get MongoDB version") err = nil } version, ok := buildInfo["version"].(string) if version == "" || !ok { version = "unknown" } cursor, err := db.database.ListCollections( db, &bson.M{}) if err != nil { err = ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { item := &struct { Name string `bson:"name"` }{} err = cursor.Decode(item) if err != nil { err = ParseError(err) return } if item.Name == "servers" { err = &errortypes.DatabaseError{ errors.New("database: Cannot connect to pritunl database"), } return } } err = cursor.Err() if err != nil { err = ParseError(err) return } return } func getDatabase(ctx context.Context, client *mongo.Client) *Database { if client == nil { return nil } database := client.Database(DefaultDatabase) return &Database{ ctx: ctx, client: client, database: database, } } func GetDatabase() *Database { return getDatabase(nil, getClient()) } func GetDatabaseCtx(ctx context.Context) *Database { return getDatabase(ctx, getClient()) } func addIndexes() (err error) { db := GetDatabase() defer db.Close() index := &Index{ Collection: db.Users(), Keys: &bson.D{ {"username", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Users(), Keys: &bson.D{ {"type", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Users(), Keys: &bson.D{ {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Users(), Keys: &bson.D{ {"token", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Logs(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 4320 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Audits(), Keys: &bson.D{ {"user", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Policies(), Keys: &bson.D{ {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.CsrfTokens(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 168 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.SecondaryTokens(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 3 * time.Minute, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Nodes(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Nodes(), Keys: &bson.D{ {"pools", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.NodePorts(), Keys: &bson.D{ {"datacenter", 1}, {"protocol", 1}, {"port", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.NodePorts(), Keys: &bson.D{ {"resource", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Nonces(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 24 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Rokeys(), Keys: &bson.D{ {"type", 1}, {"timeblock", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Rokeys(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 720 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Devices(), Keys: &bson.D{ {"user", 1}, {"mode", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Devices(), Keys: &bson.D{ {"provider", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Alerts(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Alerts(), Keys: &bson.D{ {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.AlertsEvent(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 48 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.AlertsEvent(), Keys: &bson.D{ {"source", 1}, {"resource", 1}, {"timestamp", -1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.AlertsEventLock(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 72 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Organizations(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Organizations(), Keys: &bson.D{ {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Images(), Keys: &bson.D{ {"key", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Images(), Keys: &bson.D{ {"organization", 1}, {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Images(), Keys: &bson.D{ {"storage", 1}, {"key", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Images(), Keys: &bson.D{ {"disk", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.LvmLock(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 90 * time.Second, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Disks(), Keys: &bson.D{ {"instance", 1}, {"index", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Disks(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Disks(), Keys: &bson.D{ {"organization", 1}, {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Disks(), Keys: &bson.D{ {"node", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Domains(), Keys: &bson.D{ {"last_update", -1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.DomainsRecords(), Keys: &bson.D{ {"domain", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.DomainsRecords(), Keys: &bson.D{ {"domain", 1}, {"sub_domain", 1}, {"type", 1}, {"value", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Datacenters(), Keys: &bson.D{ {"organization", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Datacenters(), Keys: &bson.D{ {"match_organizations", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.BlocksIp(), Keys: &bson.D{ {"block", 1}, {"ip", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.BlocksIp(), Keys: &bson.D{ {"instance", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Journal(), Keys: &bson.D{ {"r", 1}, {"k", 1}, {"t", 1}, {"c", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Journal(), Keys: &bson.D{ {"t", 1}, }, Expire: 2160 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Vpcs(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Vpcs(), Keys: &bson.D{ {"organization", 1}, {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Vpcs(), Keys: &bson.D{ {"vpc_id", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Vpcs(), Keys: &bson.D{ {"datacenter", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.VpcsIp(), Keys: &bson.D{ {"vpc", 1}, {"subnet", 1}, {"ip", 1}, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.VpcsIp(), Keys: &bson.D{ {"vpc", 1}, {"instance", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.VpcsIp(), Keys: &bson.D{ {"vpc", 1}, {"subnet", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Sessions(), Keys: &bson.D{ {"user", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Sessions(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 4320 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Firewalls(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Firewalls(), Keys: &bson.D{ {"organization", 1}, {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Firewalls(), Keys: &bson.D{ {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Firewalls(), Keys: &bson.D{ {"roles", 1}, {"organization", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Zones(), Keys: &bson.D{ {"datacenter", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Balancers(), Keys: &bson.D{ {"datacenter", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Authorities(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Authorities(), Keys: &bson.D{ {"organization", 1}, {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Authorities(), Keys: &bson.D{ {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Authorities(), Keys: &bson.D{ {"organization", 1}, {"roles", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"node", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"organization", 1}, {"name", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"node", 1}, {"vnc_display", 1}, }, Partial: &bson.M{ "vnc_display": &bson.M{ "$gt": 0, }, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"node", 1}, {"spice_port", 1}, }, Partial: &bson.M{ "spice_port": &bson.M{ "$gt": 0, }, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"unix_id", 1}, }, Partial: &bson.M{ "unix_id": &bson.M{ "$gt": 0, }, }, Unique: true, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Instances(), Keys: &bson.D{ {"node_ports.node_port", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Units(), Keys: &bson.D{ {"pod", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Tasks(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 48 * time.Hour, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Events(), Keys: &bson.D{ {"channel", 1}, }, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.AcmeChallenges(), Keys: &bson.D{ {"timestamp", 1}, }, Expire: 3 * time.Minute, } err = index.Create() if err != nil { return } index = &Index{ Collection: db.Geo(), Keys: &bson.D{ {"t", 1}, }, Expire: 360 * time.Hour, } err = index.Create() if err != nil { return } return } func addCollections() (err error) { db := GetDatabase() defer db.Close() cursor, err := db.database.ListCollections( db, &bson.M{}) if err != nil { err = ParseError(err) return } defer cursor.Close(db) eventsExists := false isCapped := false for cursor.Next(db) { item := &struct { Name string `bson:"name"` Options bson.M `bson:"options"` }{} err = cursor.Decode(item) if err != nil { err = ParseError(err) return } if item.Name == "events" { eventsExists = true if options, ok := item.Options["capped"]; ok { if cappedBool, ok := options.(bool); ok && cappedBool { isCapped = true } } break } } err = cursor.Err() if err != nil { err = ParseError(err) return } if eventsExists && !isCapped { logrus.WithFields(logrus.Fields{ "collection": "events", }).Warning("database: Correcting events capped collection") err = db.database.Collection("events").Drop(db) if err != nil { err = ParseError(err) return } eventsExists = false } if !eventsExists { err = db.database.RunCommand( db, bson.D{ {"create", "events"}, {"capped", true}, {"max", 5000}, {"size", 20971520}, }, ).Err() if err != nil { err = ParseError(err) return } } return } func init() { module := requires.New("database") module.After("config") module.Handler = func() (err error) { for { e := Connect() if e != nil { logrus.WithFields(logrus.Fields{ "error": e, }).Error("database: Connection error") } else { break } time.Sleep(constants.RetryDelay) } return } } ================================================ FILE: database/errors.go ================================================ package database import ( "github.com/dropbox/godropbox/errors" ) type ConnectionError struct { errors.DropboxError } type IndexError struct { errors.DropboxError } type NotFoundError struct { errors.DropboxError } type ImmutableKeyError struct { errors.DropboxError } type DuplicateKeyError struct { errors.DropboxError } type UnknownError struct { errors.DropboxError } type CertificateError struct { errors.DropboxError } type IndexConflict struct { errors.DropboxError } ================================================ FILE: database/index.go ================================================ package database import ( "bytes" "context" "fmt" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/sirupsen/logrus" ) var ( indexes = map[string]set.Set{} indexesLock = sync.Mutex{} ) type bsonIndex struct { Name string `bson:"name"` } type Index struct { Collection *Collection Keys *bson.D Unique bool Partial interface{} Expire time.Duration } func GenerateIndexName(doc bson.D) (indexName string, err error) { name := bytes.NewBufferString("") first := true for _, elem := range doc { if !first { _, err = name.WriteRune('_') if err != nil { err = &UnknownError{ errors.Wrap(err, "database: Write rune error"), } return } } _, err = name.WriteString(elem.Key) if err != nil { err = &UnknownError{ errors.Wrap(err, "database: Write string error"), } return } _, err = name.WriteRune('_') if err != nil { err = &UnknownError{ errors.Wrap(err, "database: Write rune error"), } return } value := "" switch val := elem.Value.(type) { case int, int32, int64: value = fmt.Sprintf("%d", val) case string: value = val default: err = &UnknownError{ errors.New("database: Invalid index value"), } return } _, err = name.WriteString(value) if err != nil { err = &UnknownError{ errors.Wrap(err, "database: Write string error"), } return } first = false } indexName = name.String() return } func (i *Index) Create() (err error) { opts := options.Index() if i.Unique { opts.SetUnique(true) } if i.Partial != nil { opts.SetPartialFilterExpression(i.Partial) } if i.Expire != 0 { opts.SetExpireAfterSeconds(int32(i.Expire.Seconds())) } indexName, err := GenerateIndexName(*i.Keys) if err != nil { return } opts.SetName(indexName) name, err := i.Collection.Indexes().CreateOne( context.Background(), mongo.IndexModel{ Keys: i.Keys, Options: opts, }, ) if err != nil { err = ParseError(err) if _, ok := err.(*IndexConflict); ok { err = nil err = i.Collection.Indexes().DropOne( context.Background(), indexName, ) if err != nil { return } name, err = i.Collection.Indexes().CreateOne( context.Background(), mongo.IndexModel{ Keys: i.Keys, Options: opts, }, ) if err != nil { err = ParseError(err) return } } else { return } } collName := i.Collection.Name() indexesLock.Lock() collIndexes, ok := indexes[collName] if !ok { collIndexes = set.NewSet() indexes[collName] = collIndexes } collIndexes.Add(name) indexesLock.Unlock() return } func CleanIndexes(db *Database) (err error) { indexesLock.Lock() curIndexes := indexes indexesLock.Unlock() for collName, collIndexes := range curIndexes { coll := db.GetCollection(collName) cursor, e := coll.Indexes().List(db) if e != nil { err = e return } for cursor.Next(db) { index := &bsonIndex{} err = cursor.Decode(index) if err != nil { logrus.WithFields(logrus.Fields{ "collection": collName, "error": err, }).Error("database: Failed to decode index") err = nil continue } if index.Name == "_id" || index.Name == "_id_" { continue } if collIndexes.Contains(index.Name) { continue } logrus.WithFields(logrus.Fields{ "collection": collName, "index": index.Name, }).Info("database: Dropping unused index") err = coll.Indexes().DropOne( db, index.Name, ) if err != nil { cursor.Close(db) return } } err = cursor.Err() if err != nil { logrus.WithFields(logrus.Fields{ "collection": collName, "error": err, }).Error("database: Cursor error listing indexes") } cursor.Close(db) } return } ================================================ FILE: database/utils.go ================================================ package database import ( "fmt" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/mongo-go-driver/v2/mongo/options" ) func FindProject(fields ...string) *options.FindOptionsBuilder { prcj := bson.D{} for _, field := range fields { prcj = append(prcj, bson.E{Key: field, Value: 1}) } opts := options.Find() opts.SetProjection(prcj) return opts } func FindOneProject(fields ...string) *options.FindOneOptionsBuilder { prcj := bson.D{} for _, field := range fields { prcj = append(prcj, bson.E{Key: field, Value: 1}) } opts := options.FindOne() opts.SetProjection(prcj) return opts } func GetErrorCodes(err error) (errCodes []int) { switch err := err.(type) { case mongo.CommandError: errCodes = []int{int(err.Code)} if strings.Contains(err.Name, "Conflict") { errCodes = append(errCodes, 85) } break case mongo.WriteError: errCodes = []int{err.Code} break case mongo.BulkWriteError: errCodes = []int{err.Code} break case mongo.WriteConcernError: errCodes = []int{err.Code} break case mongo.WriteException: errCodes = []int{} if err.WriteConcernError != nil { errCodes = append(errCodes, err.WriteConcernError.Code) } if err.WriteErrors != nil { for _, e := range err.WriteErrors { errCodes = append(errCodes, e.Code) } } break case mongo.WriteErrors: errCodes = []int{} for _, e := range err { eCodes := GetErrorCodes(e) errCodes = append(errCodes, eCodes...) } break case *mongo.WriteError: errCodes = []int{err.Code} break case *mongo.BulkWriteError: errCodes = []int{err.Code} break case *mongo.WriteConcernError: errCodes = []int{err.Code} break case *mongo.WriteException: errCodes = []int{} if err.WriteConcernError != nil { errCodes = append(errCodes, err.WriteConcernError.Code) } if err.WriteErrors != nil { for _, e := range err.WriteErrors { errCodes = append(errCodes, e.Code) } } break } return } func ParseError(err error) (newErr error) { if err == mongo.ErrNoDocuments { newErr = &NotFoundError{ errors.New("database: Not found"), } return } errCodes := GetErrorCodes(err) for _, errCode := range errCodes { switch errCode { case 66: newErr = &ImmutableKeyError{ errors.New("database: Immutable key"), } return case 85: newErr = &IndexConflict{ errors.New("database: Index conflict"), } return case 11000, 11001, 12582, 16460: newErr = &DuplicateKeyError{ errors.New("database: Duplicate key"), } return } } newErr = &UnknownError{ errors.Wrap(err, fmt.Sprintf( "database: Unknown error %v", errCodes)), } return } func IgnoreNotFoundError(err error) (newErr error) { if err != nil { switch err.(type) { case *NotFoundError: newErr = nil break default: newErr = err } } return } ================================================ FILE: datacenter/constants.go ================================================ package datacenter const ( Default = "default" VxlanVlan = "vxlan_vlan" WgVxlanVlan = "wg_vxlan_vlan" Wg4 = "wg4" Wg6 = "wg6" ) ================================================ FILE: datacenter/datacenter.go ================================================ package datacenter import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) type Datacenter struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` MatchOrganizations bool `bson:"match_organizations" json:"match_organizations"` Organizations []bson.ObjectID `bson:"organizations" json:"organizations"` NetworkMode string `bson:"network_mode" json:"network_mode"` WgMode string `bson:"wg_mode" json:"wg_mode"` JumboMtu int `bson:"jumbo_mtu" json:"jumbo_mtu"` PublicStorages []bson.ObjectID `bson:"public_storages" json:"public_storages"` PrivateStorage bson.ObjectID `bson:"private_storage,omitempty" json:"private_storage"` PrivateStorageClass string `bson:"private_storage_class" json:"private_storage_class"` BackupStorage bson.ObjectID `bson:"backup_storage,omitempty" json:"backup_storage"` BackupStorageClass string `bson:"backup_storage_class" json:"backup_storage_class"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` NetworkMode string `bson:"network_mode" json:"network_mode"` } func (d *Datacenter) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { d.Name = utils.FilterName(d.Name) if d.Organizations == nil || !d.MatchOrganizations { d.Organizations = []bson.ObjectID{} } if d.PublicStorages == nil { d.PublicStorages = []bson.ObjectID{} } switch d.NetworkMode { case Default: break case VxlanVlan: break case WgVxlanVlan: break case "": d.NetworkMode = Default break default: errData = &errortypes.ErrorData{ Error: "invalid_network_mode", Message: "Network mode invalid", } return } if d.NetworkMode == WgVxlanVlan { switch d.WgMode { case Wg4: break case Wg6: break case "": d.WgMode = Wg4 break default: errData = &errortypes.ErrorData{ Error: "invalid_wg_mode", Message: "WireGuard mode invalid", } return } } else { d.WgMode = "" } if d.JumboMtu != 0 && (d.JumboMtu < 600 || d.JumboMtu > 65535) { errData = &errortypes.ErrorData{ Error: "invalid_jumbo_mtu", Message: "Jumbo MTU invalid", } return } return } func (d *Datacenter) Vxlan() bool { return d.NetworkMode == VxlanVlan || d.NetworkMode == WgVxlanVlan } func (d *Datacenter) GetBaseInternalMtu() (mtuSize int) { if node.Self.JumboFrames || node.Self.JumboFramesInternal { if d.JumboMtu != 0 { mtuSize = d.JumboMtu } else { mtuSize = settings.Hypervisor.JumboMtu } } else { mtuSize = settings.Hypervisor.NormalMtu } return } func (d *Datacenter) GetBaseExternalMtu() (mtuSize int) { if node.Self.JumboFrames { if d.JumboMtu != 0 { mtuSize = d.JumboMtu } else { mtuSize = settings.Hypervisor.JumboMtu } } else { mtuSize = settings.Hypervisor.NormalMtu } return } func (d *Datacenter) GetOverlayMtu() (mtuSize int) { if d.NetworkMode == WgVxlanVlan { if node.Self.JumboFrames { if d.JumboMtu != 0 { mtuSize = d.JumboMtu } else { mtuSize = settings.Hypervisor.JumboMtu } } else { mtuSize = settings.Hypervisor.NormalMtu } if d.WgMode == Wg6 { mtuSize -= 150 } else { mtuSize -= 110 } } else { if node.Self.JumboFrames || node.Self.JumboFramesInternal { if d.JumboMtu != 0 { mtuSize = d.JumboMtu } else { mtuSize = settings.Hypervisor.JumboMtu } } else { mtuSize = settings.Hypervisor.NormalMtu } if d.NetworkMode == VxlanVlan { mtuSize -= 50 } } return } func (d *Datacenter) GetInstanceMtu() (mtuSize int) { if d.NetworkMode == WgVxlanVlan { if node.Self.JumboFrames { if d.JumboMtu != 0 { mtuSize = d.JumboMtu } else { mtuSize = settings.Hypervisor.JumboMtu } } else { mtuSize = settings.Hypervisor.NormalMtu } if d.WgMode == Wg6 { mtuSize -= 154 } else { mtuSize -= 114 } } else { if node.Self.JumboFrames || node.Self.JumboFramesInternal { if d.JumboMtu != 0 { mtuSize = d.JumboMtu } else { mtuSize = settings.Hypervisor.JumboMtu } } else { mtuSize = settings.Hypervisor.NormalMtu } if d.NetworkMode == VxlanVlan { mtuSize -= 54 } } return } func (d *Datacenter) Commit(db *database.Database) (err error) { coll := db.Datacenters() err = coll.Commit(d.Id, d) if err != nil { return } return } func (d *Datacenter) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Datacenters() err = coll.CommitFields(d.Id, d, fields) if err != nil { return } return } func (d *Datacenter) Insert(db *database.Database) (err error) { coll := db.Datacenters() if !d.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("datacenter: Datacenter already exists"), } return } resp, err := coll.InsertOne(db, d) if err != nil { err = database.ParseError(err) return } d.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: datacenter/utils.go ================================================ package datacenter import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, dcId bson.ObjectID) ( dc *Datacenter, err error) { coll := db.Datacenters() dc = &Datacenter{} err = coll.FindOneId(dcId, dc) if err != nil { return } return } func ExistsOrg(db *database.Database, orgId, dcId bson.ObjectID) ( exists bool, err error) { coll := db.Datacenters() count, err := coll.CountDocuments(db, &bson.M{ "_id": dcId, "$or": []*bson.M{ &bson.M{ "match_organizations": false, }, &bson.M{ "organizations": orgId, }, }, }) if err != nil { err = database.ParseError(err) return } if count > 0 { exists = true } return } func GetAll(db *database.Database) (dcs []*Datacenter, err error) { coll := db.Datacenters() dcs = []*Datacenter{} cursor, err := coll.Find(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dc := &Datacenter{} err = cursor.Decode(dc) if err != nil { err = database.ParseError(err) return } dcs = append(dcs, dc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (dc *Datacenter, err error) { coll := db.Datacenters() dc = &Datacenter{} err = coll.FindOne(db, query).Decode(dc) if err != nil { err = database.ParseError(err) return } return } func GetAllNamesOrg(db *database.Database, orgId bson.ObjectID) ( dcs []*Completion, err error) { coll := db.Datacenters() dcs = []*Completion{} cursor, err := coll.Find(db, &bson.M{ "$or": []*bson.M{ &bson.M{ "match_organizations": false, }, &bson.M{ "organizations": orgId, }, }, }, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{ {"name", 1}, {"network_mode", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dc := &Completion{} err = cursor.Decode(dc) if err != nil { err = database.ParseError(err) return } dcs = append(dcs, dc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( dcs []*Completion, err error) { coll := db.Certificates() dcs = []*Completion{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{ {"name", 1}, {"network_mode", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dc := &Completion{} err = cursor.Decode(dc) if err != nil { err = database.ParseError(err) return } dcs = append(dcs, dc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (dc []*Datacenter, count int64, err error) { coll := db.Datacenters() dc = []*Datacenter{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { d := &Datacenter{} err = cursor.Decode(d) if err != nil { err = database.ParseError(err) return } dc = append(dc, d) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func DistinctOrg(db *database.Database, orgId bson.ObjectID) ( ids []bson.ObjectID, err error) { coll := db.Datacenters() ids = []bson.ObjectID{} err = coll.Distinct(db, "_id", &bson.M{ "$or": []*bson.M{ &bson.M{ "match_organizations": false, }, &bson.M{ "organizations": orgId, }, }, }).Decode(&ids) if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, dcId bson.ObjectID) (err error) { coll := db.Datacenters() _, err = coll.DeleteOne(db, &bson.M{ "_id": dcId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, dcIds []bson.ObjectID) ( err error) { coll := db.Datacenters() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": dcIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, dcIds []bson.ObjectID) (err error) { coll := db.Datacenters() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": dcIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: defaults/defaults.go ================================================ package defaults import ( "fmt" "math/rand" "net" "slices" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/cloudinit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" ) func initStorage(db *database.Database) (err error) { stores, err := storage.GetAll(db) if err != nil { return } if len(stores) == 0 { store := &storage.Storage{ Name: "pritunl-images", Type: storage.Public, Endpoint: "images.pritunl.com", Bucket: "stable", Insecure: false, } errData, e := store.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Storage validate error %s", errData.Message, ), } return } err = store.Insert(db) if err != nil { return } logrus.WithFields(logrus.Fields{ "storage": store.Id.Hex(), }).Info("defaults: Created default storage") event.PublishDispatch(db, "storage.change") } return } func initOrganization(db *database.Database) ( defaultOrg bson.ObjectID, err error) { orgs, err := organization.GetAll(db, &bson.M{}) if err != nil { return } if len(orgs) == 0 { org := &organization.Organization{ Name: "org", Roles: []string{"org"}, } errData, e := org.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Organization validate error %s", errData.Message, ), } return } err = org.Insert(db) if err != nil { return } defaultOrg = org.Id logrus.WithFields(logrus.Fields{ "organization": org.Id.Hex(), }).Info("defaults: Created default organization") event.PublishDispatch(db, "organization.change") } else { for _, org := range orgs { if defaultOrg.IsZero() || org.Name == "org" { defaultOrg = org.Id } } } return } func initDatacenter(db *database.Database) ( defaultDc bson.ObjectID, err error) { dcs, err := datacenter.GetAll(db) if err != nil { return } if len(dcs) == 0 { stores, e := storage.GetAll(db) if e != nil { err = e return } publicStorages := []bson.ObjectID{} for _, store := range stores { if store.Endpoint == "images.pritunl.com" && store.Bucket == "stable" { publicStorages = append(publicStorages, store.Id) break } } dc := &datacenter.Datacenter{ Name: "us-west-1", NetworkMode: datacenter.Default, PublicStorages: publicStorages, } errData, e := dc.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Datacenter validate error %s", errData.Message, ), } return } err = dc.Insert(db) if err != nil { return } defaultDc = dc.Id logrus.WithFields(logrus.Fields{ "datacenter": dc.Id.Hex(), }).Info("defaults: Created default datacenter") event.PublishDispatch(db, "datacenter.change") } else { for _, dc := range dcs { if defaultDc.IsZero() || dc.Name == "us-west-1" { defaultDc = dc.Id } } } return } func initZone(db *database.Database, defaultDc bson.ObjectID) ( err error) { zones, err := zone.GetAll(db) if err != nil { return } if len(zones) == 0 { zne := &zone.Zone{ Name: "us-west-1a", Datacenter: defaultDc, } errData, e := zne.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Zone validate error %s", errData.Message, ), } return } err = zne.Insert(db) if err != nil { return } logrus.WithFields(logrus.Fields{ "zone": zne.Id.Hex(), }).Info("defaults: Created default zone") event.PublishDispatch(db, "zone.change") } return } func initVpc(db *database.Database, defaultOrg, defaultDc bson.ObjectID) (err error) { if defaultOrg.IsZero() { return } vcs, err := vpc.GetAll(db, &bson.M{}) if err != nil { return } if len(vcs) == 0 { start, end, step := 100, 220, 4 randomStep := rand.Intn((end-start)/step + 1) netNum := start + (randomStep * step) vc := &vpc.Vpc{ Name: "vpc", Organization: defaultOrg, Datacenter: defaultDc, VpcId: utils.RandInt(1001, 3999), Network: fmt.Sprintf("10.%d.0.0/14", netNum), Subnets: []*vpc.Subnet{ &vpc.Subnet{ Name: "primary", Network: fmt.Sprintf("10.%d.1.0/24", netNum), }, &vpc.Subnet{ Name: "management", Network: fmt.Sprintf("10.%d.2.0/24", netNum), }, }, } vc.InitVpc() errData, e := vc.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: VPC validate error %s", errData.Message, ), } return } err = vc.Insert(db) if err != nil { return } logrus.WithFields(logrus.Fields{ "vpc": vc.Id.Hex(), }).Info("defaults: Created default VPC") event.PublishDispatch(db, "vpc.change") } return } func initFirewall(db *database.Database, defaultOrg bson.ObjectID) ( err error) { if defaultOrg.IsZero() { return } fires, err := firewall.GetAll(db, &bson.M{}) if err != nil { return } if len(fires) == 0 { fire := &firewall.Firewall{ Name: "instance", Organization: defaultOrg, Roles: []string{ "instance", }, Ingress: []*firewall.Rule{ &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Icmp, }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "22", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "80", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "443", }, }, } errData, e := fire.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Firewall validate error %s", errData.Message, ), } return } err = fire.Insert(db) if err != nil { return } fire = &firewall.Firewall{ Name: "node", Organization: firewall.Global, Comment: "22/tcp - SSH\n" + "80/tcp - HTTP\n" + "443/tcp - HTTPS\n" + "4789/udp - VXLAN cross-node\n" + "20000-25000/tcp - VNC cross-node\n" + "30000-32767/tcp - TCP NodePorts\n" + "30000-32767/udp - UDP NodePorts", Roles: []string{ "node-firewall", }, Ingress: []*firewall.Rule{ &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Icmp, }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "22", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "80", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "443", }, &firewall.Rule{ SourceIps: []string{ "10.0.0.0/8", "100.64.0.0/10", "172.16.0.0/12", "192.168.0.0/16", "198.18.0.0/15", }, Protocol: firewall.Udp, Port: "4789", }, &firewall.Rule{ SourceIps: []string{ "10.0.0.0/8", "100.64.0.0/10", "172.16.0.0/12", "192.168.0.0/16", "198.18.0.0/15", }, Protocol: firewall.Tcp, Port: "20000-25000", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Tcp, Port: "30000-32767", }, &firewall.Rule{ SourceIps: []string{ "0.0.0.0/0", "::/0", }, Protocol: firewall.Udp, Port: "30000-32767", }, }, } errData, e = fire.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Firewall validate error %s", errData.Message, ), } return } err = fire.Insert(db) if err != nil { return } logrus.WithFields(logrus.Fields{ "firewall": fire.Id.Hex(), }).Info("defaults: Created default firewall") event.PublishDispatch(db, "firewall.change") } return } func initAuthority(db *database.Database, defaultOrg bson.ObjectID) ( err error) { if defaultOrg.IsZero() { return } authrs, err := authority.GetAll(db, &bson.M{}) if err != nil { return } if len(authrs) == 0 { authr := &authority.Authority{ Name: "cloud", Type: authority.SshKey, Organization: defaultOrg, Roles: []string{ "instance", }, } errData, e := authr.Validate(db) if e != nil { err = e return } if errData != nil { err = &errortypes.ApiError{ errors.Newf( "defaults: Authority validate error %s", errData.Message, ), } return } err = authr.Insert(db) if err != nil { return } logrus.WithFields(logrus.Fields{ "authority": authr.Id.Hex(), }).Info("defaults: Created default authority") event.PublishDispatch(db, "authority.change") } return } func initNode(db *database.Database, defaultOrg bson.ObjectID) ( err error) { if defaultOrg.IsZero() { return } if !node.Self.Zone.IsZero() { return } dcs, err := datacenter.GetAll(db) if err != nil { return } zones, err := zone.GetAll(db) if err != nil { return } nodes, err := node.GetAll(db) if err != nil { return } if len(dcs) != 1 || len(zones) != 1 || len(nodes) != 1 { return } dc := dcs[0] node.Self.Datacenter = zones[0].Datacenter node.Self.Zone = zones[0].Id node.Self.Roles = []string{ "node-firewall", "shape-m2", } node.Self.HostNat = true fires, err := firewall.GetOrgRoles(db, firewall.Global, []string{"node-firewall"}) if err != nil { return } if len(fires) > 0 { node.Self.Firewall = true } logrus.Info("defaults: Attempting to load network " + "configuration from cloudinit") internalIface := "" internalJumbo := false externalIface := "" externalIp := "" externalNet := "" externalMask := "" externalGateway := "" externalJumbo := false cloudConf, err := cloudinit.GetCloudConfig() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Warn("defaults: Failed to load cloudinit network config") err = nil } else { for _, iface := range cloudConf.CombinedCloudConfig.Network.Config { for _, addrInfo := range iface.Subnets { addr := utils.ParseAddress(addrInfo.Address) if addr == nil { continue } if internalIface == "" && addr.Private && !addr.Ip6 { internalIface = iface.Name internalJumbo = iface.Mtu >= 9000 logrus.WithFields(logrus.Fields{ "iface": iface.Name, "address": addr.Address.String(), "mode": addrInfo.Type, "mtu": iface.Mtu, "type": iface.Type, "vlan": iface.VlanId, "jumbo": internalJumbo, }).Info("defaults: Detected internal interface") } if externalIface == "" && addr.Public && !addr.Ip6 && addr.Network != nil { externalIface = iface.Name externalJumbo = iface.Mtu >= 9000 externalIp = addr.Address.String() externalNet = addr.Network.String() externalMask = net.IP(addr.Network.Mask).String() externalGateway = addrInfo.Gateway logrus.WithFields(logrus.Fields{ "iface": iface.Name, "address": externalIp, "network": externalNet, "netmask": externalMask, "gateway": externalGateway, "mode": addrInfo.Type, "mtu": iface.Mtu, "type": iface.Type, "vlan": iface.VlanId, "vlan_iface": iface.VlanLink, "jumbo": externalJumbo, }).Info("defaults: Detected external interface") } } } } if internalIface != "" { node.Self.InternalInterfaces = []string{internalIface} if internalJumbo { node.Self.JumboFramesInternal = true dc.NetworkMode = datacenter.VxlanVlan errData, e := dc.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } err = dc.CommitFields(db, set.NewSet("network_mode")) if err != nil { return } } } else { node.Self.InternalInterfaces = []string{ settings.Hypervisor.HostNetworkName, } } if externalIface != "" { node.Self.DefaultNoPublicAddress = true if externalJumbo && internalJumbo { node.Self.JumboFrames = true node.Self.JumboFramesInternal = true } blcks, e := block.GetAll(db) if e != nil { err = e return } var externalBlck *block.Block for _, blck := range blcks { if blck.Netmask == externalMask { for _, subnet := range blck.Subnets { if subnet == externalNet { externalBlck = blck break } } if externalBlck != nil { break } } } if externalBlck != nil { if externalGateway != "" { externalBlck.Gateway = externalGateway } excludeIp := externalIp + "/32" if !slices.Contains(externalBlck.Excludes, excludeIp) { externalBlck.Excludes = append( externalBlck.Excludes, excludeIp) } err = externalBlck.CommitFields( db, set.NewSet("gateway", "excludes")) if err != nil { return } } else { externalBlck = &block.Block{ Name: "cloud-public", Type: block.IPv4, Subnets: []string{externalNet}, Excludes: []string{externalIp + "/32"}, Netmask: externalMask, Gateway: externalGateway, } errData, e := externalBlck.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } err = externalBlck.Insert(db) if err != nil { return } } node.Self.NetworkMode = node.Static node.Self.Blocks = []*node.BlockAttachment{ &node.BlockAttachment{ Interface: externalIface, Block: externalBlck.Id, }, } } errData, err := node.Self.Validate(db) if err != nil { return } if errData != nil { err = errData.GetError() return } err = node.Self.Commit(db) if err != nil { return } shpe := &shape.Shape{ Name: "m2-small", Datacenter: node.Self.Datacenter, Memory: 2048, Processors: 1, Flexible: true, Roles: []string{ "shape-m2", }, Type: shape.Instance, DiskType: shape.Qcow2, } errData, err = shpe.Validate(db) if err != nil { return } if errData != nil { err = errData.GetError() return } err = shpe.Insert(db) if err != nil { return } logrus.WithFields(logrus.Fields{ "node": node.Self.Id.Hex(), }).Info("defaults: Configured default node") event.PublishDispatch(db, "node.change") return } func Defaults() (err error) { db := database.GetDatabase() defer db.Close() err = initStorage(db) if err != nil { return } defaultOrg, err := initOrganization(db) if err != nil { return } defaultDc, err := initDatacenter(db) if err != nil { return } err = initZone(db, defaultDc) if err != nil { return } err = initVpc(db, defaultOrg, defaultDc) if err != nil { return } err = initFirewall(db, defaultOrg) if err != nil { return } err = initAuthority(db, defaultOrg) if err != nil { return } err = initNode(db, defaultOrg) if err != nil { return } return } ================================================ FILE: demo/alert.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/alert" "github.com/pritunl/pritunl-cloud/utils" ) var Alerts = []*alert.Alert{ { Id: utils.ObjectIdHex("9cc278e67d0b4a3d173280c0"), Name: "cloud", Comment: "", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Roles: []string{"instance"}, Resource: "instance_offline", Level: 5, Frequency: 300, }, } ================================================ FILE: demo/authority.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/utils" ) var Authorities = []*authority.Authority{ { Id: utils.ObjectIdHex("688ab80d1793930f821f4f3c"), Name: "cloud", Comment: "", Type: "ssh_key", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Roles: []string{"instance"}, Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDE+MfxSp/77B/CphRquYSxnq7ee6EbcDASJHFeMx8MVoneQ0TRDdLsUJ3KwVKvx9VcXOzqOlV6SQnT/Zwfhz8NDeqePYWeN7tI4rMVRSmlg7wYj7affVpIWXmuMSfdmZytr/PCr4h/CwjA+KJdpIBqU9B0enosTt5+OwLmViL1VLk6oi6C9UNQyszx9btOZfYnEVZ+sm7iWVaqO4Z4An7cM4V9dzbT4GOI2F1AYp+RfdvktEccCcjzZSbyJkhRt5DkRx9q/PbNwF4bRNw9gKjAcxYt54BeJ0By1HUd1snTblftlN3CQskKNXlFI7fFqQfLpaO4csi8dWu8IH7Td5YV5MKlSt+ljoNhBE+5bntQWjGU209PS/DGRW62LTIF9tiCTvJhh+fmof0mKJlABH6Es1Qzr6iwKz22a8LunF9Sf/dm9Og8zZVuCPWJNWIyYjNCBhDAaGj7KSH7apGoSl5Ck9vAyiL2dv54c4tCX9dyswutZeK7+RgH91v+SsxKrp8= cloud@pritunl-demo", Principals: []string{}, Certificate: "", }, { Id: utils.ObjectIdHex("688ab80d1793930f821f4f5f"), Name: "demo", Comment: "", Type: "ssh_key", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Roles: []string{"instance"}, Key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCtianWXiLSMZ5h53POvUd0v6oIEH7dw/KZryymxa1x+CEFSaeeACWNSalcWSx48D53v7S4OytXkqjjwdsxGjJ0jfYe9c06s3fCdp0to9Dz1Xk5jeW0ojZXit7Ta+4MQH7mbZkS5SOVWo09fvoUlRrYDD+dlpG1XmYqLVCY7Z2atTVArYSQ9xNQUbXU3TgljZ2yKPX25d20y50exJWxlJwEgXo8z2ZsUJO22KL23fLs3t5Dylj5uV7gD3lEKRe+v6VGESuY8QKFKLEuOy7F+xmZazkIxOixDT7bPOz1FHGzTFUUOanYD8F9zS8TZAHuW0B4yA3Uh90NQ+7mAqW1dcX1Qu78e8xuEtKzSVvLR02NMWqdD+/tebfb1QIB2ljz9PanHFpnT+Ht0RdONgNSqMIs4HORObXnNkvCYtMLtoy5acE9zhM4P+fyr6CMSGpiqhFxXFAT0x+Xws+KJjpO6kER/vmuOsTUCsxIPlfVntzeisVnMYomdeOipdQhMELt2yE= cloud@pritunl-demo", Principals: []string{}, Certificate: "", }, } ================================================ FILE: demo/balancer.go ================================================ package demo import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/utils" ) var Balancers = []*balancer.Balancer{ { Id: utils.ObjectIdHex("61ba27ccf149d4c222b23247"), Name: "web-app", Comment: "", Type: "http", State: true, Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Certificates: []bson.ObjectID{ utils.ObjectIdHex("67b89ef24866ba90e6c459e8"), }, ClientAuthority: bson.ObjectID{}, WebSockets: false, Domains: []*balancer.Domain{ { Domain: "demo.cloud.pritunl.com", Host: "", }, }, Backends: []*balancer.Backend{ { Protocol: "http", Hostname: "10.234.10.22", Port: 8000, }, { Protocol: "http", Hostname: "10.234.10.24", Port: 8000, }, }, States: map[string]*balancer.State{ "65b5d7e1c2e9a21159765955": { Timestamp: time.Now(), Requests: 125, Retries: 0, WebSockets: 0, Online: []string{ "10.234.10.22:8000", "10.234.10.24:8000", }, UnknownHigh: []string{}, UnknownMid: []string{}, UnknownLow: []string{}, Offline: []string{}, }, }, CheckPath: "/check", }, } ================================================ FILE: demo/block.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/utils" ) var Blocks = []*aggregate.BlockAggregate{ { Block: block.Block{ Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), Name: "east-public", Comment: "", Type: "ipv4", Vlan: 0, Subnets: []string{ "1.253.67.0/24", }, Subnets6: []string{}, Excludes: []string{ "1.253.67.90/24", "1.253.67.91/24", "1.253.67.92/24", "1.253.67.93/24", "1.253.67.94/24", "1.253.67.95/24", }, Netmask: "255.255.255.0", Gateway: "1.253.67.1", Gateway6: "", }, Available: 248, Capacity: 254, }, { Block: block.Block{ Id: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), Name: "east-public6", Comment: "", Type: "ipv6", Vlan: 0, Subnets: []string{}, Subnets6: []string{ "2001:db8:85a3:4d2f::/64", }, Excludes: []string{}, Netmask: "", Gateway: "", Gateway6: "2001:db8:85a3:4d2f::1", }, }, } ================================================ FILE: demo/certificate.go ================================================ package demo import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/utils" ) var Certificates = []*certificate.Certificate{ { Id: utils.ObjectIdHex("67b89ef24866ba90e6c459e8"), Name: "cloud-pritunl-com", Comment: "", Organization: bson.ObjectID{}, Type: "lets_encrypt", Key: `-----BEGIN RSA PRIVATE KEY----- MIIJKQIBAAKCAgEAx9Y3Lk2AwV6ap7L/Sx9XC5mXaUf8hvMmDbLBqDZ1Y7xKJM2h zQ8Xm1rK9q0wzQC6qiL6xHmTpKWTzNVzGsQdM3/qNPLNA7W8PIYCzjkSe5X1YktY vxldBxYxPRJxXk5S9P8dFYVmFFKF2bvJ5pSMLq9w3z3nTm3TQtRPqWx2Vk3DqV2D QKmNtqJnhVqYvVKa3QpLLwz8xKqB1sPXLr4XqQ3bz3fLjLxPmYV5WxLhgdKLYZTv YxQPLPTJkX3Pw4XD4Qs4CrKLW5bYsqYKQ7kKDXgJmTxYzZLjZKf4vSqLxqV5bDPY rR2YxQ9TKLkYKVMpNtY5J9X2fWzyPSvXqXZfVx7D8xJzDY8YKPLXmvxKQZxLJxSx zxHQzYKJpX3YmVfqYYmfYxXYzLmYxDzSxXqLvKxVqXxQDsPxQVKfKqQx5KvxsVqD -----END RSA PRIVATE KEY-----`, Certificate: `-----BEGIN CERTIFICATE----- MIIGGTCCBQGgAwIBAgISBXx9YmN2KQm9g3Y5XmKbvx9YMA0GCSqGSIb3DQEBCwUA MDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQwwCgYDVQQD EwNSMTEwHhcNMjUwODA4MDY0NzI3WhcNMjUxMTA2MDY0NzI2WjAcMRowGAYDVQQD ExFjbG91ZC5wcml0dW5sLnJlZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC ggIBAMfWNy5NgMFemqey/0sfVwuZl2lH/IbzJg2ywag2dWO8SiTNoc0PF5tayvat MM0AuKoi+sR5k6Slk8zVcxrEHTN/6jTyzQO1vDyGAs45EnuV9WJLWL8ZXQcWMT0S cV5OUvT/HRWFZhRShdn5iQ2Sry6vcN8950Dt00LUT6lsdlZNw6ldg0CpjbaiZ4Va mL1Smt0KSy8M/MSqgdbD1y6+F6kN2893y4y8T5mFeVsS4YHSi2GU72MUDyz0yZF9 z8OFw+ELOAqyi1uW2LKmCkO5Cg14CZk8WM2S42Sn+L0qi8aleWwz2K0dmMUPUyi5 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- MIIFBjCCAu6gAwIBAgIRAIp9PhPWLzDvI4a9KQdrNPgwDQYJKoZIhvcNAQELBQAw TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw WhcNMjcwMzEyMjM1OTU5WjAzMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg RW5jcnlwdDEMMAoGA1UEAxMDUjExMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAuoe8XBsAOcvKCs3UZxD5ATylTqVhyybKUvsVAbe5KPUoHu0nsyQYOWcJ DAjs4DqwO3cOvfPlOVRBDE6uQdaZdN5R2+97/1i9qLcT9t4x1fJyyXJqC4N0lZxG AGQUmfOx2SLZzaiSqhwmej/+71gFewiVgdtxD4774zEJuwm+UE1fj5F2PVqdnoPy -----END CERTIFICATE-----`, Info: &certificate.Info{ Hash: "bba8a3941280c8466a6a2a723cc06f26", SignatureAlg: "SHA256-RSA", PublicKeyAlg: "RSA", Issuer: "R11", IssuedOn: time.Now(), ExpiresOn: time.Now().Add(2160 * time.Hour), DnsNames: []string{ "cloud.pritunl.com", "user.cloud.pritunl.com", }, }, AcmeDomains: []string{ "cloud.pritunl.com", "user.cloud.pritunl.com", }, AcmeType: "acme_dns", AcmeAuth: "acme_cloudflare", AcmeSecret: utils.ObjectIdHex("67b89e8d4866ba90e6c459ba"), }, } ================================================ FILE: demo/datacenter.go ================================================ package demo import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/utils" ) var Datacenters = []*datacenter.Datacenter{ { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Name: "us-west-1", Comment: "", MatchOrganizations: false, Organizations: []bson.ObjectID{}, NetworkMode: "vxlan_vlan", WgMode: "", PublicStorages: []bson.ObjectID{ utils.ObjectIdHex("689733b7a7a35eae0dbaea15"), }, PrivateStorage: bson.ObjectID{}, PrivateStorageClass: "", BackupStorage: bson.ObjectID{}, BackupStorageClass: "", }, } ================================================ FILE: demo/demo.go ================================================ package demo import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" ) func IsDemo() bool { return settings.System.Demo } func Blocked(c *gin.Context) bool { if !IsDemo() { return false } errData := &errortypes.ErrorData{ Error: "demo_unavailable", Message: "Not available in demo mode", } c.JSON(400, errData) return true } func BlockedSilent(c *gin.Context) bool { if !IsDemo() { return false } c.JSON(200, nil) return true } ================================================ FILE: demo/disk.go ================================================ package demo import ( "time" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/utils" ) var Disks = []*aggregate.DiskAggregate{ { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f00"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a00"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f01"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a01"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f02"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a02"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f03"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a03"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f04"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a04"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f05"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a05"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f06"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a06"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f07"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a07"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f08"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a08"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f09"), Name: "web-app", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a09"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f0a"), Name: "database", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0a"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 100, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f0b"), Name: "database", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0b"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 100, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f0c"), Name: "database", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0c"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 100, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f0d"), Name: "database", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0d"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 100, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f0e"), Name: "database", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0e"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 100, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f0f"), Name: "database", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0f"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 100, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f10"), Name: "search", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a10"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 200, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f11"), Name: "search", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a11"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 200, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f12"), Name: "vpn", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a12"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), Index: "0", Size: 20, Created: time.Now(), }, }, { Disk: disk.Disk{ Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d34f13"), Name: "vpn", Comment: "", State: "attached", Type: "qcow2", Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a13"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), Index: "0", Size: 20, Created: time.Now(), }, }, } ================================================ FILE: demo/domain.go ================================================ package demo import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/utils" ) var Domains = []*domain.Domain{ &domain.Domain{ Id: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Name: "pritunl-com", Comment: "", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Type: "cloudflare", Secret: utils.ObjectIdHex("67b89e8d4866ba90e6c459ba"), RootDomain: "pritunl.com", LockId: bson.ObjectID{}, LockTimestamp: time.Time{}, LastUpdate: time.Now(), Records: []*domain.Record{ { Id: utils.ObjectIdHex("68076c9f06fd0087c078dfdc"), Domain: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Resource: bson.ObjectID{}, Deployment: utils.ObjectIdHex("68076bb954e947708aa6d651"), Timestamp: time.Now(), DeleteTimestamp: time.Time{}, SubDomain: "demo", Type: "A", Value: "10.196.8.2", Operation: "", }, { Id: utils.ObjectIdHex("68076ca306fd0087c078dfdd"), Domain: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Resource: bson.ObjectID{}, Deployment: utils.ObjectIdHex("68076bb954e947708aa6d651"), Timestamp: time.Now(), DeleteTimestamp: time.Time{}, SubDomain: "cloud", Type: "A", Value: "10.196.8.12", Operation: "", }, { Id: utils.ObjectIdHex("68076ca406fd0087c078dfde"), Domain: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Resource: bson.ObjectID{}, Deployment: utils.ObjectIdHex("68076bb954e947708aa6d651"), Timestamp: time.Now(), DeleteTimestamp: time.Time{}, SubDomain: "user.cloud", Type: "A", Value: "10.196.8.12", Operation: "", }, { Id: utils.ObjectIdHex("6813705806fd0087c078dfe1"), Domain: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Resource: bson.ObjectID{}, Deployment: utils.ObjectIdHex("68136f7d43b4ac1351f54f0a"), Timestamp: time.Now(), DeleteTimestamp: time.Time{}, SubDomain: "demo.cloud", Type: "A", Value: "10.196.8.46", Operation: "", }, { Id: utils.ObjectIdHex("681e01394230fad44c6a5140"), Domain: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Resource: bson.ObjectID{}, Deployment: utils.ObjectIdHex("681e01308d67187e275a847a"), Timestamp: time.Now(), DeleteTimestamp: time.Time{}, SubDomain: "forum", Type: "AAAA", Value: "2001:db8:85a3:42:d5c:82ca:9ed4:854b", Operation: "", }, { Id: utils.ObjectIdHex("683e86d74230fad44c6a514d"), Domain: utils.ObjectIdHex("67b8a1d24866ba90e6c45b5b"), Resource: bson.ObjectID{}, Deployment: utils.ObjectIdHex("683dcdf13249b43a9cc5ec70"), Timestamp: time.Now(), DeleteTimestamp: time.Time{}, SubDomain: "docs", Type: "CNAME", Value: "docs.pritunl.dev", Operation: "", }, }, }, } ================================================ FILE: demo/firewall.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/utils" ) var Firewalls = []*firewall.Firewall{ { Id: utils.ObjectIdHex("688ab80d1793930f821f4f39"), Name: "instance", Comment: "", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Roles: []string{"instance"}, Ingress: []*firewall.Rule{ { SourceIps: []string{"0.0.0.0/0", "::/0"}, Protocol: "icmp", Port: "", }, { SourceIps: []string{"0.0.0.0/0", "::/0"}, Protocol: "tcp", Port: "22", }, { SourceIps: []string{"0.0.0.0/0", "::/0"}, Protocol: "tcp", Port: "80", }, { SourceIps: []string{"0.0.0.0/0", "::/0"}, Protocol: "tcp", Port: "443", }, }, }, } ================================================ FILE: demo/instance.go ================================================ package demo import ( "time" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/telemetry" "github.com/pritunl/pritunl-cloud/utils" ) var Info = &instance.Info{ Node: "node-name", Timestamp: time.Now(), Disks: []string{ "instance-name", }, FirewallRules: map[string]string{ "icmp": "0.0.0.0/0, ::/0", "tcp:22": "0.0.0.0/0, ::/0", "tcp:80": "0.0.0.0/0, ::/0", "tcp:443": "0.0.0.0/0, ::/0", }, } var Instances = []*instance.Instance{ { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a00"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.103"}, PublicIps6: []string{"2001:db8:85a3:4d2f:ac50:8355:bb57:e0f5"}, PrivateIps: []string{"10.196.1.163"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:58d5:f529:66f2:36ef"}, HostIps: []string{"198.18.84.140"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "ar16uommmvnkcx", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 43.9, Load1: 33.84, Load5: 39.55, Load15: 41.69, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a01"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.195"}, PublicIps6: []string{"2001:db8:85a3:4d2f:5e1b:773a:2463:da58"}, PrivateIps: []string{"10.196.8.168"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:bf1a:d5e4:56a2:4b27"}, HostIps: []string{"198.18.84.29"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "3z3zjl0ftwopu9", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 41.16, Load1: 31.86, Load5: 38.61, Load15: 43.48, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a02"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.201"}, PublicIps6: []string{"2001:db8:85a3:4d2f:27fe:0397:17fa:5d2e"}, PrivateIps: []string{"10.196.5.204"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:3d61:c9f7:d2d7:8b9b"}, HostIps: []string{"198.18.84.12"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "nyak3v7m6rxnqs", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 74.46, Load1: 40.97, Load5: 46.98, Load15: 56.17, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a03"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.2"}, PublicIps6: []string{"2001:db8:85a3:4d2f:41b2:61e2:ad56:6cdc"}, PrivateIps: []string{"10.196.5.162"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:c166:fabc:4223:a974"}, HostIps: []string{"198.18.84.30"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "uln933asem6gxj", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 54.41, Load1: 47.29, Load5: 56.99, Load15: 66.75, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a04"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.53"}, PublicIps6: []string{"2001:db8:85a3:4d2f:cb29:095b:a7f8:9a7e"}, PrivateIps: []string{"10.196.7.205"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:b2f4:9b35:700e:0b9a"}, HostIps: []string{"198.18.84.51"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "rgp5qyyx33b66o", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 78.56, Load1: 45.93, Load5: 50.45, Load15: 58.1, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a05"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.36"}, PublicIps6: []string{"2001:db8:85a3:4d2f:126f:552b:77d0:010e"}, PrivateIps: []string{"10.196.2.94"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:c057:f8fa:ff43:a21a"}, HostIps: []string{"198.18.84.118"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "t0pt85ptxxat2p", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 78.37, Load1: 56.02, Load5: 65.95, Load15: 70.14, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a06"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.90"}, PublicIps6: []string{"2001:db8:85a3:4d2f:aa28:1b64:c808:caeb"}, PrivateIps: []string{"10.196.7.203"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:9797:d44a:0c7e:cb9e"}, HostIps: []string{"198.18.84.216"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "5qbiaxk4w886zb", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 33.68, Load1: 42.06, Load5: 44.97, Load15: 47.24, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a07"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.70"}, PublicIps6: []string{"2001:db8:85a3:4d2f:bf64:91d6:4050:eac0"}, PrivateIps: []string{"10.196.5.238"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:0dd4:8931:8c28:5465"}, HostIps: []string{"198.18.84.13"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "f218l27uoumg35", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 48.81, Load1: 52.83, Load5: 58.26, Load15: 60.54, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a08"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.197"}, PublicIps6: []string{"2001:db8:85a3:4d2f:f5f3:98f7:82b5:ee87"}, PrivateIps: []string{"10.196.4.2"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:8d49:241d:4dd1:4663"}, HostIps: []string{"198.18.84.224"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "frb9erjro4vsnu", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 79.86, Load1: 22.14, Load5: 24.45, Load15: 28.28, Updates: []*telemetry.Update{ { Advisory: "ALSA-2025:10371", Severity: "important", Packages: []string{"kernel-6.12.0-55.20.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:10854", Severity: "important", Packages: []string{"kernel-6.12.0-55.21.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:11428", Severity: "important", Packages: []string{"kernel-6.12.0-55.22.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:12662", Severity: "important", Packages: []string{"kernel-6.12.0-55.25.1.el10_0.x86_64"}, }, }, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a09"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93465"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.63"}, PublicIps6: []string{"2001:db8:85a3:4d2f:6e3a:29b0:639f:49d4"}, PrivateIps: []string{"10.196.6.18"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:ddb4:e207:cc09:d1e6"}, HostIps: []string{"198.18.84.22"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "web-app", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "e8iaefw40jrfmr", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 33.58, Load1: 54.78, Load5: 63.5, Load15: 69.05, Updates: []*telemetry.Update{ { Advisory: "ALSA-2025:10371", Severity: "important", Packages: []string{"kernel-6.12.0-55.20.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:10854", Severity: "important", Packages: []string{"kernel-6.12.0-55.21.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:11428", Severity: "important", Packages: []string{"kernel-6.12.0-55.22.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:12662", Severity: "important", Packages: []string{"kernel-6.12.0-55.25.1.el10_0.x86_64"}, }, }, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0a"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93464"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.8"}, PublicIps6: []string{"2001:db8:85a3:4d2f:d943:7ff9:dfdc:4d68"}, PrivateIps: []string{"10.196.7.91"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:7ba9:bca3:5217:b534"}, HostIps: []string{"198.18.84.108"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "database", Comment: "", InitDiskSize: 100, Memory: 8192, Processors: 4, NetworkNamespace: "oe2641r0vib1bq", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 38.77, Load1: 58.36, Load5: 66.33, Load15: 72.82, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0b"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93464"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.247"}, PublicIps6: []string{"2001:db8:85a3:4d2f:3e3e:0d9d:8669:2c89"}, PrivateIps: []string{"10.196.8.39"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:d0ff:8b42:1d9b:92fd"}, HostIps: []string{"198.18.84.45"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "database", Comment: "", InitDiskSize: 100, Memory: 8192, Processors: 4, NetworkNamespace: "zrz0kp1nydxe5t", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 34.16, Load1: 22.62, Load5: 30.79, Load15: 34.31, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0c"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93464"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.148"}, PublicIps6: []string{"2001:db8:85a3:4d2f:74b0:8661:53c1:1d5b"}, PrivateIps: []string{"10.196.2.178"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:e7b4:670b:acf5:dfb4"}, HostIps: []string{"198.18.84.53"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "database", Comment: "", InitDiskSize: 100, Memory: 8192, Processors: 4, NetworkNamespace: "tsusrdt4e4diyb", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 77.41, Load1: 57.8, Load5: 59.57, Load15: 63.3, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0d"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93464"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.67"}, PublicIps6: []string{"2001:db8:85a3:4d2f:4b40:60d1:ed30:0b06"}, PrivateIps: []string{"10.196.5.113"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:5220:ac62:3c7c:7291"}, HostIps: []string{"198.18.84.60"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "database", Comment: "", InitDiskSize: 100, Memory: 8192, Processors: 4, NetworkNamespace: "bvmmviq5cvx1zy", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 52.76, Load1: 42.11, Load5: 44.24, Load15: 52.69, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0e"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93464"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.189"}, PublicIps6: []string{"2001:db8:85a3:4d2f:be8e:3013:d6f6:5396"}, PrivateIps: []string{"10.196.2.232"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:c924:41b8:22f3:5b3f"}, HostIps: []string{"198.18.84.33"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "database", Comment: "", InitDiskSize: 100, Memory: 8192, Processors: 4, NetworkNamespace: "qbaoak9az3asxp", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 42.56, Load1: 31.16, Load5: 39.77, Load15: 41.33, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0f"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93464"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.38"}, PublicIps6: []string{"2001:db8:85a3:4d2f:fff2:877d:227c:1c4a"}, PrivateIps: []string{"10.196.5.122"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:ae17:b804:32c5:956c"}, HostIps: []string{"198.18.84.141"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "database", Comment: "", InitDiskSize: 100, Memory: 8192, Processors: 4, NetworkNamespace: "m4l3egi9yhfq9t", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 36.46, Load1: 26.05, Load5: 29.7, Load15: 34.46, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a10"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93466"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.118"}, PublicIps6: []string{"2001:db8:85a3:4d2f:b5e8:fca8:79fa:a3b4"}, PrivateIps: []string{"10.196.4.224"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:83e0:cd9a:85c4:38da"}, HostIps: []string{"198.18.84.35"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "search", Comment: "", InitDiskSize: 200, Memory: 8192, Processors: 4, NetworkNamespace: "l6kiozfukehu7v", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 36.93, Load1: 54.95, Load5: 61.88, Load15: 66.2, Updates: []*telemetry.Update{ { Advisory: "ALSA-2025:10371", Severity: "important", Packages: []string{"kernel-6.12.0-55.20.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:10854", Severity: "important", Packages: []string{"kernel-6.12.0-55.21.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:11428", Severity: "important", Packages: []string{"kernel-6.12.0-55.22.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:12662", Severity: "important", Packages: []string{"kernel-6.12.0-55.25.1.el10_0.x86_64"}, }, }, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a11"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93466"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.230"}, PublicIps6: []string{"2001:db8:85a3:4d2f:508e:01c4:330a:418c"}, PrivateIps: []string{"10.196.4.33"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:d6a1:6424:48d3:bdfc"}, HostIps: []string{"198.18.84.16"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Shape: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "search", Comment: "", InitDiskSize: 200, Memory: 8192, Processors: 4, NetworkNamespace: "i0bt9vae2o4g8q", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 60.57, Load1: 32.94, Load5: 38.11, Load15: 40.68, Updates: []*telemetry.Update{ { Advisory: "ALSA-2025:10371", Severity: "important", Packages: []string{"kernel-6.12.0-55.20.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:10854", Severity: "important", Packages: []string{"kernel-6.12.0-55.21.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:11428", Severity: "important", Packages: []string{"kernel-6.12.0-55.22.1.el10_0.x86_64"}, }, { Advisory: "ALSA-2025:12662", Severity: "important", Packages: []string{"kernel-6.12.0-55.25.1.el10_0.x86_64"}, }, }, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a12"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93467"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e1"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.74"}, PublicIps6: []string{"2001:db8:85a3:4d2f:7340:5e52:e94c:3d8a"}, PrivateIps: []string{"10.196.7.230"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:70a2:9db6:52a4:2ab8"}, HostIps: []string{"198.18.84.126"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "vpn", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "szy246vtwpjc3g", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 55.49, Load1: 36.61, Load5: 42.46, Load15: 44.35, }, }, { Id: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a13"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Vpc: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Subnet: utils.ObjectIdHex("66a076d5fafc270786e93467"), Image: utils.ObjectIdHex("650a2c36aed15f1f1f5e96e2"), ImageBacking: false, Status: "Running", State: "running", Action: "start", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{"1.253.67.97"}, PublicIps6: []string{"2001:db8:85a3:4d2f:e4b2:fb8f:ca12:2da3"}, PrivateIps: []string{"10.196.8.7"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:8bed:8e24:e745:7657"}, HostIps: []string{"198.18.84.99"}, Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Shape: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "vpn", Comment: "", InitDiskSize: 20, Memory: 2048, Processors: 2, NetworkNamespace: "ub53x6q05snrd8", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: 47.29, Load1: 35.01, Load5: 42.87, Load15: 48.87, }, }, } ================================================ FILE: demo/log.go ================================================ package demo import ( "time" "github.com/pritunl/pritunl-cloud/log" "github.com/pritunl/pritunl-cloud/utils" ) var Logs = []*log.Entry{ &log.Entry{ Id: utils.ObjectIdHex("5a18e6ae051a45ffac0e5b67"), Level: log.Info, Timestamp: time.Unix(1498018860, 0), Message: "router: Starting redirect server", Stack: "", Fields: map[string]interface{}{ "port": 80, "production": true, "protocol": "http", }, }, &log.Entry{ Id: utils.ObjectIdHex("5a190b42051a45ffac129bbc"), Level: log.Info, Timestamp: time.Unix(1498018860, 0), Message: "router: Starting web server", Stack: "", Fields: map[string]interface{}{ "port": 443, "production": true, "protocol": "https", }, }, } ================================================ FILE: demo/node.go ================================================ package demo import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/cloud" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/ip" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/utils" ) var Nodes = []*node.Node{ { Id: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Name: "pritunl-east0", Comment: "", Types: []string{"admin", "hypervisor"}, Timestamp: time.Now(), Port: 443, NoRedirectServer: false, Protocol: "https", Hypervisor: "kvm", Vga: "virtio", VgaRender: "", AvailableRenders: []string{}, Gui: false, GuiUser: "", GuiMode: "", Certificates: []bson.ObjectID{}, AdminDomain: "", UserDomain: "", WebauthnDomain: "", RequestsMin: 23, ForwardedForHeader: "", ForwardedProtoHeader: "", ExternalInterfaces: []string{}, ExternalInterfaces6: []string{}, InternalInterfaces: []string{"bond0.2"}, AvailableInterfaces: []ip.Interface{ {Name: "bond0", Address: ""}, {Name: "bond0.2", Address: "10.8.0.10"}, {Name: "bond0.4", Address: "10.253.67.90"}, {Name: "bonding_masters", Address: ""}, {Name: "enp1s0f0", Address: ""}, {Name: "enp1s0f1", Address: ""}, {Name: "veth0", Address: ""}, }, AvailableBridges: []ip.Interface{ {Name: "pritunlhost0", Address: "198.18.84.1"}, {Name: "pritunlport0", Address: "198.19.96.1"}, }, AvailableVpcs: []*cloud.Vpc{}, CloudSubnets: []string{}, DefaultInterface: "bond0.4", NetworkMode: "static", NetworkMode6: "static", Blocks: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), }, }, Blocks6: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), }, }, Pools: []bson.ObjectID{}, Shares: []*node.Share{}, AvailableDrives: []*drive.Device{ {Id: "nvme-INTEL_27Z1P0FGN"}, {Id: "nvme-INTEL_27Z1P0FGN-part1"}, {Id: "nvme-INTEL_27Z1P0FGN-part2"}, {Id: "nvme-INTEL_27Z1P0FGN-part3"}, {Id: "nvme-INTEL_42K1P0FGN"}, {Id: "nvme-INTEL_42K1P0FGN-part1"}, {Id: "nvme-INTEL_42K1P0FGN-part2"}, {Id: "nvme-INTEL_42K1P0FGN-part3"}, }, InstanceDrives: []*drive.Device{}, NoHostNetwork: false, NoNodePortNetwork: false, HostNat: true, DefaultNoPublicAddress: true, DefaultNoPublicAddress6: false, JumboFrames: true, JumboFramesInternal: true, Iscsi: false, LocalIsos: nil, UsbPassthrough: false, UsbDevices: []*usb.Device{}, PciPassthrough: false, PciDevices: []*pci.Device{}, Hugepages: false, HugepagesSize: 0, Firewall: false, Roles: []string{"shape-m2"}, Memory: 41.02, HugePagesUsed: 0, Load1: 43.17, Load5: 43.33, Load15: 43.83, CpuUnits: 128, MemoryUnits: 512, CpuUnitsRes: 68, MemoryUnitsRes: 198, PublicIps: []string{"10.253.67.90"}, PublicIps6: []string{ "2001:db8:85a3:4d2f:1319:8a2e:370:7348", }, PrivateIps: map[string]string{ "bond0.2": "10.8.0.10", }, SoftwareVersion: constants.Version, Hostname: "pritunl-east0", VirtPath: "/var/lib/pritunl-cloud", CachePath: "/var/cache/pritunl-cloud", TempPath: "", OracleUser: "", OracleTenancy: "", OraclePublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxWtYOIzsHsLlBI1jeepJ q8dyR1JH3QLdAJ2IFGZDtHCCi46Lvmx7hC8bAutj5s37qOfBrom6UOJf0f9zEP8K y8qTb2S4XOAWBHuGpaBqFEhtpW+vIxiy26vdZN85P3xzYle0uodr86+y2bVHMHKB 0oEHnqu+CmH/r4GedBVFVBASo9C5iILsyISf4oep390V/u23RAXXNfcKvUYR4c2u fZBwlSVEDrK+X21ocJc+8VGbbLhXBvMEdqXzs1bbFzFHow8TjduxDNTbntIRpo6W 0O7xMahUHxDWDro5fAkzvpk6wUBM6yWXXgwkDLLHW50dUnqgFJgOTIHXEtPSt4eU 2wIDAQAB -----END PUBLIC KEY-----`, Operation: "", }, { Id: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Name: "pritunl-east1", Comment: "", Types: []string{"admin", "hypervisor"}, Timestamp: time.Now(), Port: 443, NoRedirectServer: false, Protocol: "https", Hypervisor: "kvm", Vga: "virtio", VgaRender: "", AvailableRenders: []string{}, Gui: false, GuiUser: "", GuiMode: "", Certificates: []bson.ObjectID{}, AdminDomain: "", UserDomain: "", WebauthnDomain: "", RequestsMin: 44, ForwardedForHeader: "", ForwardedProtoHeader: "", ExternalInterfaces: []string{}, ExternalInterfaces6: []string{}, InternalInterfaces: []string{"bond0.2"}, AvailableInterfaces: []ip.Interface{ {Name: "bond0", Address: ""}, {Name: "bond0.2", Address: "10.8.0.11"}, {Name: "bond0.4", Address: "10.253.67.91"}, {Name: "bonding_masters", Address: ""}, {Name: "enp1s0f0", Address: ""}, {Name: "enp1s0f1", Address: ""}, {Name: "veth0", Address: ""}, }, AvailableBridges: []ip.Interface{ {Name: "pritunlhost0", Address: "198.18.84.1"}, {Name: "pritunlport0", Address: "198.19.96.1"}, }, AvailableVpcs: []*cloud.Vpc{}, CloudSubnets: []string{}, DefaultInterface: "bond0.4", NetworkMode: "static", NetworkMode6: "static", Blocks: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), }, }, Blocks6: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), }, }, Pools: []bson.ObjectID{}, Shares: []*node.Share{}, AvailableDrives: []*drive.Device{ {Id: "nvme-INTEL_27Z1P0FGN"}, {Id: "nvme-INTEL_27Z1P0FGN-part1"}, {Id: "nvme-INTEL_27Z1P0FGN-part2"}, {Id: "nvme-INTEL_27Z1P0FGN-part3"}, {Id: "nvme-INTEL_42K1P0FGN"}, {Id: "nvme-INTEL_42K1P0FGN-part1"}, {Id: "nvme-INTEL_42K1P0FGN-part2"}, {Id: "nvme-INTEL_42K1P0FGN-part3"}, }, InstanceDrives: []*drive.Device{}, NoHostNetwork: false, NoNodePortNetwork: false, HostNat: true, DefaultNoPublicAddress: true, DefaultNoPublicAddress6: false, JumboFrames: true, JumboFramesInternal: true, Iscsi: false, LocalIsos: nil, UsbPassthrough: false, UsbDevices: []*usb.Device{}, PciPassthrough: false, PciDevices: []*pci.Device{}, Hugepages: false, HugepagesSize: 0, Firewall: false, Roles: []string{"shape-m2"}, Memory: 62.02, HugePagesUsed: 0, Load1: 67.12, Load5: 57.43, Load15: 55.23, CpuUnits: 128, MemoryUnits: 512, CpuUnitsRes: 78, MemoryUnitsRes: 324, PublicIps: []string{"10.253.67.91"}, PublicIps6: []string{ "2001:db8:85a3:4d2f:7c3a:9f42:8e5b:a234", }, PrivateIps: map[string]string{ "bond0.2": "10.8.0.11", }, SoftwareVersion: constants.Version, Hostname: "pritunl-east0", VirtPath: "/var/lib/pritunl-cloud", CachePath: "/var/cache/pritunl-cloud", TempPath: "", OracleUser: "", OracleTenancy: "", OraclePublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz4K8Lm3QvR7WxN5YdE2P jX9TpQ6HgM1wV0nS4KaF3ZcB8LrY5UvO2JmN7XsPqI1AgK8EoH3RdWzM9LfY2VtN kP4QxGsJ7YnR8LwVmT3AqZ5HvK2NdP1XoS8JgR4LmW7YxQ3VnH5TsK9PpL2MdX8Rg vJ3KqN5WxT1LsM4HgY7RdP8NqV2JmK5XwL3TsR8YgN4HxP1LdK9VwQ2MsT3XpR7Y nL8KgJ5WdH3TmR9XsL2PqN7VxK4MgT3HdJ8YwP2LsK5RxT1NqM4JgY7PxR8WsL3T mK9XwN2HgJ5YdL3RsP8VqT2MxK4NhR3JdY8WwL2TsM5QxN1PqK4YgJ7RxP8VsT3M PwIDAQAB -----END PUBLIC KEY-----`, Operation: "", }, { Id: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Name: "pritunl-east2", Comment: "", Types: []string{"admin", "hypervisor"}, Timestamp: time.Now(), Port: 443, NoRedirectServer: false, Protocol: "https", Hypervisor: "kvm", Vga: "virtio", VgaRender: "", AvailableRenders: []string{}, Gui: false, GuiUser: "", GuiMode: "", Certificates: []bson.ObjectID{}, AdminDomain: "", UserDomain: "", WebauthnDomain: "", RequestsMin: 26, ForwardedForHeader: "", ForwardedProtoHeader: "", ExternalInterfaces: []string{}, ExternalInterfaces6: []string{}, InternalInterfaces: []string{"bond0.2"}, AvailableInterfaces: []ip.Interface{ {Name: "bond0", Address: ""}, {Name: "bond0.2", Address: "10.8.0.12"}, {Name: "bond0.4", Address: "10.253.67.92"}, {Name: "bonding_masters", Address: ""}, {Name: "enp1s0f0", Address: ""}, {Name: "enp1s0f1", Address: ""}, {Name: "veth0", Address: ""}, }, AvailableBridges: []ip.Interface{ {Name: "pritunlhost0", Address: "198.18.84.1"}, {Name: "pritunlport0", Address: "198.19.96.1"}, }, AvailableVpcs: []*cloud.Vpc{}, CloudSubnets: []string{}, DefaultInterface: "bond0.4", NetworkMode: "static", NetworkMode6: "static", Blocks: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), }, }, Blocks6: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), }, }, Pools: []bson.ObjectID{}, Shares: []*node.Share{}, AvailableDrives: []*drive.Device{ {Id: "nvme-INTEL_27Z1P0FGN"}, {Id: "nvme-INTEL_27Z1P0FGN-part1"}, {Id: "nvme-INTEL_27Z1P0FGN-part2"}, {Id: "nvme-INTEL_27Z1P0FGN-part3"}, {Id: "nvme-INTEL_42K1P0FGN"}, {Id: "nvme-INTEL_42K1P0FGN-part1"}, {Id: "nvme-INTEL_42K1P0FGN-part2"}, {Id: "nvme-INTEL_42K1P0FGN-part3"}, }, InstanceDrives: []*drive.Device{}, NoHostNetwork: false, NoNodePortNetwork: false, HostNat: true, DefaultNoPublicAddress: true, DefaultNoPublicAddress6: false, JumboFrames: true, JumboFramesInternal: true, Iscsi: false, LocalIsos: nil, UsbPassthrough: false, UsbDevices: []*usb.Device{}, PciPassthrough: false, PciDevices: []*pci.Device{}, Hugepages: false, HugepagesSize: 0, Firewall: false, Roles: []string{"shape-m2"}, Memory: 41.02, HugePagesUsed: 0, Load1: 76.53, Load5: 67.34, Load15: 67.87, CpuUnits: 128, MemoryUnits: 512, CpuUnitsRes: 56, MemoryUnitsRes: 286, PublicIps: []string{"10.253.67.92"}, PublicIps6: []string{ "2001:db8:85a3:4d2f:2e91:5cd8:f1a6:7b89", }, PrivateIps: map[string]string{ "bond0.2": "10.8.0.12", }, SoftwareVersion: constants.Version, Hostname: "pritunl-east0", VirtPath: "/var/lib/pritunl-cloud", CachePath: "/var/cache/pritunl-cloud", TempPath: "", OracleUser: "", OracleTenancy: "", OraclePublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3Fk7Tp4NwV8XmQ6LdJ9R hY2BqG8Vz0kL5HgM7Xa4FrC9NsU1YvQ3KnR8WxL6ZqE2DfH5JtX3YmVgB8KsNp4Q rL9XvT2HdQ8YwK5JmF3NxR7Pq4VsL8TgW6ZnM1KfY9DpX2RvQ8JwB3GtL5YxH4Nm sK7VpX3LdR9FwT8MqN2HxY5BgJ6TsL4RpW8NvK3YxH5QmD9LwT2JsN8KgR4XpM7V fY3BwL8TmN5KxQ2JgH7RsP9VwM4LnT8KpY3XgN5BwJ2HsR8TmL4VxQ9JpN3KgY7B wR5TnM8LpJ2XgH4VsN9BwK3TmQ5RxL8JpY7NwH2VsR4KgM9TnL5BxJ3QwN8VpH7Y KwIDAQAB -----END PUBLIC KEY-----`, Operation: "", }, { Id: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Name: "pritunl-east3", Comment: "", Types: []string{"admin", "hypervisor"}, Timestamp: time.Now(), Port: 443, NoRedirectServer: false, Protocol: "https", Hypervisor: "kvm", Vga: "virtio", VgaRender: "", AvailableRenders: []string{}, Gui: false, GuiUser: "", GuiMode: "", Certificates: []bson.ObjectID{}, AdminDomain: "", UserDomain: "", WebauthnDomain: "", RequestsMin: 56, ForwardedForHeader: "", ForwardedProtoHeader: "", ExternalInterfaces: []string{}, ExternalInterfaces6: []string{}, InternalInterfaces: []string{"bond0.2"}, AvailableInterfaces: []ip.Interface{ {Name: "bond0", Address: ""}, {Name: "bond0.2", Address: "10.8.0.13"}, {Name: "bond0.4", Address: "10.253.67.93"}, {Name: "bonding_masters", Address: ""}, {Name: "enp1s0f0", Address: ""}, {Name: "enp1s0f1", Address: ""}, {Name: "veth0", Address: ""}, }, AvailableBridges: []ip.Interface{ {Name: "pritunlhost0", Address: "198.18.84.1"}, {Name: "pritunlport0", Address: "198.19.96.1"}, }, AvailableVpcs: []*cloud.Vpc{}, CloudSubnets: []string{}, DefaultInterface: "bond0.4", NetworkMode: "static", NetworkMode6: "static", Blocks: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), }, }, Blocks6: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), }, }, Pools: []bson.ObjectID{}, Shares: []*node.Share{}, AvailableDrives: []*drive.Device{ {Id: "nvme-INTEL_27Z1P0FGN"}, {Id: "nvme-INTEL_27Z1P0FGN-part1"}, {Id: "nvme-INTEL_27Z1P0FGN-part2"}, {Id: "nvme-INTEL_27Z1P0FGN-part3"}, {Id: "nvme-INTEL_42K1P0FGN"}, {Id: "nvme-INTEL_42K1P0FGN-part1"}, {Id: "nvme-INTEL_42K1P0FGN-part2"}, {Id: "nvme-INTEL_42K1P0FGN-part3"}, }, InstanceDrives: []*drive.Device{}, NoHostNetwork: false, NoNodePortNetwork: false, HostNat: true, DefaultNoPublicAddress: true, DefaultNoPublicAddress6: false, JumboFrames: true, JumboFramesInternal: true, Iscsi: false, LocalIsos: nil, UsbPassthrough: false, UsbDevices: []*usb.Device{}, PciPassthrough: false, PciDevices: []*pci.Device{}, Hugepages: false, HugepagesSize: 0, Firewall: false, Roles: []string{"shape-m2"}, Memory: 41.02, HugePagesUsed: 0, Load1: 24.63, Load5: 23.32, Load15: 21.43, CpuUnits: 128, MemoryUnits: 512, CpuUnitsRes: 64, MemoryUnitsRes: 298, PublicIps: []string{"10.253.67.93"}, PublicIps6: []string{ "2001:db8:85a3:4d2f:9a47:3b6e:c829:4f16", }, PrivateIps: map[string]string{ "bond0.2": "10.8.0.13", }, SoftwareVersion: constants.Version, Hostname: "pritunl-east0", VirtPath: "/var/lib/pritunl-cloud", CachePath: "/var/cache/pritunl-cloud", TempPath: "", OracleUser: "", OracleTenancy: "", OraclePublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7nP2Kx4YmRa5TgL9Hs3 dW8JnF6QzV1MpC4BwX7Ka3Gt5RvN8YmL2Hs9Dx4TnQ6JpF8LwK3Vz5YmRs7NdT9H gX2BpL4KwR8TmQ5NvJ3YxF7DsH9LwK2VmT8RgJ4NpX5BwL3HsY7TmQ9KxF2DpN8V wJ3LsT4HgR9YmK5NxB2FwL7TsH8KpQ3XmJ4DgN9BwY2RsL5HxK7VmT3NpF8YwJ4D gR2TsL9KxH5BmN3VwQ8JpY7LsT4HgK2NxR9FmL3BwJ5TsN8YpK4VxH7DgQ2LmR9N wJ3BsT5HxL8KpN2YmF4DgR7VwQ3TsJ9KxH5BmL8NpY2FgT4RwK7VsH9JmQ3LxN8D FwIDAQAB -----END PUBLIC KEY-----`, Operation: "", }, { Id: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Name: "pritunl-east4", Comment: "", Types: []string{"admin", "hypervisor"}, Timestamp: time.Now(), Port: 443, NoRedirectServer: false, Protocol: "https", Hypervisor: "kvm", Vga: "virtio", VgaRender: "", AvailableRenders: []string{}, Gui: false, GuiUser: "", GuiMode: "", Certificates: []bson.ObjectID{}, AdminDomain: "", UserDomain: "", WebauthnDomain: "", RequestsMin: 76, ForwardedForHeader: "", ForwardedProtoHeader: "", ExternalInterfaces: []string{}, ExternalInterfaces6: []string{}, InternalInterfaces: []string{"bond0.2"}, AvailableInterfaces: []ip.Interface{ {Name: "bond0", Address: ""}, {Name: "bond0.2", Address: "10.8.0.14"}, {Name: "bond0.4", Address: "10.253.67.94"}, {Name: "bonding_masters", Address: ""}, {Name: "enp1s0f0", Address: ""}, {Name: "enp1s0f1", Address: ""}, {Name: "veth0", Address: ""}, }, AvailableBridges: []ip.Interface{ {Name: "pritunlhost0", Address: "198.18.84.1"}, {Name: "pritunlport0", Address: "198.19.96.1"}, }, AvailableVpcs: []*cloud.Vpc{}, CloudSubnets: []string{}, DefaultInterface: "bond0.4", NetworkMode: "static", NetworkMode6: "static", Blocks: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), }, }, Blocks6: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), }, }, Pools: []bson.ObjectID{}, Shares: []*node.Share{}, AvailableDrives: []*drive.Device{ {Id: "nvme-INTEL_27Z1P0FGN"}, {Id: "nvme-INTEL_27Z1P0FGN-part1"}, {Id: "nvme-INTEL_27Z1P0FGN-part2"}, {Id: "nvme-INTEL_27Z1P0FGN-part3"}, {Id: "nvme-INTEL_42K1P0FGN"}, {Id: "nvme-INTEL_42K1P0FGN-part1"}, {Id: "nvme-INTEL_42K1P0FGN-part2"}, {Id: "nvme-INTEL_42K1P0FGN-part3"}, }, InstanceDrives: []*drive.Device{}, NoHostNetwork: false, NoNodePortNetwork: false, HostNat: true, DefaultNoPublicAddress: true, DefaultNoPublicAddress6: false, JumboFrames: true, JumboFramesInternal: true, Iscsi: false, LocalIsos: nil, UsbPassthrough: false, UsbDevices: []*usb.Device{}, PciPassthrough: false, PciDevices: []*pci.Device{}, Hugepages: false, HugepagesSize: 0, Firewall: false, Roles: []string{"shape-m2"}, Memory: 41.02, HugePagesUsed: 0, Load1: 88.45, Load5: 83.13, Load15: 72.34, CpuUnits: 128, MemoryUnits: 512, CpuUnitsRes: 45, MemoryUnitsRes: 242, PublicIps: []string{"10.253.67.94"}, PublicIps6: []string{ "2001:db8:85a3:4d2f:6f15:d8c3:2a94:e7b1", }, PrivateIps: map[string]string{ "bond0.2": "10.8.0.14", }, SoftwareVersion: constants.Version, Hostname: "pritunl-east0", VirtPath: "/var/lib/pritunl-cloud", CachePath: "/var/cache/pritunl-cloud", TempPath: "", OracleUser: "", OracleTenancy: "", OraclePublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9SLk4Pm3VaRtN8Yw2Hf jK5TgQ7BnM2XpL4DwV8Ha6FsR9YmJ3KxC5NtQ8LwP7DgH2VmF4TsK9XpN3BwJ8Rg mL5YxH7TsQ2NpK4VwF8DgJ3BmR9LxT5KsN2HwY7FpQ8VmJ4TgL3NxK9BwH5DsR2Y mF7KpT8LwQ3VgN4JxH9BsK2TmL5DwR8YpF3NxQ7KgJ4VsH9BmT2LwK8DxN5YpF3R gJ7TsL4KwH9VmQ2NxB8FpY3DgK5TsR9LwJ2HmN4BxF7VpQ8KgL3TsY9NwH5DmK4J xR2BgF8TpL7VsN3KwQ9YmH4DgJ5BxT2LsK8FpN3VwR7YgH4TmQ9KsL2BxJ5NpF8D RwIDAQAB -----END PUBLIC KEY-----`, Operation: "", }, { Id: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Name: "pritunl-east5", Comment: "", Types: []string{"admin", "hypervisor"}, Timestamp: time.Now(), Port: 443, NoRedirectServer: false, Protocol: "https", Hypervisor: "kvm", Vga: "virtio", VgaRender: "", AvailableRenders: []string{}, Gui: false, GuiUser: "", GuiMode: "", Certificates: []bson.ObjectID{}, AdminDomain: "", UserDomain: "", WebauthnDomain: "", RequestsMin: 12, ForwardedForHeader: "", ForwardedProtoHeader: "", ExternalInterfaces: []string{}, ExternalInterfaces6: []string{}, InternalInterfaces: []string{"bond0.2"}, AvailableInterfaces: []ip.Interface{ {Name: "bond0", Address: ""}, {Name: "bond0.2", Address: "10.8.0.15"}, {Name: "bond0.4", Address: "10.253.67.95"}, {Name: "bonding_masters", Address: ""}, {Name: "enp1s0f0", Address: ""}, {Name: "enp1s0f1", Address: ""}, {Name: "veth0", Address: ""}, }, AvailableBridges: []ip.Interface{ {Name: "pritunlhost0", Address: "198.18.84.1"}, {Name: "pritunlport0", Address: "198.19.96.1"}, }, AvailableVpcs: []*cloud.Vpc{}, CloudSubnets: []string{}, DefaultInterface: "bond0.4", NetworkMode: "static", NetworkMode6: "static", Blocks: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("689733b7a7a35eae0dbaea2f"), }, }, Blocks6: []*node.BlockAttachment{ { Interface: "bond0.4", Block: utils.ObjectIdHex("68973a47b5844593cf99cc7a"), }, }, Pools: []bson.ObjectID{}, Shares: []*node.Share{}, AvailableDrives: []*drive.Device{ {Id: "nvme-INTEL_27Z1P0FGN"}, {Id: "nvme-INTEL_27Z1P0FGN-part1"}, {Id: "nvme-INTEL_27Z1P0FGN-part2"}, {Id: "nvme-INTEL_27Z1P0FGN-part3"}, {Id: "nvme-INTEL_42K1P0FGN"}, {Id: "nvme-INTEL_42K1P0FGN-part1"}, {Id: "nvme-INTEL_42K1P0FGN-part2"}, {Id: "nvme-INTEL_42K1P0FGN-part3"}, }, InstanceDrives: []*drive.Device{}, NoHostNetwork: false, NoNodePortNetwork: false, HostNat: true, DefaultNoPublicAddress: true, DefaultNoPublicAddress6: false, JumboFrames: true, JumboFramesInternal: true, Iscsi: false, LocalIsos: nil, UsbPassthrough: false, UsbDevices: []*usb.Device{}, PciPassthrough: false, PciDevices: []*pci.Device{}, Hugepages: false, HugepagesSize: 0, Firewall: false, Roles: []string{"shape-m2"}, Memory: 41.02, HugePagesUsed: 0, Load1: 53.44, Load5: 43.75, Load15: 43.93, CpuUnits: 128, MemoryUnits: 512, CpuUnitsRes: 76, MemoryUnitsRes: 346, PublicIps: []string{"10.253.67.95"}, PublicIps6: []string{ "2001:db8:85a3:4d2f:8d2c:1fa7:b653:9e48", }, PrivateIps: map[string]string{ "bond0.2": "10.8.0.15", }, SoftwareVersion: constants.Version, Hostname: "pritunl-east0", VirtPath: "/var/lib/pritunl-cloud", CachePath: "/var/cache/pritunl-cloud", TempPath: "", OracleUser: "", OracleTenancy: "", OraclePublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1pK7Gm4XwQ9RsT2NvH8Bf jL5DgY3KpM2TwV8NxH4FsQ9BmL7YgK3VpT8DwR5NxJ2HmF4KgQ7TsL9BwY3VpN8R xK5DmH2FgT9LwJ4NsQ7BpV3KxY8TmL5HgR2DwN9FpK4VsT3BxJ7YmQ8LgH2NwK5D pF9TxR3VsL4KgY7BmN2HwJ8FpQ5TxK9DsL3VgH4NmR7BwY2KpT8FxJ9LsQ3DgN5V mH7BwK4TpL2RxF9NsY3KgQ8DmJ5VwH7BpT2LxK4FgN9RsQ3TmL8YwJ5DpH7KxF2N gV4BsT9LmQ3KwR8YpN5DxH7FgJ2TsL4BwK9VmQ3NpY8RxT5LgH2DsK7BmF4JwN9Q TwIDAQAB -----END PUBLIC KEY-----`, Operation: "", }, } ================================================ FILE: demo/organization.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/utils" ) var Organizations = []*organization.Organization{ { Id: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Roles: []string{ "pritunl", }, Name: "pritunl", Comment: "", }, } ================================================ FILE: demo/plan.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/utils" ) var Plans = []*plan.Plan{ { Id: utils.ObjectIdHex("66e8993f1fbc6db8e20819f8"), Name: "primary", Comment: "", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Statements: []*plan.Statement{ { Id: utils.ObjectIdHex("67c9bed42c125c5ddf24d0a1"), Statement: "IF instance.last_timestamp < 60 AND instance.last_heartbeat > 60 FOR 15 THEN 'stop'", }, { Id: utils.ObjectIdHex("683d645e2956cdd93d3e08d2"), Statement: "IF instance.state != 'running' THEN 'start'", }, }, }, } ================================================ FILE: demo/pod.go ================================================ package demo import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" ) var Pods = []*aggregate.PodAggregate{ { Pod: pod.Pod{ Id: utils.ObjectIdHex("688bf358d978631566998ffc"), Name: "web-app", Comment: "", Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), DeleteProtection: false, Drafts: []*pod.UnitDraft{}, }, Units: Units, }, } var Units = []*unit.Unit{ { Id: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Name: "web-app", Kind: "instance", Count: 0, Deployments: []bson.ObjectID{ utils.ObjectIdHex("688db9219da165ffad4e439c"), utils.ObjectIdHex("688dbc759da165ffad4e4ab0"), }, Spec: "```yaml" + ` --- name: web-app kind: instance zone: +/zone/us-west-1a shape: +/shape/m2-small processors: 4 memory: 4096 vpc: +/vpc/vpc subnet: +/subnet/primary image: +/image/almalinux9 roles: - instance nodePorts: - protocol: tcp externalPort: 32120 internalPort: 80 --- name: web-app-firewall kind: firewall ingress: - protocol: tcp port: 22 source: - 10.20.0.0/16 ` + "```" + ` ## Initialization * Update system * Install nginx ` + "```shell" + ` dnf -y update dnf install -y nginx sed -i "s/Test Page/cloud-$(pci get +/instance/self/id)/" /usr/share/nginx/html/index.html ` + "```" + ` ## Configuration ` + "```python {phase=reload}" + ` import string import subprocess def pci_get(query): return subprocess.run( ["pci", "get", query], stdout=subprocess.PIPE, text=True, check=True, ).stdout.strip() with open("/etc/web.conf", "w") as file: file.write(pci_get("+/unit/database/private_ips")) ` + "```" + ` ## Startup ` + "```shell {phase=reboot}" + ` systemctl start nginx ` + "```", SpecIndex: 2, LastSpec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), DeploySpec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), Hash: "80309b44139a78378c9025d40535c73f52f9d71c", }, { Id: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Name: "database", Kind: "instance", Count: 0, Deployments: []bson.ObjectID{}, Spec: "```yaml" + ` --- name: database kind: instance zone: +/zone/us-west-1a shape: +/shape/m2-small processors: 4 memory: 4096 vpc: +/vpc/vpc subnet: +/subnet/primary image: +/image/almalinux9 roles: - instance --- name: web-app-firewall kind: firewall ingress: - protocol: tcp port: 27017 source: - +/unit/web-app ` + "```" + ` ## Initialization * Update system * Install mongodb ` + "```shell" + ` dnf -y update tee /etc/yum.repos.d/mongodb-org.repo << EOF [mongodb-org-8.0] name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/9/mongodb-org/8.0/x86_64/ gpgcheck=1 enabled=1 gpgkey=https://pgp.mongodb.com/server-8.0.asc EOF dnf -y install mongodb-org ` + "```" + ` ## Startup ` + "```shell {phase=reboot}" + ` sudo systemctl start mongod ` + "```", SpecIndex: 2, LastSpec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), DeploySpec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), Hash: "d0c176ab5dafce10956e0d3d5e1320b2e496acff", }, } var Specs = []*spec.Spec{ { Id: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Index: 2, Hash: "80309b44139a78378c9025d40535c73f52f9d71c", Timestamp: time.Now().Add(-5 * time.Minute), Data: "```yaml" + ` --- name: web-app kind: instance zone: +/zone/us-west-1a shape: +/shape/m2-small processors: 4 memory: 4096 vpc: +/vpc/vpc subnet: +/subnet/primary image: +/image/almalinux9 roles: - instance nodePorts: - protocol: tcp externalPort: 32120 internalPort: 80 --- name: web-app-firewall kind: firewall ingress: - protocol: tcp port: 22 source: - 10.20.0.0/16 ` + "```" + ` ## Initialization * Update system * Install nginx ` + "```shell" + ` dnf -y update dnf install -y nginx sed -i "s/Test Page/cloud-$(pci get +/instance/self/id)/" /usr/share/nginx/html/index.html ` + "```" + ` ## Configuration ` + "```python {phase=reload}" + ` import string import subprocess def pci_get(query): return subprocess.run( ["pci", "get", query], stdout=subprocess.PIPE, text=True, check=True, ).stdout.strip() with open("/etc/web.conf", "w") as file: file.write(pci_get("+/unit/database/private_ips")) ` + "```" + ` ## Startup ` + "```shell {phase=reboot}" + ` systemctl start nginx ` + "```", }, { Id: utils.ObjectIdHex("68b67f44ee12c08a1f39fdbe"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Index: 1, Hash: "7e4a9f2c8b3d6h5j1k9m0n2p4q8r3s7t5v8w2x34", Timestamp: time.Now().Add(-5 * time.Hour), Data: "```yaml" + ` --- name: web-app kind: instance zone: +/zone/us-west-1a shape: +/shape/m2-small processors: 4 memory: 4096 vpc: +/vpc/vpc subnet: +/subnet/primary image: +/image/almalinux9 roles: - instance nodePorts: - protocol: tcp externalPort: 32120 internalPort: 80 --- name: web-app-firewall kind: firewall ingress: - protocol: tcp port: 22 source: - 10.20.0.0/16 ` + "```" + ` ## Initialization * Update system * Install nginx ` + "```shell" + ` dnf -y update dnf install -y nginx sed -i "s/Test Page/cloud-$(pci get +/instance/self/id)/" /usr/share/nginx/html/index.html ` + "```" + ` ## Startup ` + "```shell {phase=reboot}" + ` systemctl start nginx ` + "```", }, { Id: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Index: 2, Hash: "d0c176ab5dafce10956e0d3d5e1320b2e496acff", Timestamp: time.Now().Add(-10 * time.Minute), Data: "```yaml" + ` --- name: database kind: instance zone: +/zone/us-west-1a shape: +/shape/m2-small processors: 4 memory: 4096 vpc: +/vpc/vpc subnet: +/subnet/primary image: +/image/almalinux9 roles: - instance --- name: web-app-firewall kind: firewall ingress: - protocol: tcp port: 27017 source: - +/unit/web-app ` + "```" + ` ## Initialization * Update system * Install mongodb ` + "```shell" + ` dnf -y update tee /etc/yum.repos.d/mongodb-org.repo << EOF [mongodb-org-8.0] name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/9/mongodb-org/8.0/x86_64/ gpgcheck=1 enabled=1 gpgkey=https://pgp.mongodb.com/server-8.0.asc EOF dnf -y install mongodb-org ` + "```" + ` ## Startup ` + "```shell {phase=reboot}" + ` sudo systemctl start mongod ` + "```", }, { Id: utils.ObjectIdHex("68b67cb1ee12c08a1f39f78b"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Index: 1, Hash: "3c8f5a1d9e7b2k4m6n0p3q7r9s2t5v8w1x4y6z4d", Timestamp: time.Now().Add(-6 * time.Hour), Data: "```yaml" + ` --- name: database kind: instance zone: +/zone/us-west-1a shape: +/shape/m2-small processors: 4 memory: 4096 vpc: +/vpc/vpc subnet: +/subnet/primary image: +/image/almalinux9 roles: - instance ` + "```" + ` ## Initialization * Update system * Install mongodb ` + "```shell" + ` dnf -y update tee /etc/yum.repos.d/mongodb-org.repo << EOF [mongodb-org-8.0] name=MongoDB Repository baseurl=https://repo.mongodb.org/yum/redhat/9/mongodb-org/8.0/x86_64/ gpgcheck=1 enabled=1 gpgkey=https://pgp.mongodb.com/server-8.0.asc EOF dnf -y install mongodb-org ` + "```" + ` ## Startup ` + "```shell {phase=reboot}" + ` sudo systemctl start mongod ` + "```", }, } var SpecsNamed = []*spec.Named{ { Id: Specs[0].Id, Unit: Specs[0].Unit, Index: Specs[0].Index, Timestamp: Specs[0].Timestamp, }, { Id: Specs[1].Id, Unit: Specs[1].Unit, Index: Specs[1].Index, Timestamp: Specs[1].Timestamp, }, { Id: Specs[2].Id, Unit: Specs[2].Unit, Index: Specs[2].Index, Timestamp: Specs[2].Timestamp, }, { Id: Specs[3].Id, Unit: Specs[3].Unit, Index: Specs[3].Index, Timestamp: Specs[3].Timestamp, }, } var DeploymentLogs = []string{ "[2025-08-02 07:21:42] dnf install -y nginx\n", "[2025-08-02 07:21:43] Waiting for process with pid 1024 to finish.\n", "[2025-08-02 07:22:03] Last metadata expiration check: 0:00:02 ago on Sat 02 Aug 2025 07:22:01 AM UTC.\n", "[2025-08-02 07:22:04] Dependencies resolved.\n", "[2025-08-02 07:22:04] ================================================================================\n", "[2025-08-02 07:22:04] Package Arch Version Repository Size\n", "[2025-08-02 07:22:04] ================================================================================\n", "[2025-08-02 07:22:04] Installing:\n", "[2025-08-02 07:22:04] nginx x86_64 2:1.20.1-22.0.1.el9_6.3 ol9_appstream 49 k\n", "[2025-08-02 07:22:04] Installing dependencies:\n", "[2025-08-02 07:22:04] nginx-core x86_64 2:1.20.1-22.0.1.el9_6.3 ol9_appstream 589 k\n", "[2025-08-02 07:22:04] nginx-filesystem noarch 2:1.20.1-22.0.1.el9_6.3 ol9_appstream 9.6 k\n", "[2025-08-02 07:22:04] oracle-logos-httpd noarch 90.4-1.0.1.el9 ol9_baseos_latest 37 k\n", "[2025-08-02 07:22:04] Transaction Summary\n", "[2025-08-02 07:22:04] ================================================================================\n", "[2025-08-02 07:22:04] Install 4 Packages\n", "[2025-08-02 07:22:04] Total download size: 684 k\n", "[2025-08-02 07:22:04] Installed size: 1.8 M\n", "[2025-08-02 07:22:04] Downloading Packages:\n", "[2025-08-02 07:22:04] (1/4): nginx-1.20.1-22.0.1.el9_6.3.x86_64.rpm 757 kB/s | 49 kB 00:00\n", "[2025-08-02 07:22:04] (2/4): oracle-logos-httpd-90.4-1.0.1.el9.noarch 529 kB/s | 37 kB 00:00\n", "[2025-08-02 07:22:04] (3/4): nginx-filesystem-1.20.1-22.0.1.el9_6.3.n 540 kB/s | 9.6 kB 00:00\n", "[2025-08-02 07:22:04] (4/4): nginx-core-1.20.1-22.0.1.el9_6.3.x86_64. 5.7 MB/s | 589 kB 00:00\n", "[2025-08-02 07:22:04] --------------------------------------------------------------------------------\n", "[2025-08-02 07:22:04] Total 6.3 MB/s | 684 kB 00:00\n", "[2025-08-02 07:22:05] Running transaction check\n", "[2025-08-02 07:22:05] Transaction check succeeded.\n", "[2025-08-02 07:22:05] Running transaction test\n", "[2025-08-02 07:22:05] Transaction test succeeded.\n", "[2025-08-02 07:22:05] Running transaction\n", "[2025-08-02 07:22:05] Preparing : 1/1\n", "[2025-08-02 07:22:05] Running scriptlet: nginx-filesystem-2:1.20.1-22.0.1.el9_6.3.noarch 1/4\n", "[2025-08-02 07:22:05] Installing : nginx-filesystem-2:1.20.1-22.0.1.el9_6.3.noarch 1/4\n", "[2025-08-02 07:22:05] Installing : nginx-core-2:1.20.1-22.0.1.el9_6.3.x86_64 2/4\n", "[2025-08-02 07:22:05] Installing : oracle-logos-httpd-90.4-1.0.1.el9.noarch 3/4\n", "[2025-08-02 07:22:05] Installing : nginx-2:1.20.1-22.0.1.el9_6.3.x86_64 4/4\n", "[2025-08-02 07:22:06] Running scriptlet: nginx-2:1.20.1-22.0.1.el9_6.3.x86_64 4/4\n", "[2025-08-02 07:22:06] Verifying : oracle-logos-httpd-90.4-1.0.1.el9.noarch 1/4\n", "[2025-08-02 07:22:06] Verifying : nginx-2:1.20.1-22.0.1.el9_6.3.x86_64 2/4\n", "[2025-08-02 07:22:06] Verifying : nginx-core-2:1.20.1-22.0.1.el9_6.3.x86_64 3/4\n", "[2025-08-02 07:22:07] Verifying : nginx-filesystem-2:1.20.1-22.0.1.el9_6.3.noarch 4/4\n", "[2025-08-02 07:22:07] Installed:\n", "[2025-08-02 07:22:07] nginx-2:1.20.1-22.0.1.el9_6.3.x86_64\n", "[2025-08-02 07:22:07] nginx-core-2:1.20.1-22.0.1.el9_6.3.x86_64\n", "[2025-08-02 07:22:07] nginx-filesystem-2:1.20.1-22.0.1.el9_6.3.noarch\n", "[2025-08-02 07:22:07] oracle-logos-httpd-90.4-1.0.1.el9.noarch\n", "[2025-08-02 07:22:07] Complete!\n", "[2025-08-02 07:22:07] systemctl start nginx\n", "[2025-08-02 07:22:12] [INFO] ▶ agent: Queuing engine reload ◆ hash=2415297466 ◆ spec_len=618", } var Deployments = []*aggregate.Deployment{ { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d00"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a00"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.178"}, PublicIps: []string{"1.253.67.10"}, PublicIps6: []string{"2001:db8:85a3:4d2f:ac50:8355:bb57:e0f5"}, PrivateIps: []string{"10.196.3.18"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:58d5:f529:66f2:36ef"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east0", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 50.52, InstanceHugePages: 0, InstanceLoad1: 35.43, InstanceLoad5: 44.56, InstanceLoad15: 51.32, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d01"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a01"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.82"}, PublicIps: []string{"1.253.67.103"}, PublicIps6: []string{"2001:db8:85a3:4d2f:5e1b:773a:2463:da58"}, PrivateIps: []string{"10.196.6.231"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:bf1a:d5e4:56a2:4b27"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east1", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 62.72, InstanceHugePages: 0, InstanceLoad1: 25.34, InstanceLoad5: 29.71, InstanceLoad15: 33.64, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d02"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a02"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.79"}, PublicIps: []string{"1.253.67.148"}, PublicIps6: []string{"2001:db8:85a3:4d2f:27fe:0397:17fa:5d2e"}, PrivateIps: []string{"10.196.3.251"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:3d61:c9f7:d2d7:8b9b"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east2", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 56.29, InstanceHugePages: 0, InstanceLoad1: 50.22, InstanceLoad5: 58.12, InstanceLoad15: 66.57, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d03"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a03"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.109"}, PublicIps: []string{"1.253.67.214"}, PublicIps6: []string{"2001:db8:85a3:4d2f:41b2:61e2:ad56:6cdc"}, PrivateIps: []string{"10.196.2.12"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:c166:fabc:4223:a974"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east3", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 32.21, InstanceHugePages: 0, InstanceLoad1: 54.49, InstanceLoad5: 59.36, InstanceLoad15: 64.21, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d04"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a04"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.97"}, PublicIps: []string{"1.253.67.129"}, PublicIps6: []string{"2001:db8:85a3:4d2f:cb29:095b:a7f8:9a7e"}, PrivateIps: []string{"10.196.6.229"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:b2f4:9b35:700e:0b9a"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east4", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 33.96, InstanceHugePages: 0, InstanceLoad1: 14.25, InstanceLoad5: 18.58, InstanceLoad15: 24.81, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d05"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a05"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.32"}, PublicIps: []string{"1.253.67.144"}, PublicIps6: []string{"2001:db8:85a3:4d2f:126f:552b:77d0:010e"}, PrivateIps: []string{"10.196.7.148"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:c057:f8fa:ff43:a21a"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east5", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 60.63, InstanceHugePages: 0, InstanceLoad1: 58.13, InstanceLoad5: 61.62, InstanceLoad15: 64.1, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d06"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a06"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.87"}, PublicIps: []string{"1.253.67.73"}, PublicIps6: []string{"2001:db8:85a3:4d2f:aa28:1b64:c808:caeb"}, PrivateIps: []string{"10.196.4.215"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:9797:d44a:0c7e:cb9e"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east0", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 48.03, InstanceHugePages: 0, InstanceLoad1: 53.4, InstanceLoad5: 60.79, InstanceLoad15: 68.75, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d07"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a07"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.151"}, PublicIps: []string{"1.253.67.65"}, PublicIps6: []string{"2001:db8:85a3:4d2f:bf64:91d6:4050:eac0"}, PrivateIps: []string{"10.196.5.97"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:0dd4:8931:8c28:5465"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east1", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 78.17, InstanceHugePages: 0, InstanceLoad1: 34.25, InstanceLoad5: 40.18, InstanceLoad15: 46.49, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d08"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a08"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.224"}, PublicIps: []string{"1.253.67.211"}, PublicIps6: []string{"2001:db8:85a3:4d2f:f5f3:98f7:82b5:ee87"}, PrivateIps: []string{"10.196.3.34"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:8d49:241d:4dd1:4663"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east2", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 53.78, InstanceHugePages: 0, InstanceLoad1: 35.24, InstanceLoad5: 38.31, InstanceLoad15: 43.56, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d09"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("688c716d9da165ffad4b3682"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b52e4"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a09"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.9"}, PublicIps: []string{"1.253.67.121"}, PublicIps6: []string{"2001:db8:85a3:4d2f:6e3a:29b0:639f:49d4"}, PrivateIps: []string{"10.196.2.187"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:ddb4:e207:cc09:d1e6"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east3", InstanceName: "web-app", InstanceRoles: []string{"instance"}, InstanceMemory: 2048, InstanceProcessors: 2, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 42.23, InstanceHugePages: 0, InstanceLoad1: 56.09, InstanceLoad5: 57.62, InstanceLoad15: 65.05, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d0a"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0e"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0a"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.61"}, PublicIps: []string{"1.253.67.205"}, PublicIps6: []string{"2001:db8:85a3:4d2f:d943:7ff9:dfdc:4d68"}, PrivateIps: []string{"10.196.3.219"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:7ba9:bca3:5217:b534"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east4", InstanceName: "database", InstanceRoles: []string{"instance"}, InstanceMemory: 8192, InstanceProcessors: 4, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 62.84, InstanceHugePages: 0, InstanceLoad1: 40.76, InstanceLoad5: 46.28, InstanceLoad15: 48.62, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d0b"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0f"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0b"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.221"}, PublicIps: []string{"1.253.67.155"}, PublicIps6: []string{"2001:db8:85a3:4d2f:3e3e:0d9d:8669:2c89"}, PrivateIps: []string{"10.196.8.253"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:d0ff:8b42:1d9b:92fd"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east5", InstanceName: "database", InstanceRoles: []string{"instance"}, InstanceMemory: 8192, InstanceProcessors: 4, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 66.15, InstanceHugePages: 0, InstanceLoad1: 28.1, InstanceLoad5: 32.45, InstanceLoad15: 40.43, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d0c"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0a"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0c"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.99"}, PublicIps: []string{"1.253.67.59"}, PublicIps6: []string{"2001:db8:85a3:4d2f:74b0:8661:53c1:1d5b"}, PrivateIps: []string{"10.196.2.110"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:e7b4:670b:acf5:dfb4"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east0", InstanceName: "database", InstanceRoles: []string{"instance"}, InstanceMemory: 8192, InstanceProcessors: 4, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 56.45, InstanceHugePages: 0, InstanceLoad1: 27.42, InstanceLoad5: 29.06, InstanceLoad15: 36.64, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d0d"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0b"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0d"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.208"}, PublicIps: []string{"1.253.67.39"}, PublicIps6: []string{"2001:db8:85a3:4d2f:4b40:60d1:ed30:0b06"}, PrivateIps: []string{"10.196.6.194"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:5220:ac62:3c7c:7291"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east1", InstanceName: "database", InstanceRoles: []string{"instance"}, InstanceMemory: 8192, InstanceProcessors: 4, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 54.62, InstanceHugePages: 0, InstanceLoad1: 54.37, InstanceLoad5: 57.22, InstanceLoad15: 63.01, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d0e"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0c"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0e"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.210"}, PublicIps: []string{"1.253.67.19"}, PublicIps6: []string{"2001:db8:85a3:4d2f:be8e:3013:d6f6:5396"}, PrivateIps: []string{"10.196.6.223"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:c924:41b8:22f3:5b3f"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east2", InstanceName: "database", InstanceRoles: []string{"instance"}, InstanceMemory: 8192, InstanceProcessors: 4, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 43.89, InstanceHugePages: 0, InstanceLoad1: 46.21, InstanceLoad5: 53.63, InstanceLoad15: 62.25, }, { Id: utils.ObjectIdHex("651d8e7c4cf91e3b53d62d0f"), Pod: utils.ObjectIdHex("688bf358d978631566998ffc"), Unit: utils.ObjectIdHex("68b67d1aee12c08a1f39f88b"), Spec: utils.ObjectIdHex("688c7cde9da165ffad4b34f2"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("689733b2a7a35eae0dbaea0d"), Instance: utils.ObjectIdHex("651d8e7c4cf9e2e3e4d56a0f"), InstanceData: &deployment.InstanceData{ HostIps: []string{"198.18.84.50"}, PublicIps: []string{"1.253.67.60"}, PublicIps6: []string{"2001:db8:85a3:4d2f:fff2:877d:227c:1c4a"}, PrivateIps: []string{"10.196.7.165"}, PrivateIps6: []string{"fd97:30bf:d456:a3bc:ae17:b804:32c5:956c"}, }, Journals: []*deployment.Journal{ { Index: journal.DeploymentAgent, Key: "agent", Type: "agent", }, }, ZoneName: "us-west-1a", NodeName: "pritunl-east3", InstanceName: "database", InstanceRoles: []string{"instance"}, InstanceMemory: 8192, InstanceProcessors: 4, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: 41.89, InstanceHugePages: 0, InstanceLoad1: 24.24, InstanceLoad5: 25.81, InstanceLoad15: 30.23, }, } ================================================ FILE: demo/policy.go ================================================ package demo import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/policy" "github.com/pritunl/pritunl-cloud/utils" ) var Policies = []*policy.Policy{ { Id: utils.ObjectIdHex("67b8a03e4866ba90e6c45a8c"), Name: "policy", Comment: "", Disabled: false, Roles: []string{ "pritunl", }, Rules: map[string]*policy.Rule{ "location": { Type: "location", Disable: false, Values: []string{ "US", }, }, "whitelist_networks": { Type: "whitelist_networks", Disable: false, Values: []string{ "10.0.0.0/8", }, }, }, AdminSecondary: bson.ObjectID{}, UserSecondary: bson.ObjectID{}, AdminDeviceSecondary: true, UserDeviceSecondary: true, }, } ================================================ FILE: demo/pool.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" ) var Pools = []*pool.Pool{ { Id: utils.ObjectIdHex("67b89e8d4866ba90e6c459ba"), Name: "cloud-east", Comment: "", DeleteProtection: false, Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Zone: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Type: "", VgName: "cloud_east", }, } ================================================ FILE: demo/rand.go ================================================ package demo import ( "sync" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/utils" ) var ( lock = sync.Mutex{} ipStore = map[bson.ObjectID]string{} ip6Store = map[bson.ObjectID]string{} privateIpStore = map[bson.ObjectID]string{} privateIp6Store = map[bson.ObjectID]string{} ) func RandIp(instId bson.ObjectID) (addr string) { lock.Lock() defer lock.Unlock() addr = ipStore[instId] if addr == "" { addr = utils.RandIp() ipStore[instId] = addr } return } func RandIp6(instId bson.ObjectID) (addr string) { lock.Lock() defer lock.Unlock() addr = ip6Store[instId] if addr == "" { addr = utils.RandIp6() ip6Store[instId] = addr } return } func RandPrivateIp(instId bson.ObjectID) (addr string) { lock.Lock() defer lock.Unlock() addr = privateIpStore[instId] if addr == "" { addr = utils.RandPrivateIp() privateIpStore[instId] = addr } return } func RandPrivateIp6(instId bson.ObjectID) (addr string) { lock.Lock() defer lock.Unlock() addr = privateIp6Store[instId] if addr == "" { addr = utils.RandPrivateIp6() privateIp6Store[instId] = addr } return } ================================================ FILE: demo/secret.go ================================================ package demo import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/utils" ) var Secrets = []*secret.Secret{ { Id: utils.ObjectIdHex("67b89e8d4866ba90e6c459ba"), Name: "cloudflare-pritunl-com", Comment: "", Organization: bson.ObjectID{}, Type: "cloudflare", Key: "a7kX9mN2vP8Q-4jL6wS3tR5Y-uH1gF7dZ0xC-vB8nM", Value: "", Region: "", PublicKey: `-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz4K8Lm3QvR7WxN5YdE2P jX9TpQ6HgM1wV0nS4KaF3ZcB8LrY5UvO2JmN7XsPqI1AgK8EoH3RdWzM9LfY2VtN kP4QxGsJ7YnR8LwVmT3AqZ5HvK2NdP1XoS8JgR4LmW7YxQ3VnH5TsK9PpL2MdX8Rg vJ3KqN5WxT1LsM4HgY7RdP8NqV2JmK5XwL3TsR8YgN4HxP1LdK9VwQ2MsT3XpR7Y nL8KgJ5WdH3TmR9XsL2PqN7VxK4MgT3HdJ8YwP2LsK5RxT1NqM4JgY7PxR8WsL3T mK9XwN2HgJ5YdL3RsP8VqT2MxK4NhR3JdY8WwL2TsM5QxN1PqK4YgJ7RxP8VsT3M PwIDAQAB -----END PUBLIC KEY-----`, Data: "", }, } ================================================ FILE: demo/shape.go ================================================ package demo import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/utils" ) var Shapes = []*shape.Shape{ { Id: utils.ObjectIdHex("65e6e303ceeebbb3dabaec96"), Name: "m2-small", Comment: "", Type: "instance", DeleteProtection: false, Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Roles: []string{ "shape-m2", }, Flexible: true, DiskType: "qcow2", DiskPool: bson.ObjectID{}, Memory: 2048, Processors: 1, NodeCount: 1, }, { Id: utils.ObjectIdHex("65e6e2ecceeebbb3dabaec79"), Name: "m2-medium", Comment: "", Type: "instance", DeleteProtection: false, Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Roles: []string{ "shape-m2", }, Flexible: true, DiskType: "qcow2", DiskPool: bson.ObjectID{}, Memory: 4096, Processors: 2, NodeCount: 1, }, { Id: utils.ObjectIdHex("66f63282aac06d53e8c9c435"), Name: "m2-large", Comment: "", Type: "instance", DeleteProtection: false, Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Roles: []string{ "shape-m2", }, Flexible: true, DiskType: "qcow2", DiskPool: bson.ObjectID{}, Memory: 8192, Processors: 4, NodeCount: 1, }, } ================================================ FILE: demo/storage.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" ) var Storages = []*storage.Storage{ { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea15"), Name: "pritunl-images", Comment: "", Type: "public", Endpoint: "images.pritunl.com", Bucket: "stable", AccessKey: "", SecretKey: "", Insecure: false, }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea16"), Name: "pritunl-storage", Comment: "", Type: "private", Endpoint: "s3.amazonaws.com", Bucket: "pritunl-cloud-2943", AccessKey: "AKIAJTVJ15RORHDU7M1M", SecretKey: "VLBGHOVTKDP5SIRSEC8R4XFQWLCIYN4HK", Insecure: false, }, } ================================================ FILE: demo/subscription.go ================================================ package demo import ( "time" "github.com/pritunl/pritunl-cloud/subscription" ) var Subscription = &subscription.Subscription{ Active: true, Status: "active", Plan: "cloud", Quantity: 1, Amount: 5000, PeriodEnd: time.Unix(1893499200, 0), TrialEnd: time.Time{}, CancelAtPeriodEnd: false, Balance: 0, UrlKey: "demo", } ================================================ FILE: demo/user.go ================================================ package demo import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/useragent" "github.com/pritunl/pritunl-cloud/utils" ) var Users = []*user.User{ &user.User{ Id: utils.ObjectIdHex("5b6cd11857e4a9a88cbf072e"), Type: "local", Provider: bson.ObjectID{}, Username: "demo", Token: "", Secret: "", LastActive: time.Now(), LastSync: time.Now(), Roles: []string{"demo"}, Administrator: "super", Disabled: false, ActiveUntil: time.Time{}, Permissions: []string{}, }, &user.User{ Id: utils.ObjectIdHex("5a7542190accad1a8a53b568"), Type: "local", Provider: bson.ObjectID{}, Username: "pritunl", Token: "", Secret: "", LastActive: time.Time{}, LastSync: time.Time{}, Roles: []string{}, Administrator: "super", Disabled: false, ActiveUntil: time.Time{}, Permissions: []string{}, }, } var Agent = &useragent.Agent{ OperatingSystem: useragent.Linux, Browser: useragent.Chrome, Ip: "8.8.8.8", Isp: "Google", Continent: "North America", ContinentCode: "NA", Country: "United States", CountryCode: "US", Region: "Washington", RegionCode: "WA", City: "Seattle", Latitude: 47.611, Longitude: -122.337, } var Audits = []*audit.Audit{ &audit.Audit{ Id: utils.ObjectIdHex("5a17f9bf051a45ffacf2b352"), Timestamp: time.Unix(1498018860, 0), Type: "admin_login", Fields: audit.Fields{ "method": "local", }, Agent: Agent, }, } var Sessions = []*session.Session{ &session.Session{ Id: "jhgRu4n3oY0iXRYmLb77Ql5jNs2o7uWM", Type: session.User, Timestamp: time.Unix(1498018860, 0), LastActive: time.Unix(1498018860, 0), Removed: false, Agent: Agent, }, } ================================================ FILE: demo/vpc.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" ) var Vpcs = []*vpc.Vpc{ { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea23"), Name: "production", Comment: "", VpcId: 2996, Network: "10.196.0.0/14", Network6: "fd97:30bf:d456:a3bc::/64", Subnets: []*vpc.Subnet{ { Id: utils.ObjectIdHex("66a076d5fafc270786e93461"), Name: "primary", Network: "10.196.1.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93462"), Name: "management", Network: "10.196.2.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93463"), Name: "link", Network: "10.196.3.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93464"), Name: "database", Network: "10.196.4.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93465"), Name: "web", Network: "10.196.5.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93466"), Name: "search", Network: "10.196.6.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93467"), Name: "vpn", Network: "10.196.7.0/24", }, { Id: utils.ObjectIdHex("66a076d5fafc270786e93468"), Name: "balancer", Network: "10.196.8.0/24", }, }, Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), IcmpRedirects: false, Routes: []*vpc.Route{ &vpc.Route{ Destination: "10.24.0.0/16", Target: "10.196.7.2", }, }, Maps: []*vpc.Map{}, Arps: []*vpc.Arp{}, DeleteProtection: false, }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea24"), Name: "testing", Comment: "", VpcId: 2732, Network: "10.224.0.0/14", Network6: "fd97:30bf:d456:a3bc::/64", Subnets: []*vpc.Subnet{ { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea61"), Name: "primary", Network: "10.224.1.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea62"), Name: "management", Network: "10.224.2.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea63"), Name: "link", Network: "10.224.3.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea64"), Name: "database", Network: "10.224.4.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea65"), Name: "web", Network: "10.224.5.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea66"), Name: "search", Network: "10.224.6.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea67"), Name: "vpn", Network: "10.224.7.0/24", }, { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea68"), Name: "balancer", Network: "10.224.8.0/24", }, }, Organization: utils.ObjectIdHex("5a3245a50accad1a8a53bc82"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), IcmpRedirects: false, Routes: []*vpc.Route{ &vpc.Route{ Destination: "10.36.0.0/16", Target: "10.224.7.2", }, }, Maps: []*vpc.Map{}, Arps: []*vpc.Arp{}, DeleteProtection: false, }, } ================================================ FILE: demo/zone.go ================================================ package demo import ( "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" ) var Zones = []*zone.Zone{ { Id: utils.ObjectIdHex("689733b7a7a35eae0dbaea1e"), Datacenter: utils.ObjectIdHex("689733b7a7a35eae0dbaea1b"), Name: "us-west-1a", Comment: "", DnsServers: []string{}, DnsServers6: []string{}, }, } ================================================ FILE: deploy/deploy.go ================================================ package deploy import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/state" ) func Deploy(stat *state.State, runtimes *state.Runtimes) (err error) { db := database.GetDatabase() defer db.Close() start := time.Now() network := NewNetwork(stat) err = network.Deploy() if err != nil { return } runtimes.Network = time.Since(start) start = time.Now() ipset := NewIpset(stat) err = ipset.Deploy() if err != nil { return } runtimes.Ipset = time.Since(start) start = time.Now() iptables := NewIptables(stat) err = iptables.Deploy() if err != nil { return } err = ipset.Clean() if err != nil { return } runtimes.Iptables = time.Since(start) start = time.Now() disks := NewDisks(stat) err = disks.Deploy(db) if err != nil { return } runtimes.Disks = time.Since(start) start = time.Now() instances := NewInstances(stat) err = instances.Deploy(db) if err != nil { return } runtimes.Instances = time.Since(start) start = time.Now() namespaces := NewNamespace(stat) err = namespaces.Deploy(db) if err != nil { return } runtimes.Namespaces = time.Since(start) start = time.Now() pods := NewPods(stat) err = pods.Deploy(db) if err != nil { return } runtimes.Pods = time.Since(start) start = time.Now() deployments := NewDeployments(stat) err = deployments.Deploy(db) if err != nil { return } runtimes.Deployments = time.Since(start) start = time.Now() imds := NewImds(stat) err = imds.Deploy(db) if err != nil { return } runtimes.Imds = time.Since(start) start = time.Now() stat.Wait() runtimes.Wait = time.Since(start) return } ================================================ FILE: deploy/deployments.go ================================================ package deploy import ( "strconv" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) var ( deploymentsLock = utils.NewMultiTimeoutLock(5 * time.Minute) ) type Deployments struct { stat *state.State } func (d *Deployments) migrate(deply *deployment.Deployment) { nde := d.stat.Node() nodeId := d.stat.Node().Id acquired, lockId := deploymentsLock.LockOpenTimeout( deply.Id.Hex(), 3*time.Minute) if !acquired { return } go func() { defer func() { deploymentsLock.Unlock(deply.Id.Hex(), lockId) }() db := database.GetDatabase() defer db.Close() if deply.Node != nodeId { return } inst, err := instance.Get(db, deply.Instance) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "new_spec_id": deply.NewSpec.Hex(), "error": err, }).Error("deploy: Failed to get instance") return } inst.PreCommit() curSpec, err := spec.Get(db, deply.Spec) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": deply.NewSpec.Hex(), "error": err, }).Error("deploy: Failed to get current spec") return } newSpec, err := spec.Get(db, deply.NewSpec) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, }).Error("deploy: Failed to get new spec") return } errData, err := curSpec.CanMigrate(db, deply, newSpec) if err != nil || errData != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, "error_data": errData, }).Error("deploy: Incompatible migrate") deply.State = deployment.Deployed deply.NewSpec = bson.NilObjectID err = deply.CommitFields(db, set.NewSet("state", "new_spec")) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment") return } return } instFields := set.NewSet() if curSpec.Instance.Uefi != newSpec.Instance.Uefi { if newSpec.Instance.Uefi != nil { instFields.Add("uefi") inst.Uefi = *newSpec.Instance.Uefi } } if curSpec.Instance.SecureBoot != newSpec.Instance.SecureBoot { if newSpec.Instance.SecureBoot != nil { instFields.Add("secure_boot") inst.SecureBoot = *newSpec.Instance.SecureBoot } } if curSpec.Instance.CloudType != newSpec.Instance.CloudType { if newSpec.Instance.CloudType != "" { instFields.Add("cloud_type") inst.CloudType = newSpec.Instance.CloudType } } if curSpec.Instance.Tpm != newSpec.Instance.Tpm { instFields.Add("tpm") inst.Tpm = newSpec.Instance.Tpm } if curSpec.Instance.Vnc != newSpec.Instance.Vnc { instFields.Add("vnc") inst.Vnc = newSpec.Instance.Vnc } if curSpec.Instance.DeleteProtection != newSpec.Instance.DeleteProtection { instFields.Add("delete_protection") inst.DeleteProtection = newSpec.Instance.DeleteProtection } if curSpec.Instance.SkipSourceDestCheck != newSpec.Instance.SkipSourceDestCheck { instFields.Add("skip_source_dest_check") inst.SkipSourceDestCheck = newSpec.Instance.SkipSourceDestCheck } if curSpec.Instance.Gui != newSpec.Instance.Gui { instFields.Add("gui") inst.Gui = newSpec.Instance.Gui } if curSpec.Instance.HostAddress != newSpec.Instance.HostAddress { if newSpec.Instance.HostAddress != nil { instFields.Add("no_host_address") inst.NoHostAddress = !*newSpec.Instance.HostAddress } } if curSpec.Instance.PublicAddress != newSpec.Instance.PublicAddress { if newSpec.Instance.PublicAddress != nil { instFields.Add("no_public_address") inst.NoPublicAddress = !*newSpec.Instance.PublicAddress } else { instFields.Add("no_public_address") inst.NoPublicAddress = nde.DefaultNoPublicAddress } } if curSpec.Instance.PublicAddress6 != newSpec.Instance.PublicAddress6 { if newSpec.Instance.PublicAddress6 != nil { instFields.Add("no_public_address6") inst.NoPublicAddress6 = !*newSpec.Instance.PublicAddress6 } else { instFields.Add("no_public_address6") inst.NoPublicAddress6 = nde.DefaultNoPublicAddress6 } } if curSpec.Instance.DhcpServer != newSpec.Instance.DhcpServer { instFields.Add("dhcp_server") inst.DhcpServer = newSpec.Instance.DhcpServer } if curSpec.Instance.Shape != newSpec.Instance.Shape || curSpec.Instance.Processors != newSpec.Instance.Processors || curSpec.Instance.Memory != newSpec.Instance.Memory { if !newSpec.Instance.Shape.IsZero() { shpe, e := shape.Get(db, newSpec.Instance.Shape) if e != nil { err = e logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, }).Error("deploy: Failed to get spec shape") return } if inst != nil { inst.Processors = shpe.Processors instFields.Add("processors") inst.Memory = shpe.Memory instFields.Add("memory") if shpe.Flexible { if newSpec.Instance.Processors != 0 { inst.Processors = newSpec.Instance.Processors } if newSpec.Instance.Memory != 0 { inst.Memory = newSpec.Instance.Memory } } } } else if inst != nil { inst.Processors = newSpec.Instance.Processors instFields.Add("processors") inst.Memory = newSpec.Instance.Memory instFields.Add("memory") } } if !utils.CompareStringSlicesUnsorted(curSpec.Instance.Roles, newSpec.Instance.Roles) { instFields.Add("roles") inst.Roles = newSpec.Instance.Roles } if curSpec.Instance.DiffNodePorts(newSpec.Instance.NodePorts) { instFields.Add("node_ports") newNodePorts := []*nodeport.Mapping{} for _, ndePort := range newSpec.Instance.NodePorts { newNodePorts = append(newNodePorts, &nodeport.Mapping{ Protocol: ndePort.Protocol, ExternalPort: ndePort.ExternalPort, InternalPort: ndePort.InternalPort, }) } inst.UpsertNodePorts(newNodePorts) } inst.Mounts = []*instance.Mount{} for _, mount := range newSpec.Instance.Mounts { if mount.Type != spec.HostPath { continue } inst.Mounts = append(inst.Mounts, &instance.Mount{ Name: mount.Name, Type: instance.HostPath, Path: mount.Path, HostPath: mount.HostPath, }) } instFields.Add("mounts") if instFields.Len() > 0 { errData, err = inst.Validate(db) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, "error_data": errData, }).Error("deploy: Migrate failed, invalid instance options") return } dskChange, err := inst.PostCommit(db) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, "error_data": errData, }).Error("deploy: Migrate failed, instance post commit error") return } err = inst.CommitFields(db, instFields) if err != nil { _ = inst.Cleanup(db) logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, }).Error("deploy: Failed to migrate instance") return } err = inst.Cleanup(db) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, }).Error("deploy: Failed to cleanup instance") err = nil } event.PublishDispatch(db, "instance.change") if dskChange { event.PublishDispatch(db, "disk.change") } } jrnls := []*deployment.Journal{} if newSpec.Journal != nil { for _, input := range newSpec.Journal.Inputs { jrnls = append(jrnls, &deployment.Journal{ Index: input.Index, Key: input.Key, Type: input.Type, }) } } deply.Journals = jrnls deply.Action = "" deply.Spec = newSpec.Id deply.NewSpec = bson.NilObjectID err = deply.CommitFields(db, set.NewSet( "action", "spec", "new_spec", "journals")) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment") return } logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "cur_spec_id": curSpec.Id.Hex(), "new_spec_id": newSpec.Id.Hex(), }).Info("deploy: Migrated deployment") return }() } func (d *Deployments) destroy(deply *deployment.Deployment) { acquired, lockId := deploymentsLock.LockOpenTimeout( deply.Id.Hex(), 3*time.Minute) if !acquired { return } go func() { defer func() { deploymentsLock.Unlock(deply.Id.Hex(), lockId) }() db := database.GetDatabase() defer db.Close() if deply.Node != d.stat.Node().Id { return } if deply.Kind == deployment.Image && !deply.Image.IsZero() { img, err := image.Get(db, deply.Image) if err != nil { if _, ok := err.(*database.NotFoundError); ok { img = nil err = nil } else { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to get image") return } } if img != nil { err = data.DeleteImage(db, img.Id) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to remove deployment image") return } event.PublishDispatch(db, "image.change") } } inst := d.stat.GetInstace(deply.Instance) if inst != nil { if inst.DeleteProtection { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": inst.Id.Hex(), }).Warning("deploy: Cannot destroy deployment with " + "instance delete protection, archiving...") deply.Action = deployment.Archive err := deply.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment") return } return } if inst.Action != instance.Destroy { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": inst.Id.Hex(), }).Info("deploy: Delete deployment instance") err := instance.Delete(db, inst.Id) if err != nil { if _, ok := err.(*database.NotFoundError); !ok { err = nil } else { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to delete instance") return } } } } else { err := deployment.Remove(db, deply.Id) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to remove deployment") return } event.PublishDispatch(db, "pod.change") } return }() } func (d *Deployments) archive(deply *deployment.Deployment) (err error) { inst := d.stat.GetInstace(deply.Instance) disks := d.stat.GetDeploymentDisks(deply.Id) spc := d.stat.Spec(deply.Spec) nodeId := d.stat.Node().Id acquired, lockId := deploymentsLock.LockOpenTimeout( deply.Id.Hex(), 3*time.Minute) if !acquired { return } go func() { defer func() { deploymentsLock.Unlock(deply.Id.Hex(), lockId) }() db := database.GetDatabase() defer db.Close() if deply.Node != nodeId { return } if !inst.IsActive() { if len(disks) > 0 { specDisks := set.NewSet() for _, mount := range spc.Instance.Mounts { if mount.Type != spec.Disk { continue } for _, dskId := range mount.Disks { specDisks.Add(dskId) } } for _, dsk := range disks { if !specDisks.Contains(dsk.Id) { continue } err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } event.PublishDispatch(db, "disk.change") } deply.State = deployment.Archived deply.Action = "" err = deply.CommitFields(db, set.NewSet("state", "action")) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment") return } return } if inst.Action != instance.Stop { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), }).Info("deploy: Stopping instance for deployment archive") err = instance.SetAction(db, inst.Id, instance.Stop) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to set instance state") return } } }() return } func (d *Deployments) restore(deply *deployment.Deployment) (err error) { inst := d.stat.GetInstace(deply.Instance) instDisks := d.stat.GetInstaceDisks(deply.Instance) spc := d.stat.Spec(deply.Spec) nodeId := d.stat.Node().Id acquired, lockId := deploymentsLock.LockOpenTimeout( deply.Id.Hex(), 3*time.Minute) if !acquired { return } go func() { defer func() { deploymentsLock.Unlock(deply.Id.Hex(), lockId) }() db := database.GetDatabase() defer db.Close() if deply.Node != nodeId { return } if inst.IsActive() { deply.State = deployment.Deployed deply.Action = "" err = deply.CommitFields(db, set.NewSet("state", "action")) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment") return } return } index := 0 curDisks := set.NewSet() for _, dsk := range instDisks { dskIndex, _ := strconv.Atoi(dsk.Index) index = max(index, dskIndex) curDisks.Add(dsk.Id) } if inst.Action != instance.Start { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), }).Info("deploy: Starting instance for deployment restore") reservedDisks := []*disk.Disk{} deplyMounts := []*deployment.Mount{} for _, mount := range spc.Instance.Mounts { if mount.Type != spec.Disk { continue } index += 1 diskReserved := false for _, dskId := range mount.Disks { if curDisks.Contains(dskId) { diskReserved = true break } } if !diskReserved { for _, dskId := range mount.Disks { dsk, e := disk.Get(db, dskId) if e != nil { err = e for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } return } if dsk.Node != node.Self.Id || !dsk.Instance.IsZero() { continue } diskReserved, err = dsk.Reserve( db, inst.Id, index, deply.Id) if err != nil { for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } return } if !diskReserved { continue } deplyMounts = append(deplyMounts, &deployment.Mount{ Disk: dsk.Id, Path: mount.Path, Uuid: dsk.Uuid, }) reservedDisks = append(reservedDisks, dsk) break } } if !diskReserved { for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } logrus.WithFields(logrus.Fields{ "mount_path": mount.Path, }).Error("deploy: Failed to reserve disk for mount") deply.State = deployment.Archived deply.Action = "" err = deply.CommitFields(db, set.NewSet("state", "action")) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment") return } return } } if len(reservedDisks) > 0 { event.PublishDispatch(db, "disk.change") } err = instance.SetAction(db, inst.Id, instance.Start) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to set instance state") return } } }() return } func (d *Deployments) imageShutdown(db *database.Database, spc *spec.Spec, deply *deployment.Deployment, virt *vm.VirtualMachine) { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), }).Info("deploy: Stopping instance for deployment image") journals := types.NewJournals(spc) time.Sleep(3 * time.Second) err := imds.Pull(db, virt.Id, virt.Deployment, virt.ImdsHostSecret, journals) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Error("deploy: Failed to pull imds state for shutdown") } err = instance.SetAction(db, virt.Id, instance.Stop) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Error("deploy: Failed to set instance state") return } if deply.GetImageState() == "" { deply.SetImageState(deployment.Ready) err = deply.CommitFields(db, set.NewSet("image_data.state")) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Error("deploy: Failed to commit deployment state") return } } event.PublishDispatch(db, "pod.change") } func (d *Deployments) image(deply *deployment.Deployment) (err error) { acquired, lockId := deploymentsLock.LockOpenTimeout( deply.Id.Hex(), 5*time.Minute) if !acquired { return } go func() { defer func() { time.Sleep(3 * time.Second) deploymentsLock.Unlock(deply.Id.Hex(), lockId) }() db := database.GetDatabase() defer db.Close() if deply.Node != d.stat.Node().Id { return } inst := d.stat.GetInstace(deply.Instance) if inst == nil { return } virt := d.stat.GetVirt(inst.Id) spc := d.stat.Spec(deply.Spec) if inst.Guest == nil { return } if inst.IsActive() && inst.Guest.Status == types.Imaged && inst.Action != instance.Stop { d.imageShutdown(db, spc, deply, virt) return } if deply.State == deployment.Deployed && deply.GetImageState() == deployment.Ready && !inst.IsActive() && inst.Guest.Status == types.Imaged { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), }).Info("deploy: Creating deployment image") dsk, e := disk.GetInstanceIndex(db, inst.Id, "0") if e != nil { if _, ok := e.(*database.NotFoundError); ok { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to find instance disk for image") deply.SetImageState(deployment.Failed) err = deply.CommitFields(db, set.NewSet("image_data.state")) if err != nil { return } dsk = nil err = nil } else { return } } dsk.Action = disk.Snapshot err = dsk.CommitFields(db, set.NewSet("action")) if err != nil { return } deply.SetImageState(deployment.Snapshot) err = deply.CommitFields(db, set.NewSet("image_data.state")) if err != nil { return } } }() return } func (d *Deployments) domainCommit(deply *deployment.Deployment, domn *domain.Domain, newRecs []*domain.Record) { acquired, lockId := deploymentsLock.LockOpenTimeout( deply.Id.Hex(), 3*time.Minute) if !acquired { return } go func() { defer func() { deploymentsLock.Unlock(deply.Id.Hex(), lockId) }() db := database.GetDatabase() defer db.Close() time.Sleep(500 * time.Millisecond) deply, err := deployment.Get(db, deply.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } return } if deply.State != deployment.Deployed { return } logrus.WithFields(logrus.Fields{ "domain_id": domn.Id.Hex(), }).Info("deploy: Committing domain records") recs := []*deployment.RecordData{} for _, rec := range newRecs { recs = append(recs, &deployment.RecordData{ Domain: rec.SubDomain + "." + domn.RootDomain, Value: rec.Value, }) } domnData := &deployment.DomainData{ Records: recs, } err = domn.CommitRecords(db) if err != nil { logrus.WithFields(logrus.Fields{ "domain_id": domn.Id.Hex(), "error": err, }).Error("deploy: Failed to commit domain records") return } deply.DomainData = domnData err = deply.CommitFields(db, set.NewSet("domain_data")) if err != nil { return } event.PublishDispatch(db, "domain.change") event.PublishDispatch(db, "pod.change") time.Sleep(500 * time.Millisecond) newDeply, err := deployment.Get(db, deply.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { newDeply = nil err = nil } else { return } } if newDeply == nil || newDeply.State != deployment.Deployed { logrus.WithFields(logrus.Fields{ "domain_id": domn.Id.Hex(), }).Info("deploy: Undo domains commit for deactivated deployment") err = deployment.RemoveDomains(db, deply.Id) if err != nil { return } } }() } func (d *Deployments) domain(db *database.Database, deply *deployment.Deployment, spc *spec.Spec) (err error) { if spc.Domain != nil && deply.InstanceData != nil { newRecs := map[bson.ObjectID][]*domain.Record{} for _, specRec := range spc.Domain.Records { domn := d.stat.SpecDomain(specRec.Domain) if domn == nil { continue } switch specRec.Type { case spec.Public: for _, val := range deply.InstanceData.PublicIps { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.A, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.Public6: for _, val := range deply.InstanceData.PublicIps6 { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.AAAA, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.Host: for _, val := range deply.InstanceData.HostIps { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.A, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.Private: for _, val := range deply.InstanceData.PrivateIps { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.A, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.Private6: for _, val := range deply.InstanceData.PrivateIps6 { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.AAAA, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.CloudPublic: for _, val := range deply.InstanceData.CloudPublicIps { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.A, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.CloudPublic6: for _, val := range deply.InstanceData.CloudPublicIps6 { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.AAAA, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break case spec.CloudPrivate: for _, val := range deply.InstanceData.CloudPrivateIps { rec := &domain.Record{ Domain: specRec.Domain, SubDomain: specRec.Name, Deployment: deply.Id, Type: domain.A, Value: val, } errData, e := rec.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } newRecs[domn.Id] = append(newRecs[domn.Id], rec) } break } } for domnId, domnNewRecs := range newRecs { domn := d.stat.SpecDomain(domnId) if domn == nil { continue } changedDomn := domn.MergeRecords(deply.Id, domnNewRecs) if changedDomn != nil { d.domainCommit(deply, changedDomn, domnNewRecs) } } } return } func (d *Deployments) Deploy(db *database.Database) (err error) { activeDeployments := d.stat.DeploymentsDeployed() inactiveDeployments := d.stat.DeploymentsInactive() for _, deply := range inactiveDeployments { switch deply.Action { case deployment.Migrate: d.migrate(deply) break case deployment.Destroy: d.destroy(deply) break case deployment.Archive: err = d.archive(deply) if err != nil { return } break case deployment.Restore: err = d.restore(deply) if err != nil { return } break } } for _, deply := range activeDeployments { if deply.Action == deployment.Migrate { d.migrate(deply) break } if deply.State == deployment.Deployed && deply.Kind == deployment.Instance { spec := d.stat.Spec(deply.Spec) if spec != nil && spec.Domain != nil { err = d.domain(db, deply, spec) if err != nil { return } } } if deply.Kind == deployment.Image { err = d.image(deply) if err != nil { return } } } return } func NewDeployments(stat *state.State) *Deployments { return &Deployments{ stat: stat, } } ================================================ FILE: deploy/disks.go ================================================ package deploy import ( "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) var ( disksLock = utils.NewMultiTimeoutLock(5 * time.Minute) backupLimiter = utils.NewLimiter(3) ) type Disks struct { stat *state.State } func (d *Disks) provision(dsk *disk.Disk) { acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { return } go func() { defer disksLock.Unlock(dsk.Id.Hex(), lockId) db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } dsk, err := disk.Get(db, dsk.Id) if err != nil { return } if dsk.State != disk.Provision { return } newSize, backingImage, err := data.CreateDisk(db, dsk) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to provision disk") return } fields := set.NewSet("state", "backing_image") dsk.State = disk.Available dsk.BackingImage = backingImage if newSize != 0 { fields.Add("size") dsk.Size = newSize } err = dsk.CommitFields(db, fields) if err != nil { return } event.PublishDispatch(db, "disk.change") }() } func (d *Disks) snapshot(dsk *disk.Disk) { acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { return } go func() { defer disksLock.Unlock(dsk.Id.Hex(), lockId) db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } dsk, err := disk.Get(db, dsk.Id) if err != nil { return } if dsk.Action != disk.Snapshot { return } if dsk.Type != disk.Qcow2 { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_type": dsk.Type, }).Error("deploy: Disk type does not support snapshot") } else { virt := d.stat.GetVirt(dsk.Instance) if virt == nil { err := &errortypes.ReadError{ errors.New("deploy: Failed to load virt"), } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "error": err, }).Error("deploy: Failed to load virt") return } err := data.CreateSnapshot(db, dsk, virt) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to snapshot disk") } } dsk.Action = "" err = dsk.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_type": dsk.Type, "error": err, }).Error("deploy: Failed update disk state") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") }() } func (d *Disks) expand(dsk *disk.Disk) { acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { return } go func() { defer disksLock.Unlock(dsk.Id.Hex(), lockId) db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } dsk, err := disk.Get(db, dsk.Id) if err != nil { return } if dsk.Action != disk.Expand { return } inst := d.stat.GetInstace(dsk.Instance) if inst != nil { if inst.Action != instance.Stop { inst.Action = instance.Stop logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "disk_id": dsk.Id.Hex(), }).Info("deploy: Stopping instance for resize") err := inst.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to commit instance state") return } return } virt := d.stat.GetVirt(inst.Id) if virt != nil && virt.State != vm.Stopped && virt.State != vm.Failed { return } } err = data.ExpandDisk(db, dsk) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to expand disk") } dsk.Action = "" dsk.NewSize = 0 err = dsk.CommitFields(db, set.NewSet("action", "size", "new_size")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed update disk state") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") }() } func (d *Disks) backup(dsk *disk.Disk) { if !backupLimiter.Acquire() { return } acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { backupLimiter.Release() return } go func() { defer func() { time.Sleep(1 * time.Second) disksLock.Unlock(dsk.Id.Hex(), lockId) backupLimiter.Release() }() db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } dsk, err := disk.Get(db, dsk.Id) if err != nil { return } if dsk.Action != disk.Backup { return } if dsk.Type != disk.Qcow2 { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_type": dsk.Type, }).Error("deploy: Disk type does not support backup") } else { virt := d.stat.GetVirt(dsk.Instance) if virt == nil { err := &errortypes.ReadError{ errors.New("deploy: Failed to load virt"), } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "error": err, }).Error("deploy: Failed to load virt") return } err := data.CreateBackup(db, dsk, virt) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to backup disk") } } dsk.Action = "" err = dsk.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed update disk state") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") event.PublishDispatch(db, "image.change") }() } func (d *Disks) restore(dsk *disk.Disk) { if !backupLimiter.Acquire() { return } acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { backupLimiter.Release() return } go func() { defer func() { time.Sleep(1 * time.Second) disksLock.Unlock(dsk.Id.Hex(), lockId) backupLimiter.Release() }() db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } dsk, err := disk.Get(db, dsk.Id) if err != nil { return } if dsk.Action != disk.Restore { return } inst := d.stat.GetInstace(dsk.Instance) if inst != nil { if inst.Action != instance.Stop { inst.Action = instance.Stop logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "disk_id": dsk.Id.Hex(), }).Info("deploy: Stopping instance for restore") err := inst.CommitFields(db, set.NewSet("state")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to commit instance state") return } return } virt := d.stat.GetVirt(inst.Id) if virt != nil && virt.State != vm.Stopped && virt.State != vm.Failed { return } } if dsk.Type != disk.Qcow2 { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "disk_type": dsk.Type, }).Error("deploy: Disk type does not support restore") } else { err := data.RestoreBackup(db, dsk) if err != nil { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "error": err, }).Error("deploy: Failed to restore disk") } } dsk.Action = "" err = dsk.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "error": err, }).Error("deploy: Failed update disk state") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") }() } func (d *Disks) destroy(db *database.Database, dsk *disk.Disk) { var inst *instance.Instance if !dsk.Instance.IsZero() { inst = d.stat.GetInstace(dsk.Instance) } if d.stat.DiskInUse(dsk.Instance, dsk.Id) { return } acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { return } go func() { defer disksLock.Unlock(dsk.Id.Hex(), lockId) db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } dsk, err := disk.Get(db, dsk.Id) if err != nil { return } if dsk.Action != disk.Destroy { return } if dsk.DeleteProtection { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Info("deploy: Delete protection ignore disk destroy") dsk.Action = "" _ = dsk.CommitFields(db, set.NewSet("action")) event.PublishDispatch(db, "disk.change") return } if inst != nil && inst.DeleteProtection { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Info("deploy: Instance delete protection ignore disk destroy") dsk.Action = "" _ = dsk.CommitFields(db, set.NewSet("action")) event.PublishDispatch(db, "disk.change") return } err = dsk.Destroy(db) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to destroy disk") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") }() } func (d *Disks) scheduleBackup(dsk *disk.Disk) { if time.Since(dsk.LastBackup) < 24*time.Hour { return } if !backupLimiter.Acquire() { return } acquired, lockId := disksLock.LockOpen(dsk.Id.Hex()) if !acquired { backupLimiter.Release() return } go func() { defer func() { time.Sleep(1 * time.Second) disksLock.Unlock(dsk.Id.Hex(), lockId) backupLimiter.Release() }() db := database.GetDatabase() defer db.Close() if constants.Interrupt { return } if !dsk.IsActive() || dsk.Action != "" { return } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Info("deploy: Scheduling automatic disk backup") dsk.Action = disk.Backup dsk.LastBackup = time.Now() err := dsk.CommitFields(db, set.NewSet("action", "last_backup")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed update disk state") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") virt := d.stat.GetVirt(dsk.Instance) if virt == nil { err := &errortypes.ReadError{ errors.New("deploy: Failed to load virt"), } logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), "error": err, }).Error("deploy: Failed to load virt") return } err = data.CreateBackup(db, dsk, virt) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to backup disk") } dsk.Action = "" err = dsk.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed update disk state") time.Sleep(5 * time.Second) return } event.PublishDispatch(db, "disk.change") event.PublishDispatch(db, "image.change") }() } func (d *Disks) Deploy(db *database.Database) (err error) { disks := d.stat.Disks() backupHour := settings.System.DiskBackupTime backupWindow := settings.System.DiskBackupWindow utcHour := time.Now().UTC().Hour() backupActive := false if utcHour >= backupHour && utcHour <= (backupHour+backupWindow) { backupActive = true } for _, dsk := range disks { if dsk.State == disk.Provision { d.provision(dsk) } else if dsk.IsActive() { switch dsk.Action { case disk.Snapshot: d.snapshot(dsk) break case disk.Backup: d.backup(dsk) break case disk.Restore: d.restore(dsk) break case disk.Expand: d.expand(dsk) break case disk.Destroy: d.destroy(db, dsk) break default: if backupActive && dsk.Backup { d.scheduleBackup(dsk) } break } } } return } func NewDisks(stat *state.State) *Disks { return &Disks{ stat: stat, } } ================================================ FILE: deploy/imds.go ================================================ package deploy import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" ) var ( Hashes = map[bson.ObjectID]uint32{} ) type Imds struct { stat *state.State } func (s *Imds) buildInstance(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine) ( conf *types.Config, err error) { vc := s.stat.Vpc(inst.Vpc) var subnet *vpc.Subnet if vc != nil { subnet = vc.GetSubnet(inst.Subnet) } conf, err = imds.BuildConfig( inst, virt, nil, nil, vc, subnet, []*pod.Pod{}, map[bson.ObjectID][]*unit.Unit{}, map[bson.ObjectID]*deployment.Deployment{}, []*secret.Secret{}, []*certificate.Certificate{}, s.stat.GetDomains(inst.Organization), ) if err != nil { return } err = conf.ComputeHash() if err != nil { return } return } func (s *Imds) buildDeployInstance(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine) ( conf *types.Config, err error) { vc := s.stat.Vpc(inst.Vpc) var subnet *vpc.Subnet if vc != nil { subnet = vc.GetSubnet(inst.Subnet) } deply := s.stat.Deployment(virt.Deployment) if deply == nil { println("**************************************************1") println(inst.Id.Hex()) println("**************************************************1") return } spc := s.stat.Spec(deply.Spec) if spc == nil { println("**************************************************2") println(inst.Id.Hex()) println("**************************************************2") return } certs := []*certificate.Certificate{} for _, certId := range spc.Instance.Certificates { cert := s.stat.SpecCert(certId) if cert == nil || cert.Organization != inst.Organization { continue } certs = append(certs, cert) } secrs := []*secret.Secret{} for _, secrId := range spc.Instance.Secrets { secr := s.stat.SpecSecret(secrId) if secr == nil || secr.Organization != inst.Organization { continue } secrs = append(secrs, secr) } pods := []*pod.Pod{} podUnitsMap := map[bson.ObjectID][]*unit.Unit{} instPd := s.stat.Pod(deply.Pod) if instPd != nil { pods = append(pods, instPd) } instUnt := s.stat.Unit(deply.Unit) if instUnt != nil { podUnitsMap[deply.Pod] = append(podUnitsMap[deply.Pod], instUnt) } for _, podId := range spc.Instance.Pods { pd := s.stat.SpecPod(podId) if pd == nil || pd.Organization != inst.Organization { continue } pods = append(pods, pd) podUnits := s.stat.SpecPodUnits(podId) if podUnits != nil { podUnitsMap[podId] = podUnits } } conf, err = imds.BuildConfig( inst, virt, instUnt, spc, vc, subnet, pods, podUnitsMap, s.stat.DeploymentsDeployed(), secrs, certs, s.stat.GetDomains(inst.Organization), ) if err != nil { return } err = conf.ComputeHash() if err != nil { return } return } func (s *Imds) Deploy(db *database.Database) (err error) { instances := s.stat.Instances() confs := map[bson.ObjectID]*types.Config{} for _, inst := range instances { if !inst.IsActive() { continue } virt := s.stat.GetVirt(inst.Id) if virt == nil { continue } if virt.ImdsVersion < 1 { continue } var conf *types.Config if inst.Deployment.IsZero() { conf, err = s.buildInstance(db, inst, virt) if err != nil { return } } else { conf, err = s.buildDeployInstance(db, inst, virt) if err != nil { return } } if conf != nil { confs[inst.Id] = conf } } imds.SetConfigs(confs) return } func NewImds(stat *state.State) *Imds { return &Imds{ stat: stat, } } ================================================ FILE: deploy/instances.go ================================================ package deploy import ( "fmt" "sort" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/arp" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/info" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/netconf" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/qemu" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/qms" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) var ( instancesLock = utils.NewMultiTimeoutLock(5 * time.Minute) createLimiter = utils.NewLimiter(3) startLimiter = utils.NewLimiter(3) stopLimiter = utils.NewLimiter(3) diskLimiter = utils.NewLimiter(3) usbLimiter = utils.NewLimiter(1) ) type Instances struct { stat *state.State } func (s *Instances) create(inst *instance.Instance) { if !createLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpenTimeout( inst.Id.Hex(), 10*time.Minute) if !acquired { createLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) createLimiter.Release() }() db := database.GetDatabase() defer db.Close() err := qemu.Create(db, inst, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to create instance") err = instance.SetAction(db, inst.Id, instance.Stop) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to set instance state") qemu.PowerOff(db, inst.Virt) return } return } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) start(inst *instance.Instance) { if !startLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { startLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) startLimiter.Release() }() db := database.GetDatabase() defer db.Close() if inst.Restart || inst.RestartBlockIp { inst.Restart = false inst.RestartBlockIp = false err := inst.CommitFields(db, set.NewSet("restart", "restart_block_ip")) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to commit instance") return } } err := qemu.PowerOn(db, inst, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to start instance") err = instance.SetAction(db, inst.Id, instance.Stop) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to set instance state") qemu.PowerOff(db, inst.Virt) return } return } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) cleanup(inst *instance.Instance) { if !stopLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { stopLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) stopLimiter.Release() }() db := database.GetDatabase() defer db.Close() qemu.Cleanup(db, inst.Virt) err := instance.SetAction(db, inst.Id, instance.Stop) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to update instance") return } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) stop(inst *instance.Instance) { if !stopLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { stopLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) stopLimiter.Release() }() db := database.GetDatabase() defer db.Close() err := qemu.PowerOff(db, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to stop instance") return } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) restart(inst *instance.Instance) { if !stopLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { stopLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) stopLimiter.Release() }() db := database.GetDatabase() defer db.Close() err := qemu.PowerOff(db, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to restart instance") return } time.Sleep(1 * time.Second) if inst.Restart || inst.RestartBlockIp { inst.Restart = false inst.RestartBlockIp = false err = inst.CommitFields(db, set.NewSet("restart", "restart_block_ip")) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to commit instance") return } } err = qemu.PowerOn(db, inst, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to restart instance") err = instance.SetAction(db, inst.Id, instance.Stop) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to set instance state") qemu.PowerOff(db, inst.Virt) return } return } inst.Action = instance.Start err = inst.CommitFields(db, set.NewSet("action")) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to commit instance") return } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) destroy(inst *instance.Instance) { if !stopLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { stopLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) stopLimiter.Release() }() db := database.GetDatabase() defer db.Close() _, err := instance.Get(db, inst.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } return } err = qemu.Destroy(db, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to power off instance") return } err = netconf.Destroy(db, inst.Virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to destroy netconf") return } err = instance.Remove(db, inst.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to remove instance") return } } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "disk.change") }() } func (s *Instances) diskAdd(inst *instance.Instance, virt *vm.VirtualMachine, addDisks vm.SortDisks) { if !diskLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { diskLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) diskLimiter.Release() }() db := database.GetDatabase() defer db.Close() sort.Sort(addDisks) for _, dsk := range addDisks { err := permission.InitDisk(virt, dsk) if err != nil { return } err = qmp.AddDisk(inst.Id, dsk) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "disk_id": dsk.Id.Hex(), "error": err, }).Error("sync: Failed to add disk") return } } time.Sleep(200 * time.Millisecond) err := qemu.UpdateVmDisk(virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("sync: Failed to update vm disk state") } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "disk.change") }() } func (s *Instances) diskRemove(inst *instance.Instance, virt *vm.VirtualMachine, remDisks vm.SortDisks) { if !diskLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { diskLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) diskLimiter.Release() }() db := database.GetDatabase() defer db.Close() sort.Sort(sort.Reverse(remDisks)) for _, dsk := range remDisks { e := qmp.RemoveDisk(inst.Id, dsk) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "disk_id": dsk.Id.Hex(), "error": e, }).Error("sync: Failed to remove disk") return } } time.Sleep(200 * time.Millisecond) err := qemu.UpdateVmDisk(virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("sync: Failed to update vm disk state") } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "disk.change") }() } func (s *Instances) usbAdd(inst *instance.Instance, virt *vm.VirtualMachine, addUsbs []*vm.UsbDevice) { if !usbLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { usbLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) usbLimiter.Release() }() db := database.GetDatabase() defer db.Close() for _, device := range addUsbs { e := qms.AddUsb(virt, device) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "usb_address": device.Address, "usb_bus": device.Bus, "usb_product": device.Product, "usb_vendor": device.Vendor, "error": e, }).Error("sync: Failed to add usb") return } } time.Sleep(200 * time.Millisecond) err := qemu.UpdateVmUsb(virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("sync: Failed to update vm usb state") } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) usbRemove(inst *instance.Instance, virt *vm.VirtualMachine, remUsbs []*vm.UsbDevice) { if !usbLimiter.Acquire() { return } acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { usbLimiter.Release() return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { time.Sleep(3 * time.Second) instancesLock.Unlock(inst.Id.Hex(), lockId) usbLimiter.Release() }() db := database.GetDatabase() defer db.Close() for _, device := range remUsbs { err := qms.RemoveUsb(virt, device) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "usb_address": device.Address, "usb_bus": device.Bus, "usb_product": device.Product, "usb_vendor": device.Vendor, "error": err, }).Error("sync: Failed to remove usb") return } } time.Sleep(200 * time.Millisecond) err := qemu.UpdateVmUsb(virt) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("sync: Failed to update vm usb state") } event.PublishDispatch(db, "instance.change") }() } func (s *Instances) diff(db *database.Database, inst *instance.Instance) (err error) { curVirt := s.stat.GetVirt(inst.Id) if curVirt == nil { err = &errortypes.ReadError{ errors.New("deploy: Failed to load virt"), } logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to load virt") err = nil return } changed, reason := inst.Changed(curVirt) addDisks, remDisks := inst.DiskChanged(curVirt) addUsbs, remUsbs := inst.UsbChanged(curVirt) if instancesLock.Locked(inst.Id.Hex()) { return } if changed && !inst.Restart { inst.Restart = true inst.RestartReason = reason err = inst.CommitFields(db, set.NewSet("restart", "restart_reason")) if err != nil { return } } else if !changed && inst.Restart { inst.Restart = false inst.RestartReason = "" err = inst.CommitFields(db, set.NewSet("restart", "restart_reason")) if err != nil { return } } if len(remDisks) > 0 { s.diskRemove(inst, curVirt, remDisks) } if len(addDisks) > 0 { s.diskAdd(inst, curVirt, addDisks) } if len(remUsbs) > 0 { s.usbRemove(inst, curVirt, remUsbs) } if len(addUsbs) > 0 { s.usbAdd(inst, curVirt, addUsbs) } return } func (s *Instances) check(inst *instance.Instance, namespaces set.Set) ( valid bool, err error) { namespace := vm.GetNamespace(inst.Id, 0) if !namespaces.Contains(namespace) { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "net_namespace": namespace, }).Error("deploy: Instance missing namespace") return } valid = true return } func (s *Instances) routes(inst *instance.Instance) (err error) { acquired, lockId := instancesLock.LockOpen(inst.Id.Hex()) if !acquired { return } go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer func() { instancesLock.Unlock(inst.Id.Hex(), lockId) }() namespace := vm.GetNamespace(inst.Id, 0) vc := s.stat.Vpc(inst.Vpc) if vc == nil { err = &errortypes.NotFoundError{ errors.New("deploy: Instance vpc not found"), } logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "net_namespace": namespace, "error": err, }).Error("deploy: Failed to deploy instance routes") return } curRoutes := set.NewSet() curRoutes6 := set.NewSet() newRoutes := set.NewSet() newRoutes6 := set.NewSet() var icmpRedirects bool var routes []vpc.Route var routes6 []vpc.Route routesStore, ok := store.GetRoutes(inst.Id) if !ok { icmpRedirects, routes, routes6, err = qemu.GetRoutes(inst.Id) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "net_namespace": namespace, "error": err, }).Error("deploy: Failed to deploy instance routes") return } if routes == nil || routes6 == nil { return } store.SetRoutes(inst.Id, icmpRedirects, routes, routes6) } else { icmpRedirects = routesStore.IcmpRedirects routes = routesStore.Routes routes6 = routesStore.Routes6 } for _, route := range routes { curRoutes.Add(route) } for _, route := range routes6 { curRoutes6.Add(route) } if vc.Routes != nil { for _, route := range vc.Routes { if !strings.Contains(route.Destination, ":") { newRoutes.Add(*route) } else { newRoutes6.Add(*route) } } } changed := false addRoutes := newRoutes.Copy() addRoutes6 := newRoutes6.Copy() remRoutes := curRoutes.Copy() remRoutes6 := curRoutes6.Copy() addRoutes.Subtract(curRoutes) addRoutes6.Subtract(curRoutes6) remRoutes.Subtract(newRoutes) remRoutes6.Subtract(newRoutes6) if icmpRedirects != vc.IcmpRedirects { changed = true icmpRedirectsCtl := 0 if vc.IcmpRedirects { icmpRedirectsCtl = 1 } utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "sysctl", "-w", fmt.Sprintf("net.ipv4.conf.br0.send_redirects=%d", icmpRedirectsCtl), ) } for routeInf := range remRoutes.Iter() { route := routeInf.(vpc.Route) changed = true utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ip", "route", "del", route.Destination, "via", route.Target, "metric", "97", ) } for routeInf := range remRoutes6.Iter() { route := routeInf.(vpc.Route) changed = true utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ip", "-6", "route", "del", route.Destination, "via", route.Target, "metric", "97", ) } for routeInf := range addRoutes.Iter() { route := routeInf.(vpc.Route) changed = true utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "netns", "exec", namespace, "ip", "route", "add", route.Destination, "via", route.Target, "metric", "97", ) } for routeInf := range addRoutes6.Iter() { route := routeInf.(vpc.Route) changed = true utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "netns", "exec", namespace, "ip", "-6", "route", "add", route.Destination, "via", route.Target, "metric", "97", ) } if changed { store.RemRoutes(inst.Id) } var curRecords set.Set recordsStore, ok := store.GetArp(inst.Id) if !ok { curRecords, err = arp.GetRecords(namespace) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to deploy instance arp table") return } if routes == nil || routes6 == nil { return } store.SetArp(inst.Id, curRecords) } else { curRecords = recordsStore.Records } newRecords := s.stat.ArpRecords(namespace) if curRecords == nil || newRecords == nil { logrus.WithFields(logrus.Fields{ "cur_records_nil": curRecords == nil, "new_records_nil": newRecords == nil, }).Error("deploy: Missing arp records") return } changed, err = arp.ApplyState(namespace, curRecords, newRecords) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "error": err, }).Error("deploy: Failed to deploy instance arp table") return } if changed { store.RemArp(inst.Id) } }() return } func (s *Instances) Deploy(db *database.Database) (err error) { instances := s.stat.Instances() namespaces := s.stat.Namespaces() namespacesSet := set.NewSet() for _, namespace := range namespaces { namespacesSet.Add(namespace) } cpuUnits := 0 memoryUnits := 0.0 now := time.Now() infoTtl := time.Duration(settings.Hypervisor.InfoTtl) * time.Second for _, inst := range instances { virt := s.stat.GetVirt(inst.Id) if virt != nil { if inst.State == vm.Running && (virt.State == vm.Stopped || virt.State == vm.Failed) { inst.Action = instance.Cleanup s.stat.WaitAdd() go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer s.stat.WaitDone() err := virt.CommitState(db, instance.Cleanup) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("qemu: Failed to commit instance state") } }() } else { s.stat.WaitAdd() go func() { defer utils.RecoverLog("deploy: Panic in instance action") defer s.stat.WaitDone() err := virt.Commit(db) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("qemu: Failed to commit instance state") } }() } } if inst.Info == nil || now.Sub(inst.Info.Timestamp) > infoTtl { s.stat.WaitAdd() go func(inst *instance.Instance) { defer utils.RecoverLog("deploy: Panic in instance action") defer s.stat.WaitDone() inst.Info = info.NewInstance(s.stat, inst) err := inst.CommitFields(db, set.NewSet("info")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("qemu: Failed to commit instance info") } }(inst) } } instIds := set.NewSet() for _, inst := range instances { virt := s.stat.GetVirt(inst.Id) instIds.Add(inst.Id) if inst.Action == instance.Destroy { if inst.DeleteProtection { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), }).Info("deploy: Delete protection ignore instance destroy") if virt != nil && virt.State == vm.Running { inst.Action = instance.Start } else { inst.Action = instance.Stop } err = inst.CommitFields(db, set.NewSet("action")) if err != nil { return } event.PublishDispatch(db, "instance.change") } else { s.destroy(inst) } continue } cpuUnits += inst.Processors memoryUnits += float64(inst.Memory) / float64(1024) if virt == nil { if inst.Action == instance.Start { s.create(inst) } continue } switch inst.Action { case instance.Start: if virt.State == vm.Stopped || virt.State == vm.Failed { dsks := s.stat.GetInstaceDisks(inst.Id) for _, dsk := range dsks { if !dsk.IsActive() || dsk.Action != "" { continue } } s.start(inst) continue } valid, e := s.check(inst, namespacesSet) if e != nil { err = e return } if !valid { continue } err = s.diff(db, inst) if err != nil { return } err = s.routes(inst) if err != nil { return } break case instance.Cleanup: s.cleanup(inst) continue case instance.Stop: if virt.State == vm.Running { s.stop(inst) continue } break case instance.Restart: if virt.State == vm.Running { dsks := s.stat.GetInstaceDisks(inst.Id) for _, dsk := range dsks { if !dsk.IsActive() || dsk.Action != "" { continue } } s.restart(inst) continue } else if virt.State == vm.Stopped || virt.State == vm.Failed { inst.Action = instance.Start err = inst.CommitFields(db, set.NewSet("action")) if err != nil { return } continue } break } } virts := s.stat.VirtsMap() for _, virt := range virts { if !instIds.Contains(virt.Id) { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("sync: Unknown instance") } } node.Self.CpuUnitsRes = cpuUnits node.Self.MemoryUnitsRes = memoryUnits return } func NewInstances(stat *state.State) *Instances { return &Instances{ stat: stat, } } ================================================ FILE: deploy/ipset.go ================================================ package deploy import ( "github.com/pritunl/pritunl-cloud/ipset" "github.com/pritunl/pritunl-cloud/state" ) type Ipset struct { stat *state.State } func (t *Ipset) Deploy() (err error) { instaces := t.stat.Instances() namespaces := t.stat.Namespaces() nodeFirewall := t.stat.NodeFirewall() firewalls := t.stat.Firewalls() err = ipset.UpdateState(instaces, namespaces, nodeFirewall, firewalls) if err != nil { return } return } func (t *Ipset) Clean() (err error) { instaces := t.stat.Instances() nodeFirewall := t.stat.NodeFirewall() firewalls := t.stat.Firewalls() err = ipset.UpdateNamesState(instaces, nodeFirewall, firewalls) if err != nil { return } return } func NewIpset(stat *state.State) *Ipset { return &Ipset{ stat: stat, } } ================================================ FILE: deploy/iptables.go ================================================ package deploy import ( "github.com/pritunl/pritunl-cloud/iptables" "github.com/pritunl/pritunl-cloud/state" ) type Iptables struct { stat *state.State } func (t *Iptables) Deploy() (err error) { nodeSelf := t.stat.Node() vpcs := t.stat.Vpcs() instaces := t.stat.Instances() namespaces := t.stat.Namespaces() nodeFirewall := t.stat.NodeFirewall() firewalls := t.stat.Firewalls() firewallMaps := t.stat.FirewallMaps() iptables.UpdateStateRecover(nodeSelf, vpcs, instaces, namespaces, nodeFirewall, firewalls, firewallMaps) return } func NewIptables(stat *state.State) *Iptables { return &Iptables{ stat: stat, } } ================================================ FILE: deploy/namespace.go ================================================ package deploy import ( "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/interfaces" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) var ( firstRun = true namespaceLock = utils.NewMultiTimeoutLock(5 * time.Minute) namespaceLimiter = utils.NewLimiter(5) ) type Namespace struct { stat *state.State } func (n *Namespace) Deploy(db *database.Database) (err error) { instances := n.stat.Instances() namespaces := n.stat.Namespaces() ifaces := n.stat.Interfaces() curNamespaces := set.NewSet() curVirtIfaces := set.NewSet() curExternalIfaces := set.NewSet() nodeNetworkMode := node.Self.NetworkMode if nodeNetworkMode == "" { nodeNetworkMode = node.Dhcp } nodeNetworkMode6 := node.Self.NetworkMode6 if nodeNetworkMode6 == "" { nodeNetworkMode6 = node.Dhcp } externalNetwork := false if (nodeNetworkMode != node.Disabled && nodeNetworkMode != node.Cloud) || (nodeNetworkMode6 != node.Disabled && nodeNetworkMode6 != node.Cloud) { externalNetwork = true } for _, inst := range instances { if !inst.IsActive() { continue } curNamespaces.Add(vm.GetNamespace(inst.Id, 0)) if externalNetwork { curVirtIfaces.Add(vm.GetIfaceNodeExternal(inst.Id, 0)) } curVirtIfaces.Add(vm.GetIfaceNodeInternal(inst.Id, 0)) curVirtIfaces.Add(vm.GetIfaceHost(inst.Id, 0)) if externalNetwork { curExternalIfaces.Add(vm.GetIfaceExternal(inst.Id, 0)) } curVirtIfaces.Add(vm.GetIfaceNodePort(inst.Id, 0)) } firstRun = false for _, iface := range ifaces { if len(iface) != 14 || !(strings.HasPrefix(iface, "j") || strings.HasPrefix(iface, "r") || utils.HasPreSuf(iface, "h", "0") || utils.HasPreSuf(iface, "m", "0")) { continue } if !curVirtIfaces.Contains(iface) { utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "del", iface, ) interfaces.RemoveVirtIface(iface) } } for _, namespace := range namespaces { if len(namespace) != 14 || !strings.HasPrefix(namespace, "n") { continue } if !curNamespaces.Contains(namespace) { _, err = utils.ExecCombinedOutputLogged( []string{ "No such file", }, "ip", "netns", "del", namespace, ) if err != nil { return } } } return } func NewNamespace(stat *state.State) *Namespace { return &Namespace{ stat: stat, } } ================================================ FILE: deploy/network.go ================================================ package deploy import ( "fmt" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/bridges" "github.com/pritunl/pritunl-cloud/hnetwork" "github.com/pritunl/pritunl-cloud/interfaces" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vxlan" "github.com/sirupsen/logrus" ) var ( nodePortInitialized = false nodePortCurGateway = "" ) type Network struct { stat *state.State } func (d *Network) Deploy() (err error) { err = hnetwork.ApplyState(d.stat) if err != nil { return } err = NodePortApplyState(d.stat) if err != nil { return } err = vxlan.ApplyState(d.stat) if err != nil { return } interfaces.SyncIfaces(d.stat.VxLan()) return } func NewNetwork(stat *state.State) *Network { return &Network{ stat: stat, } } func nodePortCreate() (err error) { err = iproute.BridgeAdd("", settings.Hypervisor.NodePortNetworkName) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", settings.Hypervisor.NodePortNetworkName, "up", ) if err != nil { return } bridges.ClearCache() return } func nodePortGetAddr() (addr string, err error) { address, _, err := iproute.AddressGetIface( "", settings.Hypervisor.NodePortNetworkName) if err != nil { return } if address != nil { addr = address.Local + fmt.Sprintf("/%d", address.Prefix) } return } func nodePortSetAddr(addr string) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", settings.Hypervisor.NodePortNetworkName, "up", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "addr", "flush", "dev", settings.Hypervisor.NodePortNetworkName, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "addr", "add", addr, "dev", settings.Hypervisor.NodePortNetworkName, ) if err != nil { return } return } func nodePortClearAddr() (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", settings.Hypervisor.NodePortNetworkName, "up", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "addr", "flush", "dev", settings.Hypervisor.NodePortNetworkName, ) if err != nil { return } return } func nodePortRemoveNetwork(stat *state.State) (err error) { if nodePortCurGateway != "" || stat.HasInterfaces( settings.Hypervisor.HostNetworkName) { err = nodePortClearAddr() if err != nil { return } nodePortCurGateway = "" } return } func NodePortApplyState(stat *state.State) (err error) { initializeInst := false nodeNetName := settings.Hypervisor.NodePortNetworkName if !nodePortInitialized { addr, e := nodePortGetAddr() if e != nil { err = e return } initializeInst = true nodePortInitialized = true nodePortCurGateway = addr } if !stat.HasInterfaces(nodeNetName) { logrus.WithFields(logrus.Fields{ "iface": nodeNetName, }).Info("nodeport: Creating node port interface") err = nodePortCreate() if err != nil { return } } nodePortBlock, err := block.GetNodePortBlock(stat.Node().Id) if err != nil { return } gatewayCidr := nodePortBlock.GetGatewayCidr() if gatewayCidr == "" { logrus.WithFields(logrus.Fields{ "node_port_block": nodePortBlock.Id.Hex(), }).Error("nodeport: Node port network block gateway is invalid") err = nodePortRemoveNetwork(stat) if err != nil { return } return } if nodePortCurGateway != gatewayCidr { logrus.WithFields(logrus.Fields{ "node_port_block": nodePortBlock.Id.Hex(), "node_port_block_gateway": gatewayCidr, }).Info("nodeport: Updating node port network bridge") err = nodePortSetAddr(gatewayCidr) if err != nil { return } nodePortCurGateway = gatewayCidr initializeInst = true } if initializeInst { logrus.WithFields(logrus.Fields{ "node_port_block": nodePortBlock.Id.Hex(), }).Info("nodeport: Updating instance nodeport network") instances := stat.Instances() for _, inst := range instances { utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "set", vm.GetIfaceNodePort(inst.Id, 0), "master", nodeNetName, ) } } return } ================================================ FILE: deploy/services.go ================================================ package deploy import ( "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/scheduler" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var ( podsLock = utils.NewMultiTimeoutLock(3 * time.Minute) podsLimiter = utils.NewLimiter(50) ) type Pods struct { stat *state.State } func (s *Pods) processSchedule(schd *scheduler.Scheduler) { if !podsLimiter.Acquire() { return } acquired, lockId := podsLock.LockOpen(schd.Id.Hex()) if !acquired { return } go func() { defer func() { time.Sleep(1 * time.Second) podsLock.Unlock(schd.Id.Hex(), lockId) podsLimiter.Release() }() err := s.deploySchedule(schd) if err != nil { logrus.WithFields(logrus.Fields{ "unit": schd.Id.Hex(), "error": err, }).Error("deploy: Unit deploy failed") return } }() } func (s *Pods) deploySchedule(schd *scheduler.Scheduler) (err error) { db := database.GetDatabase() defer db.Close() unt, err := unit.Get(db, schd.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { logrus.WithFields(logrus.Fields{ "unit": schd.Id.Hex(), }).Warn("deploy: Canceling deployment on unit not found") err = schd.ClearTickets(db) if err != nil { return } return } return } spc, err := spec.Get(db, schd.Spec) if err != nil { if _, ok := err.(*database.NotFoundError); ok { logrus.WithFields(logrus.Fields{ "unit": schd.Id.Hex(), }).Warn("deploy: Canceling deployment on spec not found") err = schd.ClearTickets(db) if err != nil { return } return } return } tickets := schd.Tickets[s.stat.Node().Id] if len(tickets) > 0 { now := time.Now() for _, ticket := range tickets { start := schd.Created.Add( time.Duration(ticket.Offset) * time.Second) if now.After(start) { exists, e := schd.Refresh(db) if e != nil { err = e return } if !exists { logrus.WithFields(logrus.Fields{ "pod": unt.Pod.Hex(), "unit": unt.Id.Hex(), }).Info("deploy: Pod deploy schedule lost") return } if schd.Consumed >= schd.Count { return } if !schd.Ready() { logrus.WithFields(logrus.Fields{ "pod": unt.Pod.Hex(), "unit": unt.Id.Hex(), }).Info("deploy: Reached maximum schedule attempts") err = schd.ClearTickets(db) if err != nil { return } return } reserved, e := s.DeploySpec(db, schd, unt, spc) if e != nil { err = e limit, _ := schd.Failure(db) if limit { logrus.WithFields(logrus.Fields{ "pod": unt.Pod.Hex(), "unit": unt.Id.Hex(), }).Info("deploy: Reached maximum schedule attempts") } return } if reserved { err = schd.Consume(db) if err != nil { return } } else { limit, e := schd.Failure(db) if e != nil { err = e return } if limit { logrus.WithFields(logrus.Fields{ "pod": unt.Pod.Hex(), "unit": unt.Id.Hex(), }).Info("deploy: Reached maximum schedule attempts") } } } } } return } func (s *Pods) DeploySpec(db *database.Database, schd *scheduler.Scheduler, unt *unit.Unit, spc *spec.Spec) (reserved bool, err error) { img, err := image.Get(db, spc.Instance.Image) if err != nil { return } jrnls := []*deployment.Journal{} if spc.Journal != nil { for _, input := range spc.Journal.Inputs { jrnls = append(jrnls, &deployment.Journal{ Index: input.Index, Key: input.Key, Type: input.Type, }) } } deply := &deployment.Deployment{ Pod: unt.Pod, Unit: unt.Id, Organization: unt.Organization, Timestamp: time.Now(), Spec: spc.Id, Datacenter: node.Self.Datacenter, Zone: node.Self.Zone, Node: node.Self.Id, Kind: unt.Kind, State: deployment.Reserved, Journals: jrnls, } errData, err := spc.Refresh(db) if err != nil { return } if errData != nil { err = errData.GetError() return } errData, err = deply.Validate(db) if err != nil { return } if errData != nil { err = errData.GetError() return } err = deply.Insert(db) if err != nil { return } defer func() { if err != nil { e := deployment.Remove(db, deply.Id) if e != nil { logrus.WithFields(logrus.Fields{ "error": e, }).Error("deploy: Failed to cleanup deployment") return } } }() err = unt.Refresh(db) if err != nil { return } reserved, err = unt.Reserve(db, deply.Id, schd.OverrideCount) if err != nil { return } if !reserved { err = deployment.Remove(db, deply.Id) if err != nil { return } return } inst := &instance.Instance{ Organization: unt.Organization, Zone: spc.Instance.Zone, Vpc: spc.Instance.Vpc, Subnet: spc.Instance.Subnet, Shape: spc.Instance.Shape, Node: node.Self.Id, Image: spc.Instance.Image, Uefi: true, Tpm: spc.Instance.Tpm, Vnc: spc.Instance.Vnc, DhcpServer: spc.Instance.DhcpServer, CloudScript: "", DeleteProtection: spc.Instance.DeleteProtection, SkipSourceDestCheck: spc.Instance.SkipSourceDestCheck, Gui: spc.Instance.Gui, Name: spc.Name, Comment: "", InitDiskSize: 10, Memory: spc.Instance.Memory, Processors: spc.Instance.Processors, Roles: spc.Instance.Roles, SystemKind: img.GetSystemKind(), NoPublicAddress: false, NoPublicAddress6: false, NoHostAddress: false, Deployment: deply.Id, } switch img.GetSystemType() { case image.Bsd: inst.CloudType = instance.BSD inst.SecureBoot = false case image.LinuxUnsigned: inst.CloudType = instance.Linux inst.SecureBoot = false case image.LinuxLegacy: inst.CloudType = instance.LinuxLegacy inst.SecureBoot = true default: inst.CloudType = instance.Linux inst.SecureBoot = true } if spc.Instance.Uefi != nil { inst.Uefi = *spc.Instance.Uefi } if spc.Instance.SecureBoot != nil { inst.SecureBoot = *spc.Instance.SecureBoot } if spc.Instance.CloudType != "" { inst.CloudType = spc.Instance.CloudType } if spc.Instance.HostAddress != nil { inst.NoHostAddress = !*spc.Instance.HostAddress } if spc.Instance.PublicAddress != nil { inst.NoPublicAddress = !*spc.Instance.PublicAddress } else { inst.NoPublicAddress = node.Self.DefaultNoPublicAddress } if spc.Instance.PublicAddress6 != nil { inst.NoPublicAddress6 = !*spc.Instance.PublicAddress6 } else { inst.NoPublicAddress6 = node.Self.DefaultNoPublicAddress6 } if spc.Instance.DiskSize != 0 { inst.InitDiskSize = spc.Instance.DiskSize } if len(spc.Instance.NodePorts) > 0 { for _, ndePort := range spc.Instance.NodePorts { inst.NodePorts = append(inst.NodePorts, &nodeport.Mapping{ Protocol: ndePort.Protocol, ExternalPort: ndePort.ExternalPort, InternalPort: ndePort.InternalPort, }) } } for _, mount := range spc.Instance.Mounts { if mount.Type != spec.HostPath { continue } inst.Mounts = append(inst.Mounts, &instance.Mount{ Name: mount.Name, Type: instance.HostPath, Path: mount.Path, HostPath: mount.HostPath, }) } err = inst.GenerateId() if err != nil { return } errData, err = inst.Validate(db) if err != nil { return } if errData != nil { reserved = false err = errData.GetError() return } if len(inst.NodePorts) > 0 { err = inst.SyncNodePorts(db) if err != nil { return } } index := 0 reservedDisks := []*disk.Disk{} deplyMounts := []*deployment.Mount{} for _, mount := range spc.Instance.Mounts { if mount.Type != spec.Disk { continue } index += 1 diskReserved := false for _, dskId := range mount.Disks { dsk, e := disk.Get(db, dskId) if e != nil { err = e for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } return } if dsk.Node != node.Self.Id || !dsk.Instance.IsZero() { continue } diskReserved, err = dsk.Reserve(db, inst.Id, index, deply.Id) if err != nil { for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } return } if !diskReserved { continue } deplyMounts = append(deplyMounts, &deployment.Mount{ Disk: dsk.Id, Path: mount.Path, Uuid: dsk.Uuid, }) reservedDisks = append(reservedDisks, dsk) break } if !diskReserved { for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } logrus.WithFields(logrus.Fields{ "mount_path": mount.Path, }).Error("deploy: Failed to reserve disk for mount") err = deployment.Remove(db, deply.Id) if err != nil { return } reserved = false return } } err = inst.Insert(db) if err != nil { _ = inst.Cleanup(db) for _, dsk := range reservedDisks { err = dsk.Unreserve(db, inst.Id, deply.Id) if err != nil { return } } return } err = inst.Cleanup(db) if err != nil { return } deply.State = deployment.Deployed deply.Instance = inst.Id deply.Mounts = deplyMounts err = deply.CommitFields(db, set.NewSet("state", "instance", "mounts")) if err != nil { return } event.PublishDispatch(db, "pod.change") return } func (s *Pods) Deploy(db *database.Database) (err error) { schds := s.stat.Schedulers() for _, schd := range schds { if schd.Kind != scheduler.InstanceUnitKind { continue } if len(schd.Tickets) == 0 { deleted, e := scheduler.Remove(db, schd.Id) if e != nil { err = e return } if deleted { logrus.WithFields(logrus.Fields{ "unit": schd.Id.Hex(), }).Error("deploy: All nodes failed to schedule deployment") } } tickets := schd.Tickets[s.stat.Node().Id] if tickets != nil && len(tickets) > 0 { now := time.Now() for _, ticket := range tickets { start := schd.Created.Add( time.Duration(ticket.Offset) * time.Second) if now.After(start) { s.processSchedule(schd) break } } } } return } func NewPods(stat *state.State) *Pods { return &Pods{ stat: stat, } } ================================================ FILE: deployment/constants.go ================================================ package deployment import ( "time" "github.com/dropbox/godropbox/container/set" ) const ( Provision = "provision" Reserved = "reserved" Deployed = "deployed" Archived = "archived" Destroy = "destroy" Archive = "archive" Migrate = "migrate" Restore = "restore" Ready = "ready" Snapshot = "snapshot" Complete = "complete" Failed = "failed" Instance = "instance" Image = "image" Firewall = "firewall" Domain = "domain" Healthy = "healthy" Unknown = "unknown" Unhealthy = "unhealthy" ThresholdMin = 10 ActionLimit = 1 * time.Minute ) var ( ValidStates = set.NewSet( Provision, Reserved, Deployed, Archived, ) ValidActions = set.NewSet( "", Destroy, Archive, Migrate, Restore, ) ) ================================================ FILE: deployment/deployment.go ================================================ package deployment import ( "fmt" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Deployment struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Pod bson.ObjectID `bson:"pod" json:"pod"` Unit bson.ObjectID `bson:"unit" json:"unit"` Organization bson.ObjectID `bson:"organization" json:"organization"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Tags []string `bson:"tags" json:"tags"` Spec bson.ObjectID `bson:"spec" json:"spec"` NewSpec bson.ObjectID `bson:"new_spec" json:"new_spec"` Kind string `bson:"kind" json:"kind"` State string `bson:"state" json:"state"` Action string `bson:"action" json:"action"` Status string `bson:"status" json:"status"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Zone bson.ObjectID `bson:"zone" json:"zone"` Node bson.ObjectID `bson:"node" json:"node"` Instance bson.ObjectID `bson:"instance" json:"instance"` Image bson.ObjectID `bson:"image" json:"image"` Mounts []*Mount `bson:"mounts" json:"mounts"` Journals []*Journal `bson:"journals" json:"journals"` InstanceData *InstanceData `bson:"instance_data,omitempty" json:"instance_data"` ImageData *ImageData `bson:"image_data,omitempty" json:"image_data"` DomainData *DomainData `bson:"domain_data,omitempty" json:"domain_data"` Actions map[bson.ObjectID]*Action `bson:"actions,omitempty" json:"actions"` } type InstanceData struct { HostIps []string `bson:"host_ips" json:"host_ips"` PublicIps []string `bson:"public_ips" json:"public_ips"` PublicIps6 []string `bson:"public_ips6" json:"public_ips6"` PrivateIps []string `bson:"private_ips" json:"private_ips"` PrivateIps6 []string `bson:"private_ips6" json:"private_ips6"` CloudPrivateIps []string `bson:"cloud_private_ips" json:"cloud_private_ips"` CloudPublicIps []string `bson:"cloud_public_ips" json:"cloud_public_ips"` CloudPublicIps6 []string `bson:"cloud_public_ips6" json:"cloud_public_ips6"` } type DomainData struct { Records []*RecordData `bson:"records" json:"records"` } type RecordData struct { Domain string `bson:"domain" json:"domain"` Value string `bson:"value" json:"value"` } type ImageData struct { State string `bson:"state" json:"state"` } type Mount struct { Disk bson.ObjectID `bson:"disk" json:"disk"` Path string `bson:"path" json:"path"` Uuid string `bson:"uuid" json:"uuid"` } type Journal struct { Index int32 `bson:"index" json:"index"` Key string `bson:"key" json:"key"` Type string `bson:"type" json:"type"` } type Action struct { Statement bson.ObjectID `bson:"statement" json:"statement"` Since time.Time `bson:"since" json:"since"` Executed time.Time `bson:"executed" json:"executed"` Action string `bson:"action" json:"action"` } func (d *Deployment) IsHealthy() bool { return d.Status == Healthy } func (d *Deployment) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { if d.Timestamp.IsZero() { d.Timestamp = time.Now() } if d.Tags == nil { d.Tags = []string{} } tags := []string{} for _, tag := range d.Tags { tag = utils.FilterName(tag) if tag == "" || tag == "latest" { continue } tags = append(tags, tag) } d.Tags = tags if d.Actions == nil { d.Actions = map[bson.ObjectID]*Action{} } if !ValidStates.Contains(d.State) { errData = &errortypes.ErrorData{ Error: "invalid_state", Message: "Invalid deployment state", } return } if !ValidActions.Contains(d.Action) { errData = &errortypes.ErrorData{ Error: "invalid_action", Message: "Invalid deployment action", } return } switch d.Status { case Healthy: break case Unhealthy: break case Unknown: break case "": d.Status = Unhealthy break default: errData = &errortypes.ErrorData{ Error: "invalid_status", Message: "Deployment status is invalid", } return } switch d.Kind { case Instance: break case Image: break default: errData = &errortypes.ErrorData{ Error: "invalid_kind", Message: "Deployment kind is invalid", } return } return } func (d *Deployment) HandleStatement(db *database.Database, statementId bson.ObjectID, thresholdSec int, action string) ( newAction string, err error) { thresholdSec = utils.Max(ThresholdMin, thresholdSec) threshold := time.Duration(thresholdSec) * time.Second if action != "" { curAction := d.Actions[statementId] if curAction == nil { err = d.CommitAction(db, &Action{ Statement: statementId, Since: time.Now(), Action: action, }) if err != nil { return } newAction = "" return } else if curAction.Action != action { if !curAction.Executed.IsZero() && time.Since( curAction.Executed) < ActionLimit { newAction = "" return } curAction.Since = time.Now() curAction.Executed = time.Time{} curAction.Action = action err = d.CommitAction(db, curAction) if err != nil { return } newAction = "" return } else if time.Since(curAction.Since) >= threshold { if !curAction.Executed.IsZero() && time.Since( curAction.Executed) < ActionLimit { newAction = "" return } curAction.Executed = time.Now() err = d.CommitAction(db, curAction) if err != nil { return } newAction = action return } } else { curAction := d.Actions[statementId] if curAction != nil { if !curAction.Executed.IsZero() && time.Since( curAction.Executed) < ActionLimit { newAction = "" return } err = d.RemoveAction(db, curAction) if err != nil { return } } newAction = "" return } return } func (d *Deployment) SetImageState(state string) { if d.ImageData == nil { d.ImageData = &ImageData{} } d.ImageData.State = state } func (d *Deployment) GetImageState() string { if d.ImageData == nil { return "" } return d.ImageData.State } func (d *Deployment) ImageReady() bool { return !d.Image.IsZero() && d.ImageData != nil && d.ImageData.State == Complete } func (d *Deployment) CommitAction(db *database.Database, action *Action) (err error) { coll := db.Deployments() _, err = coll.UpdateOne(db, bson.M{ "_id": d.Id, }, bson.M{ "$set": bson.M{ fmt.Sprintf("actions.%s", action.Statement.Hex()): action, }, }) if err != nil { err = database.ParseError(err) return } return } func (d *Deployment) RemoveAction(db *database.Database, action *Action) (err error) { coll := db.Deployments() _, err = coll.UpdateOne(db, bson.M{ "_id": d.Id, }, bson.M{ "$unset": bson.M{ fmt.Sprintf("actions.%s", action.Statement.Hex()): "", }, }) if err != nil { err = database.ParseError(err) return } return } func (d *Deployment) Commit(db *database.Database) (err error) { coll := db.Deployments() err = coll.Commit(d.Id, d) if err != nil { return } return } func (d *Deployment) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Deployments() err = coll.CommitFields(d.Id, d, fields) if err != nil { return } return } func (d *Deployment) Insert(db *database.Database) (err error) { coll := db.Deployments() resp, err := coll.InsertOne(db, d) if err != nil { err = database.ParseError(err) return } d.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: deployment/utils.go ================================================ package deployment import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/journal" ) func Get(db *database.Database, deplyId bson.ObjectID) ( deply *Deployment, err error) { coll := db.Deployments() deply = &Deployment{} err = coll.FindOneId(deplyId, deply) if err != nil { return } return } func GetUnit(db *database.Database, unitId, deplyId bson.ObjectID) ( deply *Deployment, err error) { coll := db.Deployments() deply = &Deployment{} err = coll.FindOne(db, &bson.M{ "_id": deplyId, "unit": unitId, }).Decode(deply) if err != nil { err = database.ParseError(err) return } return } func GetOrg(db *database.Database, orgId, unitId bson.ObjectID) ( deply *Deployment, err error) { coll := db.Deployments() deply = &Deployment{} err = coll.FindOne(db, &bson.M{ "_id": unitId, "organization": orgId, }).Decode(deply) if err != nil { err = database.ParseError(err) return } return } func GetUnitOrg(db *database.Database, orgId, unitId, deplyId bson.ObjectID) ( deply *Deployment, err error) { coll := db.Deployments() deply = &Deployment{} err = coll.FindOne(db, &bson.M{ "_id": deplyId, "unit": unitId, "organization": orgId, }).Decode(deply) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( deplys []*Deployment, err error) { coll := db.Deployments() deplys = []*Deployment{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { deply := &Deployment{} err = cursor.Decode(deply) if err != nil { err = database.ParseError(err) return } deplys = append(deplys, deply) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllSorted(db *database.Database, query *bson.M) ( deplys []*Deployment, err error) { coll := db.Deployments() deplys = []*Deployment{} cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"timestamp", -1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { deply := &Deployment{} err = cursor.Decode(deply) if err != nil { err = database.ParseError(err) return } deplys = append(deplys, deply) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllActiveIds(db *database.Database) (deplyIds set.Set, err error) { coll := db.Deployments() deplyIds = set.NewSet() cursor, err := coll.Find( db, bson.M{ "state": bson.M{ "$in": []string{ Reserved, Deployed, }, }, }, options.Find(). SetProjection(bson.M{ "_id": 1, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { deply := &Deployment{} err = cursor.Decode(deply) if err != nil { err = database.ParseError(err) return } deplyIds.Add(deply.Id) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllStates(db *database.Database) ( deplysMap map[bson.ObjectID]*Deployment, err error) { coll := db.Deployments() deplysMap = map[bson.ObjectID]*Deployment{} cursor, err := coll.Find( db, bson.M{}, options.Find(). SetProjection(bson.M{ "_id": 1, "state": 1, "action": 1, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { deply := &Deployment{} err = cursor.Decode(deply) if err != nil { err = database.ParseError(err) return } deplysMap[deply.Id] = deply } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func RemoveDomains(db *database.Database, deplyId bson.ObjectID) ( err error) { recs, err := domain.GetRecordAll(db, &bson.M{ "deployment": deplyId, }) if err != nil { return } domnIdsSet := set.NewSet() for _, rec := range recs { domnIdsSet.Add(rec.Domain) } domnIds := []bson.ObjectID{} for domnIdInf := range domnIdsSet.Iter() { domnIds = append(domnIds, domnIdInf.(bson.ObjectID)) } if len(domnIds) > 0 { domns, e := domain.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": domnIds, }, }) if e != nil { err = e return } for _, domn := range domns { err = domn.LoadRecords(db, false) if err != nil { return } domn.PreCommit() changed := false for _, rec := range domn.Records { if rec.Deployment == deplyId { changed = true rec.Operation = domain.DELETE } } if changed { err = domn.CommitRecords(db) if err != nil { return } } } } event.PublishDispatch(db, "domain.change") return } func Remove(db *database.Database, deplyId bson.ObjectID) (err error) { coll := db.Deployments() err = journal.RemoveAll(db, deplyId) if err != nil { return } err = RemoveDomains(db, deplyId) if err != nil { return } _, err = coll.DeleteOne(db, &bson.M{ "_id": deplyId, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } event.PublishDispatch(db, "domain.change") event.PublishDispatch(db, "pod.change") return } func RemoveMulti(db *database.Database, unitId bson.ObjectID, deplyIds []bson.ObjectID) (err error) { coll := db.Deployments() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": deplyIds, }, "unit": unitId, }, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func ArchiveMulti(db *database.Database, unitId bson.ObjectID, deplyIds []bson.ObjectID) (err error) { coll := db.Deployments() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": deplyIds, }, "unit": unitId, "state": Deployed, }, &bson.M{ "$set": &bson.M{ "action": Archive, }, }) if err != nil { err = database.ParseError(err) return } return } func RestoreMulti(db *database.Database, unitId bson.ObjectID, deplyIds []bson.ObjectID) (err error) { coll := db.Deployments() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": deplyIds, }, "unit": unitId, "state": Archived, }, &bson.M{ "$set": &bson.M{ "action": Restore, }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: device/constants.go ================================================ package device const ( U2f = "u2f" WebAuthn = "webauthn" Secondary = "secondary" Phone = "phone" Call = "call" Message = "message" Low = 1 Medium = 5 High = 10 ) ================================================ FILE: device/device.go ================================================ package device import ( "crypto/ecdsa" "crypto/elliptic" "crypto/x509" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/go-webauthn/webauthn/webauthn" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Device struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` User bson.ObjectID `bson:"user" json:"user"` Name string `bson:"name" json:"name"` Type string `bson:"type" json:"type"` Mode string `bson:"mode" json:"mode"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Disabled bool `bson:"disabled" json:"disabled"` ActiveUntil time.Time `bson:"activeactive_until_until" json:"active_until"` LastActive time.Time `bson:"last_active" json:"last_active"` AlertLevels []int `bson:"alert_levels" json:"alert_levels"` Number string `bson:"number" json:"number"` U2fRaw []byte `bson:"u2f_raw" json:"-"` U2fCounter uint32 `bson:"u2f_counter" json:"-"` U2fKeyHandle []byte `bson:"u2f_key_handle" json:"-"` U2fPublicKey []byte `bson:"u2f_public_key" json:"-"` WanId []byte `bson:"wan_id" json:"-"` WanPublicKey []byte `bson:"wan_public_key" json:"-"` WanAttestationType string `bson:"wan_attestation_type" json:"-"` WanAuthenticator *webauthn.Authenticator `bson:"wan_authenticator" json:"-"` WanRpId string `bson:"wan_rp_id" json:"wan_rp_id"` } func (d *Device) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { d.Name = utils.FilterStr(d.Name, 32) if len(d.Name) == 0 { errData = &errortypes.ErrorData{ Error: "device_name_missing", Message: "Device name is required", } return } if len(d.Name) > 22 { errData = &errortypes.ErrorData{ Error: "device_name_invalid", Message: "Device name is too long", } return } switch d.Mode { case Secondary: if d.Type != U2f && d.Type != WebAuthn { errData = &errortypes.ErrorData{ Error: "device_type_invalid", Message: "Device type is invalid", } return } break case Phone: if d.Type != Call && d.Type != Message { errData = &errortypes.ErrorData{ Error: "device_type_invalid", Message: "Device type is invalid", } return } if len(d.Number) == 10 { d.Number = "+1" + d.Number } if len(d.Number) < 10 { errData = &errortypes.ErrorData{ Error: "device_number_invalid", Message: "Device phone number invalid", } return } break default: errData = &errortypes.ErrorData{ Error: "device_mode_invalid", Message: "Device mode is invalid", } return } if d.AlertLevels == nil { d.AlertLevels = []int{} } for _, level := range d.AlertLevels { switch level { case Low, Medium, High: break default: errData = &errortypes.ErrorData{ Error: "device_alert_level_invalid", Message: "Device alert level is invalid", } return } } return } func (d *Device) SetActive(db *database.Database) (err error) { d.LastActive = time.Now() err = d.CommitFields(db, set.NewSet("last_active")) if err != nil { return } return } func (d *Device) MarshalWebauthn(cred *webauthn.Credential) { if d.Type == U2f { d.U2fCounter = cred.Authenticator.SignCount } else { d.WanId = cred.ID d.WanPublicKey = cred.PublicKey d.WanAttestationType = cred.AttestationType d.WanAuthenticator = &cred.Authenticator } return } func (d *Device) UnmarshalWebauthn() (cred webauthn.Credential, err error) { if d.Type == U2f { pubKeyItf, e := x509.ParsePKIXPublicKey(d.U2fPublicKey) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "device: Failed to parse device public key"), } return } pubKey, ok := pubKeyItf.(*ecdsa.PublicKey) if !ok { err = &errortypes.ParseError{ errors.New("device: Device public key invalid type"), } return } pubKeyByte := elliptic.Marshal(pubKey.Curve, pubKey.X, pubKey.Y) cred = webauthn.Credential{ ID: d.U2fKeyHandle, PublicKey: pubKeyByte, AttestationType: "fido-u2f", Authenticator: webauthn.Authenticator{ AAGUID: d.U2fRaw, SignCount: d.U2fCounter, }, } return } cred = webauthn.Credential{ ID: d.WanId, PublicKey: d.WanPublicKey, AttestationType: d.WanAttestationType, Authenticator: *d.WanAuthenticator, } return } func (d *Device) CheckLevel(level int) bool { if d.AlertLevels == nil { return false } for _, lvl := range d.AlertLevels { if level == lvl { return true } } return false } func (d *Device) Commit(db *database.Database) (err error) { coll := db.Devices() err = coll.Commit(d.Id, d) if err != nil { return } return } func (d *Device) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Devices() err = coll.CommitFields(d.Id, d, fields) if err != nil { return } return } func (d *Device) Insert(db *database.Database) (err error) { coll := db.Devices() _, err = coll.InsertOne(db, d) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: device/facet.go ================================================ package device import ( "github.com/pritunl/pritunl-cloud/settings" ) type FacetVersion struct { Major int `json:"major"` Minor int `json:"minor"` } type TrustedFacet struct { Ids []string `json:"ids"` Version *FacetVersion `json:"version"` } type Facets struct { TrustedFacets []*TrustedFacet `json:"trustedFacets"` } func GetFacets() (facets *Facets) { return &Facets{ TrustedFacets: []*TrustedFacet{ &TrustedFacet{ Ids: settings.Local.Facets, Version: &FacetVersion{ Major: 1, Minor: 0, }, }, }, } } ================================================ FILE: device/utils.go ================================================ package device import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" ) func Get(db *database.Database, devcId bson.ObjectID) ( devc *Device, err error) { coll := db.Devices() devc = &Device{} err = coll.FindOneId(devcId, devc) if err != nil { return } return } func GetUser(db *database.Database, devcId bson.ObjectID, userId bson.ObjectID) (devc *Device, err error) { coll := db.Devices() devc = &Device{} err = coll.FindOne(db, &bson.M{ "_id": devcId, "user": userId, }).Decode(devc) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, userId bson.ObjectID) ( devices []*Device, err error) { coll := db.Devices() devices = []*Device{} cursor, err := coll.Find(db, &bson.M{ "user": userId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { devc := &Device{} err = cursor.Decode(devc) if err != nil { err = database.ParseError(err) return } devices = append(devices, devc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllSorted(db *database.Database, userId bson.ObjectID) ( devices []*Device, err error) { coll := db.Devices() devices = []*Device{} cursor, err := coll.Find(db, &bson.M{ "user": userId, }, options.Find(). SetSort(bson.D{ {"mode", 1}, {"name", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { devc := &Device{} err = cursor.Decode(devc) if err != nil { err = database.ParseError(err) return } devices = append(devices, devc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllMode(db *database.Database, userId bson.ObjectID, mode string) (devices []*Device, err error) { coll := db.Devices() devices = []*Device{} cursor, err := coll.Find(db, &bson.M{ "user": userId, "mode": mode, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { devc := &Device{} err = cursor.Decode(devc) if err != nil { err = database.ParseError(err) return } devices = append(devices, devc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func CountSecondary(db *database.Database, userId bson.ObjectID) ( count int64, err error) { coll := db.Devices() count, err = coll.CountDocuments(db, &bson.M{ "user": userId, "mode": Secondary, }) if err != nil { err = database.ParseError(err) return } return } func New(userId bson.ObjectID, typ, mode string) (devc *Device) { devc = &Device{ Id: bson.NewObjectID(), Type: typ, Mode: mode, User: userId, Timestamp: time.Now(), LastActive: time.Now(), } return } func Remove(db *database.Database, id bson.ObjectID) (err error) { coll := db.Devices() _, err = coll.DeleteOne(db, &bson.M{ "_id": id, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveUser(db *database.Database, id bson.ObjectID, userId bson.ObjectID) (err error) { coll := db.Devices() _, err = coll.DeleteOne(db, &bson.M{ "_id": id, "user": userId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveAll(db *database.Database, userId bson.ObjectID) (err error) { coll := db.Devices() _, err = coll.DeleteMany(db, &bson.M{ "user": userId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } ================================================ FILE: dhcpc/constants.go ================================================ package dhcpc import ( "time" ) const ( MaxMessageSize = 1500 DefaultInterval = 60 * time.Second DhcpTimeout = 10 * time.Second DhcpRetries = 3 ) ================================================ FILE: dhcpc/dhcpc.go ================================================ package dhcpc import ( "flag" "fmt" "net" "os" "strconv" "strings" "sync" "time" "github.com/pritunl/tools/logger" ) type Dhcpc struct { Interval time.Duration ImdsAddress string ImdsPort int ImdsSecret string DhcpIface string DhcpIface6 string DhcpIp *net.IPNet DhcpIp6 *net.IPNet lease *Lease syncTrigger chan struct{} } func (d *Dhcpc) startSync() { im := &Imds{ Address: d.ImdsAddress, Port: d.ImdsPort, Secret: d.ImdsSecret, } ticker := time.NewTicker(60 * time.Second) defer ticker.Stop() for { e := im.Sync(d.lease) if e != nil { logger.WithFields(logger.Fields{ "error": e, }).Error("dhcpc: Failed to sync lease with imds") time.Sleep(1 * time.Second) } logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), "gateway": d.lease.Gateway.String(), "server": d.lease.ServerAddress.String(), "time": d.lease.LeaseTime.String(), }).Info("dhcpc: Synced") select { case <-ticker.C: case <-d.syncTrigger: ticker.Reset(60 * time.Second) } } } func (d *Dhcpc) sync() { select { case d.syncTrigger <- struct{}{}: default: } } func (d *Dhcpc) run4() (err error) { d.lease.Gateway = nil d.lease.ServerAddress = nil d.lease.LeaseTime = 0 d.lease.TransactionId = "" for { for { ok, e := d.lease.Exchange4() if e != nil { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), "error": e, }).Error("dhcpc: Failed to exchange lease4") time.Sleep(500 * time.Millisecond) continue } if !ok { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, }).Error("dhcpc: Failed to receive lease4") time.Sleep(1000 * time.Millisecond) } break } logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), "gateway": d.lease.Gateway.String(), "server": d.lease.ServerAddress.String(), "time": d.lease.LeaseTime.String(), }).Info("dhcpc: Exchanged ipv4") d.sync() ready4 := false for i := 0; i < 20; i++ { ready4, _ = d.lease.IfaceReady() if ready4 { break } time.Sleep(500 * time.Millisecond) } if !ready4 { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), }).Error("dhcpc: Interface4 ready timeout") } for { time.Sleep(d.Interval) ready4, _ := d.lease.IfaceReady() if !ready4 { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), }).Error("dhcpc: Interface4 not ready") break } ok, e := d.lease.Renew4() if e != nil { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), "error": e, }).Error("dhcpc: Failed to renew lease4") break } if !ok { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), "error": err, }).Error("dhcpc: Failed to receive lease4 renewal") break } logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.lease.Address.String(), "gateway": d.lease.Gateway.String(), "server": d.lease.ServerAddress.String(), "time": d.lease.LeaseTime.String(), }).Info("dhcpc: Renewed ipv4") d.sync() } } } func (d *Dhcpc) run6() (err error) { d.lease.ServerAddress6 = nil d.lease.LeaseTime6 = 0 d.lease.TransactionId6 = "" d.lease.IaId6 = [4]byte{} d.lease.ServerId6 = nil for { for { ok, e := d.lease.Exchange6() if e != nil { logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), "error": e, }).Error("dhcpc: Failed to exchange lease6") time.Sleep(500 * time.Millisecond) continue } if !ok { logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, }).Error("dhcpc: Failed to receive lease6") time.Sleep(1000 * time.Millisecond) } break } logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), "server6": d.lease.ServerAddress6.String(), "time6": d.lease.LeaseTime6.String(), }).Info("dhcpc: Exchanged ipv6") d.sync() ready6 := false for i := 0; i < 20; i++ { _, ready6 = d.lease.IfaceReady() if ready6 { break } time.Sleep(500 * time.Millisecond) } if !ready6 { logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), }).Error("dhcpc: Interface6 ready timeout") } for { time.Sleep(d.Interval) _, ready6 := d.lease.IfaceReady() if !ready6 { logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), }).Error("dhcpc: Interface6 not ready") break } ok, e := d.lease.Renew6() if e != nil { logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), "error": e, }).Error("dhcpc: Failed to renew lease6") break } if !ok { logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), "error": err, }).Error("dhcpc: Failed to receive lease6 renewal") break } logger.WithFields(logger.Fields{ "interface6": d.DhcpIface6, "address6": d.lease.Address6.String(), "server6": d.lease.ServerAddress6.String(), "time6": d.lease.LeaseTime6.String(), }).Info("dhcpc: Renewed ipv6") d.sync() } } } func (d *Dhcpc) Run(ip4, ip6 bool) { d.lease = &Lease{ Iface: d.DhcpIface, Iface6: d.DhcpIface6, Address: d.DhcpIp, Address6: d.DhcpIp6, } go d.startSync() waiters := &sync.WaitGroup{} if ip4 { waiters.Add(1) go func() { defer waiters.Done() for { err := d.run4() if err != nil { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.DhcpIp, "address6": d.DhcpIp6, "error": err, }).Error("dhcpc: Run error") } time.Sleep(3 * time.Second) } }() } if ip6 { waiters.Add(1) go func() { defer waiters.Done() for { err := d.run6() if err != nil { logger.WithFields(logger.Fields{ "interface": d.DhcpIface, "address": d.DhcpIp, "address6": d.DhcpIp6, "error": err, }).Error("dhcpc: Run error") } time.Sleep(3 * time.Second) } }() } waiters.Wait() } func Main() (err error) { imdsAddress := os.Getenv("IMDS_ADDRESS") imdsPort := os.Getenv("IMDS_PORT") imdsSecret := os.Getenv("IMDS_SECRET") dhcpIface := os.Getenv("DHCP_IFACE") dhcpIface6 := os.Getenv("DHCP_IFACE6") dhcpIp := os.Getenv("DHCP_IP") dhcpIp6 := os.Getenv("DHCP_IP6") internval := os.Getenv("DHCP_INTERVAL") os.Unsetenv("IMDS_ADDRESS") os.Unsetenv("IMDS_PORT") os.Unsetenv("IMDS_SECRET") os.Unsetenv("DHCP_IFACE") os.Unsetenv("DHCP_IFACE6") os.Unsetenv("DHCP_IP") os.Unsetenv("DHCP_IP6") os.Unsetenv("DHCP_INTERVAL") logger.Init( logger.SetTimeFormat(""), ) logLock := sync.Mutex{} logger.AddHandler(func(record *logger.Record) { logLock.Lock() fmt.Print(record.String()) logLock.Unlock() }) ip4 := false flag.BoolVar(&ip4, "ip4", false, "Enable IPv4") ip6 := false flag.BoolVar(&ip6, "ip6", false, "Enable IPv6") flag.Parse() imdsPortInt, _ := strconv.Atoi(imdsPort) internvalInt, _ := strconv.Atoi(internval) client := &Dhcpc{ ImdsAddress: strings.Split(imdsAddress, "/")[0], ImdsPort: imdsPortInt, ImdsSecret: imdsSecret, DhcpIface: dhcpIface, DhcpIface6: dhcpIface6, syncTrigger: make(chan struct{}, 1), } if internvalInt != 0 { client.Interval = time.Duration(internvalInt) * time.Second } else { client.Interval = DefaultInterval } if dhcpIp != "" { ip, ipnet, _ := net.ParseCIDR(dhcpIp) if ip != nil && ipnet != nil { ipnet.IP = ip client.DhcpIp = ipnet } } if dhcpIp6 != "" { ip, ipnet, _ := net.ParseCIDR(dhcpIp6) if ip != nil && ipnet != nil { ipnet.IP = ip client.DhcpIp6 = ipnet } } client.Run(ip4, ip6) return } ================================================ FILE: dhcpc/imds.go ================================================ package dhcpc import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var ( httpClient = &http.Client{ Timeout: 10 * time.Second, } ) type SyncData struct { Address string `json:"address"` Gateway string `json:"gateway"` Address6 string `json:"address6"` Gateway6 string `json:"gateway6"` } type Imds struct { Address string `json:"address"` Port int `json:"port"` Secret string `json:"secret"` } func (m *Imds) NewRequest(method, pth string, data interface{}) ( req *http.Request, err error) { u := &url.URL{} u.Scheme = "http" u.Host = fmt.Sprintf("%s:%d", m.Address, m.Port) u.Path = pth var body io.Reader if data != nil { reqDataBuf := &bytes.Buffer{} err = json.NewEncoder(reqDataBuf).Encode(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcpc: Failed to parse request data"), } return } body = reqDataBuf } req, err = http.NewRequest(method, u.String(), body) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to create imds request"), } return } req.Header.Set("User-Agent", "pritunl-dhcp") req.Header.Set("Auth-Token", m.Secret) if data != nil { req.Header.Set("Content-Type", "application/json") } return } func (m *Imds) Sync(lease *Lease) (err error) { req, err := m.NewRequest("PUT", "/dhcp", lease) if err != nil { return } resp, err := httpClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Imds request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData := &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } if errData != nil && errData.Message != "" { body = errData.Message } err = &errortypes.RequestError{ errors.Newf("dhcpc: Imds server sync error %d - %s", resp.StatusCode, body), } return } return } ================================================ FILE: dhcpc/lease.go ================================================ package dhcpc import ( "net" "time" "github.com/insomniacslk/dhcp/dhcpv6" ) type Lease struct { Iface string `json:"iface"` Iface6 string `json:"iface6"` Address *net.IPNet `json:"address"` Gateway net.IP `json:"gateway"` Address6 *net.IPNet `json:"address6"` ServerAddress net.IP `json:"server"` ServerAddress6 net.IP `json:"server6"` LeaseTime time.Duration `json:"ttl"` LeaseTime6 time.Duration `json:"ttl6"` PreferredLifetime6 time.Duration `json:"-"` ValidLifetime6 time.Duration `json:"-"` TransactionId string `json:"-"` TransactionId6 string `json:"-"` IaId6 [4]byte `json:"-"` ServerId6 dhcpv6.DUID `json:"-"` } func (l *Lease) IfaceReady() (ready4, ready6 bool) { iface, err := net.InterfaceByName(l.Iface) if err != nil { return } addrs, _ := iface.Addrs() for _, addr := range addrs { ipnet, ok := addr.(*net.IPNet) if ok { if l.Address != nil && ipnet.IP.Equal(l.Address.IP) { ready4 = true } if l.Address6 != nil && ipnet.IP.Equal(l.Address6.IP) { ready6 = true } } } return } ================================================ FILE: dhcpc/lease4.go ================================================ package dhcpc import ( "context" "net" "github.com/dropbox/godropbox/errors" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" ) func (l *Lease) Renew4() (ok bool, err error) { if l.Address == nil || l.Address.IP == nil || l.ServerAddress == nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcpc: Cannot call renew with unset address"), } return } iface, err := net.InterfaceByName(l.Iface) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcpc: Failed to find interface"), } return } dhLease, err := buildDhLease(l, iface.HardwareAddr) if err != nil { return } serverAddr := &net.UDPAddr{ IP: l.ServerAddress, Port: nclient4.ServerPort, } client, err := nclient4.New(l.Iface, nclient4.WithServerAddr(serverAddr), nclient4.WithTimeout(DhcpTimeout), nclient4.WithRetry(DhcpRetries), nclient4.WithUnicast(&net.UDPAddr{ IP: l.Address.IP, Port: nclient4.ClientPort, }), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to create client"), } return } defer client.Close() ctx, cancel := context.WithTimeout(context.Background(), DhcpTimeout) defer cancel() renewedLease, err := client.Renew(ctx, dhLease, dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to exchange renewal"), } return } renewed := extractDhLease(renewedLease) if renewed != nil { ok = true l.Address = renewed.Address l.Gateway = renewed.Gateway l.ServerAddress = renewed.ServerAddress l.LeaseTime = renewed.LeaseTime l.TransactionId = renewed.TransactionId } return } func (l *Lease) Exchange4() (ok bool, err error) { client, err := nclient4.New( l.Iface, nclient4.WithTimeout(DhcpTimeout), nclient4.WithRetry(DhcpRetries), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to create client"), } return } defer client.Close() ctx, cancel := context.WithTimeout(context.Background(), DhcpTimeout) defer cancel() opts := []dhcpv4.Modifier{ dhcpv4.WithOption(dhcpv4.OptMaxMessageSize(MaxMessageSize)), } if l.Address != nil && l.Address.IP != nil { opts = append(opts, dhcpv4.WithOption( dhcpv4.OptRequestedIPAddress(l.Address.IP))) } nclientLease, err := client.Request( ctx, opts..., ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: IPv4 exchange failed"), } return } lease := extractDhLease(nclientLease) if lease != nil { ok = true l.Address = lease.Address l.Gateway = lease.Gateway l.ServerAddress = lease.ServerAddress l.LeaseTime = lease.LeaseTime l.TransactionId = lease.TransactionId } return } ================================================ FILE: dhcpc/lease6.go ================================================ package dhcpc import ( "context" "net" "github.com/dropbox/godropbox/errors" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/nclient6" "github.com/insomniacslk/dhcp/iana" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" ) func (l *Lease) Renew6() (ok bool, err error) { if l.Address6 == nil || l.Address6.IP == nil || l.ServerAddress == nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcpc: Cannot call renew with unset address"), } return } iface, err := net.InterfaceByName(l.Iface) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcpc: Failed to find interface"), } return } client, err := nclient6.New( l.Iface, nclient6.WithTimeout(DhcpTimeout), nclient6.WithRetry(DhcpRetries), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to create dhcp6 client"), } return } defer client.Close() ctx, cancel := context.WithTimeout(context.Background(), DhcpTimeout) defer cancel() serverAddr := &net.UDPAddr{ IP: l.ServerAddress6, Port: 547, } iaAddr := &dhcpv6.OptIAAddress{ IPv6Addr: l.Address6.IP, } if l.PreferredLifetime6 > 0 { iaAddr.PreferredLifetime = l.PreferredLifetime6 } if l.ValidLifetime6 > 0 { iaAddr.ValidLifetime = l.ValidLifetime6 } msg, err := dhcpv6.NewMessage() if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to create dhcp6 message"), } return } msg.MessageType = dhcpv6.MessageTypeRenew msg.AddOption(dhcpv6.OptClientID(&dhcpv6.DUIDLLT{ HWType: iana.HWTypeEthernet, LinkLayerAddr: iface.HardwareAddr, })) msg.AddOption(dhcpv6.OptServerID(l.ServerId6)) // msg.AddOption(&dhcpv6.OptFQDN{ // Flags: 0x01, // DomainName: &rfc1035label.Labels{ // Labels: []string{"instance-name"}, // }, // }) // msg.AddOption(dhcpv6.OptRequestedOption( // dhcpv6.OptionDNSRecursiveNameServer, // dhcpv6.OptionDomainSearchList, // )) // msg.AddOption(dhcpv6.OptElapsedTime(0)) msg.UpdateOption(&dhcpv6.OptIANA{ IaId: l.IaId6, Options: dhcpv6.IdentityOptions{ Options: []dhcpv6.Option{iaAddr}, }, }) reply, err := client.SendAndRead( ctx, serverAddr, msg, nclient6.IsMessageType(dhcpv6.MessageTypeReply), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to renew DHCPv6 lease"), } return } renewed := extractDhcpv6Lease(reply, l.Iface) if renewed != nil && renewed.Address6 != nil { ok = true l.Address6 = renewed.Address6 l.ServerAddress6 = renewed.ServerAddress6 l.PreferredLifetime6 = renewed.PreferredLifetime6 l.ValidLifetime6 = renewed.ValidLifetime6 l.LeaseTime6 = renewed.LeaseTime6 l.TransactionId6 = renewed.TransactionId6 l.ServerId6 = renewed.ServerId6 } return } func (l *Lease) Exchange6() (ok bool, err error) { iface, err := net.InterfaceByName(l.Iface) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcpc: Failed to find interface"), } return } client, err := nclient6.New( l.Iface, nclient6.WithTimeout(DhcpTimeout), nclient6.WithRetry(DhcpRetries), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: Failed to create DHCPv6 client"), } return } defer client.Close() ctx, cancel := context.WithTimeout(context.Background(), DhcpTimeout) defer cancel() l.IaId6 = [4]byte{0, 0, 0, 1} modifiers := []dhcpv6.Modifier{ dhcpv6.WithClientID(&dhcpv6.DUIDLLT{ HWType: iana.HWTypeEthernet, LinkLayerAddr: iface.HardwareAddr, }), dhcpv6.WithRequestedOptions( dhcpv6.OptionDNSRecursiveNameServer, dhcpv6.OptionDomainSearchList, ), //dhcpv6.WithFQDN(0x01, "instance-name"), dhcpv6.WithIAID(l.IaId6), } if l.Address6 != nil && l.Address6.IP != nil { iaAddr := &dhcpv6.OptIAAddress{ IPv6Addr: l.Address6.IP, } iaNa := &dhcpv6.OptIANA{ IaId: l.IaId6, Options: dhcpv6.IdentityOptions{ Options: []dhcpv6.Option{iaAddr}, }, } modifiers = append(modifiers, dhcpv6.WithOption(iaNa)) } reply, err := client.RapidSolicit(ctx, modifiers...) if err != nil { reply, err = client.Solicit(ctx, modifiers...) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "dhcpc: DHCPv6 exchange failed"), } return } } lease := extractDhcpv6Lease(reply, l.Iface) if lease != nil && lease.Address6 != nil { ok = true l.Address6 = lease.Address6 l.ServerAddress6 = lease.ServerAddress6 l.PreferredLifetime6 = lease.PreferredLifetime6 l.ValidLifetime6 = lease.ValidLifetime6 l.LeaseTime6 = lease.LeaseTime6 l.TransactionId6 = lease.TransactionId6 l.ServerId6 = lease.ServerId6 } return } func extractDhcpv6Lease(reply *dhcpv6.Message, ifaceName string) *Lease { if reply == nil { return nil } lease := &Lease{ Iface: ifaceName, TransactionId6: reply.TransactionID.String(), } serverID := reply.Options.ServerID() if serverID != nil { lease.ServerId6 = reply.Options.ServerID() } // // Extract server address from relay message or use link-local // relayMsg := reply.GetOneOption(dhcpv6.OptionRelayMsg) // if relayMsg != nil { // // Server address might be in relay message // if rm, ok := relayMsg.(*dhcpv6.OptRelayMessage); ok && rm.RelayMessage != nil { // lease.ServerAddress = rm.RelayMessage.PeerAddr // } // } // // Extract unicast server address from Option 12 if available // unicastOpt := reply.GetOneOption(dhcpv6.OptionUnicast) // if unicastOpt != nil { // // Option 12 contains the server's unicast IPv6 address // if unicastData := unicastOpt.ToBytes(); len(unicastData) >= 16 { // lease.ServerAddress6 = net.IP(unicastData[:16]) // } // } // Fallback to multicast if unicast not available if lease.ServerAddress6 == nil { lease.ServerAddress6 = dhcpv6.AllDHCPRelayAgentsAndServers } iana := reply.Options.OneIANA() if iana != nil { lease.IaId6 = iana.IaId for _, opt := range iana.Options.Options { if addr, ok := opt.(*dhcpv6.OptIAAddress); ok { lease.Address6 = &net.IPNet{ IP: addr.IPv6Addr, Mask: net.CIDRMask(64, 128), } lease.PreferredLifetime6 = addr.PreferredLifetime lease.ValidLifetime6 = addr.ValidLifetime lease.LeaseTime6 = addr.ValidLifetime break } } } return lease } ================================================ FILE: dhcpc/systemd.go ================================================ package dhcpc import ( "fmt" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/features" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) const systemdNamespaceTemplate = `[Unit] Description=Pritunl Cloud DHCP Client After=network.target [Service] Type=simple User=%s Environment="IMDS_ADDRESS=%s" Environment="IMDS_PORT=%d" Environment="IMDS_SECRET=%s" Environment="DHCP_IFACE=%s" Environment="DHCP_IFACE6=%s" Environment="DHCP_IP=%s" Environment="DHCP_IP6=%s" Environment="DHCP_INTERVAL=%d" ExecStart=/usr/bin/pritunl-cloud %s dhcp-client TimeoutStopSec=5 Restart=always RestartSec=3 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true NetworkNamespacePath=/var/run/netns/%s AmbientCapabilities=CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_NET_ADMIN ` const systemdTemplate = `[Unit] Description=Pritunl Cloud DHCP Client After=network.target [Service] Type=simple User=root Environment="IMDS_ADDRESS=%s" Environment="IMDS_PORT=%d" Environment="IMDS_SECRET=%s" Environment="DHCP_IFACE=%s" Environment="DHCP_IFACE6=%s" Environment="DHCP_IP=%s" Environment="DHCP_IP6=%s" Environment="DHCP_INTERVAL=%d" ExecStart=/usr/sbin/ip netns exec %s /usr/bin/pritunl-cloud %s dhcp-client TimeoutStopSec=5 Restart=always RestartSec=3 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true AmbientCapabilities=CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_NET_ADMIN ` func WriteService(vmId bson.ObjectID, namespace, imdsSecret, dhcpIface, dhcpIface6, dhcpIp, dhcpIp6 string, ip4, ip6, systemdNamespace bool) (err error) { unitPath := paths.GetUnitPathDhcpc(vmId) if imdsSecret == "" { err = &errortypes.ParseError{ errors.New("dhcpc: Cannot start dhcp client with empty secret"), } return } if dhcpIface == "" { err = &errortypes.ParseError{ errors.New("dhcpc: Cannot start dhcp client with empty iface"), } return } args := []string{} if ip4 { args = append(args, "-ip4") } if ip6 { args = append(args, "-ip6") } output := "" if systemdNamespace { output = fmt.Sprintf( systemdNamespaceTemplate, permission.GetUserName(vmId), settings.Hypervisor.ImdsAddress, settings.Hypervisor.ImdsPort, imdsSecret, dhcpIface, dhcpIface6, dhcpIp, dhcpIp6, settings.Hypervisor.DhcpRenewTtl, strings.Join(args, " "), namespace, ) } else { output = fmt.Sprintf( systemdTemplate, settings.Hypervisor.ImdsAddress, settings.Hypervisor.ImdsPort, imdsSecret, dhcpIface, dhcpIface6, dhcpIp, dhcpIp6, settings.Hypervisor.DhcpRenewTtl, strings.Join(args, " "), namespace, ) } err = utils.CreateWrite(unitPath, output, 0600) if err != nil { return } return } func Start(db *database.Database, virt *vm.VirtualMachine, iface, iface6 string, ip4, ip6 bool) (err error) { namespace := vm.GetNamespace(virt.Id, 0) hasSystemdNamespace := features.HasSystemdNamespace() unit := paths.GetUnitNameDhcpc(virt.Id) logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), "systemd_unit": unit, }).Info("dhcpc: Starting virtual machine dhcp client") _ = systemd.Stop(unit) err = WriteService(virt.Id, namespace, virt.ImdsDhcpSecret, iface, iface6, virt.DhcpIp, virt.DhcpIp6, ip4, ip6, hasSystemdNamespace) if err != nil { return } err = systemd.Reload() if err != nil { return } err = systemd.Start(unit) if err != nil { return } return } func Stop(virt *vm.VirtualMachine) (err error) { unit := paths.GetUnitNameDhcpc(virt.Id) _ = systemd.Stop(unit) return } ================================================ FILE: dhcpc/utils.go ================================================ package dhcpc import ( "encoding/hex" "net" "github.com/dropbox/godropbox/errors" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/nclient4" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" ) func buildDhLease(lease *Lease, addr net.HardwareAddr) ( dhLease *nclient4.Lease, err error) { xid := TransactionIdUnmarshal(lease.TransactionId) offer, err := dhcpv4.New( dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer), dhcpv4.WithTransactionID(xid), dhcpv4.WithHwAddr(addr), dhcpv4.WithYourIP(lease.Address.IP), dhcpv4.WithServerIP(lease.ServerAddress), dhcpv4.WithGatewayIP(lease.Gateway), dhcpv4.WithOption(dhcpv4.OptSubnetMask( net.IPMask(lease.Address.Mask))), dhcpv4.WithOption(dhcpv4.OptRouter(lease.Gateway)), dhcpv4.WithOption(dhcpv4.OptServerIdentifier(lease.ServerAddress)), dhcpv4.WithOption(dhcpv4.OptIPAddressLeaseTime(lease.LeaseTime)), ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcpc: Failed to create offer message"), } return } ack, err := dhcpv4.New( dhcpv4.WithMessageType(dhcpv4.MessageTypeAck), dhcpv4.WithTransactionID(xid), dhcpv4.WithHwAddr(addr), dhcpv4.WithYourIP(lease.Address.IP), dhcpv4.WithServerIP(lease.ServerAddress), dhcpv4.WithGatewayIP(lease.Gateway), dhcpv4.WithOption(dhcpv4.OptSubnetMask( net.IPMask(lease.Address.Mask))), dhcpv4.WithOption(dhcpv4.OptRouter(lease.Gateway)), dhcpv4.WithOption(dhcpv4.OptServerIdentifier(lease.ServerAddress)), dhcpv4.WithOption(dhcpv4.OptIPAddressLeaseTime(lease.LeaseTime)), ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcpc: Failed to create ack message"), } return } dhLease = &nclient4.Lease{ Offer: offer, ACK: ack, } return } func extractDhLease(dhLease *nclient4.Lease) (lease *Lease) { if dhLease == nil || dhLease.ACK == nil { return } ack := dhLease.ACK lease = &Lease{ Address: &net.IPNet{ IP: ack.YourIPAddr, Mask: net.IPMask(net.IP{255, 255, 255, 0}), }, Gateway: ack.GatewayIPAddr, ServerAddress: ack.ServerIPAddr, TransactionId: ack.TransactionID.String(), } if subnet := ack.SubnetMask(); subnet != nil { lease.Address.Mask = subnet } if lease.Gateway.Equal(net.IPv4zero) || lease.Gateway == nil { if routers := ack.Router(); len(routers) > 0 { lease.Gateway = routers[0] } } serverID := ack.ServerIdentifier() if serverID != nil { lease.ServerAddress = serverID } leaseTime := ack.IPAddressLeaseTime(0) if leaseTime > 0 { lease.LeaseTime = leaseTime } return } func TransactionIdUnmarshal(str string) dhcpv4.TransactionID { var tranId dhcpv4.TransactionID if len(str) >= 2 && str[:2] == "0x" { str = str[2:] } bytes, err := hex.DecodeString(str) if err != nil { return tranId } if len(bytes) != 4 { return tranId } copy(tranId[:], bytes) return tranId } ================================================ FILE: dhcps/dhcp4.go ================================================ package dhcps import ( "fmt" "net" "time" "github.com/dropbox/godropbox/errors" "github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4/server4" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type Server4 struct { Iface string `json:"iface"` ClientIp string `json:"client_ip"` GatewayIp string `json:"gateway_ip"` PrefixLen int `json:"prefix_len"` DnsServers []string `json:"dns_servers"` Mtu int `json:"mtu"` Lifetime int `json:"lifetime"` Debug bool `json:"debug"` dnsServersIp []net.IP server *server4.Server lifetime time.Duration } func (s *Server4) handler(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) { err := s.handleMsg(conn, peer, req) if err != nil { if s.Debug { logrus.WithFields(logrus.Fields{ "peer": peer.String(), "error": err, }).Error("dhcps: DHCPv4 handler error") } } } func (s *Server4) handleMsg(conn net.PacketConn, peer net.Addr, req *dhcpv4.DHCPv4) (err error) { if req.MessageType() != dhcpv4.MessageTypeDiscover && req.MessageType() != dhcpv4.MessageTypeRequest { return } if s.Debug { fmt.Printf("Peer: %s\n", peer.String()) fmt.Println(req.Summary()) } resp, err := dhcpv4.NewReplyFromRequest(req) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to create reply"), } return } if req.MessageType() == dhcpv4.MessageTypeRequest { resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) } else { resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) } gatewayIp := net.ParseIP(s.GatewayIp) clientIp := net.ParseIP(s.ClientIp) resp.YourIPAddr = clientIp resp.UpdateOption(dhcpv4.OptRouter(gatewayIp)) resp.UpdateOption(dhcpv4.OptSubnetMask( net.CIDRMask(s.PrefixLen, net.IPv4len*8))) resp.UpdateOption(dhcpv4.OptServerIdentifier(gatewayIp)) resp.UpdateOption(dhcpv4.OptIPAddressLeaseTime(s.lifetime)) requested := req.ParameterRequestList() if requested.Has(dhcpv4.OptionDomainNameServer) { resp.UpdateOption(dhcpv4.OptDNS(s.dnsServersIp...)) } if s.Mtu != 0 { resp.UpdateOption(dhcpv4.Option{ Code: dhcpv4.OptionInterfaceMTU, Value: dhcpv4.Uint16(s.Mtu), }) } if s.Debug { fmt.Printf("Peer: %s\n", peer.String()) fmt.Println(resp.Summary()) } _, err = conn.WriteTo(resp.ToBytes(), peer) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dhcps: DHCPv4 resp write error"), } return } return } func (s *Server4) Start() (err error) { logrus.WithFields(logrus.Fields{ "iface": s.Iface, "client_ip": s.ClientIp, "gateway_ip": s.GatewayIp, "prefix_len": s.PrefixLen, "dns_servers": s.DnsServers, "mtu": s.Mtu, "lifetime": s.Lifetime, "debug": s.Debug, }).Info("dhcps: Starting server4") s.lifetime = time.Duration(s.Lifetime) * time.Second if s.DnsServers != nil && len(s.DnsServers) > 0 { dnsServers := []net.IP{} for _, dnsServer := range s.DnsServers { dnsServers = append(dnsServers, net.ParseIP(dnsServer)) } s.dnsServersIp = dnsServers } host4 := &net.UDPAddr{ Port: dhcpv4.ServerPort, } s.server, err = server4.NewServer(s.Iface, host4, s.handler) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dhcps: Failed to create server4"), } return } err = s.server.Serve() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dhcps: Failed to start server4"), } return } return } ================================================ FILE: dhcps/dhcp6.go ================================================ package dhcps import ( "fmt" "net" "time" "github.com/dropbox/godropbox/errors" "github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6/server6" "github.com/insomniacslk/dhcp/iana" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type Server6 struct { Iface string `json:"iface"` ClientIp string `json:"client_ip"` GatewayIp string `json:"gateway_ip"` PrefixLen int `json:"prefix_len"` DnsServers []string `json:"dns_servers"` Mtu int `json:"mtu"` Lifetime int `json:"lifetime"` Debug bool `json:"debug"` serverId dhcpv6.DUID dnsServersIp []net.IP server *server6.Server lifetime time.Duration prefix *net.IPNet clientAddr net.IP } func (s *Server6) handler(conn net.PacketConn, peer net.Addr, req dhcpv6.DHCPv6) { err := s.handleMsg(conn, peer, req) if err != nil { if s.Debug { logrus.WithFields(logrus.Fields{ "peer": peer.String(), "error": err, }).Error("dhcps: DHCPv6 handler error") } } } func (s *Server6) handleMsg(conn net.PacketConn, peer net.Addr, req dhcpv6.DHCPv6) (err error) { msg, err := req.GetInnerMessage() if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: DHCPv6 get inner message error"), } return } clientId := msg.Options.ClientID() if clientId == nil { err = &errortypes.ParseError{ errors.New("dhcps: DHCPv6 missing client id"), } return } serverId := msg.Options.ServerID() if s.Debug { fmt.Printf("Peer: %s\n", peer.String()) fmt.Println(msg.Summary()) } switch msg.Type() { case dhcpv6.MessageTypeSolicit, dhcpv6.MessageTypeConfirm, dhcpv6.MessageTypeRebind: if serverId != nil { err = &errortypes.ParseError{ errors.New("dhcps: DHCPv6 invalid server id"), } return } case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeRenew, dhcpv6.MessageTypeRelease, dhcpv6.MessageTypeDecline: if serverId == nil { err = &errortypes.ParseError{ errors.New("dhcps: DHCPv6 missing server id"), } return } if !serverId.Equal(s.serverId) { err = &errortypes.ParseError{ errors.New("dhcps: DHCPv6 server id mismatch"), } return } } var resp dhcpv6.DHCPv6 switch msg.Type() { case dhcpv6.MessageTypeSolicit: rapidCommit := msg.GetOneOption(dhcpv6.OptionRapidCommit) if rapidCommit != nil { resp, err = dhcpv6.NewReplyFromMessage(msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: DHCPv6 new reply "+ "from message error"), } return } } else { resp, err = dhcpv6.NewAdvertiseFromSolicit(msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: DHCPv6 new advertise "+ "from solicit error"), } return } } break case dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeConfirm, dhcpv6.MessageTypeRenew, dhcpv6.MessageTypeRebind, dhcpv6.MessageTypeRelease, dhcpv6.MessageTypeInformationRequest: resp, err = dhcpv6.NewReplyFromMessage(msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: DHCPv6 new reply "+ "from message error"), } return } break default: err = &errortypes.ParseError{ errors.New("dhcps: Unknown DHCPv6 message type"), } return } resp.AddOption(dhcpv6.OptServerID(s.serverId)) err = s.process(msg, req, resp) if err != nil { return } if s.Debug { fmt.Printf("Peer: %s\n", peer.String()) fmt.Println(resp.Summary()) } _, err = conn.WriteTo(resp.ToBytes(), peer) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dhcps: DHCPv6 reply write error"), } return } return } func (s *Server6) process(msg *dhcpv6.Message, req, resp dhcpv6.DHCPv6) (err error) { switch msg.Type() { case dhcpv6.MessageTypeSolicit, dhcpv6.MessageTypeRequest, dhcpv6.MessageTypeConfirm, dhcpv6.MessageTypeRenew, dhcpv6.MessageTypeRebind: break default: err = &errortypes.ParseError{ errors.Newf("dhcps: DHCPv6 ignore message type %s", msg.Type()), } return } oia := &dhcpv6.OptIANA{ T1: s.lifetime / 2, T2: time.Duration(float32(s.lifetime) / 1.5), } roia := msg.Options.OneIANA() if roia != nil { copy(oia.IaId[:], roia.IaId[:]) } else { copy(oia.IaId[:], []byte("CLOUD")) } oiaAddr := &dhcpv6.OptIAAddress{ IPv6Addr: s.clientAddr, PreferredLifetime: s.lifetime, ValidLifetime: s.lifetime, } oia.Options = dhcpv6.IdentityOptions{ Options: []dhcpv6.Option{ oiaAddr, }, } resp.AddOption(oia) iapd := msg.Options.OneIAPD() if iapd != nil { respIapd := &dhcpv6.OptIAPD{ T1: s.lifetime / 2, T2: time.Duration(float32(s.lifetime) / 1.5), } copy(respIapd.IaId[:], iapd.IaId[:]) prefixOpt := &dhcpv6.OptIAPrefix{ PreferredLifetime: s.lifetime, ValidLifetime: s.lifetime, Prefix: s.prefix, } respIapd.Options = dhcpv6.PDOptions{ Options: []dhcpv6.Option{ prefixOpt, }, } resp.AddOption(respIapd) } if msg.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) && s.dnsServersIp != nil { resp.UpdateOption(dhcpv6.OptDNS(s.dnsServersIp...)) } fqdn := msg.GetOneOption(dhcpv6.OptionFQDN) if fqdn != nil { resp.AddOption(fqdn) } resp.AddOption(&dhcpv6.OptStatusCode{ StatusCode: iana.StatusSuccess, StatusMessage: "success", }) return } func (s *Server6) Start() (err error) { logrus.WithFields(logrus.Fields{ "iface": s.Iface, "client_ip": s.ClientIp, "gateway_ip": s.GatewayIp, "prefix_len": s.PrefixLen, "dns_servers": s.DnsServers, "mtu": s.Mtu, "lifetime": s.Lifetime, "debug": s.Debug, }).Info("dhcps: Starting server6") s.lifetime = time.Duration(s.Lifetime) * time.Second iface, err := net.InterfaceByName(s.Iface) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcps: Failed to find network interface"), } return } clientAddr, prefix, err := net.ParseCIDR(fmt.Sprintf( "%s/%d", s.ClientIp, s.PrefixLen)) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to parse client IP and prefix"), } return } s.clientAddr = clientAddr s.prefix = prefix if !s.prefix.Contains(s.clientAddr) { err = &errortypes.ParseError{ errors.New("dhcps: Client IP not within prefix"), } return } if len(s.DnsServers) > 0 { dnsServers := []net.IP{} for _, dnsServer := range s.DnsServers { dnsServers = append(dnsServers, net.ParseIP(dnsServer)) } s.dnsServersIp = dnsServers } s.serverId = &dhcpv6.DUIDLLT{ HWType: iana.HWTypeEthernet, LinkLayerAddr: iface.HardwareAddr, Time: dhcpv6.GetTime(), } host6 := &net.UDPAddr{ IP: net.ParseIP("::"), Port: dhcpv6.DefaultServerPort, } s.server, err = server6.NewServer(iface.Name, host6, s.handler) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dhcps: Failed to create server6"), } return } err = s.server.Serve() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dhcps: Failed to start server6"), } return } return } ================================================ FILE: dhcps/ndp.go ================================================ package dhcps import ( "fmt" "net" "net/netip" "time" "github.com/dropbox/godropbox/errors" "github.com/mdlayher/ndp" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type ServerNdp struct { Iface string `json:"iface"` ClientIp string `json:"client_ip"` GatewayIp string `json:"gateway_ip"` PrefixLen int `json:"prefix_len"` DnsServers []string `json:"dns_servers"` Mtu int `json:"mtu"` Lifetime int `json:"lifetime"` Delay int `json:"delay"` Debug bool `json:"debug"` iface *net.Interface gatewayAddr netip.Addr prefixAddr netip.Addr lifetime time.Duration delay time.Duration } func (s *ServerNdp) Start() (err error) { logrus.WithFields(logrus.Fields{ "iface": s.Iface, "client_ip": s.ClientIp, "gateway_ip": s.GatewayIp, "prefix_len": s.PrefixLen, "dns_servers": s.DnsServers, "mtu": s.Mtu, "lifetime": s.Lifetime, "delay": s.Delay, "debug": s.Debug, }).Info("dhcps: Starting ndp server") s.lifetime = time.Duration(s.Lifetime) * time.Second s.delay = time.Duration(s.Delay) * time.Second s.iface, err = net.InterfaceByName(s.Iface) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcps: Failed to find network interface"), } return } s.gatewayAddr, err = netip.ParseAddr(s.GatewayIp) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to parse gateway addr"), } return } _, prefixNet, err := net.ParseCIDR(fmt.Sprintf( "%s/%d", s.ClientIp, s.PrefixLen)) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to parse client IP and prefix"), } return } prefix, err := netip.ParsePrefix(prefixNet.String()) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to parse client addr and prefix"), } return } s.prefixAddr = prefix.Masked().Addr() for { err = s.run() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("dhcps: NDP server error") } time.Sleep(s.delay) } } func (s *ServerNdp) run() (err error) { conn, _, err := ndp.Listen(s.iface, ndp.LinkLocal) if err != nil { err = &errortypes.NetworkError{ errors.Wrap(err, "dhcps: Failed to listen for NDP messages"), } return } defer conn.Close() err = conn.JoinGroup(netip.MustParseAddr("ff02::2")) if err != nil { err = &errortypes.NetworkError{ errors.Wrap(err, "dhcps: Failed to join NDP group"), } return } err = s.sendAdvertise(conn, netip.IPv6LinkLocalAllNodes()) if err != nil { return } return } func (s *ServerNdp) sendAdvertise(conn *ndp.Conn, dst netip.Addr) (err error) { opts := []ndp.Option{ &ndp.PrefixInformation{ Prefix: s.prefixAddr, PrefixLength: uint8(s.PrefixLen), OnLink: true, AutonomousAddressConfiguration: false, ValidLifetime: s.lifetime, PreferredLifetime: s.lifetime, }, &ndp.LinkLayerAddress{ Direction: ndp.Source, Addr: s.iface.HardwareAddr, }, } if s.Mtu != 0 { opts = append(opts, &ndp.MTU{ MTU: uint32(s.Mtu), }) } if len(s.DnsServers) > 0 { dnsAddrs := make([]netip.Addr, 0, len(s.DnsServers)) for _, dns := range s.DnsServers { addr, err := netip.ParseAddr(dns) if err == nil && addr.Is6() { dnsAddrs = append(dnsAddrs, addr) } } if len(dnsAddrs) > 0 { opts = append(opts, &ndp.RecursiveDNSServer{ Lifetime: s.lifetime, Servers: dnsAddrs, }) } } msgRa := &ndp.RouterAdvertisement{ CurrentHopLimit: 64, RouterSelectionPreference: ndp.Medium, RouterLifetime: s.lifetime, ManagedConfiguration: true, OtherConfiguration: true, Options: opts, } if s.Debug { logrus.WithFields(logrus.Fields{ "gateway": s.gatewayAddr.String(), "prefix": s.prefixAddr.String(), "prefix_len": s.PrefixLen, "router_lifetime": s.lifetime, }).Info("dhcps: Sending router advertisement") } err = conn.WriteTo(msgRa, nil, dst) if err != nil { err = &errortypes.NetworkError{ errors.Wrap(err, "dhcps: Failed to write NDP message"), } return } return } func (s *ServerNdp) readSolicitations(conn *ndp.Conn) (err error) { if s.Debug { logrus.WithFields(logrus.Fields{ "gateway": s.gatewayAddr.String(), "prefix": s.prefixAddr.String(), "prefix_len": s.PrefixLen, "router_lifetime": s.lifetime, }).Info("dhcps: Reading router solicitations") } err = conn.SetReadDeadline(time.Now().Add(200 * time.Millisecond)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "dhcps: Failed to set deadline"), } return } msg, _, from, err := conn.ReadFrom() if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { err = nil return } err = &errortypes.ReadError{ errors.Wrap(err, "dhcps: Failed to read NDP message"), } return } if _, ok := msg.(*ndp.RouterSolicitation); ok { if s.Debug { logrus.WithFields(logrus.Fields{ "from": from.String(), }).Info("dhcps: Received Router Solicitation") } err = s.sendAdvertise(conn, from) if err != nil { return } } return } ================================================ FILE: dhcps/systemd.go ================================================ package dhcps import ( "encoding/json" "fmt" "os" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/features" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" ) const ( dhcpCaps = "CAP_NET_BIND_SERVICE CAP_NET_BROADCAST" ndpCaps = "CAP_NET_BIND_SERVICE CAP_NET_BROADCAST CAP_NET_RAW" ) const systemdTemplate = `[Unit] Description=Pritunl Cloud %s After=network.target [Service] Environment=CONFIG='%s' Type=simple User=root ExecStart=/usr/sbin/ip netns exec %s %s %s TimeoutStopSec=5 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true AmbientCapabilities=%s ` const systemdNamespaceTemplate = `[Unit] Description=Pritunl Cloud %s After=network.target [Service] Environment=CONFIG='%s' Type=simple User=%s ExecStart=%s %s TimeoutStopSec=5 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true NetworkNamespacePath=/var/run/netns/%s AmbientCapabilities=%s ` func UpdateEbtables(vmId bson.ObjectID, namespace string) (err error) { iface := vm.GetIface(vmId, 0) _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "iptables", "-t", "mangle", "-A", "POSTROUTING", "-o", settings.Hypervisor.BridgeIfaceName, "-p", "udp", "-m", "udp", "--sport", "67", "-j", "CHECKSUM", "--checksum-fill", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ip6tables", "-t", "mangle", "-A", "POSTROUTING", "-o", settings.Hypervisor.BridgeIfaceName, "-p", "udp", "-m", "udp", "--sport", "547", "-j", "CHECKSUM", "--checksum-fill", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ebtables", "-I", "OUTPUT", "-o", iface, "-p", "IPv4", "--ip-protocol", "udp", "--ip-sport", "67", "-j", "ACCEPT", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ebtables", "-A", "OUTPUT", "-p", "IPv4", "--ip-protocol", "udp", "--ip-sport", "67", "-j", "DROP", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ebtables", "-I", "OUTPUT", "-o", iface, "-p", "IPv6", "--ip6-protocol", "udp", "--ip6-sport", "547", "-j", "ACCEPT", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ebtables", "-A", "OUTPUT", "-p", "IPv6", "--ip6-protocol", "udp", "--ip6-sport", "547", "-j", "DROP", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ebtables", "-I", "OUTPUT", "-o", iface, "-p", "IPv6", "--ip6-protocol", "ipv6-icmp", "--ip6-icmp-type", "134", "-j", "ACCEPT", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "ebtables", "-A", "OUTPUT", "-p", "IPv6", "--ip6-protocol", "ipv6-icmp", "--ip6-icmp-type", "134", "-j", "DROP", ) if err != nil { return } return } func ClearEbtables(vmId bson.ObjectID, namespace string) (err error) { iface := vm.GetIface(vmId, 0) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "iptables", "-t", "mangle", "-D", "POSTROUTING", "-o", settings.Hypervisor.BridgeIfaceName, "-p", "udp", "-m", "udp", "--sport", "67", "-j", "CHECKSUM", "--checksum-fill", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ip6tables", "-t", "mangle", "-D", "POSTROUTING", "-o", settings.Hypervisor.BridgeIfaceName, "-p", "udp", "-m", "udp", "--sport", "547", "-j", "CHECKSUM", "--checksum-fill", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ebtables", "-D", "OUTPUT", "-o", iface, "-p", "IPv4", "--ip-protocol", "udp", "--ip-sport", "67", "-j", "ACCEPT", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ebtables", "-D", "OUTPUT", "-p", "IPv4", "--ip-protocol", "udp", "--ip-sport", "67", "-j", "DROP", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ebtables", "-D", "OUTPUT", "-o", iface, "-p", "IPv6", "--ip6-protocol", "udp", "--ip6-sport", "547", "-j", "ACCEPT", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ebtables", "-D", "OUTPUT", "-p", "IPv6", "--ip6-protocol", "udp", "--ip6-sport", "547", "-j", "DROP", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ebtables", "-D", "OUTPUT", "-o", iface, "-p", "IPv6", "--ip6-protocol", "ipv6-icmp", "--ip6-icmp-type", "134", "-j", "ACCEPT", ) _, _ = utils.ExecCombinedOutput( "", "ip", "netns", "exec", namespace, "ebtables", "-D", "OUTPUT", "-p", "IPv6", "--ip6-protocol", "ipv6-icmp", "--ip6-icmp-type", "134", "-j", "DROP", ) return } func WriteService(vmId bson.ObjectID, namespace string, config interface{}, systemdNamespace bool) (err error) { param := "" unitPath := "" caps := "" curPath, err := os.Executable() if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to get executable path"), } return } confData, err := json.Marshal(config) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dhcps: Failed to marshal config"), } return } switch config.(type) { case *Server4: param = "dhcp4-server" unitPath = paths.GetUnitPathDhcp4(vmId, 0) caps = dhcpCaps break case *Server6: param = "dhcp6-server" unitPath = paths.GetUnitPathDhcp6(vmId, 0) caps = dhcpCaps break case *ServerNdp: param = "ndp-server" unitPath = paths.GetUnitPathNdp(vmId, 0) caps = ndpCaps break default: err = &errortypes.TypeError{ errors.New("dhcps: Unknown config type"), } return } output := "" if systemdNamespace { output = fmt.Sprintf( systemdNamespaceTemplate, param, string(confData), permission.GetUserName(vmId), curPath, param, namespace, caps, ) } else { output = fmt.Sprintf( systemdTemplate, param, string(confData), namespace, curPath, param, caps, ) } err = utils.CreateWrite(unitPath, output, 0644) if err != nil { return } return } func Start(db *database.Database, virt *vm.VirtualMachine, dc *datacenter.Datacenter, zne *zone.Zone, vc *vpc.Vpc) (err error) { namespace := vm.GetNamespace(virt.Id, 0) hasSystemdNamespace := features.HasSystemdNamespace() logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("dhcps: Starting virtual machine dhcp server") if virt.NetworkAdapters == nil || len(virt.NetworkAdapters) < 1 { err = &errortypes.ParseError{ errors.New("dhcps: Missing virt network adapter"), } return } subnetId := virt.NetworkAdapters[0].Subnet vcNet, err := vc.GetNetwork() if err != nil { return } cidr, _ := vcNet.Mask.Size() addr, gatewayAddr, err := vc.GetIp(db, subnetId, virt.Id) if err != nil { return } addr6 := vc.GetIp6(virt.Id) gatewayAddr6 := vc.GetGatewayIp6(virt.Id) mtu := dc.GetInstanceMtu() server4 := &Server4{ Iface: settings.Hypervisor.BridgeIfaceName, ClientIp: addr.String(), GatewayIp: gatewayAddr.String(), PrefixLen: cidr, DnsServers: []string{ strings.Split(settings.Hypervisor.ImdsAddress, "/")[0], zne.GetDnsServerPrimary(), zne.GetDnsServerSecondary(), }, Mtu: mtu, Lifetime: settings.Hypervisor.DhcpLifetime, } server6 := &Server6{ Iface: settings.Hypervisor.BridgeIfaceName, ClientIp: addr6.String(), GatewayIp: gatewayAddr6.String(), PrefixLen: 64, DnsServers: []string{}, Mtu: mtu, Lifetime: settings.Hypervisor.DhcpLifetime, } serverNdp := &ServerNdp{ Iface: settings.Hypervisor.BridgeIfaceName, ClientIp: addr6.String(), GatewayIp: gatewayAddr6.String(), PrefixLen: 64, DnsServers: []string{}, Mtu: mtu, Lifetime: settings.Hypervisor.DhcpLifetime, Delay: settings.Hypervisor.NdpRaInterval, } err = UpdateEbtables(virt.Id, namespace) if err != nil { return } unitServer4 := paths.GetUnitNameDhcp4(virt.Id, 0) unitServer6 := paths.GetUnitNameDhcp6(virt.Id, 0) unitServerNdp := paths.GetUnitNameNdp(virt.Id, 0) _ = systemd.Stop(unitServer4) _ = systemd.Stop(unitServer6) _ = systemd.Stop(unitServerNdp) err = WriteService(virt.Id, namespace, server4, hasSystemdNamespace) if err != nil { return } err = WriteService(virt.Id, namespace, server6, hasSystemdNamespace) if err != nil { return } err = WriteService(virt.Id, namespace, serverNdp, hasSystemdNamespace) if err != nil { return } err = systemd.Reload() if err != nil { return } err = systemd.Start(unitServer4) if err != nil { return } err = systemd.Start(unitServer6) if err != nil { return } err = systemd.Start(unitServerNdp) if err != nil { return } return } func Stop(virt *vm.VirtualMachine) (err error) { namespace := vm.GetNamespace(virt.Id, 0) unitServer4 := paths.GetUnitNameDhcp4(virt.Id, 0) unitServer6 := paths.GetUnitNameDhcp6(virt.Id, 0) unitServerNdp := paths.GetUnitNameNdp(virt.Id, 0) _ = systemd.Stop(unitServer4) _ = systemd.Stop(unitServer6) _ = systemd.Stop(unitServerNdp) err = ClearEbtables(virt.Id, namespace) if err != nil { return } return } ================================================ FILE: disk/constants.go ================================================ package disk import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" ) const ( Provision = "provision" Available = "available" Attached = "attached" Snapshot = "snapshot" Backup = "backup" Expand = "expand" Restore = "restore" Destroy = "destroy" Qcow2 = "qcow2" Lvm = "lvm" Xfs = "xfs" Ext4 = "ext4" LvmXfs = "lvm_xfs" LvmExt4 = "lvm_ext4" Linux = "linux" LinuxLegacy = "linux_legacy" LinuxUnsigned = "linux_unsigned" Bsd = "bsd" AlpineLinux = "alpinelinux" ArchLinux = "archlinux" RedHat = "redhat" Fedora = "fedora" Ubuntu = "ubuntu" FreeBSD = "freebsd" ) var ( Vacant = bson.NilObjectID ValidSystemTypes = set.NewSet( Linux, LinuxLegacy, LinuxUnsigned, Bsd, ) ValidSystemKinds = set.NewSet( AlpineLinux, ArchLinux, RedHat, Fedora, Ubuntu, FreeBSD, ) ) ================================================ FILE: disk/disk.go ================================================ package disk import ( "fmt" "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/lock" "github.com/pritunl/pritunl-cloud/lvm" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Disk struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Created time.Time `bson:"created" json:"created"` State string `bson:"state" json:"state"` Action string `bson:"action" json:"action"` Type string `bson:"type" json:"type"` SystemType string `bson:"system_type" json:"system_type"` SystemKind string `bson:"system_kind" json:"system_kind"` Uuid string `bson:"uuid" json:"uuid"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Zone bson.ObjectID `bson:"zone" json:"zone"` Node bson.ObjectID `bson:"node" json:"node"` Pool bson.ObjectID `bson:"pool" json:"pool"` Organization bson.ObjectID `bson:"organization" json:"organization"` Instance bson.ObjectID `bson:"instance" json:"instance"` SourceInstance bson.ObjectID `bson:"source_instance" json:"source_instance"` Deployment bson.ObjectID `bson:"deployment" json:"deployment"` DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` FileSystem string `bson:"file_system" json:"file_system"` Image bson.ObjectID `bson:"image" json:"image"` RestoreImage bson.ObjectID `bson:"restore_image" json:"restore_image"` Backing bool `bson:"backing" json:"backing"` BackingImage string `bson:"backing_image" json:"backing_image"` Index string `bson:"index" json:"index"` Size int `bson:"size" json:"size"` LvSize int `bson:"lv_size" json:"lv_size"` NewSize int `bson:"new_size" json:"new_size"` Backup bool `bson:"backup" json:"backup"` LastBackup time.Time `bson:"last_backup" json:"last_backup"` curIndex string `bson:"-" json:"-"` curInstance bson.ObjectID `bson:"-" json:"-"` } func (d *Disk) IsActive() bool { return d.State == Available || d.State == Attached } func (d *Disk) IsAvailable() bool { if d.State == Available && d.Action == "" && d.Instance.IsZero() { return true } return false } func (d *Disk) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { d.Name = utils.FilterName(d.Name) if d.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "organization_required", Message: "Missing required organization", } return } if !d.Instance.IsZero() && d.Index != "" { index, e := strconv.Atoi(d.Index) if e != nil { errData = &errortypes.ErrorData{ Error: "index_invalid", Message: "Disk index invalid", } return } if index < 0 || index > 10 { errData = &errortypes.ErrorData{ Error: "index_out_of_range", Message: "Disk index out of range", } return } d.Index = strconv.Itoa(index) } if d.Datacenter.IsZero() { errData = &errortypes.ErrorData{ Error: "invalid_datacenter", Message: "Missing required datacenter", } return } if d.Zone.IsZero() { errData = &errortypes.ErrorData{ Error: "invalid_zone", Message: "Missing required zone", } return } if d.Node.IsZero() { errData = &errortypes.ErrorData{ Error: "invalid_node", Message: "Missing required node", } return } if d.Type == "" { d.Type = Qcow2 } switch d.Type { case Qcow2: d.Pool = bson.NilObjectID if d.Node.IsZero() { errData = &errortypes.ErrorData{ Error: "node_required", Message: "Missing required node", } return } break case Lvm: d.Node = bson.NilObjectID if d.Pool.IsZero() { errData = &errortypes.ErrorData{ Error: "pool_required", Message: "Missing required pool", } return } if d.Backing || d.BackingImage != "" { errData = &errortypes.ErrorData{ Error: "backing_image_invalid", Message: "LVM disk cannot have backing image", } return } break default: errData = &errortypes.ErrorData{ Error: "unknown_type", Message: "Unknown disk type", } return } if d.SystemType == "" { d.SystemType = Linux } if !ValidSystemTypes.Contains(d.SystemType) { errData = &errortypes.ErrorData{ Error: "invalid_system_type", Message: "Disk system type invalid", } return } if d.SystemKind != "" && !ValidSystemKinds.Contains(d.SystemKind) { errData = &errortypes.ErrorData{ Error: "invalid_system_kind", Message: "Disk system kind invalid", } return } switch d.FileSystem { case Xfs, Ext4, "": d.LvSize = 0 break case LvmXfs, LvmExt4: if d.LvSize > d.Size { errData = &errortypes.ErrorData{ Error: "lv_size_invalid", Message: "LV size cannot be greater than disk size", } } break default: errData = &errortypes.ErrorData{ Error: "unknown_file_system", Message: "Unknown disk file system", } return } if d.Backup && d.BackingImage != "" { errData = &errortypes.ErrorData{ Error: "backing_image_backup", Message: "Cannot enable backups with backing image", } return } if d.Action == Restore && d.RestoreImage.IsZero() { errData = &errortypes.ErrorData{ Error: "restore_missing_image", Message: "Cannot restore without image set", } return } if !d.Instance.IsZero() { disks, e := GetInstance(db, d.Instance) if e != nil { err = e return } for _, dsk := range disks { if dsk.Id != d.Id && dsk.Index == d.Index { errData = &errortypes.ErrorData{ Error: "disk_index_in_use", Message: "Disk index is already in use on instance", } return } } } else { if !strings.HasPrefix(d.Index, "hold") { d.Index = fmt.Sprintf("hold_%s", bson.NewObjectID().Hex()) } d.Deployment = Vacant } if d.State == "" { d.State = Provision } if d.Size < 10 { d.Size = 10 } if d.Action == Expand { if d.NewSize == 0 { errData = &errortypes.ErrorData{ Error: "new_size_missing", Message: "Cannot expand without new size", } return } if d.NewSize < d.Size { errData = &errortypes.ErrorData{ Error: "new_size_invalid", Message: "New size cannot be less then current size", } return } } else { d.NewSize = 0 } if d.DeleteProtection && d.curInstance != d.Instance { errData = &errortypes.ErrorData{ Error: "delete_protection_index", Message: "Cannot change instance with delete protection enabled", } return } if d.DeleteProtection && d.curIndex != d.Index { errData = &errortypes.ErrorData{ Error: "delete_protection_index", Message: "Cannot change index with delete protection enabled", } return } return } func (d *Disk) PreCommit() { d.curIndex = d.Index d.curInstance = d.Instance } func (d *Disk) Reserve(db *database.Database, instId bson.ObjectID, index int, deplyId bson.ObjectID) (reserved bool, err error) { coll := db.Disks() resp, err := coll.UpdateOne(db, &bson.M{ "_id": d.Id, "instance": Vacant, }, &bson.M{ "$set": &bson.M{ "instance": instId, "index": strconv.Itoa(index), "deployment": deplyId, }, }) if err != nil { err = database.ParseError(err) return } if resp.MatchedCount == 1 && resp.ModifiedCount == 1 { reserved = true } return } func (d *Disk) Unreserve(db *database.Database, instId bson.ObjectID, deplyId bson.ObjectID) (err error) { coll := db.Disks() _, err = coll.UpdateOne(db, &bson.M{ "_id": d.Id, "instance": instId, "deployment": deplyId, }, &bson.M{ "$set": &bson.M{ "index": fmt.Sprintf("hold_%s", bson.NewObjectID().Hex()), "instance": Vacant, "deployment": Vacant, }, }) if err != nil { err = database.ParseError(err) return } return } func (d *Disk) Commit(db *database.Database) (err error) { coll := db.Disks() err = coll.Commit(d.Id, d) if err != nil { return } return } func (d *Disk) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Disks() err = coll.CommitFields(d.Id, d, fields) if err != nil { return } return } func (d *Disk) Insert(db *database.Database) (err error) { coll := db.Disks() d.Created = time.Now() _, err = coll.InsertOne(db, d) if err != nil { err = database.ParseError(err) return } return } func (d *Disk) Destroy(db *database.Database) (err error) { dskPath := paths.GetDiskPath(d.Id) if d.DeleteProtection { logrus.WithFields(logrus.Fields{ "disk_id": d.Id.Hex(), }).Info("disk: Delete protection ignore disk destroy") d.Action = "" err = d.CommitFields(db, set.NewSet("action")) if err != nil { return } event.PublishDispatch(db, "disk.change") return } if d.Type == Lvm { pl, e := pool.Get(db, d.Pool) if e != nil { err = e return } vgName := pl.VgName lvName := d.Id.Hex() acquired, e := lock.LvmLock(db, vgName, lvName) if e != nil { err = e return } if !acquired { err = &errortypes.WriteError{ errors.New("data: Failed to acquire LVM lock"), } return } defer func() { err2 := lock.LvmUnlock(db, vgName, lvName) if err2 != nil { logrus.WithFields(logrus.Fields{ "error": err2, }).Error("data: Failed to unlock lvm") } }() logrus.WithFields(logrus.Fields{ "disk_id": d.Id.Hex(), "vg_name": vgName, "lv_name": lvName, }).Info("qemu: Destroying LVM disk") err = lvm.RemoveLv(vgName, lvName) if err != nil { return } } else { logrus.WithFields(logrus.Fields{ "disk_id": d.Id.Hex(), "disk_path": dskPath, }).Info("qemu: Destroying QCOW disk") err = utils.RemoveAll(dskPath) if err != nil { return } } err = Remove(db, d.Id) if err != nil { return } return } ================================================ FILE: disk/sort.go ================================================ package disk type Disks []*Disk func (d Disks) Len() int { return len(d) } func (d Disks) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d Disks) Less(i, j int) bool { return d[i].Index < d[j].Index } ================================================ FILE: disk/utils.go ================================================ package disk import ( "fmt" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, diskId bson.ObjectID) ( dsk *Disk, err error) { coll := db.Disks() dsk = &Disk{} err = coll.FindOneId(diskId, dsk) if err != nil { return } return } func GetOne(db *database.Database, query *bson.M) (dsk *Disk, err error) { coll := db.Disks() dsk = &Disk{} err = coll.FindOne(db, query).Decode(dsk) if err != nil { err = database.ParseError(err) return } return } func GetOrg(db *database.Database, orgId, diskId bson.ObjectID) ( dsk *Disk, err error) { coll := db.Disks() dsk = &Disk{} err = coll.FindOne(db, &bson.M{ "_id": diskId, "organization": orgId, }).Decode(dsk) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( disks []*Disk, err error) { coll := db.Disks() disks = []*Disk{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Disk{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } disks = append(disks, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllMap(db *database.Database, query *bson.M) ( disks map[bson.ObjectID]*Disk, err error) { coll := db.Disks() disks = map[bson.ObjectID]*Disk{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dsk := &Disk{} err = cursor.Decode(dsk) if err != nil { err = database.ParseError(err) return } disks[dsk.Id] = dsk } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (disks []*Disk, count int64, err error) { coll := db.Disks() disks = []*Disk{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dsk := &Disk{} err = cursor.Decode(dsk) if err != nil { err = database.ParseError(err) return } disks = append(disks, dsk) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetInstance(db *database.Database, instId bson.ObjectID) ( disks []*Disk, err error) { coll := db.Disks() disks = []*Disk{} cursor, err := coll.Find( db, &bson.M{ "instance": instId, }, options.Find(). SetSort(bson.D{{"index", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dsk := &Disk{} err = cursor.Decode(dsk) if err != nil { err = database.ParseError(err) return } disks = append(disks, dsk) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetInstanceIndex(db *database.Database, instId bson.ObjectID, index string) (dsk *Disk, err error) { coll := db.Disks() dsk = &Disk{} err = coll.FindOne(db, &bson.M{ "instance": instId, "index": index, }).Decode(dsk) if err != nil { err = database.ParseError(err) return } return } func GetNode(db *database.Database, nodeId bson.ObjectID, nodePools []bson.ObjectID) (disks []*Disk, err error) { coll := db.Disks() disks = []*Disk{} cursor, err := coll.Find(db, &bson.M{ "$or": []bson.M{ {"node": nodeId}, {"pool": &bson.M{ "$in": nodePools, }}, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dsk := &Disk{} err = cursor.Decode(dsk) if err != nil { err = database.ParseError(err) return } disks = append(disks, dsk) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, diskId bson.ObjectID) (err error) { coll := db.Disks() _, err = coll.DeleteOne(db, &bson.M{ "_id": diskId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func Detach(db *database.Database, dskIds bson.ObjectID) (err error) { coll := db.Disks() err = coll.UpdateId(dskIds, &bson.M{ "$set": &bson.M{ "index": fmt.Sprintf("hold_%s", bson.NewObjectID().Hex()), "instance": Vacant, "deployment": Vacant, }, }) if err != nil { err = database.ParseError(err) return } return } func Delete(db *database.Database, dskId bson.ObjectID) (err error) { coll := db.Disks() err = coll.UpdateId(dskId, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func DeleteOrg(db *database.Database, orgId, dskId bson.ObjectID) ( err error) { coll := db.Disks() _, err = coll.UpdateOne(db, &bson.M{ "_id": dskId, "organization": orgId, }, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func DeleteMulti(db *database.Database, dskIds []bson.ObjectID) ( err error) { coll := db.Disks() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": dskIds, }, "delete_protection": &bson.M{ "$ne": true, }, }, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func DeleteMultiOrg(db *database.Database, orgId bson.ObjectID, dskIds []bson.ObjectID) (err error) { coll := db.Disks() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": dskIds, }, "organization": orgId, "delete_protection": &bson.M{ "$ne": true, }, }, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func UpdateMulti(db *database.Database, dskIds []bson.ObjectID, doc *bson.M) (err error) { coll := db.Disks() query := &bson.M{ "_id": &bson.M{ "$in": dskIds, }, } if (*doc)["action"] == Destroy { (*query)["delete_protection"] = &bson.M{ "$ne": true, } } _, err = coll.UpdateMany(db, query, &bson.M{ "$set": doc, }) if err != nil { err = database.ParseError(err) return } return } func UpdateMultiOrg(db *database.Database, orgId bson.ObjectID, dskIds []bson.ObjectID, doc *bson.M) (err error) { coll := db.Disks() query := &bson.M{ "_id": &bson.M{ "$in": dskIds, }, "organization": orgId, } if (*doc)["action"] == Destroy { (*query)["delete_protection"] = &bson.M{ "$ne": true, } } _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": dskIds, }, "organization": orgId, }, &bson.M{ "$set": doc, }) if err != nil { err = database.ParseError(err) return } return } func GetAllKeys(db *database.Database, ndeId bson.ObjectID) ( keys set.Set, err error) { coll := db.Disks() keys = set.NewSet() cursor, err := coll.Find(db, &bson.M{ "node": ndeId, }, options.Find(). SetProjection(bson.D{ {"node", 1}, {"backing_image", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dsk := &Disk{} err = cursor.Decode(dsk) if err != nil { err = database.ParseError(err) return } if dsk.BackingImage != "" { keys.Add(dsk.BackingImage) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func SetDeleteProtection(db *database.Database, instId bson.ObjectID, protection bool) (err error) { coll := db.Disks() _, err = coll.UpdateMany(db, &bson.M{ "instance": instId, }, &bson.M{ "$set": &bson.M{ "delete_protection": protection, }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: dns/aws.go ================================================ package dns import ( "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/route53" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" ) type Aws struct { sess *session.Session sessRoute53 *route53.Route53 cacheZoneId map[string]string } func (a *Aws) Connect(db *database.Database, secr *secret.Secret) (err error) { if secr.Type != secret.AWS { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Secret type not AWS"), } return } a.cacheZoneId = map[string]string{} a.sess, err = session.NewSession(&aws.Config{ Region: aws.String(secr.Region), Credentials: credentials.NewStaticCredentials( secr.Key, secr.Value, ""), }) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS session error"), } return } a.sessRoute53 = route53.New(a.sess) return } func (a *Aws) DnsZoneFind(domain string) (zoneId string, err error) { domain = extractDomain(domain) zoneId = a.cacheZoneId[domain] if zoneId != "" { return } input := &route53.ListHostedZonesInput{} result, err := a.sessRoute53.ListHostedZones(input) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS route53 zone lookup error"), } return } for _, zone := range result.HostedZones { if matchDomains(*zone.Name, domain) { zoneId = *zone.Id break } } if zoneId == "" { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS route53 zone not found"), } return } a.cacheZoneId[domain] = zoneId return } func (a *Aws) DnsCommit(db *database.Database, domain, recordType string, ops []*Operation) (err error) { domain = cleanDomain(domain) zoneId, err := a.DnsZoneFind(domain) if err != nil { return } deleteResourceRecs := []*route53.ResourceRecord{} updateResourceRecs := []*route53.ResourceRecord{} operations := []string{} for _, op := range ops { if recordType == "AAAA" { val := normalizeIp(op.Value) if val == "" { err = &errortypes.ParseError{ errors.Newf("dns: Invalid ipv6 address %s", op.Value), } return } op.Value = val } resourceRec := &route53.ResourceRecord{ Value: aws.String(op.Value), } switch op.Operation { case UPSERT, RETAIN: operations = append(operations, "add:"+op.Value) updateResourceRecs = append(updateResourceRecs, resourceRec) case DELETE: curVals, e := a.DnsFind(db, domain, recordType) if e != nil { err = e return } exists := false for _, val := range curVals { if val == op.Value { exists = true break } } if !exists { logrus.WithFields(logrus.Fields{ "domain": domain, "operation": "remove:" + op.Value, }).Info("domain: Skipping delete on changed record") continue } operations = append(operations, "remove:"+op.Value) deleteResourceRecs = append(deleteResourceRecs, resourceRec) } } logrus.WithFields(logrus.Fields{ "domain": domain, "operations": operations, }).Info("domain: AWS dns batch operation") if len(updateResourceRecs) == 0 && len(deleteResourceRecs) > 0 { input := &route53.ChangeResourceRecordSetsInput{ ChangeBatch: &route53.ChangeBatch{ Changes: []*route53.Change{ { Action: aws.String("DELETE"), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String(domain), Type: aws.String(recordType), TTL: aws.Int64(int64( settings.Acme.DnsAwsTtl)), ResourceRecords: deleteResourceRecs, }, }, }, Comment: aws.String("Pritunl delete record"), }, HostedZoneId: aws.String(zoneId), } _, err = a.sessRoute53.ChangeResourceRecordSets(input) if err != nil { if strings.Contains(err.Error(), "delete") && strings.Contains(err.Error(), "not found") { err = nil } else { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS record delete error"), } return } } } if len(updateResourceRecs) > 0 { input := &route53.ChangeResourceRecordSetsInput{ ChangeBatch: &route53.ChangeBatch{ Changes: []*route53.Change{ { Action: aws.String("UPSERT"), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String(domain), Type: aws.String(recordType), TTL: aws.Int64(int64( settings.Acme.DnsAwsTtl)), ResourceRecords: updateResourceRecs, }, }, }, Comment: aws.String("Pritunl update record"), }, HostedZoneId: aws.String(zoneId), } _, err = a.sessRoute53.ChangeResourceRecordSets(input) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS record update error"), } return } } return } func (a *Aws) DnsFind(db *database.Database, domain, recordType string) ( vals []string, err error) { vals = []string{} domain = cleanDomain(domain) zoneId, err := a.DnsZoneFind(domain) if err != nil { return } input := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zoneId), StartRecordName: aws.String(domain), StartRecordType: aws.String(recordType), } result, err := a.sessRoute53.ListResourceRecordSets(input) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS record list error"), } return } for _, recordSet := range result.ResourceRecordSets { if recordSet.Type != nil && *recordSet.Type == recordType && recordSet.Name != nil && matchDomains(*recordSet.Name, domain) { for _, record := range recordSet.ResourceRecords { if record.Value != nil { val := *record.Value if recordType == "AAAA" { val = normalizeIp(val) } if val == "" { continue } vals = append(vals, val) } } } } return } func (a *Aws) DnsTxtGet(db *database.Database, domain string) ( vals []string, err error) { vals = []string{} zoneId, err := a.DnsZoneFind(domain) if err != nil { return } input := &route53.ListResourceRecordSetsInput{ HostedZoneId: aws.String(zoneId), StartRecordName: aws.String(domain), StartRecordType: aws.String("TXT"), } result, err := a.sessRoute53.ListResourceRecordSets(input) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS route53 record set error"), } return } for _, recordSet := range result.ResourceRecordSets { if recordSet.Type != nil && *recordSet.Type == "TXT" && recordSet.Name != nil && matchDomains(*recordSet.Name, domain) { for _, record := range recordSet.ResourceRecords { if record.Value != nil { vals = append(vals, *record.Value) } } } } return } func (a *Aws) DnsTxtUpsert(db *database.Database, domain, val string) (err error) { zoneId, err := a.DnsZoneFind(domain) if err != nil { return } input := &route53.ChangeResourceRecordSetsInput{ ChangeBatch: &route53.ChangeBatch{ Changes: []*route53.Change{ { Action: aws.String("UPSERT"), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String(domain), Type: aws.String("TXT"), TTL: aws.Int64(int64(settings.Acme.DnsAwsTtl)), ResourceRecords: []*route53.ResourceRecord{ { Value: aws.String(val), }, }, }, }, }, Comment: aws.String("Pritunl update TXT record"), }, HostedZoneId: aws.String(zoneId), } _, err = a.sessRoute53.ChangeResourceRecordSets(input) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS route53 record set error"), } return } return } func (a *Aws) DnsTxtDelete(db *database.Database, domain, val string) (err error) { zoneId, err := a.DnsZoneFind(domain) if err != nil { return } input := &route53.ChangeResourceRecordSetsInput{ ChangeBatch: &route53.ChangeBatch{ Changes: []*route53.Change{ { Action: aws.String("DELETE"), ResourceRecordSet: &route53.ResourceRecordSet{ Name: aws.String(domain), Type: aws.String("TXT"), TTL: aws.Int64(int64(settings.Acme.DnsAwsTtl)), ResourceRecords: []*route53.ResourceRecord{ { Value: aws.String(val), }, }, }, }, }, Comment: aws.String("Pritunl delete TXT record"), }, HostedZoneId: aws.String(zoneId), } _, err = a.sessRoute53.ChangeResourceRecordSets(input) if err != nil { if strings.Contains(err.Error(), "delete") && strings.Contains(err.Error(), "not found") { err = nil } else { err = &errortypes.ApiError{ errors.Wrap(err, "acme: AWS record change error"), } return } } return } ================================================ FILE: dns/cloudflare.go ================================================ package dns import ( "github.com/cloudflare/cloudflare-go" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Cloudflare struct { sess *cloudflare.API token string cacheZoneId map[string]string } func (c *Cloudflare) Connect(db *database.Database, secr *secret.Secret) (err error) { if secr.Type != secret.Cloudflare { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Secret type not cloudflare"), } return } c.sess, err = cloudflare.NewWithAPIToken(utils.FilterStr(secr.Key, 256)) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "dns: Failed to connect to cloudflare api"), } return } c.cacheZoneId = map[string]string{} return } func (c *Cloudflare) DnsZoneFind(db *database.Database, domain string) ( zoneId string, err error) { domain = extractDomain(domain) zoneId = c.cacheZoneId[domain] if zoneId != "" { return } zones, err := c.sess.ListZones(db) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Failed to list cloudflare zones"), } return } for _, zone := range zones { if matchDomains(zone.Name, domain) { zoneId = zone.ID break } } if zoneId == "" { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Cloudflare zone not found"), } return } c.cacheZoneId[domain] = zoneId return } func (c *Cloudflare) DnsCommit(db *database.Database, domain, recordType string, ops []*Operation) (err error) { domain = cleanDomain(domain) zoneId, err := c.DnsZoneFind(db, domain) if err != nil { return } listParams := cloudflare.ListDNSRecordsParams{ Type: recordType, Name: domain, } records, _, err := c.sess.ListDNSRecords( db, cloudflare.ZoneIdentifier(zoneId), listParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to get DNS records"), } return } recordIds := map[string]string{} for _, record := range records { if record.Type == recordType && matchDomains(record.Name, domain) { val := record.Content if recordType == "AAAA" { val = normalizeIp(val) } if val == "" { continue } recordIds[val] = record.ID } } for _, op := range ops { if recordType == "AAAA" { val := normalizeIp(op.Value) if val == "" { err = &errortypes.ParseError{ errors.Newf("dns: Invalid ipv6 address %s", op.Value), } return } op.Value = val } } for _, op := range ops { if op.Operation != DELETE { continue } recordId := recordIds[op.Value] if recordId == "" { continue } delete(recordIds, op.Value) logrus.WithFields(logrus.Fields{ "operation": "delete", "record_id": recordId, "domain": domain, "value": op.Value, }).Info("domain: Cloudflare dns operation") err = c.sess.DeleteDNSRecord( db, cloudflare.ZoneIdentifier(zoneId), recordId, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dns: Failed to delete record"), } return } } for _, op := range ops { if op.Operation != RETAIN && op.Operation != UPSERT { continue } recordId := recordIds[op.Value] if recordId == "" { continue } delete(recordIds, op.Value) op.Operation = "" } for _, op := range ops { if op.Operation != RETAIN && op.Operation != UPSERT { continue } updateVal := "" recordId := "" for val, recId := range recordIds { updateVal = val recordId = recId break } if recordId != "" { delete(recordIds, updateVal) } if recordId == "" { logrus.WithFields(logrus.Fields{ "operation": "create", "domain": domain, "value": op.Value, }).Info("domain: Cloudflare dns operation") createParams := cloudflare.CreateDNSRecordParams{ Type: recordType, Name: domain, Content: op.Value, TTL: settings.Acme.DnsCloudflareTtl, } _, err = c.sess.CreateDNSRecord( db, cloudflare.ZoneIdentifier(zoneId), createParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to create record"), } return } } else { logrus.WithFields(logrus.Fields{ "operation": "update", "record_id": recordId, "domain": domain, "value": op.Value, }).Info("domain: Cloudflare dns operation") updateParams := cloudflare.UpdateDNSRecordParams{ ID: recordId, Type: recordType, Name: domain, Content: op.Value, TTL: settings.Acme.DnsCloudflareTtl, } _, err = c.sess.UpdateDNSRecord( db, cloudflare.ZoneIdentifier(zoneId), updateParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to update record"), } return } } } for val, recordId := range recordIds { logrus.WithFields(logrus.Fields{ "operation": "delete_unknown", "record_id": recordId, "domain": domain, "value": val, }).Info("domain: Cloudflare dns operation") err = c.sess.DeleteDNSRecord( db, cloudflare.ZoneIdentifier(zoneId), recordId, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "dns: Failed to delete record"), } return } } return } func (c *Cloudflare) DnsFind(db *database.Database, domain, recordType string) (vals []string, err error) { vals = []string{} domain = cleanDomain(domain) zoneId, err := c.DnsZoneFind(db, domain) if err != nil { return } listParams := cloudflare.ListDNSRecordsParams{ Type: recordType, Name: domain, } records, _, err := c.sess.ListDNSRecords( db, cloudflare.ZoneIdentifier(zoneId), listParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to get DNS records"), } return } for _, record := range records { if record.Type == recordType && matchDomains(record.Name, domain) { val := record.Content if recordType == "AAAA" { val = normalizeIp(val) } if val == "" { continue } vals = append(vals, val) break } } return } func (c *Cloudflare) DnsTxtGet(db *database.Database, domain string) (vals []string, err error) { vals = []string{} domain = cleanDomain(domain) zoneId, err := c.DnsZoneFind(db, domain) if err != nil { return } listParams := cloudflare.ListDNSRecordsParams{ Type: "TXT", Name: domain, } records, _, err := c.sess.ListDNSRecords( db, cloudflare.ZoneIdentifier(zoneId), listParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to get DNS records"), } return } for _, record := range records { if record.Type == "TXT" && matchDomains(record.Name, domain) { vals = append(vals, record.Content) break } } return } func (c *Cloudflare) DnsTxtUpsert(db *database.Database, domain, val string) (err error) { domain = cleanDomain(domain) zoneId, err := c.DnsZoneFind(db, domain) if err != nil { return } listParams := cloudflare.ListDNSRecordsParams{ Type: "TXT", Name: domain, } records, _, err := c.sess.ListDNSRecords( db, cloudflare.ZoneIdentifier(zoneId), listParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to get DNS records"), } return } recordId := "" for _, record := range records { if record.Type == "TXT" && matchDomains(record.Name, domain) { recordId = record.ID break } } if recordId == "" { createParams := cloudflare.CreateDNSRecordParams{ Type: "TXT", Name: domain, Content: val, TTL: settings.Acme.DnsCloudflareTtl, } _, err = c.sess.CreateDNSRecord( db, cloudflare.ZoneIdentifier(zoneId), createParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to create DNS record"), } return } } else { updateParams := cloudflare.UpdateDNSRecordParams{ Type: "TXT", Name: domain, Content: val, TTL: settings.Acme.DnsCloudflareTtl, } _, err = c.sess.UpdateDNSRecord( db, cloudflare.ResourceIdentifier(recordId), updateParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to update DNS record"), } return } } return } func (c *Cloudflare) DnsTxtDelete(db *database.Database, domain, val string) (err error) { domain = cleanDomain(domain) zoneId, err := c.DnsZoneFind(db, domain) if err != nil { return } listParams := cloudflare.ListDNSRecordsParams{ Type: "TXT", Name: domain, } records, _, err := c.sess.ListDNSRecords( db, cloudflare.ZoneIdentifier(zoneId), listParams, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to get DNS records"), } return } recordId := "" for _, record := range records { if record.Type == "TXT" && matchDomains(record.Name, domain) && matchTxt(record.Content, val) { recordId = record.ID break } } if recordId != "" { err = c.sess.DeleteDNSRecord( db, cloudflare.ZoneIdentifier(zoneId), recordId, ) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "acme: Failed to delete DNS record"), } return } } return } ================================================ FILE: dns/constants.go ================================================ package dns const ( UPSERT = "upsert" DELETE = "delete" RETAIN = "retain" ) ================================================ FILE: dns/dns.go ================================================ package dns import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/secret" ) type Operation struct { Operation string Value string } type Service interface { Connect(db *database.Database, secr *secret.Secret) (err error) DnsCommit(db *database.Database, domain, recordType string, ops []*Operation) (err error) DnsFind(db *database.Database, domain, recordType string) ( vals []string, err error) } ================================================ FILE: dns/errors.go ================================================ package dns import ( "github.com/dropbox/godropbox/errors" ) type NotFoundError struct { errors.DropboxError } type ServiceError struct { errors.DropboxError } type UnknownError struct { errors.DropboxError } ================================================ FILE: dns/google.go ================================================ package dns import ( "context" "encoding/json" "sort" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" "google.golang.org/api/dns/v1" "google.golang.org/api/option" ) type Google struct { service *dns.Service project string cacheZoneId map[string]string } type googleKey struct { Type string `json:"type"` ProjectId string `json:"project_id"` PrivateKeyId string `json:"private_key_id"` PrivateKey string `json:"private_key"` ClientEmail string `json:"client_email"` ClientId string `json:"client_id"` AuthUri string `json:"auth_uri"` TokenUri string `json:"token_uri"` AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"` ClientX509CertURL string `json:"client_x509_cert_url"` } type googleZoneInfo struct { name string dnsName string dnsNameClean string } func (g *Google) Connect(db *database.Database, secr *secret.Secret) (err error) { if secr.Type != secret.GoogleCloud { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Secret type not GCP"), } return } g.cacheZoneId = map[string]string{} googleKey := &googleKey{} if secr.Key != "" { err = json.Unmarshal([]byte(secr.Key), googleKey) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "acme: Failed to parse Google Cloud credentials", ), } return } g.project = googleKey.ProjectId } else { err = &errortypes.ParseError{ errors.New("acme: GCP project ID not found"), } return } ctx := context.Background() opts := []option.ClientOption{ option.WithCredentialsJSON([]byte(secr.Key)), } g.service, err = dns.NewService(ctx, opts...) if err != nil { err = &errortypes.ApiError{ errors.Wrap( err, "acme: Failed to create Google Cloud DNS service", ), } return } return } func (g *Google) DnsZoneFind(db *database.Database, domain string) ( zoneId string, err error) { domainClean := strings.Trim(domain, ".") zoneId = g.cacheZoneId[domainClean] if zoneId != "" { return } ctx := context.Background() zones, err := g.service.ManagedZones.List(g.project).Context(ctx).Do() if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Google Cloud zone list error"), } return } zoneList := []*googleZoneInfo{} for _, zone := range zones.ManagedZones { if zone.DnsName != "" { zoneList = append(zoneList, &googleZoneInfo{ name: zone.Name, dnsName: zone.DnsName, dnsNameClean: strings.Trim(zone.DnsName, "."), }) } } for i := 0; i < len(zoneList); i++ { for j := i + 1; j < len(zoneList); j++ { if len(zoneList[i].dnsNameClean) < len(zoneList[j].dnsNameClean) { zoneList[i], zoneList[j] = zoneList[j], zoneList[i] } } } for _, zone := range zoneList { if matchDomains(zone.dnsNameClean, domainClean) { zoneId = zone.name break } if strings.HasSuffix(domainClean, "."+zone.dnsNameClean) { zoneId = zone.name break } } if zoneId == "" { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Google Cloud DNS zone not found"), } return } g.cacheZoneId[domainClean] = zoneId return } func (g *Google) DnsCommit(db *database.Database, domain, recordType string, ops []*Operation) (err error) { domain = cleanDomain(domain) zoneId, err := g.DnsZoneFind(db, domain) if err != nil { return } ctx := context.Background() listCall := g.service.ResourceRecordSets.List(g.project, zoneId) listCall.Name(domain + ".") listCall.Type(recordType) existingRecords, err := listCall.Context(ctx).Do() if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: GCP record list error"), } return } change := &dns.Change{ Additions: []*dns.ResourceRecordSet{}, Deletions: []*dns.ResourceRecordSet{}, } operations := []string{} var existingRecSet *dns.ResourceRecordSet for _, recSet := range existingRecords.Rrsets { if matchDomains(recSet.Name, domain+".") && recSet.Type == recordType { existingRecSet = recSet break } } addValues := []string{} removeValues := []string{} for _, op := range ops { if recordType == "AAAA" { val := normalizeIp(op.Value) if val == "" { err = &errortypes.ParseError{ errors.Newf("dns: Invalid ipv6 address %s", op.Value), } return } op.Value = val } switch op.Operation { case UPSERT, RETAIN: operations = append(operations, "add:"+op.Value) addValues = append(addValues, op.Value) case DELETE: removeValues = append(removeValues, op.Value) operations = append(operations, "remove:"+op.Value) } } existingValues := set.NewSet() if existingRecSet != nil { for _, value := range existingRecSet.Rrdatas { existingValues.Add(value) } } newValues := existingValues.Copy() for _, value := range removeValues { newValues.Remove(value) } for _, value := range addValues { newValues.Add(value) } if existingValues.IsEqual(newValues) { return } if existingRecSet != nil { change.Deletions = append(change.Deletions, existingRecSet) } if newValues.Len() > 0 { ttl := int64(settings.Acme.DnsGoogleCloudTtl) if existingRecSet != nil && existingRecSet.Ttl > 0 { ttl = existingRecSet.Ttl } values := []string{} for valueInf := range newValues.Iter() { values = append(values, valueInf.(string)) } sort.Strings(values) newRrSet := &dns.ResourceRecordSet{ Name: domain + ".", Type: recordType, Ttl: ttl, Rrdatas: values, } change.Additions = append(change.Additions, newRrSet) } logrus.WithFields(logrus.Fields{ "domain": domain, "operations": operations, }).Info("domain: Google Cloud dns batch operation") _, err = g.service.Changes.Create(g.project, zoneId, change).Context(ctx).Do() if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Google Cloud record change error"), } return } return } func (g *Google) DnsFind(db *database.Database, domain, recordType string) ( vals []string, err error) { vals = []string{} domain = cleanDomain(domain) zoneId, err := g.DnsZoneFind(db, domain) if err != nil { return } ctx := context.Background() listCall := g.service.ResourceRecordSets.List(g.project, zoneId) listCall.Name(domain + ".") listCall.Type(recordType) records, err := listCall.Context(ctx).Do() if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: GCP record list error"), } return } for _, recSet := range records.Rrsets { if matchDomains(recSet.Name, domain+".") && recSet.Type == recordType { for _, rdata := range recSet.Rrdatas { val := rdata if recordType == "AAAA" { val = normalizeIp(val) } if val == "" { continue } vals = append(vals, val) } } } return } ================================================ FILE: dns/oracle.go ================================================ package dns import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/oracle/oci-go-sdk/v65/dns" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Oracle struct { token string cacheZoneId map[string]string provider *secret.OracleProvider } func (o *Oracle) OracleUser() string { return "" } func (o *Oracle) OraclePrivateKey() string { return "" } func (o *Oracle) Connect(db *database.Database, secr *secret.Secret) (err error) { if secr.Type != secret.OracleCloud { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Secret type not Oracle Cloud"), } return } o.cacheZoneId = map[string]string{} o.provider, err = secr.GetOracleProvider() if err != nil { return } return } func (o *Oracle) DnsZoneFind(db *database.Database, domain string) ( zoneId string, err error) { domain = extractDomain(domain) zoneId = o.cacheZoneId[domain] if zoneId != "" { return } compartmentId, err := o.provider.CompartmentOCID() if err != nil { return } req := dns.ListZonesRequest{ CompartmentId: &compartmentId, } client, err := o.provider.GetDnsClient() if err != nil { return } zones, err := client.ListZones(db, req) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone list error"), } return } for _, zone := range zones.Items { if matchDomains(*zone.Name, domain) { zoneId = *zone.Id break } } if zoneId == "" { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone not found"), } return } o.cacheZoneId[domain] = zoneId return } func (o *Oracle) DnsCommit(db *database.Database, domain, recordType string, ops []*Operation) (err error) { zoneName := extractDomain(domain) domain = cleanDomain(domain) items := []dns.RecordOperation{} values := set.NewSet() operations := []string{} for _, op := range ops { if recordType == "AAAA" { val := normalizeIp(op.Value) if val == "" { err = &errortypes.ParseError{ errors.Newf("dns: Invalid ipv6 address %s", op.Value), } return } op.Value = val } values.Add(op.Value) switch op.Operation { case RETAIN: break case UPSERT: operations = append(operations, "add:"+op.Value) items = append(items, dns.RecordOperation{ Domain: &domain, Rtype: utils.PointerString(recordType), Ttl: utils.PointerInt( settings.Acme.DnsOracleCloudTtl), Rdata: utils.PointerString(op.Value), Operation: dns.RecordOperationOperationAdd, }) break case DELETE: operations = append(operations, "remove:"+op.Value) items = append(items, dns.RecordOperation{ Domain: &domain, Rtype: utils.PointerString(recordType), Ttl: utils.PointerInt( settings.Acme.DnsOracleCloudTtl), Rdata: utils.PointerString(op.Value), Operation: dns.RecordOperationOperationRemove, }) break } } client, err := o.provider.GetDnsClient() if err != nil { return } getReq := dns.GetZoneRecordsRequest{ ZoneNameOrId: utils.PointerString(zoneName), Domain: utils.PointerString(domain), } resp, err := client.GetZoneRecords(db, getReq) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone record get error"), } return } for _, record := range resp.Items { if record.Rtype != nil && *record.Rtype == recordType && record.Rdata != nil { val := *record.Rdata if recordType == "AAAA" { val = normalizeIp(val) } if val == "" { continue } if values.Contains(val) { continue } operations = append(operations, "remove_unknown:"+*record.Rdata) items = append(items, dns.RecordOperation{ Domain: &domain, Rtype: utils.PointerString(recordType), Ttl: utils.PointerInt( settings.Acme.DnsOracleCloudTtl), Rdata: utils.PointerString(*record.Rdata), Operation: dns.RecordOperationOperationRemove, }) } } if len(items) == 0 { return } logrus.WithFields(logrus.Fields{ "domain": domain, "operations": operations, }).Info("domain: Oracle dns batch operation") req := dns.PatchZoneRecordsRequest{ ZoneNameOrId: utils.PointerString(zoneName), PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{ Items: items, }, } _, err = client.PatchZoneRecords(db, req) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone patch error"), } return } return } func (o *Oracle) DnsFind(db *database.Database, domain, recordType string) (vals []string, err error) { zoneName := extractDomain(domain) domain = cleanDomain(domain) req := dns.GetZoneRecordsRequest{ ZoneNameOrId: utils.PointerString(zoneName), Domain: utils.PointerString(domain), } client, err := o.provider.GetDnsClient() if err != nil { return } resp, err := client.GetZoneRecords(db, req) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone record get error"), } return } for _, record := range resp.Items { if record.Rtype != nil && *record.Rtype == recordType && record.Rdata != nil { val := *record.Rdata if recordType == "AAAA" { val = normalizeIp(val) } if val == "" { continue } vals = append(vals, val) } } return } func (o *Oracle) DnsTxtGet(db *database.Database, domain string) (vals []string, err error) { zoneName := extractDomain(domain) domain = cleanDomain(domain) req := dns.GetZoneRecordsRequest{ ZoneNameOrId: utils.PointerString(zoneName), Domain: utils.PointerString(domain), } client, err := o.provider.GetDnsClient() if err != nil { return } resp, err := client.GetZoneRecords(db, req) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone record get error"), } return } for _, record := range resp.Items { if record.Rtype != nil && *record.Rtype == "TXT" && record.Rdata != nil { vals = append(vals, *record.Rdata) } } return } func (o *Oracle) DnsTxtUpsert(db *database.Database, domain, val string) (err error) { zoneName := extractDomain(domain) domain = cleanDomain(domain) req := dns.PatchZoneRecordsRequest{ ZoneNameOrId: utils.PointerString(zoneName), PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{ Items: []dns.RecordOperation{ { Domain: &domain, Rtype: utils.PointerString("TXT"), Ttl: utils.PointerInt( settings.Acme.DnsOracleCloudTtl), Rdata: utils.PointerString(val), Operation: dns.RecordOperationOperationAdd, }, }, }, } client, err := o.provider.GetDnsClient() if err != nil { return } _, err = client.PatchZoneRecords(db, req) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone patch error"), } return } return } func (o *Oracle) DnsTxtDelete(db *database.Database, domain, val string) (err error) { zoneName := extractDomain(domain) domain = cleanDomain(domain) req := dns.PatchZoneRecordsRequest{ ZoneNameOrId: utils.PointerString(zoneName), PatchZoneRecordsDetails: dns.PatchZoneRecordsDetails{ Items: []dns.RecordOperation{ { Domain: &domain, Rtype: utils.PointerString("TXT"), Rdata: utils.PointerString(val), Operation: dns.RecordOperationOperationRemove, }, }, }, } client, err := o.provider.GetDnsClient() if err != nil { return } _, err = client.PatchZoneRecords(db, req) if err != nil { err = &errortypes.ApiError{ errors.Wrap(err, "acme: Oracle zone patch error"), } return } return } ================================================ FILE: dns/utils.go ================================================ package dns import ( "net" "strings" "golang.org/x/net/publicsuffix" ) func matchDomains(x, y string) bool { if strings.Trim(x, ".") == strings.Trim(y, ".") { return true } return false } func matchTxt(x, y string) bool { if strings.Trim(x, "\"") == strings.Trim(y, "\"") { return true } return false } func normalizeIp(addr string) string { ip := net.ParseIP(addr) if ip == nil { return "" } return strings.ToLower(ip.String()) } func extractDomain(domain string) string { domain = strings.Trim(domain, ".") topDomain, err := publicsuffix.EffectiveTLDPlusOne(domain) if err != nil { return domain } return topDomain } func cleanDomain(domain string) string { return strings.Trim(domain, ".") } ================================================ FILE: dnss/constants.go ================================================ package dnss const ( Ttl = 10 ) ================================================ FILE: dnss/database.go ================================================ package dnss import ( "net" "sync/atomic" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/imds/types" ) var ( database atomic.Pointer[Database] ) type Database struct { A map[string][]net.IP `json:"a"` AAAA map[string][]net.IP `json:"aaaa"` CNAME map[string]string `json:"cname"` } func init() { database.Store(&Database{ A: map[string][]net.IP{}, AAAA: map[string][]net.IP{}, CNAME: map[string]string{}, }) } func UpdateDatabase(db *Database) { database.Store(db) } func LoadConfig(domains []*types.Domain) { db := &Database{ A: map[string][]net.IP{}, AAAA: map[string][]net.IP{}, CNAME: map[string]string{}, } for _, domn := range domains { switch domn.Type { case domain.A: db.A[domn.Domain] = append(db.A[domn.Domain], domn.Ip) case domain.AAAA: db.AAAA[domn.Domain] = append(db.AAAA[domn.Domain], domn.Ip) case domain.CNAME: db.CNAME[domn.Domain] = domn.Target } } UpdateDatabase(db) } ================================================ FILE: dnss/dnss.go ================================================ package dnss import ( "context" "time" "github.com/coredns/coredns/plugin/forward" "github.com/coredns/coredns/plugin/pkg/proxy" "github.com/coredns/coredns/plugin/pkg/transport" "github.com/dropbox/godropbox/errors" "github.com/miekg/dns" "github.com/pritunl/pritunl-cloud/errortypes" ) type Server struct { mux *dns.ServeMux udp *dns.Server tcp *dns.Server } func (s *Server) ListenUdp() (err error) { err = s.udp.ListenAndServe() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dnss: Server udp listen error"), } return } return } func (s *Server) ListenTcp() (err error) { err = s.tcp.ListenAndServe() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "dnss: Server tcp listen error"), } return } return } func (s *Server) Shutdown() (err error) { e := s.tcp.Shutdown() if e != nil { err = e } e = s.udp.Shutdown() if e != nil { err = e } return } func NewServer(host string) (server *Server) { mux := dns.NewServeMux() prxy := proxy.NewProxy("google", "8.8.8.8:53", transport.DNS) prxy.SetReadTimeout(2 * time.Second) prxy.Start(60 * time.Second) fwd := forward.New() fwd.SetProxy(prxy) custom := &Plugin{ Next: fwd, } mux.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) { custom.ServeDNS(context.Background(), w, r) }) return &Server{ mux: mux, udp: &dns.Server{ Addr: host, Net: "udp", Handler: mux, }, tcp: &dns.Server{ Addr: host, Net: "tcp", Handler: mux, }, } } ================================================ FILE: dnss/plugin.go ================================================ package dnss import ( "context" "github.com/coredns/coredns/plugin" "github.com/miekg/dns" ) type Plugin struct { Next plugin.Handler } func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { if len(r.Question) == 0 { return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r) } q := r.Question[0] name := q.Name qtype := q.Qtype db := database.Load() found := false var answers []dns.RR targetCname, okCname := db.CNAME[name] ipsA, okA := db.A[name] ipsAAAA, okAAAA := db.AAAA[name] internalDomain := false if okCname || okA || okAAAA { internalDomain = true } if okCname { answers = append(answers, &dns.CNAME{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: Ttl, }, Target: targetCname, }) found = true if qtype == dns.TypeA || qtype == dns.TypeAAAA { switch qtype { case dns.TypeA: if ips, ok := db.A[targetCname]; ok { for _, ip := range ips { answers = append(answers, &dns.A{ Hdr: dns.RR_Header{ Name: targetCname, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: Ttl, }, A: ip, }) } } else { targetQuery := new(dns.Msg) targetQuery.SetQuestion(targetCname, dns.TypeA) rw := &Response{ ResponseWriter: w, } code, err := p.Next.ServeDNS(ctx, rw, targetQuery) if err == nil && rw.msg != nil { answers = append(answers, rw.msg.Answer...) } m := new(dns.Msg) m.SetReply(r) m.Authoritative = false m.RecursionAvailable = true m.Answer = answers w.WriteMsg(m) return code, err } case dns.TypeAAAA: if ips, ok := db.AAAA[targetCname]; ok { for _, ip := range ips { answers = append(answers, &dns.AAAA{ Hdr: dns.RR_Header{ Name: targetCname, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: Ttl, }, AAAA: ip, }) } } else { targetQuery := new(dns.Msg) targetQuery.SetQuestion(targetCname, dns.TypeAAAA) rw := &Response{ ResponseWriter: w, } code, err := p.Next.ServeDNS(ctx, rw, targetQuery) if err == nil && rw.msg != nil { answers = append(answers, rw.msg.Answer...) } msg := new(dns.Msg) msg.SetReply(r) msg.Authoritative = false msg.RecursionAvailable = true msg.Answer = answers w.WriteMsg(msg) return code, err } } } msg := new(dns.Msg) msg.SetReply(r) msg.Authoritative = true msg.RecursionAvailable = true msg.Answer = answers w.WriteMsg(msg) return dns.RcodeSuccess, nil } switch qtype { case dns.TypeA: if okA { for _, ip := range ipsA { answers = append(answers, &dns.A{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: Ttl, }, A: ip, }) } found = true } case dns.TypeAAAA: if okAAAA { for _, ip := range ipsAAAA { answers = append(answers, &dns.AAAA{ Hdr: dns.RR_Header{ Name: name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: Ttl, }, AAAA: ip, }) } found = true } } if found { msg := new(dns.Msg) msg.SetReply(r) msg.Authoritative = true msg.RecursionAvailable = true msg.Answer = answers w.WriteMsg(msg) return dns.RcodeSuccess, nil } else if internalDomain { msg := new(dns.Msg) msg.SetReply(r) msg.Authoritative = true msg.RecursionAvailable = true w.WriteMsg(msg) return dns.RcodeSuccess, nil } return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r) } func (p *Plugin) Name() string { return "pritunl-cloud" } ================================================ FILE: dnss/response.go ================================================ package dnss import ( "github.com/miekg/dns" ) type Response struct { dns.ResponseWriter msg *dns.Msg } func (r *Response) WriteMsg(m *dns.Msg) error { r.msg = m return nil } ================================================ FILE: domain/constants.go ================================================ package domain import "github.com/pritunl/mongo-go-driver/v2/bson" const ( Local = "local" AWS = "aws" Cloudflare = "cloudflare" OracleCloud = "oracle_cloud" A = "A" AAAA = "AAAA" CNAME = "CNAME" TXT = "TXT" INSERT = "insert" UPDATE = "update" DELETE = "delete" ) var ( Vacant = bson.NilObjectID ) ================================================ FILE: domain/domain.go ================================================ package domain import ( "context" "sort" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/dns" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Domain struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Type string `bson:"type" json:"type"` Secret bson.ObjectID `bson:"secret" json:"secret"` RootDomain string `bson:"root_domain" json:"root_domain"` LockId bson.ObjectID `bson:"lock_id" json:"lock_id"` LockTimestamp time.Time `bson:"lock_timestamp" json:"lock_timestamp"` LastUpdate time.Time `bson:"last_update" json:"last_update"` Records []*Record `bson:"-" json:"records"` OrigRecords []*Record `bson:"-" json:"-"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` } func (d *Domain) Locked() bool { return !d.LockId.IsZero() && time.Since(d.LockTimestamp) < time.Duration( settings.System.DomainLockTtl)*time.Second } func (d *Domain) Copy() *Domain { domn := *d recs := make([]*Record, len(domn.Records)) for i, rec := range domn.Records { recs[i] = rec.Copy() } domn.Records = recs origRecs := make([]*Record, len(domn.OrigRecords)) for i, rec := range domn.OrigRecords { origRecs[i] = rec.Copy() } domn.OrigRecords = origRecs return &domn } func (d *Domain) Json() { newRecords := make([]*Record, 0, len(d.Records)) for _, rec := range d.Records { if !rec.IsDeleted() { newRecords = append(newRecords, rec) } } sort.Sort(Records(newRecords)) d.Records = newRecords } func (d *Domain) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { d.Name = utils.FilterName(d.Name) d.RootDomain = utils.FilterDomain(d.RootDomain) if d.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "organization_required", Message: "Missing required organization", } return } switch d.Type { case Local, "": d.Type = Local d.Secret = bson.NilObjectID break case AWS: break case Cloudflare: break case OracleCloud: break default: errData = &errortypes.ErrorData{ Error: "type_invalid", Message: "Type invalid", } return } if d.Type != Local && d.Secret.IsZero() { errData = &errortypes.ErrorData{ Error: "secret_invalid", Message: "Secret invalid", } return } newRecords := []*Record{} for _, record := range d.Records { record.Domain = d.Id if record.Operation == DELETE && record.Id.IsZero() { continue } errData, err = record.Validate(db) if err != nil { return } if errData != nil { return } newRecords = append(newRecords, record) } d.Records = newRecords return } func (d *Domain) PreCommit() { d.OrigRecords = d.Records } func (d *Domain) CommitRecords(db *database.Database) (err error) { err = d.commitRecords(db, true) if err != nil { return } return } func (d *Domain) CommitRecordsSilent(db *database.Database) (err error) { err = d.commitRecords(db, false) if err != nil { return } return } func (d *Domain) commitRecords(db *database.Database, setTtl bool) (err error) { acquired := false var lockId bson.ObjectID for i := 0; i < 100; i++ { lockId, acquired, err = Lock(db, d.Id) if err != nil { return } if acquired { break } time.Sleep(200 * time.Millisecond) } if !acquired { err = &errortypes.RequestError{ errors.New("domain: Failed to acquire domain lock"), } return } ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() time.Sleep(100 * time.Millisecond) e := Unlock(db, d.Id, lockId) if e != nil { logrus.WithFields(logrus.Fields{ "domain": d.Id.Hex(), "error": e, }).Error("domain: Failed to unlock domain") } }() go func() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: e := Relock(db, d.Id, lockId) if e != nil { logrus.WithFields(logrus.Fields{ "domain": d.Id.Hex(), "error": e, }).Error("domain: Failed to relock domain") } } } }() var secr *secret.Secret if d.Type != Local { secr, err = secret.GetOrg(db, d.Organization, d.Secret) if err != nil { return } } newRecords := []*Record{} for _, record := range d.Records { if record.Operation == DELETE || record.IsDeleted() { record.Operation = DELETE for _, origRecord := range d.OrigRecords { if record.Id == origRecord.Id { record = origRecord record.Operation = DELETE break } } } newRecords = append(newRecords, record) } d.Records = newRecords batches := map[string]map[string]*Record{} for _, record := range d.Records { batchKey := record.SubDomain + ":" + record.Type if batches[batchKey] == nil { batches[batchKey] = map[string]*Record{} } curRecord := batches[batchKey][record.Value] if curRecord == nil || record.Priority() > curRecord.Priority() { batches[batchKey][record.Value] = record } } if setTtl { d.LastUpdate = time.Now() err = d.CommitFields(db, set.NewSet("last_update")) if err != nil { return } } if d.Type == OracleCloud { err = d.asyncBatches(db, secr, batches) if err != nil { return } } else { err = d.syncBatches(db, secr, batches) if err != nil { return } } return } func (d *Domain) syncBatches(db *database.Database, secr *secret.Secret, batches map[string]map[string]*Record) (err error) { for _, recordMap := range batches { records := make([]*Record, 0, len(recordMap)) for _, record := range recordMap { records = append(records, record) } err = d.UpdateRecords(db, secr, records) if err != nil { return } } return } func (d *Domain) asyncBatches(db *database.Database, secr *secret.Secret, batches map[string]map[string]*Record) (err error) { waiters := &sync.WaitGroup{} waiters.Add(len(batches)) semaphore := make( chan struct{}, settings.Acme.DnsMaxConcurrent, ) errs := make(chan error, len(batches)) for _, recordMap := range batches { records := make([]*Record, 0, len(recordMap)) for _, record := range recordMap { records = append(records, record) } go func() { semaphore <- struct{}{} defer func() { <-semaphore waiters.Done() }() e := d.UpdateRecords(db, secr, records) if e != nil { errs <- e } }() } waiters.Wait() close(errs) select { case err = <-errs: return err default: return nil } } func (d *Domain) UpdateRecords(db *database.Database, secr *secret.Secret, records []*Record) (err error) { ops := []*dns.Operation{} subDomain := "" dnsType := "" for _, rec := range records { if subDomain == "" { subDomain = rec.SubDomain } else if rec.SubDomain != subDomain { err = &errortypes.ParseError{ errors.Newf("domain: Update subdomain inconsistent"), } return } if dnsType == "" { dnsType = rec.Type } else if rec.Type != dnsType { err = &errortypes.ParseError{ errors.Newf("domain: Update type inconsistent"), } return } switch rec.Operation { case INSERT, UPDATE: ops = append(ops, &dns.Operation{ Operation: dns.UPSERT, Value: rec.Value, }) break case DELETE: ops = append(ops, &dns.Operation{ Operation: dns.DELETE, Value: rec.Value, }) break default: ops = append(ops, &dns.Operation{ Operation: dns.RETAIN, Value: rec.Value, }) } } domain := subDomain + "." + d.RootDomain if d.Type != Local { svc, e := d.GetDnsService(db) if e != nil { err = e return } err = svc.Connect(db, secr) if err != nil { return } err = svc.DnsCommit(db, domain, dnsType, ops) if err != nil { return } } for _, rec := range records { rec.Timestamp = time.Now() switch rec.Operation { case INSERT: err = rec.Insert(db) if err != nil { return } break case DELETE: err = rec.Remove(db) if err != nil { return } break default: err = rec.Commit(db) if err != nil { return } } } return } func (d *Domain) MergeRecords(deployId bson.ObjectID, newRecs []*Record) (newDomn *Domain) { recMap := map[string]map[string]map[string]*Record{} for _, rec := range d.Records { if rec.Deployment != deployId || rec.IsDeleted() { continue } if recMap[rec.SubDomain] == nil { recMap[rec.SubDomain] = map[string]map[string]*Record{} } if recMap[rec.SubDomain][rec.Type] == nil { recMap[rec.SubDomain][rec.Type] = map[string]*Record{} } recMap[rec.SubDomain][rec.Type][rec.Value] = rec } for _, newRec := range newRecs { if recMap[newRec.SubDomain] == nil { recMap[newRec.SubDomain] = map[string]map[string]*Record{} } if recMap[newRec.SubDomain][newRec.Type] == nil { recMap[newRec.SubDomain][newRec.Type] = map[string]*Record{} } rec := recMap[newRec.SubDomain][newRec.Type][newRec.Value] if rec == nil { if newDomn == nil { newDomn = d.Copy() newDomn.PreCommit() } newRec.Operation = INSERT newDomn.Records = append(newDomn.Records, newRec) } else { delete(recMap[newRec.SubDomain][newRec.Type], newRec.Value) } } for subDomain, typeMap := range recMap { for typeName, valueMap := range typeMap { for value := range valueMap { if newDomn == nil { newDomn = d.Copy() newDomn.PreCommit() } for i, domainRec := range newDomn.Records { if domainRec.SubDomain == subDomain && domainRec.Type == typeName && domainRec.Value == value { newDomn.Records[i].Operation = DELETE break } } } } } return } func (d *Domain) Commit(db *database.Database) (err error) { coll := db.Domains() err = coll.Commit(d.Id, d) if err != nil { return } return } func (d *Domain) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Domains() err = coll.CommitFields(d.Id, d, fields) if err != nil { return } return } func (d *Domain) Insert(db *database.Database) (err error) { coll := db.Domains() if !d.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("domain: Domain already exists"), } return } _, err = coll.InsertOne(db, d) if err != nil { err = database.ParseError(err) return } return } func (d *Domain) GetDnsService(db *database.Database) ( svc dns.Service, err error) { switch d.Type { case AWS: svc = &dns.Aws{} break case Cloudflare: svc = &dns.Cloudflare{} break case OracleCloud: svc = &dns.Oracle{} break default: err = &errortypes.UnknownError{ errors.Newf("domain: Unknown domain type"), } return } return } func (d *Domain) preloadRecords(recs []*Record) { if recs == nil { d.Records = []*Record{} } else { d.Records = recs } } func (d *Domain) LoadRecords(db *database.Database, skipDeleted bool) (err error) { coll := db.DomainsRecords() recs := []*Record{} cursor, err := coll.Find(db, &bson.M{ "domain": d.Id, }, options.Find(). SetSort(bson.D{{"sub_domain", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { rec := &Record{} err = cursor.Decode(rec) if err != nil { err = database.ParseError(err) return } if skipDeleted && (rec.Operation == DELETE || !rec.DeleteTimestamp.IsZero()) { continue } recs = append(recs, rec) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } d.Records = recs return } ================================================ FILE: domain/record.go ================================================ package domain import ( "net" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/miekg/dns" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) type Record struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Domain bson.ObjectID `bson:"domain" json:"domain"` Resource bson.ObjectID `bson:"resource" json:"resource"` Deployment bson.ObjectID `bson:"deployment" json:"deployment"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` DeleteTimestamp time.Time `bson:"delete_timestamp" json:"delete_timestamp"` SubDomain string `bson:"sub_domain" json:"sub_domain"` Type string `bson:"type" json:"type"` Value string `bson:"value" json:"value"` Operation string `bson:"-" json:"operation"` } func (r *Record) Priority() int { switch r.Operation { case INSERT: return 3 case UPDATE: return 2 case DELETE: return 1 default: return 0 } } func (r *Record) IsDeleted() bool { return !r.DeleteTimestamp.IsZero() } func (r *Record) Copy() *Record { rec := *r return &rec } func (r *Record) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { r.SubDomain = utils.FilterDomain(r.SubDomain) if r.SubDomain == "" { errData = &errortypes.ErrorData{ Error: "subdomain_required", Message: "Missing required sub-domain", } return } if r.Domain.IsZero() { errData = &errortypes.ErrorData{ Error: "domain_required", Message: "Missing required domain", } return } switch r.Type { case A: ip := net.ParseIP(r.Value) if ip == nil || ip.To4() == nil { errData = &errortypes.ErrorData{ Error: "invalid_value", Message: "Domain value is invalid", } return } r.Value = ip.String() case AAAA: ip := net.ParseIP(r.Value) if ip == nil || ip.To4() != nil { errData = &errortypes.ErrorData{ Error: "invalid_value", Message: "Domain value is invalid", } return } r.Value = ip.String() case CNAME: if !strings.HasSuffix(r.Value, ".") { r.Value += "." } if !dns.IsFqdn(r.Value) { errData = &errortypes.ErrorData{ Error: "invalid_value", Message: "Domain value is invalid", } return } r.Value = strings.TrimSuffix(r.Value, ".") default: err = &errortypes.UnknownError{ errors.New("domain: Unknown record type"), } return } if r.Value == "" { errData = &errortypes.ErrorData{ Error: "value_required", Message: "Missing required value", } return } return } func (r *Record) Commit(db *database.Database) (err error) { coll := db.DomainsRecords() err = coll.Commit(r.Id, r) if err != nil { return } return } func (r *Record) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.DomainsRecords() err = coll.CommitFields(r.Id, r, fields) if err != nil { return } return } func (r *Record) Remove(db *database.Database) (err error) { coll := db.DomainsRecords() if r.DeleteTimestamp.IsZero() { r.DeleteTimestamp = time.Now() err = coll.CommitFields(r.Id, r, set.NewSet("delete_timestamp")) if err != nil { return } return } deleteTtl := time.Duration(settings.System.DomainDeleteTtl) * time.Second if time.Since(r.DeleteTimestamp) < deleteTtl { return } _, err = coll.DeleteOne(db, &bson.M{ "_id": r.Id, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func (r *Record) Insert(db *database.Database) (err error) { coll := db.DomainsRecords() opts := options.FindOneAndUpdate(). SetUpsert(true). SetReturnDocument(options.After) newRec := &Record{} err = coll.FindOneAndUpdate(db, &bson.M{ "domain": r.Domain, "sub_domain": r.SubDomain, "type": r.Type, "value": r.Value, }, &bson.M{ "$set": r, }, opts).Decode(&newRec) if err != nil { err = database.ParseError(err) return } r.Id = newRec.Id return } ================================================ FILE: domain/sort.go ================================================ package domain import ( "strings" ) type Records []*Record func (r Records) Len() int { return len(r) } func (r Records) Swap(i, j int) { r[i], r[j] = r[j], r[i] } func (r Records) Less(i, j int) bool { partsI := strings.Split(r[i].SubDomain, ".") partsJ := strings.Split(r[j].SubDomain, ".") for idx := 0; idx < len(partsI)/2; idx++ { partsI[idx], partsI[len(partsI)-1-idx] = partsI[len( partsI)-1-idx], partsI[idx] } for idx := 0; idx < len(partsJ)/2; idx++ { partsJ[idx], partsJ[len(partsJ)-1-idx] = partsJ[len( partsJ)-1-idx], partsJ[idx] } minLen := len(partsI) if len(partsJ) < minLen { minLen = len(partsJ) } for idx := 0; idx < minLen; idx++ { if partsI[idx] != partsJ[idx] { return partsI[idx] < partsJ[idx] } } return len(partsI) < len(partsJ) } ================================================ FILE: domain/utils.go ================================================ package domain import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" ) func Refresh(db *database.Database, domnId bson.ObjectID) { coll := db.Domains() domn := &Domain{} err := coll.FindOne(db, &bson.M{ "_id": domnId, }).Decode(domn) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "domain": domn.Id.Hex(), "error": err, }).Error("domain: Domain refresh failed to find domain") return } if domn.Locked() { logrus.WithFields(logrus.Fields{ "domain": domn.Id.Hex(), }).Info("domain: Skipping refresh on locked domain") return } err = domn.LoadRecords(db, false) if err != nil { return } err = domn.CommitRecordsSilent(db) if err != nil { logrus.WithFields(logrus.Fields{ "domain": domn.Id.Hex(), "error": err, }).Error("domain: Domain refresh failed") return } deleteTtl := time.Duration(settings.System.DomainDeleteTtl) * time.Second now := time.Now() for _, rec := range domn.Records { if rec.IsDeleted() && now.Sub(rec.DeleteTimestamp) > deleteTtl { _, err = coll.DeleteOne(db, &bson.M{ "_id": rec.Id, "delete_timestamp": rec.DeleteTimestamp, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } } } return } func Get(db *database.Database, domnId bson.ObjectID) ( domn *Domain, err error) { coll := db.Domains() domn = &Domain{} err = coll.FindOneId(domnId, domn) if err != nil { return } return } func GetOrg(db *database.Database, orgId, domnId bson.ObjectID) ( domn *Domain, err error) { coll := db.Domains() domn = &Domain{} err = coll.FindOne(db, &bson.M{ "_id": domnId, "organization": orgId, }).Decode(domn) if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (domn *Domain, err error) { coll := db.Domains() domn = &Domain{} err = coll.FindOne(db, query).Decode(domn) if err != nil { err = database.ParseError(err) return } return } func ExistsOrg(db *database.Database, orgId, domnId bson.ObjectID) ( exists bool, err error) { coll := db.Domains() n, err := coll.CountDocuments(db, &bson.M{ "_id": domnId, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } if n > 0 { exists = true } return } func GetAll(db *database.Database, query *bson.M) ( domns []*Domain, err error) { coll := db.Domains() domns = []*Domain{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dmn := &Domain{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } domns = append(domns, dmn) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetLoadedAllIds(db *database.Database, domnIds []bson.ObjectID) ( domns []*Domain, err error) { coll := db.DomainsRecords() domainRecs := map[bson.ObjectID][]*Record{} cursor, err := coll.Find(db, &bson.M{ "domain": &bson.M{ "$in": domnIds, }, }, options.Find(). SetSort(bson.D{{"sub_domain", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { rec := &Record{} err = cursor.Decode(rec) if err != nil { err = database.ParseError(err) return } domainRecs[rec.Domain] = append(domainRecs[rec.Domain], rec) } coll = db.Domains() domns = []*Domain{} cursor, err = coll.Find(db, &bson.M{ "_id": &bson.M{ "$in": domnIds, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dmn := &Domain{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } dmn.preloadRecords(domainRecs[dmn.Id]) domns = append(domns, dmn) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func PreloadedRecords(domns []*Domain, recs []*Record) []*Domain { domainRecs := map[bson.ObjectID][]*Record{} for _, rec := range recs { domainRecs[rec.Domain] = append(domainRecs[rec.Domain], rec) } for _, domn := range domns { domn.preloadRecords(domainRecs[domn.Id]) } return domns } func GetAllName(db *database.Database, query *bson.M) ( domns []*Domain, err error) { coll := db.Domains() domns = []*Domain{} cursor, err := coll.Find( db, query, options.Find(). SetProjection(bson.D{ {"name", 1}, {"organization", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dmn := &Domain{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } domns = append(domns, dmn) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetRecordAll(db *database.Database, query *bson.M) ( recs []*Record, err error) { coll := db.DomainsRecords() recs = []*Record{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { rec := &Record{} err = cursor.Decode(rec) if err != nil { err = database.ParseError(err) return } recs = append(recs, rec) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Lock(db *database.Database, domnId bson.ObjectID) ( lockId bson.ObjectID, acquired bool, err error) { coll := db.Domains() newLockId := bson.NewObjectID() now := time.Now() ttl := now.Add(-time.Duration( settings.System.DomainLockTtl) * time.Second) resp, err := coll.UpdateOne(db, &bson.M{ "_id": domnId, "$or": []bson.M{ {"lock_id": Vacant}, {"lock_timestamp": bson.M{"$lt": ttl}}, }, }, &bson.M{ "$set": &bson.M{ "lock_id": newLockId, "lock_timestamp": now, }, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil return } return } if resp.ModifiedCount > 0 { lockId = newLockId acquired = true } return } func Relock(db *database.Database, domnId, lockId bson.ObjectID) (err error) { coll := db.Domains() _, err = coll.UpdateOne(db, &bson.M{ "_id": domnId, "lock_id": lockId, }, &bson.M{ "$set": &bson.M{ "lock_timestamp": time.Now(), }, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil return } return } return } func Unlock(db *database.Database, domnId, lockId bson.ObjectID) (err error) { coll := db.Domains() _, err = coll.UpdateOne(db, &bson.M{ "_id": domnId, "lock_id": lockId, }, &bson.M{ "$set": &bson.M{ "lock_id": Vacant, }, "$unset": &bson.M{ "lock_timestamp": 1, }, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil return } return } return } func Remove(db *database.Database, domnId bson.ObjectID) (err error) { coll := db.Domains() _, err = coll.DeleteOne(db, &bson.M{ "_id": domnId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, domnId bson.ObjectID) ( err error) { coll := db.Domains() _, err = coll.DeleteOne(db, &bson.M{ "_id": domnId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, domnIds []bson.ObjectID) (err error) { coll := db.Domains() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": domnIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, domnIds []bson.ObjectID) (err error) { coll := db.Domains() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": domnIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: drive/drive.go ================================================ package drive import ( "sync" "time" ) var ( syncLast time.Time syncLock sync.Mutex syncCache []*Device ) type Device struct { Id string `bson:"id" json:"id"` } ================================================ FILE: drive/utils.go ================================================ package drive import ( "crypto/md5" "fmt" "io/ioutil" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) func GetDevices() (devices []*Device, err error) { if time.Since(syncLast) < 30*time.Second { devices = syncCache return } syncLock.Lock() defer syncLock.Unlock() diskIds, err := ioutil.ReadDir("/dev/disk/by-id/") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "drive: Failed to list disk IDs"), } return } for _, item := range diskIds { filename := item.Name() device := &Device{ Id: filename, } devices = append(devices, device) } syncCache = devices syncLast = time.Now() return } func GetDriveHashId(id string) string { hash := md5.New() hash.Write([]byte(id)) return fmt.Sprintf("%x", hash.Sum(nil)) } ================================================ FILE: engine/bash.go ================================================ package engine import ( "bufio" "io" "os" "os/exec" "regexp" "strings" "sync" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) const shellEnvExport = ` echo "" env echo "" ` var colorRe = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`) type BashEngine struct { cmd *exec.Cmd cwd string shell string stdout io.ReadCloser stderr io.ReadCloser curEnvKeys set.Set starter *Engine } func (b *BashEngine) Init(strt *Engine) (err error) { b.starter = strt b.curEnvKeys = set.NewSet() _, err = exec.LookPath("bash") if err == nil { b.shell = "bash" } else { b.shell = "sh" err = nil } for _, pairStr := range os.Environ() { pair := strings.SplitN(pairStr, "=", 2) b.curEnvKeys.Add(pair[0]) } return } func (b *BashEngine) streamOut(reader io.Reader) (env []string) { scanner := bufio.NewScanner(reader) envCapture := false for scanner.Scan() { line := scanner.Text() cleanLine := colorRe.ReplaceAllString(line, "") if envCapture { if strings.HasPrefix(line, "") { envCapture = false } else { env = append(env, line) } } else { if strings.HasPrefix(line, "") { envCapture = true } else if cleanLine != "" { b.starter.ProcessOutput(cleanLine) } } } return } func (b *BashEngine) streamErr(reader io.Reader) { scanner := bufio.NewScanner(reader) envCapture := false for scanner.Scan() { line := scanner.Text() cleanLine := colorRe.ReplaceAllString(line, "") if envCapture { if strings.HasPrefix(line, "echo \"\"") { envCapture = false } else if strings.TrimSpace(line) == "env" { } else if cleanLine != "" { b.starter.ProcessOutput(cleanLine) } } else { if strings.HasPrefix(line, "echo \"\"") { envCapture = true } else if cleanLine != "" { b.starter.ProcessOutput(cleanLine) } } } } func (b *BashEngine) Run(block string) (err error) { cmd := exec.Command(b.shell, "-v", "-c", block+shellEnvExport) cmd.Env = b.starter.GetEnviron() cmd.Dir = b.starter.GetCwd() stdout, err := cmd.StdoutPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to create stdout pipe"), } return } stderr, err := cmd.StderrPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to create stderr pipe"), } return } err = cmd.Start() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to start command"), } return } env := []string{} wg := sync.WaitGroup{} wg.Add(2) go func() { env = b.streamOut(stdout) wg.Done() }() go func() { b.streamErr(stderr) wg.Done() }() err = cmd.Wait() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Exit error in "+b.shell), } return } wg.Wait() for _, pairStr := range env { pair := strings.SplitN(pairStr, "=", 2) if len(pair) != 2 { continue } key, val := pair[0], pair[1] if key == "PWD" { b.starter.UpdateCwd(val) } else if !b.curEnvKeys.Contains(key) { b.starter.UpdateEnv(key, val) } } return } ================================================ FILE: engine/constants.go ================================================ package engine const ( QueueSize = 256 ) ================================================ FILE: engine/engine.go ================================================ package engine import ( "fmt" "os" "strings" "sync" "sync/atomic" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/tools/logger" ) type Engine struct { cwd string env map[string]string blocks []*Block bash *BashEngine python *PythonEngine lock sync.Mutex outputLock sync.Mutex fault atomic.Value queue chan []*Block OnStatus func(status string) } func (e *Engine) UpdateEnv(key, val string) { e.env[key] = val } func (e *Engine) UpdateCwd(cwd string) { e.cwd = cwd } func (e *Engine) ProcessOutput(output string) { e.outputLock.Lock() fmt.Println(output) e.outputLock.Unlock() } func (e *Engine) GetEnv() map[string]string { return e.env } func (e *Engine) GetCwd() string { return e.cwd } func (e *Engine) GetEnviron() (env []string) { env = []string{} for _, pair := range os.Environ() { key := strings.SplitN(pair, "=", 2)[0] if e.env[key] == "" { env = append(env, pair) } } for key, val := range e.env { env = append(env, key+"="+val) } return } func (e *Engine) Init() (err error) { e.queue = make(chan []*Block, QueueSize) e.fault.Store(false) e.cwd, err = os.Getwd() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "starter: Failed to get working dir"), } return } e.env = map[string]string{} e.bash = &BashEngine{} err = e.bash.Init(e) if err != nil { return } e.python = &PythonEngine{} err = e.python.Init(e) if err != nil { return } defer func() { err2 := e.python.Exit() if err2 != nil { panic(err2) } }() return } func (e *Engine) StartRunner() { go e.runner() } func (e *Engine) getBlocks() (blocks []*Block) { blocks = <-e.queue for { select { case req := <-e.queue: blocks = req default: return blocks } } } func (e *Engine) UpdateSpec(data string) (err error) { blocks, err := Parse(data) if err != nil { return } e.blocks = blocks return } func (e *Engine) runner() { for { blocks := e.getBlocks() if !e.fault.Load().(bool) { e.OnStatus(types.ReloadingClean) } else { e.OnStatus(types.ReloadingFault) } _, err := e.Run(Reload, blocks) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("agent: Failed to run spec") e.OnStatus(types.Fault) } else { e.OnStatus(types.Running) } } } func (e *Engine) Run(phase string, blocks []*Block) (fatal bool, err error) { for i, block := range blocks { switch phase { case Initial: break case Reboot: if block.Phase != Reboot && block.Phase != Reload { continue } break case Reload: if block.Phase != Reload { continue } break } err = e.runBlock(block.Type, block.Code) if err != nil { for _, block := range blocks[i:] { switch phase { case Initial: if block.Phase != Reload { fatal = true } case Reboot: if block.Phase != Reboot && block.Phase != Reload { continue } if block.Phase != Reload { fatal = true } case Reload: if block.Phase != Reload { continue } } } e.fault.Store(true) return } } e.fault.Store(false) return } func (e *Engine) runBlock(blockType, block string) (err error) { switch blockType { case "shell": err = e.bash.Run(block) if err != nil { return } case "python": err = e.python.Run(block) if err != nil { return } } return } func (e *Engine) Queue(data string) { blocks, err := Parse(data) if err != nil { return } e.lock.Lock() if len(e.queue) >= QueueSize-16 { return } e.queue <- blocks e.lock.Unlock() } ================================================ FILE: engine/parser.go ================================================ package engine import ( "regexp" "strings" ) type Block struct { Type string Phase string Code string LineNum int } const ( Initial = "initial" Reboot = "reboot" Reload = "reload" Image = "image" ) var ( codeBlockRe = regexp.MustCompile(`^([a-zA-Z]+)\s*(\{([^}]+)\})?$`) ) func Parse(data string) (blocks []*Block, err error) { blocks = []*Block{} var curBlock *Block for n, line := range strings.Split(data, "\n") { if curBlock == nil { if strings.HasPrefix(line, "```") { lang, attrs := parseCodeBlockHeader(line[3:]) phase := Initial if attrs != nil { switch attrs["phase"] { case Initial: phase = Initial case Reboot: phase = Reboot case Reload: phase = Reload } } switch lang { case "shell": curBlock = &Block{ Type: "shell", Phase: phase, } case "python": curBlock = &Block{ Type: "python", Phase: phase, } } } } else { if line == "```" { curBlock.LineNum = n + 1 blocks = append(blocks, curBlock) curBlock = nil } else { curBlock.Code += line + "\n" } } } return } func parseCodeBlockHeader(input string) (language string, attrs map[string]string) { attrs = map[string]string{} matches := codeBlockRe.FindStringSubmatch(input) if len(matches) == 0 { return } language = matches[1] if len(matches) < 3 { return } attrPairs := strings.Split(matches[2], ",") for _, pair := range attrPairs { pair = strings.TrimPrefix(pair, "{") pair = strings.TrimSuffix(pair, "}") keyValue := strings.SplitN(pair, "=", 2) if len(keyValue) == 2 { key := strings.TrimSpace(keyValue[0]) value := strings.Trim(strings.TrimSpace(keyValue[1]), `"`) attrs[key] = value } } return } ================================================ FILE: engine/python.go ================================================ package engine import ( "bufio" "container/list" "encoding/json" "fmt" "io" "os/exec" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var ( PythonExec = "python3" ) const pyEngine = `#!/usr/bin/env python3 import platform import os import json import traceback def _pystarter_get_startup(): python_version = platform.python_version() compiler = platform.python_compiler() os_name = platform.system().lower() + " " + platform.release() build_date = " ".join(platform.python_build()[1].split()[1:4]) return f"Python {python_version} ({build_date}) [{compiler}] on {os_name}" def export(name, arg): name = ''.join(c for c in name if c.isalnum() or c == '_') arg = json.dumps(arg) print(f"{name}={arg}") print(_pystarter_get_startup()) print("") while True: data = input() if not data: continue data = json.loads(data) if data["type"] == "exit": exit(0) elif data["type"] == "env": for key, val in data["env"].items(): os.environ[key] = val print("") elif data["type"] == "chdir": os.chdir(data["input"]) print("") elif data["type"] == "exec": try: exec(data["input"]) except Exception as err: print(f"An error occurred: {err}") print(f"Exception type: {type(err).__name__}") traceback.print_exc() print(f"" f"{os.getcwd()}") ` type pythonData struct { Type string `json:"type"` Input string `json:"input"` Env map[string]string `json:"env"` } type PythonEngine struct { cmd *exec.Cmd cwd string stdin io.WriteCloser stdout io.ReadCloser stderr io.ReadCloser cmdErr error inputLines *list.List waiter chan bool initialized bool starter *Engine output string } func (p *PythonEngine) Init(strt *Engine) (err error) { p.starter = strt return } func (p *PythonEngine) start() (err error) { p.waiter = make(chan bool, 16) p.inputLines = list.New() p.cmd = exec.Command(PythonExec, "-u", "-c", pyEngine) p.cmd.Env = p.starter.GetEnviron() p.cmd.Dir = p.starter.GetCwd() p.stdout, err = p.cmd.StdoutPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to get py stdout"), } return } p.stderr, err = p.cmd.StderrPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to get py stderr"), } return } p.stdin, err = p.cmd.StdinPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to get py stdin"), } return } err = p.cmd.Start() if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to start py"), } return } p.wait() p.copyOutput(p.stdout) p.copyOutput(p.stderr) <-p.waiter return } func (p *PythonEngine) flushOutput() { if p.output == "" { return } output := p.output p.output = "" output = strings.TrimRight(output, "\n") p.starter.ProcessOutput(output) } func (p *PythonEngine) copyOutput(src io.ReadCloser) { go func() { scanner := bufio.NewScanner(src) for scanner.Scan() { output := scanner.Text() if !p.initialized { if strings.Contains(output, "") { p.initialized = true p.waiter <- true } else { p.starter.ProcessOutput(output) } continue } execDoneStart := strings.Index(output, "") exportStart := strings.Index(output, "") if execDoneStart != -1 { execDoneStart += 25 execDoneEnd := strings.Index(output, "") if execDoneEnd == -1 { err := &errortypes.ExecError{ errors.Newf( "starter: Incomplete exec response '%s'", output), } panic(err) } p.starter.UpdateCwd(output[execDoneStart:execDoneEnd]) p.waiter <- true } else if exportStart != -1 { exportStart += 22 exportEnd := strings.Index(output, "") if exportEnd == -1 { err := &errortypes.ExecError{ errors.Newf( "starter: Incomplete export '%s'", output), } panic(err) } pair := strings.SplitN(output[exportStart:exportEnd], "=", 2) if len(pair) == 2 { key, val := pair[0], pair[1] if val[0] == '"' && val[len(val)-1] == '"' { val = val[1 : len(val)-1] } p.starter.UpdateEnv(key, val) } } else if strings.Contains(output, "") { p.waiter <- true } else { p.output += output + "\n" } } }() } func (p *PythonEngine) wait() { go func() { defer func() { if p.stdin != nil { _ = p.stdin.Close() } }() err := p.cmd.Wait() if err != nil { p.flushOutput() err = &errortypes.ExecError{ errors.Wrap(err, "starter: Exit error in py"), } p.cmdErr = err p.waiter <- true return } p.waiter <- true }() } func (p *PythonEngine) updateEnv() (err error) { defer func() { p.flushOutput() }() p.waiter = make(chan bool, 16) data := &pythonData{ Type: "env", Env: p.starter.GetEnv(), } dataIn, err := json.Marshal(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "starter: Failed to marshal env"), } return } _, err = fmt.Fprintln(p.stdin, string(dataIn)) if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to update env in py"), } return } <-p.waiter return } func (p *PythonEngine) updateCwd() (err error) { defer func() { p.flushOutput() }() p.waiter = make(chan bool, 16) data := &pythonData{ Type: "chdir", Input: p.starter.GetCwd(), } dataIn, err := json.Marshal(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "starter: Failed to marshal env"), } return } _, err = fmt.Fprintln(p.stdin, string(dataIn)) if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to update env in py"), } return } <-p.waiter return } func (p *PythonEngine) run(code string) (err error) { defer func() { p.flushOutput() }() p.waiter = make(chan bool, 16) data := &pythonData{ Type: "exec", Input: code, } dataIn, err := json.Marshal(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "starter: Failed to marshal code"), } return } _, err = fmt.Fprintln(p.stdin, string(dataIn)) if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to run code in py"), } return } <-p.waiter return } func (p *PythonEngine) Exit() (err error) { p.waiter = make(chan bool, 16) if p.cmd == nil { return } data := &pythonData{ Type: "exit", } dataIn, err := json.Marshal(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "starter: Failed to marshal exit signal"), } return } _, err = fmt.Fprintln(p.stdin, string(dataIn)) if err != nil { err = &errortypes.ExecError{ errors.Wrap(err, "starter: Failed to run exit signal in py"), } return } <-p.waiter if p.cmdErr != nil { err = p.cmdErr } return } func (p *PythonEngine) Run(code string) (err error) { if p.cmd == nil { err = p.start() if err != nil { return } } err = p.updateEnv() if err != nil { return } err = p.updateCwd() if err != nil { return } for _, line := range strings.Split(code, "\n") { p.starter.ProcessOutput(">>> " + line) } err = p.run(code) if err != nil { return } if p.cmdErr != nil { err = p.cmdErr return } return } ================================================ FILE: errortypes/errortypes.go ================================================ package errortypes import ( "github.com/dropbox/godropbox/errors" ) type UnknownError struct { errors.DropboxError } type NotFoundError struct { errors.DropboxError } type ReadError struct { errors.DropboxError } type WriteError struct { errors.DropboxError } type ParseError struct { errors.DropboxError } type AuthenticationError struct { errors.DropboxError } type VerificationError struct { errors.DropboxError } type ApiError struct { errors.DropboxError } type DatabaseError struct { errors.DropboxError } type RequestError struct { errors.DropboxError } type ConnectionError struct { errors.DropboxError } type TimeoutError struct { errors.DropboxError } type ExecError struct { errors.DropboxError } type NetworkError struct { errors.DropboxError } type TypeError struct { errors.DropboxError } type ErrorData struct { Error string `json:"error"` Message string `json:"error_msg"` } func (e *ErrorData) GetError() (err error) { err = &ParseError{ errors.Newf("error: Parse error %s - %s", e.Error, e.Message), } return } func GetErrorMessage(err error) string { if err == nil { return "" } if intErr, ok := err.(errors.DropboxError); ok { return intErr.GetMessage() } return err.Error() } ================================================ FILE: eval/constants.go ================================================ package eval import ( "github.com/dropbox/godropbox/container/set" ) type Equal struct{} type NotEqual struct{} type Less struct{} type LessEqual struct{} type Greater struct{} type GreaterEqual struct{} type If struct{} type And struct{} type Or struct{} type For struct{} type Then struct{} const ( StatementMaxLength = 1024 StatementMaxParts = 30 ) var StatementSafeCharacters = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ' ', '.', '_', '-', '=', '>', '<', '!', '\'', '(', ')', ) ================================================ FILE: eval/errortypes.go ================================================ package eval import ( "bytes" "fmt" "html/template" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" ) type EvalError struct { Statement string Index int ErrIndex int Length int errors.DropboxError } func NewEvalError(statement string, index, errorIndex int, length int, templMsg string, args ...interface{}) (err error) { evalErr := &EvalError{ Statement: statement, Index: index + 1, ErrIndex: errorIndex + 1, Length: length, } tmpl, err := template.New("eval").Parse(templMsg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "eval: Failed to parse eval error template"), } return } errorMsg := &bytes.Buffer{} err = tmpl.Execute(errorMsg, evalErr) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "eval: Failed to execute eval error template"), } return } errorMsgStr := fmt.Sprintf(errorMsg.String(), args...) errorMsgStr += fmt.Sprintf(" index=%d", evalErr.Index) errorMsgStr += fmt.Sprintf(" error_index=%d", evalErr.ErrIndex) errorMsgStr += fmt.Sprintf(" statement=\"%s\"", evalErr.Statement) evalErr.DropboxError = errors.New(errorMsgStr) err = evalErr return } ================================================ FILE: eval/eval.go ================================================ package eval import ( "reflect" "strconv" "strings" ) type Data map[string]map[string]interface{} type Parser struct { statement string parts []string partsLen int data Data } func (p *Parser) parseRef(ref string, pos int) (val interface{}, err error) { n := len(ref) if n == 0 { return } if ref[0] == '\'' { if ref[n-1] != '\'' { err = NewEvalError( p.statement, pos, pos, p.partsLen, "eval: Invalid string {{.ErrIndex}}", ) return } val = ref[1 : n-1] return } switch ref { case "true": val = true return case "false": val = true return case "==": val = Equal{} return case "!=": val = NotEqual{} return case "<": val = Less{} return case "<=": val = LessEqual{} return case ">": val = Greater{} return case ">=": val = GreaterEqual{} return case "IF": val = If{} return case "AND": val = And{} return case "OR": val = Or{} return case "FOR": val = For{} return case "THEN": val = Then{} return } if ref == "true" { val = true return } else if ref == "false" { val = false return } intVal, e := strconv.Atoi(ref) if e == nil { val = intVal return } floatVal, e := strconv.ParseFloat(ref, 64) if e == nil { val = floatVal return } split := strings.Split(ref, ".") if len(split) != 2 { err = NewEvalError( p.statement, pos, pos, p.partsLen, "eval: Invalid reference {{.ErrIndex}}", ) return } group := p.data[split[0]] if group == nil { err = NewEvalError( p.statement, pos, pos, p.partsLen, "eval: Invalid reference group {{.ErrIndex}}", ) return } groupVal := group[split[1]] if groupVal == nil { err = NewEvalError( p.statement, pos, pos, p.partsLen, "eval: Invalid reference group key {{.ErrIndex}}", ) return } else { if fVal, ok := groupVal.(float64); ok { if fVal == float64(int(fVal)) { val = int(fVal) } else { val = fVal } } else { val = groupVal } } return } func (p *Parser) parseComp(left, right, comp interface{}) bool { if reflect.TypeOf(left) != reflect.TypeOf(right) { return false } switch leftVal := left.(type) { case bool: switch comp.(type) { case Equal: return leftVal == right.(bool) case NotEqual: return leftVal != right.(bool) case Less: return false case LessEqual: return false case Greater: return false case GreaterEqual: return false default: panic("Invalid comp") } case string: switch comp.(type) { case Equal: return leftVal == right.(string) case NotEqual: return leftVal != right.(string) case Less: return leftVal < right.(string) case LessEqual: return leftVal <= right.(string) case Greater: return leftVal > right.(string) case GreaterEqual: return leftVal >= right.(string) default: panic("Invalid comp") } case int: switch comp.(type) { case Equal: return leftVal == right.(int) case NotEqual: return leftVal != right.(int) case Less: return leftVal < right.(int) case LessEqual: return leftVal <= right.(int) case Greater: return leftVal > right.(int) case GreaterEqual: return leftVal >= right.(int) default: panic("Invalid comp") } case float64: switch comp.(type) { case Equal: return leftVal == right.(float64) case NotEqual: return leftVal != right.(float64) case Less: return leftVal < right.(float64) case LessEqual: return leftVal <= right.(float64) case Greater: return leftVal > right.(float64) case GreaterEqual: return leftVal >= right.(float64) default: panic("Invalid comp") } default: panic("Invalid type") } return false } func (p *Parser) Eval() (resp string, threshold int, err error) { p.parts = strings.Fields(p.statement) p.partsLen = len(p.parts) if p.partsLen < 6 { err = NewEvalError( p.statement, 0, 0, p.partsLen, "eval: Statement under min parts", ) return } else if p.partsLen > 30 { err = NewEvalError( p.statement, 0, 0, p.partsLen, "eval: Statement exceeds max parts", ) return } else if len(p.statement) > 1024 { err = NewEvalError( p.statement, 0, 0, p.partsLen, "eval: Statement exceeds max length", ) return } if p.parts[0] != "IF" { err = NewEvalError( p.statement, 0, 0, p.partsLen, "eval: Statement part {{.ErrorIndex}} invalid", ) return } i := 1 var expr interface{} results := []bool{} final := false for x := 0; x < 100; x++ { if p.partsLen < i+4 { err = NewEvalError( p.statement, i, i, p.partsLen, "eval: Incomplete expression", ) return } index := i i += 4 leftOp, e := p.parseRef(p.parts[index], index) if e != nil { err = e return } comp, e := p.parseRef(p.parts[index+1], index+1) if e != nil { err = e return } rightOp, e := p.parseRef(p.parts[index+2], index+2) if e != nil { err = e return } next, e := p.parseRef(p.parts[index+3], index+3) if e != nil { err = e return } switch leftOp.(type) { case string: break case int: break case float64: break case bool: break default: err = NewEvalError( p.statement, index, index, p.partsLen, "eval: Invalid left operator {{.ErrIndex}}", ) return } switch rightOp.(type) { case string: break case int: break case float64: break case bool: break default: err = NewEvalError( p.statement, index, index+2, p.partsLen, "eval: Invalid right operator {{.ErrIndex}}", ) return } switch comp.(type) { case Equal: break case NotEqual: break case Less: break case LessEqual: break case Greater: break case GreaterEqual: break default: err = NewEvalError( p.statement, index, index+1, p.partsLen, "eval: Invalid comparison operator {{.ErrIndex}}", ) return } result := p.parseComp(leftOp, rightOp, comp) results = append(results, result) if _, ok := next.(For); ok { if index+6 != p.partsLen-1 { err = NewEvalError( p.statement, index, index+6, p.partsLen, "eval: Expected %d length", index+6, ) return } next, e = p.parseRef(p.parts[index+5], index+5) if e != nil { err = e return } if _, ok := next.(Then); !ok { err = NewEvalError( p.statement, index, index+5, p.partsLen, "eval: Expected THAN at {{.ErrIndex}}", ) return } forVal, e := p.parseRef(p.parts[index+4], index+4) if e != nil { err = e return } if forInt, ok := forVal.(int); ok { threshold = forInt } else { err = NewEvalError( p.statement, index, index+4, p.partsLen, "eval: Expected FOR value to be int", ) return } index += 2 } switch next.(type) { case And: if expr == nil { expr = And{} } else if _, ok := expr.(And); !ok { err = NewEvalError( p.statement, index, index+2, p.partsLen, "eval: Cannot mix OR with AND", ) return } break case Or: if expr == nil { expr = Or{} } else if _, ok := expr.(Or); !ok { err = NewEvalError( p.statement, index, index+2, p.partsLen, "eval: Cannot mix OR with AND", ) return } break case Then: if index+4 != p.partsLen-1 { err = NewEvalError( p.statement, index, index, p.partsLen, "eval: Expected %d length", index+4, ) return } if _, ok := expr.(Or); ok { for _, result := range results { if result { final = true break } } } else { if len(results) == 0 { final = false } else { final = true for _, result := range results { if !result { final = false break } } } } if final { respInf, e := p.parseRef(p.parts[index+4], index+4) if e != nil { err = e return } if respStr, ok := respInf.(string); ok { resp = respStr } else { err = NewEvalError( p.statement, index, index+4, p.partsLen, "eval: Result must be string", ) return } } return default: err = NewEvalError( p.statement, index, index+3, p.partsLen, "eval: Invalid continuation", ) return } } err = NewEvalError( p.statement, 0, 0, p.partsLen, "eval: Infinite loop", ) return } func Eval(data Data, statement string) (resp string, threshold int, err error) { parsr := &Parser{ statement: statement, data: data, } resp, threshold, err = parsr.Eval() if err != nil { return } return } ================================================ FILE: eval/utils.go ================================================ package eval import ( "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" ) func Validate(statement string) (err error) { if len(statement) == 0 { err = &errortypes.ParseError{ errors.New("eval: Empty statement"), } return } if len(statement) > StatementMaxLength { err = &errortypes.ParseError{ errors.Newf("eval: Statement exceeds max length"), } return } for i, c := range statement { if !StatementSafeCharacters.Contains(c) { err = &errortypes.ParseError{ errors.Newf("eval: Illegal char (%s) at %d", string(c), i+1), } return } } return } ================================================ FILE: event/event.go ================================================ package event import ( "fmt" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/requires" "github.com/sirupsen/logrus" ) var ( listeners = map[string][]func(*EventPublish){} ) type Event struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Channel string `bson:"channel" json:"channel"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Data bson.M `bson:"data" json:"data"` } type EventPublish struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Channel string `bson:"channel" json:"channel"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Data interface{} `bson:"data" json:"data"` } type CustomEvent interface { GetId() bson.ObjectID GetData() interface{} } type Dispatch struct { Type string `bson:"type" json:"type"` } func getCursorId(db *database.Database, coll *database.Collection, channels []string) (id bson.ObjectID, err error) { msg := &EventPublish{} var query *bson.M if len(channels) == 1 { query = &bson.M{ "channel": channels[0], } } else { query = &bson.M{ "channel": &bson.M{ "$in": channels, }, } } for i := 0; i < 2; i++ { err = coll.FindOne( db, query, options.FindOne(). SetSort(bson.D{{"$natural", -1}}), ).Decode(msg) if err != nil { err = database.ParseError(err) if i > 0 { return } switch err.(type) { case *database.NotFoundError: // Cannot use client-side ObjectId for tailable collection err = Publish(db, channels[0], nil) if err != nil { err = database.ParseError(err) return } continue default: return } } else { break } } id = msg.Id return } func getCursorIdRetry(channels []string) bson.ObjectID { db := database.GetDatabase() defer db.Close() for { coll := db.Events() cursorId, err := getCursorId(db, coll, channels) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Subscribe cursor error") db.Close() db = database.GetDatabase() time.Sleep(constants.RetryDelay) continue } return cursorId } } func Publish(db *database.Database, channel string, data interface{}) ( err error) { coll := db.Events() msg := &EventPublish{ Id: bson.NewObjectID(), Channel: channel, Timestamp: time.Now(), Data: data, } _, err = coll.InsertOne(db, msg) if err != nil { err = database.ParseError(err) return } return } func PublishDispatch(db *database.Database, typ string) ( err error) { evt := &Dispatch{ Type: typ, } err = Publish(db, "dispatch", evt) if err != nil { return } return } func Subscribe(channels []string, duration time.Duration, onMsg func(*EventPublish, error) bool) { db := database.GetDatabase() defer db.Close() coll := db.Events() cursorId := getCursorIdRetry(channels) var channelBson interface{} if len(channels) == 1 { channelBson = channels[0] } else { channelBson = &bson.M{ "$in": channels, } } queryOpts := options.Find(). SetSort(bson.D{{"$natural", 1}}). SetMaxAwaitTime(duration). SetCursorType(options.TailableAwait) query := bson.M{ "_id": bson.M{ "$gt": cursorId, }, "channel": channelBson, } var cursor *mongo.Cursor var err error for { cursor, err = coll.Find( db, query, queryOpts, ) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener find error") if !onMsg(nil, err) { return } } else { break } time.Sleep(constants.RetryDelay) } defer func() { defer func() { recover() }() if r := recover(); r != nil { logrus.WithFields(logrus.Fields{ "error": errors.New(fmt.Sprintf("%s", r)), }).Error("event: Event panic") } cursor.Close(db) }() for { for cursor.Next(db) { msg := &EventPublish{} err = cursor.Decode(msg) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener decode error") if !onMsg(nil, err) { return } time.Sleep(constants.RetryDelay) break } cursorId = msg.Id if msg.Data == nil { // Blank msg for cursor continue } if !onMsg(msg, nil) { return } } err = cursor.Err() if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener cursor error") if !onMsg(nil, err) { return } time.Sleep(constants.RetryDelay) } cursor.Close(db) db.Close() db = database.GetDatabase() coll = db.Events() query := &bson.M{ "_id": &bson.M{ "$gt": cursorId, }, "channel": channelBson, } for { cursor, err = coll.Find( db, query, queryOpts, ) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener find error") if !onMsg(nil, err) { return } } else { break } time.Sleep(constants.RetryDelay) } } } func SubscribeType(channels []string, duration time.Duration, newEvent func() CustomEvent, onMsg func(CustomEvent, error) bool) { db := database.GetDatabase() defer db.Close() coll := db.Events() cursorId := getCursorIdRetry(channels) var channelBson interface{} if len(channels) == 1 { channelBson = channels[0] } else { channelBson = &bson.M{ "$in": channels, } } queryOpts := options.Find(). SetSort(bson.D{{"$natural", 1}}). SetMaxAwaitTime(duration). SetCursorType(options.TailableAwait) query := &bson.M{ "_id": &bson.M{ "$gt": cursorId, }, "channel": channelBson, } var cursor *mongo.Cursor var err error for { cursor, err = coll.Find( db, query, queryOpts, ) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener find error") if !onMsg(nil, err) { return } } else { break } time.Sleep(constants.RetryDelay) } defer func() { defer func() { recover() }() cursor.Close(db) }() for { for cursor.Next(db) { msg := newEvent() err = cursor.Decode(msg) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener decode error") if !onMsg(nil, err) { return } time.Sleep(constants.RetryDelay) break } cursorId = msg.GetId() if msg.GetData() == nil { // Blank msg for cursor continue } if !onMsg(msg, nil) { return } } err = cursor.Err() if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener cursor error") if !onMsg(nil, err) { return } time.Sleep(constants.RetryDelay) } cursor.Close(db) db.Close() db = database.GetDatabase() coll = db.Events() query := &bson.M{ "_id": &bson.M{ "$gt": cursorId, }, "channel": channelBson, } for { cursor, err = coll.Find( db, query, queryOpts, ) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener find error") if !onMsg(nil, err) { return } } else { break } time.Sleep(constants.RetryDelay) } } } func Register(channel string, callback func(*EventPublish)) { callbacks := listeners[channel] if callbacks == nil { callbacks = []func(*EventPublish){} } listeners[channel] = append(callbacks, callback) } func subscribe(channels []string) { Subscribe(channels, 10*time.Second, func(msg *EventPublish, err error) bool { if msg == nil || err != nil { return true } for _, listener := range listeners[msg.Channel] { listener(msg) } return true }) } func init() { module := requires.New("event") module.After("settings") module.Handler = func() (err error) { go func() { channels := []string{} for channel := range listeners { channels = append(channels, channel) } if len(channels) > 0 { subscribe(channels) } }() return } } ================================================ FILE: event/listener.go ================================================ package event import ( "fmt" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/sirupsen/logrus" ) type Listener struct { db *database.Database state bool channels []string stream chan *Event once sync.Once } func (l *Listener) Listen() chan *Event { return l.stream } func (l *Listener) Close() { l.state = false l.once.Do(func() { close(l.stream) }) } func (l *Listener) sub(cursorId bson.ObjectID) { coll := l.db.Events() var channelBson interface{} if len(l.channels) == 1 { channelBson = l.channels[0] } else { channelBson = &bson.M{ "$in": l.channels, } } queryOpts := options.Find(). SetSort(bson.D{{"$natural", 1}}). SetMaxAwaitTime(10 * time.Second). SetCursorType(options.TailableAwait) query := &bson.M{ "_id": &bson.M{ "$gt": cursorId, }, "channel": channelBson, } var cursor *mongo.Cursor var err error for { cursor, err = coll.Find( l.db, query, queryOpts, ) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener find error") } else { break } if !l.state { return } time.Sleep(constants.RetryDelay) if !l.state { return } } defer func() { defer func() { recover() }() if r := recover(); r != nil { logrus.WithFields(logrus.Fields{ "error": errors.New(fmt.Sprintf("%s", r)), }).Error("event: Event panic") } cursor.Close(l.db) }() for { for cursor.Next(l.db) { msg := &Event{} err = cursor.Decode(msg) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener decode error") time.Sleep(constants.RetryDelay) break } cursorId = msg.Id if msg.Data == nil { // Blank msg for cursor continue } if !l.state { return } l.stream <- msg } if !l.state { return } err = cursor.Err() if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener cursor error") time.Sleep(constants.RetryDelay) } if !l.state { return } cursor.Close(l.db) coll = l.db.Events() query := &bson.M{ "_id": &bson.M{ "$gt": cursorId, }, "channel": channelBson, } for { cursor, err = coll.Find( l.db, query, queryOpts, ) if err != nil { err = database.ParseError(err) logrus.WithFields(logrus.Fields{ "error": err, }).Error("event: Listener find error") } else { break } if !l.state { return } time.Sleep(constants.RetryDelay) if !l.state { return } } } } func (l *Listener) init() (err error) { coll := l.db.Events() cursorId, err := getCursorId(l.db, coll, l.channels) if err != nil { err = database.ParseError(err) return } l.state = true go func() { if r := recover(); r != nil { logrus.WithFields(logrus.Fields{ "error": errors.New(fmt.Sprintf("%s", r)), }).Error("event: Listener panic") } l.sub(cursorId) }() return } func SubscribeListener(db *database.Database, channels []string) ( lst *Listener, err error) { lst = &Listener{ db: db, channels: channels, stream: make(chan *Event, 10), } err = lst.init() if err != nil { return } return } ================================================ FILE: event/socket.go ================================================ package event import ( "context" "net/http" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/gorilla/websocket" ) var ( Upgrader = websocket.Upgrader{ HandshakeTimeout: 30 * time.Second, ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } WebSockets = set.NewSet() WebSocketsLock = sync.Mutex{} ) type WebSocket struct { Conn *websocket.Conn Ticker *time.Ticker Listener *Listener Cancel context.CancelFunc Closed bool } func (w *WebSocket) Close() { w.Closed = true func() { defer func() { recover() }() w.Cancel() }() func() { defer func() { recover() }() w.Ticker.Stop() }() func() { defer func() { recover() }() w.Listener.Close() }() func() { defer func() { recover() }() w.Conn.Close() }() } func WebSocketsStop() { WebSocketsLock.Lock() for socketInf := range WebSockets.Iter() { func() { socket := socketInf.(*WebSocket) socket.Close() }() } WebSockets = set.NewSet() WebSocketsLock.Unlock() } ================================================ FILE: features/qemu.go ================================================ package features import ( "io/ioutil" "os" "strconv" "strings" "sync" "syscall" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) var ( verCache = version{} verCacheLock = sync.Mutex{} verCacheTime = time.Time{} ) const ( Libexec = "/usr/libexec/qemu-kvm" System = "/usr/bin/qemu-system-x86_64" ) type version struct { Major int Minor int Patch int } func GetQemuPath() (path string, err error) { exists, err := utils.Exists(System) if err != nil { return } if exists { path = System } else { path = Libexec } return } func GetQemuVersion() (major, minor, patch int, err error) { verCacheLock.Lock() if time.Since(verCacheTime) < 1*time.Minute { major = verCache.Major minor = verCache.Minor patch = verCache.Patch verCacheLock.Unlock() return } verCacheLock.Unlock() qemuPath, err := GetQemuPath() if err != nil { return } output, _ := utils.ExecCombinedOutputLogged( nil, qemuPath, "--version", ) lines := strings.Split(output, "\n") for _, line := range lines { fields := strings.Fields(line) if len(fields) < 4 || strings.ToLower(fields[2]) != "version" { continue } versions := strings.Split(fields[3], ".") if len(versions) != 3 { continue } var e error major, e = strconv.Atoi(versions[0]) if e != nil { continue } minor, e = strconv.Atoi(versions[1]) if e != nil { major = 0 continue } patch, e = strconv.Atoi(versions[2]) if e != nil { major = 0 minor = 0 continue } break } if major == 0 { err = &errortypes.ParseError{ errors.Newf("qemu: Invalid Qemu version '%s'", output), } return } verCacheLock.Lock() verCache.Major = major verCache.Minor = minor verCache.Patch = patch verCacheTime = time.Now() verCacheLock.Unlock() return } func GetKernelVersion() (major, minor, patch int, err error) { uname := &syscall.Utsname{} err = syscall.Uname(uname) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "qemu: Failed to get syscall uname"), } return } version := utils.Int8Str(uname.Release[:]) versions := strings.Split(version, "-") if len(versions) < 2 { err = &errortypes.ParseError{ errors.Newf( "qemu: Failed to parse uname version 1 '%s'", version, ), } return } versions = strings.Split(versions[0], ".") if len(versions) < 3 { err = &errortypes.ParseError{ errors.Newf( "qemu: Failed to parse uname version 2 '%s'", version, ), } return } major, err = strconv.Atoi(versions[0]) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "qemu: Failed to parse uname version 3 '%s'", version, ), } return } minor, err = strconv.Atoi(versions[1]) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "qemu: Failed to parse uname version 4 '%s'", version, ), } return } patch, err = strconv.Atoi(versions[2]) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "qemu: Failed to parse uname version 5 '%s'", version, ), } return } return } func GetUringSupport() (supported bool, err error) { kallsyms, err := ioutil.ReadFile("/proc/kallsyms") if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "features: Failed to read /proc/kallsyms"), } return } if !strings.Contains(string(kallsyms), "io_uring_init") { return } major, minor, _, err := GetKernelVersion() if err != nil { return } if major < 5 { return } else if major == 5 && minor < 2 { return } major, minor, _, err = GetQemuVersion() if err != nil { return } if major < 6 { return } else if major == 6 && minor < 2 { return } sysctlData, err := ioutil.ReadFile("/proc/sys/kernel/io_uring_disabled") if err != nil { if os.IsNotExist(err) { err = nil supported = true return } err = &errortypes.ReadError{ errors.Wrapf(err, "features: Failed to read io_uring_disabled"), } return } disabledStr := strings.TrimSpace(string(sysctlData)) disabled, parseErr := strconv.Atoi(disabledStr) if parseErr != nil { err = &errortypes.ParseError{ errors.Wrapf( parseErr, "features: Failed to parse io_uring_disabled value '%s'", disabledStr, ), } return } if disabled == 2 { return } supported = true return } func GetExtUringSupport() (supported bool, err error) { major, minor, _, err := GetQemuVersion() if err != nil { return } if major > 10 { supported = true } if major == 10 && minor >= 2 { supported = true } return } func GetMemoryBackendSupport() (supported bool, err error) { major, _, _, err := GetQemuVersion() if err != nil { return } if major >= 6 { supported = true } return } func GetRunWithSupport() (supported bool, err error) { major, _, _, err := GetQemuVersion() if err != nil { return } if major >= 9 { supported = true } return } ================================================ FILE: features/systemd.go ================================================ package features import ( "strconv" "strings" "github.com/pritunl/pritunl-cloud/utils" ) func GetSystemdVersion() (ver int) { output, _ := utils.ExecCombinedOutputLogged( nil, "/usr/bin/systemctl", "--version", ) lines := strings.Split(output, "\n") for _, line := range lines { if !strings.Contains(line, "systemd") { continue } fields := strings.Fields(line) if len(fields) < 2 { continue } n, err := strconv.Atoi(fields[1]) if err != nil { continue } ver = n break } return } func HasSystemdNamespace() bool { ver := GetSystemdVersion() if ver >= 243 { return true } return false } ================================================ FILE: finder/constants.go ================================================ package finder const ( DomainKind = "domain" VpcKind = "vpc" SubnetKind = "subnet" DatacenterKind = "datacenter" NodeKind = "node" PoolKind = "pool" ZoneKind = "zone" ShapeKind = "shape" DiskKind = "disk" ImageKind = "image" BuildKind = "build" InstanceKind = "instance" FirewallKind = "firewall" PlanKind = "plan" CertificateKind = "certificate" SecretKind = "secret" PodKind = "pod" UnitKind = "unit" JournalKind = "journal" ) ================================================ FILE: finder/resources.go ================================================ package finder import ( "regexp" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) type Resources struct { Organization bson.ObjectID Datacenter *datacenter.Datacenter Zone *zone.Zone Vpc *vpc.Vpc Subnet *vpc.Subnet Shape *shape.Shape Node *node.Node Pool *pool.Pool Image *image.Image Disks []*disk.Disk Instance *instance.Instance Plan *plan.Plan Domain *domain.Domain Certificate *certificate.Certificate Secret *secret.Secret Deployment *deployment.Deployment Pod *PodBase Unit *UnitBase Selector string } var tokenRe = regexp.MustCompile( `\+\/([a-zA-Z0-9-]*)\/([a-zA-Z0-9-_.]*)(?:(?:\/|\:)([a-zA-Z0-9-_.]*)(?:\/([a-zA-Z0-9-_.]*))?)?`) func (r *Resources) Find(db *database.Database, token string) ( kind string, err error) { matches := tokenRe.FindStringSubmatch(token) if len(matches) < 3 { err = &errortypes.ParseError{ errors.Newf("spec: Invalid token '%s'", token), } return } kind = matches[1] resource := matches[2] tag := "" r.Selector = "" if len(matches) > 3 { if strings.Contains(token, ":") { tag = matches[3] if len(matches) > 4 { r.Selector = matches[4] } } else { r.Selector = matches[3] } } switch kind { case DomainKind: r.Domain, err = domain.GetOne(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case VpcKind: query := bson.M{ "name": resource, "organization": r.Organization, } if r.Datacenter != nil { query["datacenter"] = r.Datacenter.Id } else if r.Zone != nil { query["datacenter"] = r.Zone.Datacenter } r.Vpc, err = vpc.GetOne(db, &query) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case SubnetKind: if r.Vpc != nil { subnet := r.Vpc.GetSubnetName(resource) r.Subnet = subnet } break case DatacenterKind: r.Datacenter, err = datacenter.GetOne(db, &bson.M{ "name": resource, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case NodeKind: r.Node, err = node.GetOne(db, &bson.M{ "name": resource, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case PoolKind: r.Pool, err = pool.GetOne(db, &bson.M{ "name": resource, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case ZoneKind: r.Zone, err = zone.GetOne(db, &bson.M{ "name": resource, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } r.Datacenter, err = datacenter.Get(db, r.Zone.Datacenter) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case ShapeKind: query := bson.M{ "name": resource, } if r.Datacenter != nil { query["datacenter"] = r.Datacenter.Id } else if r.Zone != nil { query["datacenter"] = r.Zone.Datacenter } r.Shape, err = shape.GetOne(db, &query) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case ImageKind: if image.Releases.Contains(resource) { imgs, e := image.GetAll(db, &bson.M{ "release": resource, "organization": &bson.M{ "$in": []bson.ObjectID{r.Organization, image.Global}, }, }) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } var latestImg *image.Image for _, img := range imgs { if latestImg == nil { latestImg = img } else if img.Build > latestImg.Build { latestImg = img } if img.Build == tag { r.Image = img break } } if latestImg != nil && (tag == "" || tag == "latest") { r.Image = latestImg } } if r.Image == nil { r.Image, err = image.GetOne(db, &bson.M{ "name": resource, "organization": &bson.M{ "$in": []bson.ObjectID{r.Organization, image.Global}, }, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } } break case BuildKind: r.Unit, err = GetUnitBase(db, r.Organization, resource) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if r.Unit != nil { if tag == "" || tag == "latest" { deplys, e := deployment.GetAllSorted(db, &bson.M{ "unit": r.Unit.Id, }) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } for _, deply := range deplys { r.Deployment = deply break } } else { deplys, e := deployment.GetAllSorted(db, &bson.M{ "unit": r.Unit.Id, "tags": tag, }) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } for _, deply := range deplys { r.Deployment = deply break } } } break case DiskKind: r.Disks, err = disk.GetAll(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case InstanceKind: r.Instance, err = instance.GetOne(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case PlanKind: r.Plan, err = plan.GetOne(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case CertificateKind: r.Certificate, err = certificate.GetOne(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case SecretKind: r.Secret, err = secret.GetOne(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case PodKind: r.Pod, err = GetPodBase(db, &bson.M{ "name": resource, "organization": r.Organization, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break case UnitKind: r.Unit, err = GetUnitBase(db, r.Organization, resource) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } break default: err = &errortypes.ParseError{ errors.Newf("spec: Unknown kind '%s'", kind), } return } return } type PodBase struct { Id bson.ObjectID `bson:"_id,omitempty"` Organization bson.ObjectID `bson:"organization"` Name string `bson:"name"` } type UnitBase struct { Id bson.ObjectID `bson:"_id,omitempty"` Pod bson.ObjectID `bson:"pod"` Organization bson.ObjectID `bson:"organization"` Name string `bson:"name"` } func GetPodBase(db *database.Database, query *bson.M) ( pd *PodBase, err error) { coll := db.Pods() pd = &PodBase{} err = coll.FindOne(db, query).Decode(pd) if err != nil { err = database.ParseError(err) return } return } func GetUnitBase(db *database.Database, orgId bson.ObjectID, name string) (unt *UnitBase, err error) { coll := db.Units() unt = &UnitBase{} err = coll.FindOne(db, &bson.M{ "name": name, "organization": orgId, }).Decode(unt) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: firewall/constants.go ================================================ package firewall import "github.com/pritunl/mongo-go-driver/v2/bson" const ( All = "all" Icmp = "icmp" Tcp = "tcp" Udp = "udp" Multicast = "multicast" Broadcast = "broadcast" ) var ( Global = bson.NilObjectID ) ================================================ FILE: firewall/firewall.go ================================================ package firewall import ( "fmt" "net" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Rule struct { SourceIps []string `bson:"source_ips" json:"source_ips"` Protocol string `bson:"protocol" json:"protocol"` Port string `bson:"port" json:"port"` } type Mapping struct { Ipvs bool `bson:"ipvs" json:"ipvs"` Address string `bson:"adress" json:"adress"` Protocol string `bson:"protocol" json:"protocol"` ExternalPort int `bson:"external_port" json:"external_port"` InternalPort int `bson:"internal_port" json:"internal_port"` } func (r *Rule) SetName(ipv6 bool) (name string) { switch r.Protocol { case All: if ipv6 { name = "pr6_all" } else { name = "pr4_all" } break case Icmp: if ipv6 { name = "pr6_icmp" } else { name = "pr4_icmp" } break case Multicast: if ipv6 { name = "pr6_multi" } else { name = "pr4_multi" } break case Broadcast: if ipv6 { name = "pr6_broad" } else { name = "pr4_broad" } break case Tcp, Udp: if ipv6 { name = fmt.Sprintf( "pr6_%s_%s", r.Protocol, strings.Replace(r.Port, "-", "_", 1), ) } else { name = fmt.Sprintf( "pr4_%s_%s", r.Protocol, strings.Replace(r.Port, "-", "_", 1), ) } break default: break } return } type Firewall struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Roles []string `bson:"roles" json:"roles"` Ingress []*Rule `bson:"ingress" json:"ingress"` } func (f *Firewall) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { f.Name = utils.FilterName(f.Name) if f.Roles == nil { f.Roles = []string{} } if f.Ingress == nil { f.Ingress = []*Rule{} } for _, rule := range f.Ingress { switch rule.Protocol { case All: rule.Port = "" break case Icmp: rule.Port = "" break case Tcp, Udp, Multicast, Broadcast: ports := strings.Split(rule.Port, "-") portInt, e := strconv.Atoi(ports[0]) if e != nil { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } if portInt < 1 || portInt > 65535 { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } parsedPort := strconv.Itoa(portInt) if len(ports) > 1 { portInt2, e := strconv.Atoi(ports[1]) if e != nil { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } if portInt < 1 || portInt > 65535 || portInt2 <= portInt { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } parsedPort += "-" + strconv.Itoa(portInt2) } rule.Port = parsedPort break default: errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_protocol", Message: "Invalid ingress rule protocol", } return } if rule.Protocol == Multicast || rule.Protocol == Broadcast { rule.SourceIps = []string{} } else { for i, sourceIp := range rule.SourceIps { if sourceIp == "" { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_source_ip", Message: "Empty ingress rule source IP", } return } if !strings.Contains(sourceIp, "/") { if strings.Contains(sourceIp, ":") { sourceIp += "/128" } else { sourceIp += "/32" } } _, sourceCidr, e := net.ParseCIDR(sourceIp) if e != nil { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_source_ip", Message: "Invalid ingress rule source IP", } return } rule.SourceIps[i] = sourceCidr.String() } } } return } func (f *Firewall) Commit(db *database.Database) (err error) { coll := db.Firewalls() err = coll.Commit(f.Id, f) if err != nil { return } return } func (f *Firewall) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Firewalls() err = coll.CommitFields(f.Id, f, fields) if err != nil { return } return } func (f *Firewall) Insert(db *database.Database) (err error) { coll := db.Firewalls() if !f.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("firewall: Firewall already exists"), } return } resp, err := coll.InsertOne(db, f) if err != nil { err = database.ParseError(err) return } f.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: firewall/spec.go ================================================ package firewall import ( "strings" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/vm" ) func GetSpecRules(instances []*instance.Instance, deploymentsNode map[bson.ObjectID]*deployment.Deployment, specsMap map[bson.ObjectID]*spec.Spec, specsUnitsMap map[bson.ObjectID]*unit.Unit, deploymentsDeployedMap map[bson.ObjectID]*deployment.Deployment) ( firewalls map[string][]*Rule, err error) { firewalls = map[string][]*Rule{} for _, inst := range instances { if inst.Deployment.IsZero() { continue } deply := deploymentsNode[inst.Deployment] if deply == nil { continue } spc := specsMap[deply.Spec] if spc == nil { continue } if spc.Firewall == nil || spc.Firewall.Ingress == nil { continue } if !inst.IsActive() { continue } namespaces := []string{} for i := range inst.Virt.NetworkAdapters { namespaces = append(namespaces, vm.GetNamespace(inst.Id, i)) } for _, specRule := range spc.Firewall.Ingress { rule := &Rule{ Protocol: specRule.Protocol, Port: specRule.Port, SourceIps: specRule.SourceIps, } for _, ref := range specRule.Sources { if ref.Kind != spec.Unit { continue } ruleUnit := specsUnitsMap[ref.Id] if ruleUnit == nil { continue } for _, ruleDeplyId := range ruleUnit.Deployments { ruleDeply := deploymentsDeployedMap[ruleDeplyId] if ruleDeply == nil { continue } instData := ruleDeply.InstanceData if instData == nil { continue } switch ref.Selector { case "", "private_ips": for _, ip := range instData.PrivateIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "private_ips6": for _, ip := range instData.PrivateIps6 { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "public_ips": for _, ip := range instData.PublicIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "public_ips6": for _, ip := range instData.PublicIps6 { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "cloud_private_ips": for _, ip := range instData.CloudPrivateIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "cloud_public_ips": for _, ip := range instData.CloudPublicIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "cloud_public_ips6": for _, ip := range instData.CloudPublicIps6 { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "host_ips": for _, ip := range instData.HostIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } } } } if len(rule.SourceIps) == 0 { continue } for _, namespace := range namespaces { firewalls[namespace] = append(firewalls[namespace], rule) } } } return } func GetSpecRulesSlow(db *database.Database, nodeId bson.ObjectID, instances []*instance.Instance) ( firewalls map[string][]*Rule, nodePortsMap map[string][]*nodeport.Mapping, err error) { deployments, err := deployment.GetAll(db, &bson.M{ "node": nodeId, }) if err != nil { return } deploymentsNode := map[bson.ObjectID]*deployment.Deployment{} deploymentsDeployedMap := map[bson.ObjectID]*deployment.Deployment{} deploymentsIdSet := set.NewSet() podIdsSet := set.NewSet() unitIdsSet := set.NewSet() specIdsSet := set.NewSet() for _, deply := range deployments { deploymentsNode[deply.Id] = deply deploymentsIdSet.Add(deply.Id) if deply.State == deployment.Deployed { deploymentsDeployedMap[deply.Id] = deply } podIdsSet.Add(deply.Pod) unitIdsSet.Add(deply.Unit) specIdsSet.Add(deply.Spec) } specIds := []bson.ObjectID{} for specId := range specIdsSet.Iter() { specIds = append(specIds, specId.(bson.ObjectID)) } specs, err := spec.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specIds, }, }) if err != nil { return } specUnitsSet := set.NewSet() specsMap := map[bson.ObjectID]*spec.Spec{} for _, spc := range specs { specsMap[spc.Id] = spc if spc.Firewall != nil { for _, rule := range spc.Firewall.Ingress { for _, ref := range rule.Sources { specUnitsSet.Add(ref.Id) } } } } specUnitIds := []bson.ObjectID{} for unitId := range specUnitsSet.Iter() { specUnitIds = append(specUnitIds, unitId.(bson.ObjectID)) } specUnits, err := unit.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specUnitIds, }, }) if err != nil { return } specDeploymentsSet := set.NewSet() specsUnitsMap := map[bson.ObjectID]*unit.Unit{} for _, specUnit := range specUnits { specsUnitsMap[specUnit.Id] = specUnit for _, deplyId := range specUnit.Deployments { specDeploymentsSet.Add(deplyId) } } specDeploymentIds := []bson.ObjectID{} for deplyIdInf := range specDeploymentsSet.Iter() { deplyId := deplyIdInf.(bson.ObjectID) if !deploymentsIdSet.Contains(deplyId) { specDeploymentIds = append(specDeploymentIds, deplyId) } } specDeployments, err := deployment.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specDeploymentIds, }, }) if err != nil { return } for _, specDeployment := range specDeployments { deploymentsIdSet.Add(specDeployment.Id) if specDeployment.State == deployment.Deployed { deploymentsDeployedMap[specDeployment.Id] = specDeployment } } podIds := []bson.ObjectID{} for podId := range podIdsSet.Iter() { podIds = append(podIds, podId.(bson.ObjectID)) } pods, err := pod.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": podIds, }, }) if err != nil { return } podsMap := map[bson.ObjectID]*pod.Pod{} for _, pd := range pods { podsMap[pd.Id] = pd } unitIds := []bson.ObjectID{} for unitId := range unitIdsSet.Iter() { unitIds = append(unitIds, unitId.(bson.ObjectID)) } units := []*unit.Unit{} if len(unitIds) > 0 { units, err = unit.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": unitIds, }, }) if err != nil { return } } unitsMap := map[bson.ObjectID]*unit.Unit{} podDeploymentsSet := set.NewSet() for _, unt := range units { unitsMap[unt.Id] = unt for _, deplyId := range unt.Deployments { podDeploymentsSet.Add(deplyId) } } podDeploymentIds := []bson.ObjectID{} for deplyIdInf := range podDeploymentsSet.Iter() { deplyId := deplyIdInf.(bson.ObjectID) if !deploymentsIdSet.Contains(deplyId) { podDeploymentIds = append(podDeploymentIds, deplyId) } } podDeployments, err := deployment.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": podDeploymentIds, }, }) if err != nil { return } for _, podDeployment := range podDeployments { deploymentsIdSet.Add(podDeployment.Id) if podDeployment.State == deployment.Deployed { deploymentsDeployedMap[podDeployment.Id] = podDeployment } } nodePortsMap = map[string][]*nodeport.Mapping{} firewalls = map[string][]*Rule{} for _, inst := range instances { nodePortsMap[inst.NetworkNamespace] = append( nodePortsMap[inst.NetworkNamespace], inst.NodePorts...) if inst.Deployment.IsZero() { continue } deply := deploymentsNode[inst.Deployment] if deply == nil { continue } spc := specsMap[deply.Spec] if spc == nil { continue } if spc.Firewall == nil || spc.Firewall.Ingress == nil { continue } if !inst.IsActive() { continue } namespaces := []string{} for i := range inst.Virt.NetworkAdapters { namespaces = append(namespaces, vm.GetNamespace(inst.Id, i)) } for _, specRule := range spc.Firewall.Ingress { rule := &Rule{ Protocol: specRule.Protocol, Port: specRule.Port, SourceIps: specRule.SourceIps, } for _, ref := range specRule.Sources { if ref.Kind != spec.Unit { continue } ruleUnit := specsUnitsMap[ref.Id] if ruleUnit == nil { continue } for _, ruleDeplyId := range ruleUnit.Deployments { ruleDeply := deploymentsDeployedMap[ruleDeplyId] if ruleDeply == nil { continue } instData := ruleDeply.InstanceData if instData == nil { continue } switch ref.Selector { case "", "private_ips": for _, ip := range instData.PrivateIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "private_ips6": for _, ip := range instData.PrivateIps6 { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "public_ips": for _, ip := range instData.PublicIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "public_ips6": for _, ip := range instData.PublicIps6 { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "cloud_private_ips": for _, ip := range instData.CloudPrivateIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "cloud_public_ips": for _, ip := range instData.CloudPublicIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "cloud_public_ips6": for _, ip := range instData.CloudPublicIps6 { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } case "host_ips": for _, ip := range instData.HostIps { rule.SourceIps = append( rule.SourceIps, strings.Split(ip, "/")[0]+"/32", ) } } } } if len(rule.SourceIps) == 0 { continue } for _, namespace := range namespaces { firewalls[namespace] = append(firewalls[namespace], rule) } } } return } ================================================ FILE: firewall/utils.go ================================================ package firewall import ( "fmt" "sort" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) func Get(db *database.Database, fireId bson.ObjectID) ( fire *Firewall, err error) { coll := db.Firewalls() fire = &Firewall{} err = coll.FindOneId(fireId, fire) if err != nil { return } return } func GetOrg(db *database.Database, orgId, fireId bson.ObjectID) ( fire *Firewall, err error) { coll := db.Firewalls() fire = &Firewall{} err = coll.FindOne(db, &bson.M{ "_id": fireId, "organization": orgId, }).Decode(fire) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( fires []*Firewall, err error) { coll := db.Firewalls() fires = []*Firewall{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { fire := &Firewall{} err = cursor.Decode(fire) if err != nil { err = database.ParseError(err) return } fires = append(fires, fire) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetRoles(db *database.Database, roles []string) ( fires []*Firewall, err error) { coll := db.Firewalls() fires = []*Firewall{} cursor, err := coll.Find( db, &bson.M{ "organization": Global, "roles": &bson.M{ "$in": roles, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { fire := &Firewall{} err = cursor.Decode(fire) if err != nil { err = database.ParseError(err) return } fires = append(fires, fire) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetMapRoles(db *database.Database, roles []string) ( fires map[string][]*Firewall, err error) { coll := db.Firewalls() fires = map[string][]*Firewall{} cursor, err := coll.Find(db, &bson.M{ "roles": &bson.M{ "$in": roles, }, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { fire := &Firewall{} err = cursor.Decode(fire) if err != nil { err = database.ParseError(err) return } for _, role := range fire.Roles { roleFires := fires[role] if roleFires == nil { roleFires = []*Firewall{} } fires[role] = append(roleFires, fire) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOrgMapRoles(db *database.Database, orgId bson.ObjectID) ( fires map[string][]*Firewall, err error) { coll := db.Firewalls() fires = map[string][]*Firewall{} cursor, err := coll.Find(db, &bson.M{ "organization": orgId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { fire := &Firewall{} err = cursor.Decode(fire) if err != nil { err = database.ParseError(err) return } for _, role := range fire.Roles { roleFires := fires[role] if roleFires == nil { roleFires = []*Firewall{} } fires[role] = append(roleFires, fire) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOrgRoles(db *database.Database, orgId bson.ObjectID, roles []string) (fires []*Firewall, err error) { coll := db.Firewalls() fires = []*Firewall{} cursor, err := coll.Find( db, &bson.M{ "organization": orgId, "roles": &bson.M{ "$in": roles, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { fire := &Firewall{} err = cursor.Decode(fire) if err != nil { err = database.ParseError(err) return } fires = append(fires, fire) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (fires []*Firewall, count int64, err error) { coll := db.Firewalls() fires = []*Firewall{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { fire := &Firewall{} err = cursor.Decode(fire) if err != nil { err = database.ParseError(err) return } fires = append(fires, fire) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, fireId bson.ObjectID) (err error) { coll := db.Firewalls() _, err = coll.DeleteOne(db, &bson.M{ "_id": fireId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, fireId bson.ObjectID) ( err error) { coll := db.Firewalls() _, err = coll.DeleteOne(db, &bson.M{ "_id": fireId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, fireIds []bson.ObjectID) ( err error) { coll := db.Firewalls() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": fireIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, fireIds []bson.ObjectID) (err error) { coll := db.Firewalls() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": fireIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } func MergeIngress(fires []*Firewall) (rules []*Rule) { rules = []*Rule{} rulesMap := map[string]*Rule{} rulesKey := []string{} for _, fire := range fires { for _, ingress := range fire.Ingress { key := fmt.Sprintf("%s-%s", ingress.Protocol, ingress.Port) rule := rulesMap[key] if rule == nil { rule = &Rule{ Protocol: ingress.Protocol, Port: ingress.Port, SourceIps: ingress.SourceIps, } rulesMap[key] = rule rulesKey = append(rulesKey, key) } else { sourceIps := set.NewSet() for _, sourceIp := range rule.SourceIps { sourceIps.Add(sourceIp) } for _, sourceIp := range ingress.SourceIps { if sourceIps.Contains(sourceIp) { continue } sourceIps.Add(sourceIp) rule.SourceIps = append(rule.SourceIps, sourceIp) } } } } sort.Strings(rulesKey) for _, key := range rulesKey { rules = append(rules, rulesMap[key]) } return } func GetAllIngress(db *database.Database, nodeSelf *node.Node, instances []*instance.Instance, specRules map[string][]*Rule, nodePortsMap map[string][]*nodeport.Mapping) ( nodeFirewall []*Rule, firewalls map[string][]*Rule, mappings map[string][]*Mapping, instNamespaces map[bson.ObjectID][]string, err error) { if nodeSelf.Firewall { fires, e := GetRoles(db, nodeSelf.Roles) if e != nil { err = e return } ingress := MergeIngress(fires) nodeFirewall = ingress } instNamespaces = map[bson.ObjectID][]string{} nodePortIps := map[string]string{} firewalls = map[string][]*Rule{} for _, inst := range instances { if !inst.IsActive() { continue } namespaces := []string{} for i := range inst.Virt.NetworkAdapters { namespaces = append(namespaces, vm.GetNamespace(inst.Id, i)) } instNamespaces[inst.Id] = namespaces if len(inst.NodePortIps) > 0 && len(namespaces) > 0 { nodePortIps[namespaces[0]] = inst.NodePortIps[0] } fires, e := GetOrgRoles(db, inst.Organization, inst.Roles) if e != nil { err = e return } ingress := MergeIngress(fires) for _, namespace := range namespaces { _, ok := firewalls[namespace] if ok { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "namespace": namespace, }).Error("firewall: Namespace conflict") err = &errortypes.ParseError{ errors.New("firewall: Namespace conflict"), } return } firewalls[namespace] = ingress } } for namespace, rules := range specRules { firewalls[namespace] = append(firewalls[namespace], rules...) } mappings = map[string][]*Mapping{} externalPorts := map[int]string{} for namespace, ndePorts := range nodePortsMap { for _, ndePort := range ndePorts { ipvs := false extNamespace := externalPorts[ndePort.ExternalPort] if extNamespace != "" { ipvs = true if extNamespace != "-" { for _, mapping := range mappings[extNamespace] { if mapping.ExternalPort == ndePort.ExternalPort { mapping.Ipvs = true } } externalPorts[ndePort.ExternalPort] = "-" } } else { externalPorts[ndePort.ExternalPort] = namespace } mappings[namespace] = append(mappings[namespace], &Mapping{ Ipvs: ipvs, Address: nodePortIps[namespace], Protocol: ndePort.Protocol, ExternalPort: ndePort.ExternalPort, InternalPort: ndePort.InternalPort, }) } } return } func GetAllIngressPreloaded(nodeSelf *node.Node, instances []*instance.Instance, specRules map[string][]*Rule, nodePortsMap map[string][]*nodeport.Mapping, firesMap map[string][]*Firewall) ( nodeFirewall []*Rule, firewalls map[string][]*Rule, mappings map[string][]*Mapping, instNamespaces map[bson.ObjectID][]string, err error) { if nodeSelf.Firewall { fires := []*Firewall{} for _, role := range nodeSelf.Roles { for _, fire := range firesMap[role] { if fire.Organization == Global { fires = append(fires, fire) } } } ingress := MergeIngress(fires) nodeFirewall = ingress } instNamespaces = map[bson.ObjectID][]string{} nodePortIps := map[string]string{} firewalls = map[string][]*Rule{} for _, inst := range instances { if !inst.IsActive() { continue } namespaces := []string{} for i := range inst.Virt.NetworkAdapters { namespaces = append(namespaces, vm.GetNamespace(inst.Id, i)) } instNamespaces[inst.Id] = namespaces if len(inst.NodePortIps) > 0 && len(namespaces) > 0 { nodePortIps[namespaces[0]] = inst.NodePortIps[0] } fires := []*Firewall{} for _, role := range inst.Roles { for _, fire := range firesMap[role] { if fire.Organization == inst.Organization { fires = append(fires, fire) } } } ingress := MergeIngress(fires) for _, namespace := range namespaces { _, ok := firewalls[namespace] if ok { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "namespace": namespace, }).Error("firewall: Namespace conflict") err = &errortypes.ParseError{ errors.New("firewall: Namespace conflict"), } return } firewalls[namespace] = ingress } } for namespace, rules := range specRules { firewalls[namespace] = append(firewalls[namespace], rules...) } mappings = map[string][]*Mapping{} externalPorts := map[int]string{} for namespace, ndePorts := range nodePortsMap { for _, ndePort := range ndePorts { ipvs := false extNamespace := externalPorts[ndePort.ExternalPort] if extNamespace != "" { ipvs = true if extNamespace != "-" { for _, mapping := range mappings[extNamespace] { if mapping.ExternalPort == ndePort.ExternalPort { mapping.Ipvs = true } } externalPorts[ndePort.ExternalPort] = "-" } } else { externalPorts[ndePort.ExternalPort] = namespace } mappings[namespace] = append(mappings[namespace], &Mapping{ Ipvs: ipvs, Address: nodePortIps[namespace], Protocol: ndePort.Protocol, ExternalPort: ndePort.ExternalPort, InternalPort: ndePort.InternalPort, }) } } return } ================================================ FILE: geo/geo.go ================================================ package geo import ( "bytes" "encoding/json" "net/http" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) var ( client = &http.Client{ Timeout: 10 * time.Second, } ) type Geo struct { Address string `bson:"_id" json:"address"` Isp string `bson:"i" json:"isp"` Continent string `bson:"z" json:"continent"` ContinentCode string `bson:"q" json:"continent_code"` Country string `bson:"c" json:"country"` CountryCode string `bson:"w" json:"country_code"` Region string `bson:"r" json:"region"` RegionCode string `bson:"e" json:"region_code"` City string `bson:"a" json:"city"` Longitude float64 `bson:"x" json:"longitude"` Latitude float64 `bson:"y" json:"latitude"` Timestamp time.Time `bson:"t" json:"-"` } type geoData struct { License string `json:"license"` Address string `json:"address"` } func get(addr string) (ge *Geo, err error) { if settings.System.License == "" { return } reqGeoData := &geoData{ License: settings.System.License, Address: addr, } reqData := &bytes.Buffer{} err = json.NewEncoder(reqData).Encode(reqGeoData) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "geo: Failed to parse request data"), } return } req, err := http.NewRequest( "GET", settings.Auth.Server+"/geo", reqData, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "geo: Failed to create request"), } return } resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "geo: Failed to send request"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "geo: Geo request error") if err != nil { return } ge = &Geo{} err = json.NewDecoder(resp.Body).Decode(ge) if err != nil { ge = nil err = &errortypes.ParseError{ errors.Wrap(err, "geo: Failed to parse response"), } return } return } func Get(db *database.Database, addr string) (ge *Geo, err error) { ge = &Geo{} coll := db.Geo() err = coll.FindOneId(addr, ge) if err != nil { switch err.(type) { case *database.NotFoundError: ge = nil err = nil default: return } } if ge == nil { ge, err = get(addr) if err != nil { return } if ge != nil { ge.Timestamp = time.Now() _, _ = coll.InsertOne(db, ge) } else { ge = &Geo{} } } return } ================================================ FILE: go.mod ================================================ module github.com/pritunl/pritunl-cloud go 1.25.9 require ( github.com/aws/aws-sdk-go v1.55.5 github.com/cloudflare/cloudflare-go v0.104.0 github.com/coredns/coredns v1.14.3 github.com/dropbox/godropbox v0.0.0-20230623171840-436d2007a9fd github.com/duosecurity/duo_api_golang v0.0.0-20250430191550-ac36954387e7 github.com/gin-gonic/gin v1.10.0 github.com/go-webauthn/webauthn v0.12.3 github.com/google/uuid v1.6.0 github.com/gorilla/securecookie v1.1.2 github.com/gorilla/sessions v1.4.0 github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f github.com/mdlayher/ndp v1.1.0 github.com/miekg/dns v1.1.72 github.com/minio/minio-go/v7 v7.0.76 github.com/oracle/oci-go-sdk/v65 v65.74.0 github.com/pritunl/mongo-go-driver/v2 v2.3.0 github.com/pritunl/tools v1.2.5 github.com/sirupsen/logrus v1.9.3 github.com/twilio/twilio-go v1.23.0 github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 golang.org/x/crypto v0.50.0 golang.org/x/net v0.53.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sys v0.43.0 google.golang.org/api v0.276.0 gopkg.in/yaml.v2 v2.4.0 ) require ( cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/apparentlymart/go-cidr v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.12.2 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/coredns/caddy v1.1.4 // indirect github.com/dnstap/golang-dnstap v0.4.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/farsightsec/golang-framestream v0.3.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect github.com/fxamacker/cbor/v2 v2.9.1 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.1 // indirect github.com/go-webauthn/x v0.1.20 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-tpm v0.9.3 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect github.com/googleapis/gax-go/v2 v2.22.0 // indirect github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdlayher/packet v1.1.2 // indirect github.com/mdlayher/socket v0.6.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pires/go-proxyproto v0.12.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/sony/gobreaker v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/ugorji/go/codec v1.2.12 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.uber.org/mock v0.6.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect golang.org/x/arch v0.10.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect google.golang.org/grpc v1.80.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= github.com/apparentlymart/go-cidr v1.1.1 h1:oEEk8CE0HP0YpHxsegk/TaOtR2FLHdWv4p3eM4ceUwg= github.com/apparentlymart/go-cidr v1.1.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bytedance/sonic v1.12.2 h1:oaMFuRTpMHYLpCntGca65YWt5ny+wAceDERTkT2L9lg= github.com/bytedance/sonic v1.12.2/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.0 h1:zNprn+lsIP06C/IqCHs3gPQIvnvpKbbxyXQP1iU4kWM= github.com/bytedance/sonic/loader v0.2.0/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/cloudflare-go v0.104.0 h1:R/lB0dZupaZbOgibAH/BRrkFbZ6Acn/WsKg2iX2xXuY= github.com/cloudflare/cloudflare-go v0.104.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/coredns/caddy v1.1.4 h1:+Lls5xASB0QsA2jpCroCOwpPlb5GjIGlxdjXxdX0XVo= github.com/coredns/caddy v1.1.4/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= github.com/coredns/coredns v1.14.3 h1:hWWoTdONblKIWhC8QPkxLEGIbewhR5xyTedqLVPsvvE= github.com/coredns/coredns v1.14.3/go.mod h1:15BWsGGxupagKQ3p09pIIZ5kcgmyawquey6gqNWRaEI= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnstap/golang-dnstap v0.4.0 h1:KRHBoURygdGtBjDI2w4HifJfMAhhOqDuktAokaSa234= github.com/dnstap/golang-dnstap v0.4.0/go.mod h1:FqsSdH58NAmkAvKcpyxht7i4FoBjKu8E4JUPt8ipSUs= github.com/dropbox/godropbox v0.0.0-20230623171840-436d2007a9fd h1:s2vYw+2c+7GR1ccOaDuDcKsmNB/4RIxyu5liBm1VRbs= github.com/dropbox/godropbox v0.0.0-20230623171840-436d2007a9fd/go.mod h1:Vr/Q4p40Kce7JAHDITjDhiy/zk07W4tqD5YVi5FD0PA= github.com/duosecurity/duo_api_golang v0.0.0-20250430191550-ac36954387e7 h1:2QX96efe1AvKmqAdqeAn3efxI3lr+EULVbzRxZ/rKGQ= github.com/duosecurity/duo_api_golang v0.0.0-20250430191550-ac36954387e7/go.mod h1:hJ6IPTuCAvWv+i9ubnPZB3VpVRuj/+SAblWFcI0mjEU= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/farsightsec/golang-framestream v0.3.0 h1:/spFQHucTle/ZIPkYqrfshQqPe2VQEzesH243TjIwqA= github.com/farsightsec/golang-framestream v0.3.0/go.mod h1:eNde4IQyEiA5br02AouhEHCu3p3UzrCdFR4LuQHklMI= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ= github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4= github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/FmkayNE= github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY= github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw= github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc= github.com/google/go-tpm v0.9.3/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.15 h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas= github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.22.0 h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4= github.com/googleapis/gax-go/v2 v2.22.0/go.mod h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU= github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f h1:dd33oobuIv9PcBVqvbEiCXEbNTomOHyj3WFuC5YiPRU= github.com/insomniacslk/dhcp v0.0.0-20250417080101-5f8cf70e8c5f/go.mod h1:zhFlBeJssZ1YBCMZ5Lzu1pX4vhftDvU10WUVb1uXKtM= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/ndp v1.1.0 h1:QylGKGVtH60sKZUE88+IW5ila1Z/M9/OXhWdsVKuscs= github.com/mdlayher/ndp v1.1.0/go.mod h1:FmgESgemgjl38vuOIyAHWUUL6vQKA/pQNkvXdWsdQFM= github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU= github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18= github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= github.com/minio/minio-go/v7 v7.0.76 h1:9nxHH2XDai61cT/EFhyIw/wW4vJfpPNvl7lSFpRt+Ng= github.com/minio/minio-go/v7 v7.0.76/go.mod h1:AVM3IUN6WwKzmwBxVdjzhH8xq+f57JSbbvzqvUzR6eg= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/oracle/oci-go-sdk/v65 v65.74.0 h1:oA2VXpecSTwc45QJGsKNoxCBwbUMuXLQ2W4pLZZarro= github.com/oracle/oci-go-sdk/v65 v65.74.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pritunl/mongo-go-driver/v2 v2.3.0 h1:ZQ8ZujYwwY+1TEXwf8pGytdprmgGpe/HRP7fa4xbRow= github.com/pritunl/mongo-go-driver/v2 v2.3.0/go.mod h1:U8W2Evrwe7KTE6DFgMV1Ub/WLXzJht/lyXnLHEK5E+8= github.com/pritunl/tools v1.2.5 h1:qbVj1QQdhhgQLBa2JnjBlVbPJt/t33veI/QN+kJq28s= github.com/pritunl/tools v1.2.5/go.mod h1:BiNzTb2ZCesQ5k/Mx0mhOwGXNJNdZk+4jqg39GjRXKU= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twilio/twilio-go v1.23.0 h1:cIJD6XnVuRqnMVp8LswoOTEi4/JK9WctOTUvUR2gLf0= github.com/twilio/twilio-go v1.23.0/go.mod h1:zRkMjudW7v7MqQ3cWNZmSoZJ7EBjPZ4OpNh2zm7Q6ko= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90 h1:rB0J+hLNltG1Qv+UF+MkdFz89XMps5BOAFJN4xWjc+s= github.com/ua-parser/uap-go v0.0.0-20250213224047-9c035f085b90/go.mod h1:BUbeWZiieNxAuuADTBNb3/aeje6on3DhU3rpWsQSB1E= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= golang.org/x/arch v0.10.0 h1:S3huipmSclq3PJMNe76NGwkBR504WFkQ5dhzWzP8ZW8= golang.org/x/arch v0.10.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY= google.golang.org/api v0.276.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4 h1:yOzSCGPx+cp5VO7IxvZ9SBFF7j1tZVcNtlHR2iYKtVo= google.golang.org/genproto/googleapis/api v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:Q9HWtNeE7tM9npdIsEvqXj1QJIvVoeAV3rtXtS715Cw= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM= google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= ================================================ FILE: guest/guest.go ================================================ package guest import ( "time" "github.com/pritunl/pritunl-cloud/utils" ) var ( socketsLock = utils.NewMultiTimeoutLock(1 * time.Minute) ) type Command struct { Execute string `json:"execute"` Arguments map[string]interface{} `json:"arguments,omitempty"` } type Response struct { Return map[string]interface{} `json:"return"` Error map[string]interface{} `json:"error,omitempty"` RawData []byte `json:"-"` } ================================================ FILE: guest/power.go ================================================ package guest import ( "encoding/json" "net" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/utils" ) func Shutdown(vmId bson.ObjectID) (err error) { lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) sockPath := paths.GetGuestPath(vmId) exists, err := utils.Exists(sockPath) if err != nil { return } if !exists { err = &errortypes.ReadError{ errors.New("guest: Guest agent socket missing"), } return } conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "guest: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "guest: Failed set deadline"), } return } cmd := Command{ Execute: "guest-shutdown", Arguments: map[string]interface{}{ "mode": "powerdown", }, } cmdData, err := json.Marshal(cmd) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "guest: Failed to marshal socket data"), } return } cmdData = append(cmdData, '\n') _, err = conn.Write(cmdData) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "guest: Failed to write socket"), } return } buffer := make([]byte, 8192) n, err := conn.Read(buffer) if err != nil { err = nil return } var response Response response.RawData = buffer[:n] err = json.Unmarshal(response.RawData, &response) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "guest: Failed to parse socket data"), } return } if response.Error != nil { err = &errortypes.ReadError{ errors.Newf("guest: Guest returned error %v", response.Error), } return } return } ================================================ FILE: hnetwork/hnetwork.go ================================================ package hnetwork import ( "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) var ( initialized = false curGateway = "" curRule *IptablesRule ) type IptablesRule struct { Source string Output string } func (h *IptablesRule) Add() (err error) { args := []string{ "-t", "nat", "-A", "POSTROUTING", } if h.Source != "" { args = append(args, "-s", h.Source) } if h.Output != "" { args = append(args, "-o", h.Output) } args = append(args, "-m", "comment", "--comment", "pritunl_cloud_host_nat", "-j", "MASQUERADE", ) _, err = utils.ExecCombinedOutputLogged( []string{ "matching rule exist", "match by that name", }, "iptables", args..., ) if err != nil { return } if err != nil { return } return } func (h *IptablesRule) Remove() (err error) { args := []string{ "-t", "nat", "-D", "POSTROUTING", } if h.Source != "" { args = append(args, "-s", h.Source) } if h.Output != "" { args = append(args, "-o", h.Output) } args = append(args, "-m", "comment", "--comment", "pritunl_cloud_host_nat", "-j", "MASQUERADE", ) _, err = utils.ExecCombinedOutputLogged( []string{ "matching rule exist", "match by that name", }, "iptables", args..., ) if err != nil { return } return } func loadIptablesNat() (rules []*IptablesRule, err error) { rules = []*IptablesRule{} output, err := utils.ExecOutput("", "iptables", "-t", "nat", "-S") if err != nil { return } for _, line := range strings.Split(output, "\n") { if !strings.Contains(line, "POSTROUTING") || !strings.Contains(line, "MASQUERADE") || !strings.Contains(line, "pritunl_cloud_host_nat") { continue } cmd := strings.Fields(line) cmdLen := len(cmd) if cmdLen < 3 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("hnetwork: Invalid iptables state") err = &errortypes.ParseError{ errors.New("hnetwork: Invalid iptables state"), } return } rule := &IptablesRule{} for i, item := range cmd { if item == "-s" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("hnetwork: Invalid iptables host nat source") err = &errortypes.ParseError{ errors.New( "hnetwork: Invalid iptables host nat source"), } return } rule.Source = cmd[i+1] } if item == "-o" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("hnetwork: Invalid iptables host nat output") err = &errortypes.ParseError{ errors.New( "hnetwork: Invalid iptables host nat output"), } return } rule.Output = cmd[i+1] } } rules = append(rules, rule) } return } func removeNetwork(stat *state.State) (err error) { if curGateway != "" || stat.HasInterfaces( settings.Hypervisor.HostNetworkName) { err = clearAddr() if err != nil { return } curGateway = "" } return } func ApplyState(stat *state.State) (err error) { initializeInst := false hostNetName := settings.Hypervisor.HostNetworkName if !initialized { addr, e := getAddr() if e != nil { err = e return } rules, e := loadIptablesNat() if e != nil { err = e return } if len(rules) > 1 { for _, rule := range rules { err = rule.Remove() if err != nil { return } } } else if len(rules) == 1 { curRule = rules[0] } initializeInst = true initialized = true curGateway = addr } if !stat.HasInterfaces(hostNetName) { logrus.WithFields(logrus.Fields{ "iface": hostNetName, }).Info("hnetwork: Creating host interface") err = create() if err != nil { return } curGateway = "" initializeInst = true } hostBlock, err := block.GetNodeBlock(stat.Node().Id) if err != nil { return } gatewayCidr := hostBlock.GetGatewayCidr() if gatewayCidr == "" { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), }).Error("hnetwork: Host network block gateway is invalid") err = removeNetwork(stat) if err != nil { return } return } if curGateway != gatewayCidr { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), "host_block_gateway": gatewayCidr, }).Info("hnetwork: Updating host network bridge") err = setAddr(gatewayCidr) if err != nil { return } curGateway = gatewayCidr initializeInst = true } if stat.Node().HostNat { hostNet, e := hostBlock.GetNetwork() if e != nil { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), "error": e, }).Error("hnetwork: Host nat block network invalid") } else { newRule := &IptablesRule{ Source: hostNet.String(), Output: stat.Node().DefaultInterface, } if curRule == nil || curRule.Source != newRule.Source || curRule.Output != newRule.Output { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), "host_source": newRule.Source, "host_output": newRule.Output, }).Info("hnetwork: Updating host network nat") if curRule != nil { err = curRule.Remove() if err != nil { return } curRule = nil } err = newRule.Add() if err != nil { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), "host_source": newRule.Source, "host_output": newRule.Output, "error": err, }).Error("hnetwork: Host nat add rule failed") err = nil } else { curRule = newRule } initializeInst = true } } } else if curRule != nil { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), "host_source": curRule.Source, "host_output": curRule.Output, }).Info("hnetwork: Updating host network nat") err = curRule.Remove() if err != nil { return } curRule = nil } if initializeInst { logrus.WithFields(logrus.Fields{ "host_block": hostBlock.Id.Hex(), }).Info("hnetwork: Updating instance host network") instances := stat.Instances() for _, inst := range instances { utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "set", vm.GetIfaceHost(inst.Id, 0), "master", hostNetName, ) } } return } ================================================ FILE: hnetwork/utils.go ================================================ package hnetwork import ( "fmt" "github.com/pritunl/pritunl-cloud/bridges" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func create() (err error) { err = iproute.BridgeAdd("", settings.Hypervisor.HostNetworkName) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", settings.Hypervisor.HostNetworkName, "up", ) if err != nil { return } bridges.ClearCache() return } func getAddr() (addr string, err error) { address, _, err := iproute.AddressGetIface( "", settings.Hypervisor.HostNetworkName) if err != nil { return } if address != nil { addr = address.Local + fmt.Sprintf("/%d", address.Prefix) } return } func setAddr(addr string) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", settings.Hypervisor.HostNetworkName, "up", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "addr", "flush", "dev", settings.Hypervisor.HostNetworkName, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "addr", "add", addr, "dev", settings.Hypervisor.HostNetworkName, ) if err != nil { return } return } func clearAddr() (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", settings.Hypervisor.HostNetworkName, "up", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "addr", "flush", "dev", settings.Hypervisor.HostNetworkName, ) if err != nil { return } return } ================================================ FILE: hugepages/hugepages.go ================================================ package hugepages import ( "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) func HugepageSize() (count int, size uint64, err error) { virt, err := utils.GetMemInfo() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "hugepages: Failed to read virtual memory"), } return } count = int(virt.HugePagesTotal) size = virt.HugePageSize if size < 1024 { err = &errortypes.ReadError{ errors.Newf("hugepages: Invalid hugepage size %d", size), } return } return } func UpdateHugepagesSize() (err error) { err = utils.ExistsMkdir(settings.Hypervisor.HugepagesPath, 0755) if err != nil { return } nodeHugepagesSize := node.Self.HugepagesSize if nodeHugepagesSize == 0 { return } curHugepagesCount, hugepageSize, err := HugepageSize() if err != nil { return } hugepagesSize := uint64(nodeHugepagesSize) * 1024 hugepagesCount := int(hugepagesSize / hugepageSize) if curHugepagesCount != hugepagesCount { logrus.WithFields(logrus.Fields{ "cur_nr_hugepages": curHugepagesCount, "new_nr_hugepages": hugepagesCount, }).Info("hugepages: Updating hugepages size") _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", fmt.Sprintf("vm.nr_hugepages=%d", hugepagesCount), ) if err != nil { return } } return } ================================================ FILE: image/constants.go ================================================ package image import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/tools/set" ) const ( Uefi = "uefi" Bios = "bios" Unknown = "unknown" Linux = "linux" LinuxLegacy = "linux_legacy" LinuxUnsigned = "linux_unsigned" Bsd = "bsd" RedHat = "redhat" Fedora = "fedora" Ubuntu = "ubuntu" AlmaLinux8 = "almalinux8" AlmaLinux9 = "almalinux9" AlmaLinux10 = "almalinux10" AlmaLinux11 = "almalinux11" AlmaLinux12 = "almalinux12" AlmaLinux13 = "almalinux13" AlmaLinux14 = "almalinux14" AlmaLinux15 = "almalinux15" AlmaLinux16 = "almalinux16" AlpineLinux = "alpinelinux" ArchLinux = "archlinux" Fedora42 = "fedora42" Fedora43 = "fedora43" Fedora44 = "fedora44" Fedora45 = "fedora45" Fedora46 = "fedora46" Fedora47 = "fedora47" Fedora48 = "fedora48" Fedora49 = "fedora49" Fedora50 = "fedora50" Fedora51 = "fedora51" Fedora52 = "fedora52" Fedora53 = "fedora53" Fedora54 = "fedora54" Fedora55 = "fedora55" Fedora56 = "fedora56" Fedora57 = "fedora57" Fedora58 = "fedora58" Fedora59 = "fedora59" Fedora60 = "fedora60" Fedora61 = "fedora61" Fedora62 = "fedora62" FreeBSD = "freebsd" OracleLinux7 = "oraclelinux7" OracleLinux8 = "oraclelinux8" OracleLinux9 = "oraclelinux9" OracleLinux10 = "oraclelinux10" OracleLinux11 = "oraclelinux11" OracleLinux12 = "oraclelinux12" OracleLinux13 = "oraclelinux13" OracleLinux14 = "oraclelinux14" OracleLinux15 = "oraclelinux15" OracleLinux16 = "oraclelinux16" RockyLinux8 = "rockylinux8" RockyLinux9 = "rockylinux9" RockyLinux10 = "rockylinux10" RockyLinux11 = "rockylinux11" RockyLinux12 = "rockylinux12" RockyLinux13 = "rockylinux13" RockyLinux14 = "rockylinux14" RockyLinux15 = "rockylinux15" RockyLinux16 = "rockylinux16" Ubuntu2404 = "ubuntu2404" Ubuntu2604 = "ubuntu2604" Ubuntu2804 = "ubuntu2804" Ubuntu3004 = "ubuntu3004" Ubuntu3204 = "ubuntu3204" Ubuntu3404 = "ubuntu3404" Ubuntu3604 = "ubuntu3604" Ubuntu3804 = "ubuntu3804" Ubuntu4004 = "ubuntu4004" Ubuntu4204 = "ubuntu4204" Ubuntu4404 = "ubuntu4404" ) var ( Global = bson.NilObjectID Releases = set.NewSet( AlmaLinux8, AlmaLinux9, AlmaLinux10, AlmaLinux11, AlmaLinux12, AlmaLinux13, AlmaLinux14, AlmaLinux15, AlmaLinux16, AlpineLinux, ArchLinux, Fedora42, Fedora43, Fedora44, Fedora45, Fedora46, Fedora47, Fedora48, Fedora49, Fedora50, Fedora51, Fedora52, Fedora53, Fedora54, Fedora55, Fedora56, Fedora57, Fedora58, Fedora59, Fedora60, Fedora61, Fedora62, FreeBSD, OracleLinux7, OracleLinux8, OracleLinux9, OracleLinux10, OracleLinux11, OracleLinux12, OracleLinux13, OracleLinux14, OracleLinux15, OracleLinux16, RockyLinux8, RockyLinux9, RockyLinux10, RockyLinux11, RockyLinux12, RockyLinux13, RockyLinux14, RockyLinux15, RockyLinux16, Ubuntu2404, Ubuntu2604, Ubuntu2804, Ubuntu3004, Ubuntu3204, Ubuntu3404, Ubuntu3604, Ubuntu3804, Ubuntu4004, Ubuntu4204, Ubuntu4404, ) ValidSystemTypes = set.NewSet( Linux, LinuxLegacy, LinuxUnsigned, Bsd, ) ValidSystemKinds = set.NewSet( AlpineLinux, ArchLinux, RedHat, Fedora, Ubuntu, FreeBSD, ) ) ================================================ FILE: image/errortypes.go ================================================ package image import ( "github.com/dropbox/godropbox/errors" ) type LostImageError struct { errors.DropboxError } ================================================ FILE: image/image.go ================================================ package image import ( "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Image struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Disk bson.ObjectID `bson:"disk" json:"disk"` Name string `bson:"name" json:"name"` Release string `bson:"release" json:"release"` Build string `bson:"build" json:"build"` Comment string `bson:"comment" json:"comment"` Deployment bson.ObjectID `bson:"deployment" json:"deployment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Signed bool `bson:"signed" json:"signed"` Type string `bson:"type" json:"type"` SystemType string `bson:"system_type" json:"system_type"` SystemKind string `bson:"system_kind" json:"system_kind"` Firmware string `bson:"firmware" json:"firmware"` Storage bson.ObjectID `bson:"storage" json:"storage"` Key string `bson:"key" json:"key"` LastModified time.Time `bson:"last_modified" json:"last_modified"` StorageClass string `bson:"storage_class" json:"storage_class"` Hash string `bson:"hash" json:"hash"` Etag string `bson:"etag" json:"etag"` Tags []string `bson:"-" json:"tags"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Release string `bson:"release" json:"release"` Build string `bson:"build" json:"build"` Organization bson.ObjectID `bson:"organization" json:"organization"` Deployment bson.ObjectID `bson:"deployment" json:"deployment"` Type string `bson:"type" json:"type"` Firmware string `bson:"firmware" json:"firmware"` Key string `bson:"key" json:"key"` Storage bson.ObjectID `bson:"storage" json:"storage"` Tags []string `bson:"-" json:"tags"` } func (i *Image) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { i.Name = utils.FilterName(i.Name) if i.Firmware == "" { i.Firmware = Uefi } if i.SystemType == "" { i.SystemType = Linux } if !ValidSystemTypes.Contains(i.SystemType) { errData = &errortypes.ErrorData{ Error: "invalid_system_type", Message: "Image system type invalid", } return } if i.SystemKind != "" && !ValidSystemKinds.Contains(i.SystemKind) { errData = &errortypes.ErrorData{ Error: "invalid_system_kind", Message: "Image system kind invalid", } return } return } func (i *Image) Parse() { if i.Name == "" { i.Name = i.Key } if i.Signed { i.Name, i.Release, i.Build = ParseImageName(i.Key) } } func (i *Image) GetSystemType() string { if i.SystemType != "" { return i.SystemType } name := strings.ToLower(i.Name) if strings.Contains(name, "bsd") { return Bsd } if strings.Contains(name, "alpinelinux") { return LinuxUnsigned } if strings.Contains(name, "archlinux") { return LinuxUnsigned } if strings.Contains(name, "oraclelinux7") { return LinuxLegacy } if strings.Contains(name, "redhat7") { return LinuxLegacy } return Linux } func (i *Image) GetSystemKind() string { if i.SystemKind != "" { return i.SystemKind } name := strings.ToLower(i.Name) if strings.Contains(name, "freebsd") { return FreeBSD } if strings.Contains(name, "alpinelinux") { return AlpineLinux } if strings.Contains(name, "archlinux") { return ArchLinux } if strings.Contains(name, "ubuntu") { return Ubuntu } if strings.Contains(name, "fedora") { return Fedora } if strings.Contains(name, "redhat") || strings.Contains(name, "almalinux") || strings.Contains(name, "oraclelinux") || strings.Contains(name, "rockylinux") { return RedHat } return "" } func (i *Image) Json() { i.Parse() } func (i *Image) Commit(db *database.Database) (err error) { coll := db.Images() err = coll.Commit(i.Id, i) if err != nil { return } return } func (i *Image) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Images() err = coll.CommitFields(i.Id, i, fields) if err != nil { return } return } func (i *Image) Insert(db *database.Database) (err error) { coll := db.Images() _, err = coll.InsertOne(db, i) if err != nil { err = database.ParseError(err) return } return } func (i *Image) Upsert(db *database.Database) (err error) { coll := db.Images() fields := bson.M{ "name": i.Name, "deployment": i.Deployment, "organization": i.Organization, "disk": i.Disk, "signed": i.Signed, "type": i.Type, "system_type": i.SystemType, "system_kind": i.SystemKind, "firmware": i.Firmware, "storage": i.Storage, "key": i.Key, "last_modified": i.LastModified, "storage_class": i.StorageClass, "hash": i.Hash, "etag": i.Etag, } resp, err := coll.UpdateOne( db, &bson.M{ "storage": i.Storage, "key": i.Key, }, &bson.M{ "$set": fields, }, options.UpdateOne().SetUpsert(true), ) if err != nil { err = database.ParseError(err) return } if resp.UpsertedID != nil { i.Id = resp.UpsertedID.(bson.ObjectID) } return } func (i *Image) Sync(db *database.Database) (err error) { coll := db.Images() i.Parse() i.SystemType = i.GetSystemType() i.SystemKind = i.GetSystemKind() if strings.HasPrefix(i.Key, "backup/") || strings.HasPrefix(i.Key, "snapshot/") { resp, e := coll.UpdateOne( db, &bson.M{ "storage": i.Storage, "key": i.Key, }, &bson.M{ "$set": &bson.M{ "organization": bson.NilObjectID, "release": i.Release, "build": i.Build, "storage": i.Storage, "key": i.Key, "signed": i.Signed, "type": i.Type, "system_type": i.SystemType, "system_kind": i.SystemKind, "firmware": i.Firmware, "etag": i.Etag, "last_modified": i.LastModified, "storage_class": i.StorageClass, }, "$setOnInsert": &bson.M{ "name": i.Name, "disk": bson.NilObjectID, "deployment": bson.NilObjectID, }, }, ) if e != nil { err = database.ParseError(e) if _, ok := err.(*database.NotFoundError); ok { err = &LostImageError{ errors.Wrap(err, "image: Lost image"), } } return } if resp.UpsertedID != nil { i.Id = resp.UpsertedID.(bson.ObjectID) } } else { resp, e := coll.UpdateOne( db, &bson.M{ "storage": i.Storage, "key": i.Key, }, &bson.M{ "$set": &bson.M{ "organization": bson.NilObjectID, "name": i.Name, "release": i.Release, "build": i.Build, "storage": i.Storage, "key": i.Key, "signed": i.Signed, "type": i.Type, "system_type": i.SystemType, "system_kind": i.SystemKind, "firmware": i.Firmware, "etag": i.Etag, "last_modified": i.LastModified, }, "$setOnInsert": &bson.M{ "disk": bson.NilObjectID, "deployment": bson.NilObjectID, }, }, options.UpdateOne().SetUpsert(true), ) if e != nil { err = database.ParseError(e) return } if resp.UpsertedID != nil { i.Id = resp.UpsertedID.(bson.ObjectID) } } return } func (i *Image) Remove(db *database.Database) (err error) { if !i.Deployment.IsZero() { err = deployment.Remove(db, i.Deployment) if err != nil { return } } err = Remove(db, i.Id) if err != nil { return } return } ================================================ FILE: image/sort.go ================================================ package image import ( "github.com/pritunl/pritunl-cloud/utils" ) type ImagesSort []*Image func (x ImagesSort) Len() int { return len(x) } func (x ImagesSort) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x ImagesSort) Less(i, j int) bool { return utils.NaturalCompare(x[i].Name, x[j].Name) < 0 } type CompletionsSort []*Completion func (x CompletionsSort) Len() int { return len(x) } func (x CompletionsSort) Swap(i, j int) { x[i], x[j] = x[j], x[i] } func (x CompletionsSort) Less(i, j int) bool { return utils.NaturalCompare(x[i].Name, x[j].Name) < 0 } ================================================ FILE: image/utils.go ================================================ package image import ( "crypto/md5" "fmt" "path/filepath" "regexp" "strings" "time" "github.com/dropbox/godropbox/container/set" minio "github.com/minio/minio-go/v7" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) var ( etagReg = regexp.MustCompile("[^a-zA-Z0-9]+") distroRe = regexp.MustCompile(`^([a-z]+)([0-9]*)`) dateRe = regexp.MustCompile(`_(\d{2})(\d{2})(\d{2})?$`) ) func GetEtag(info minio.ObjectInfo) string { etag := info.ETag if etag == "" { modifiedHash := md5.New() modifiedHash.Write( []byte(info.LastModified.Format(time.RFC3339))) etag = fmt.Sprintf("%x", modifiedHash.Sum(nil)) } return etagReg.ReplaceAllString(etag, "") } func ParseImageName(key string) (name, release, build string) { baseName := strings.TrimSuffix(key, filepath.Ext(key)) dateMatch := dateRe.FindStringSubmatch(baseName) if len(dateMatch) != 3 && len(dateMatch) != 4 { name = key return } yearStr, monthStr := dateMatch[1], dateMatch[2] build = yearStr + monthStr if len(dateMatch) == 4 { build += dateMatch[3] } base := strings.TrimSuffix(baseName, dateMatch[0]) tokens := strings.Split(base, "_") if len(tokens) == 0 { name = key return } distroMatch := distroRe.FindStringSubmatch(tokens[0]) if len(distroMatch) < 2 { name = key return } distro := distroMatch[1] version := "" if len(distroMatch) >= 3 { version = distroMatch[2] } if version == "" { name = fmt.Sprintf("%s-%s%s", distro, yearStr, monthStr) } else { name = fmt.Sprintf("%s%s-%s%s", distro, version, yearStr, monthStr) } if Releases.Contains(distro + version) { release = distro + version } return } func Get(db *database.Database, imgId bson.ObjectID) ( img *Image, err error) { coll := db.Images() img = &Image{} err = coll.FindOneId(imgId, img) if err != nil { return } return } func GetKey(db *database.Database, storeId bson.ObjectID, key string) ( img *Image, err error) { coll := db.Images() img = &Image{} err = coll.FindOne(db, &bson.M{ "storage": storeId, "key": key, }).Decode(img) if err != nil { err = database.ParseError(err) return } return } func GetOrg(db *database.Database, orgId, imgId bson.ObjectID) ( img *Image, err error) { coll := db.Images() img = &Image{} err = coll.FindOne(db, &bson.M{ "_id": imgId, "organization": orgId, }).Decode(img) if err != nil { err = database.ParseError(err) return } return } func GetOrgPublic(db *database.Database, orgId, imgId bson.ObjectID) ( img *Image, err error) { coll := db.Images() img = &Image{} err = coll.FindOne(db, &bson.M{ "_id": imgId, "organization": Global, }).Decode(img) if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (img *Image, err error) { coll := db.Images() img = &Image{} err = coll.FindOne(db, query).Decode(img) if err != nil { err = database.ParseError(err) return } return } func Distinct(db *database.Database, storeId bson.ObjectID) ( keys []string, err error) { coll := db.Images() keys = []string{} err = coll.Distinct(db, "key", &bson.M{ "storage": storeId, }).Decode(&keys) if err != nil { err = database.ParseError(err) return } return } func ExistsOrg(db *database.Database, orgId, imgId bson.ObjectID) ( exists bool, err error) { coll := db.Images() n, err := coll.CountDocuments(db, &bson.M{ "_id": imgId, "organization": Global, }) if err != nil { err = database.ParseError(err) return } if n > 0 { exists = true } return } func GetAll(db *database.Database, query *bson.M) ( imgs []*Image, err error) { coll := db.Images() imgs = []*Image{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { img := &Image{} err = cursor.Decode(img) if err != nil { err = database.ParseError(err) return } imgs = append(imgs, img) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllCompletion(db *database.Database, query *bson.M) ( imgs []*Completion, err error) { coll := db.Images() imgs = []*Completion{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{ {"_id", 1}, {"name", 1}, {"release", 1}, {"build", 1}, {"organization", 1}, {"deployment", 1}, {"type", 1}, {"firmware", 1}, {"key", 1}, {"storage", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { img := &Completion{} err = cursor.Decode(img) if err != nil { err = database.ParseError(err) return } imgs = append(imgs, img) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) ( imgs []*Image, count int64, err error) { coll := db.Images() imgs = []*Image{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"key", 1}}). SetSkip(skip). SetLimit(pageCount), ) defer cursor.Close(db) for cursor.Next(db) { img := &Image{} err = cursor.Decode(img) if err != nil { err = database.ParseError(err) return } imgs = append(imgs, img) img = &Image{} } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( images []*Image, err error) { coll := db.Images() images = []*Image{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"key", 1}}). SetProjection(bson.D{ {"name", 1}, {"key", 1}, {"signed", 1}, {"firmware", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { img := &Image{} err = cursor.Decode(img) if err != nil { err = database.ParseError(err) return } img.Json() images = append(images, img) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllKeys(db *database.Database) (keys set.Set, err error) { coll := db.Images() keys = set.NewSet() cursor, err := coll.Find( db, &bson.M{}, options.Find(). SetSort(bson.D{{"key", 1}}). SetProjection(bson.D{ {"_id", 1}, {"etag", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { img := &Image{} err = cursor.Decode(img) if err != nil { err = database.ParseError(err) return } keys.Add(fmt.Sprintf("%s-%s", img.Id.Hex(), img.Etag)) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, imgId bson.ObjectID) (err error) { coll := db.Images() _, err = coll.DeleteOne(db, &bson.M{ "_id": imgId, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } return } ================================================ FILE: imds/config.go ================================================ package imds import ( "sync" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" ) var ( curConfs = map[bson.ObjectID]*types.Config{} curConfsLock = sync.Mutex{} ) func BuildConfig(inst *instance.Instance, virt *vm.VirtualMachine, unt *unit.Unit, spc *spec.Spec, vc *vpc.Vpc, subnet *vpc.Subnet, pods []*pod.Pod, podUnitsMap map[bson.ObjectID][]*unit.Unit, deployments map[bson.ObjectID]*deployment.Deployment, secrs []*secret.Secret, certs []*certificate.Certificate, domains []*types.Domain) (conf *types.Config, err error) { conf = &types.Config{ ImdsHostSecret: virt.ImdsHostSecret, ClientIps: inst.PrivateIps, Node: types.NewNode(node.Self), Instance: types.NewInstance(inst), Vpc: types.NewVpc(vc), Subnet: types.NewSubnet(subnet), Pods: types.NewPods(pods, podUnitsMap, deployments), Secrets: types.NewSecrets(secrs), Certificates: types.NewCertificates(certs), Domains: domains, } if spc != nil { conf.Spec = spc.Id conf.SpecData = spc.Data conf.Journals = types.NewJournals(spc) } return } func SetConfigs(cnfs map[bson.ObjectID]*types.Config) { curConfsLock.Lock() curConfs = cnfs curConfsLock.Unlock() } func GetConfigs() ( cnfs map[bson.ObjectID]*types.Config) { curConfsLock.Lock() cnfs = curConfs curConfsLock.Unlock() return } ================================================ FILE: imds/imds.go ================================================ package imds import ( "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "net" "net/http" "strings" "sync" "sync/atomic" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/advisory" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds/server/utils" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/telemetry" pritunlutils "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/errors" "github.com/sirupsen/logrus" ) var ( hashes = map[bson.ObjectID]uint32{} hashesLock = sync.Mutex{} counter = atomic.Uint64{} ) const ( counterMax = 2000000000 ) func mergeUpdateDetails(db *database.Database, instId bson.ObjectID, updates []*telemetry.Update) (err error) { if len(updates) == 0 { return } coll := db.Instances() inst := &instance.Instance{} err = coll.FindOne(db, &bson.M{ "_id": instId, }, database.FindOneProject("guest.updates")).Decode(inst) if err != nil { err = database.ParseError(err) err = database.IgnoreNotFoundError(err) return } if inst.Guest == nil { return } detailsMap := map[string][]*advisory.Advisory{} for _, upd := range inst.Guest.Updates { if upd.Advisory != "" && len(upd.Details) > 0 { detailsMap[upd.Advisory] = upd.Details } } for _, upd := range updates { if details, ok := detailsMap[upd.Advisory]; ok { upd.Details = details } } return } func Sync(db *database.Database, namespace string, instId, deplyId bson.ObjectID, conf *types.Config) (err error) { sockPath := paths.GetImdsSockPath(instId) exists, err := utils.Exists(sockPath) if err != nil { return } if !exists { return } client := &http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", sockPath) }, }, Timeout: 6 * time.Second, } var body io.Reader hashesLock.Lock() curHash := hashes[instId] hashesLock.Unlock() if conf != nil && curHash != conf.Hash { reqDataBuf := &bytes.Buffer{} err = json.NewEncoder(reqDataBuf).Encode(conf) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "agent: Failed to parse request data"), } return } body = reqDataBuf } req, err := http.NewRequest("PUT", "http://unix/sync", body) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to create imds request"), } return } req.Header.Set("User-Agent", "pritunl-imds") req.Header.Set("Auth-Token", conf.ImdsHostSecret) if body != nil { req.Header.Set("Content-Type", "application/json") } resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "agent: Imds request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData := &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } if errData != nil && errData.Message != "" { body = errData.Message } err = &errortypes.RequestError{ errors.Newf( "agent: Imds host sync error %d - %s", resp.StatusCode, body), } return } ste := &types.State{} err = json.NewDecoder(resp.Body).Decode(ste) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to decode imds host sync resp"), } return } hashesLock.Lock() hashes[instId] = ste.Hash hashesLock.Unlock() if ste.Status != "" { coll := db.Instances() data := bson.M{ "guest.status": ste.Status, "guest.timestamp": time.Now(), "guest.heartbeat": ste.Timestamp, "guest.memory": ste.Memory, "guest.hugepages": ste.HugePages, "guest.load1": ste.Load1, "guest.load5": ste.Load5, "guest.load15": ste.Load15, } if ste.DhcpIp != nil { data["dhcp_ip"] = ste.DhcpIp.String() } if ste.DhcpIp6 != nil { data["dhcp_ip6"] = ste.DhcpIp6.String() } if ste.Updates != nil { err = mergeUpdateDetails(db, instId, ste.Updates) if err != nil { return } data["guest.updates"] = ste.Updates } _, err = coll.UpdateOne(db, &bson.M{ "_id": instId, }, bson.M{ "$set": data, }) if err != nil { err = database.ParseError(err) return } var kind int32 var resource bson.ObjectID if !deplyId.IsZero() { kind = journal.DeploymentAgent resource = deplyId } else { kind = journal.InstanceAgent resource = instId } for _, entry := range ste.Output { if entry.Level < 1 || entry.Level > 9 { continue } if len(entry.Message) > 100000 { entry.Message = entry.Message[:100000] } jrnl := &journal.Journal{ Resource: resource, Kind: kind, Level: entry.Level, Timestamp: entry.Timestamp, Count: int32(counter.Add(1) % counterMax), Message: entry.Message, } err = jrnl.Insert(db) if err != nil { return } } if ste.Journals != nil { indexes := map[string]int32{} for _, jrnl := range conf.Journals { indexes[jrnl.Key] = jrnl.Index } for key, output := range ste.Journals { index := indexes[key] if index == 0 { continue } for _, entry := range output { if entry.Level < 1 || entry.Level > 9 { continue } if len(entry.Message) > 100000 { entry.Message = entry.Message[:100000] } jrnl := &journal.Journal{ Resource: resource, Kind: index, Level: entry.Level, Timestamp: entry.Timestamp, Count: int32(counter.Add(1) % counterMax), Message: entry.Message, } err = jrnl.Insert(db) if err != nil { return } } } } curIp := "" curIp6 := "" curIpPrefix := "" curIpPrefix6 := "" curIpCached := false curIpCached6 := false newIp := "" newIp6 := "" newIpPrefix := "" newIpPrefix6 := "" clearIpCache := false if ste.DhcpIp != nil { addrStore, ok := store.GetAddress(instId) if ok { curIpCached = true curIp = addrStore.Addr if ste.DhcpIface == ste.DhcpIface6 { curIpCached6 = true curIp6 = addrStore.Addr6 } } else { address, address6, e := iproute.AddressGetIfaceMod( namespace, ste.DhcpIface) if e == nil && address != nil { curIpCached = false curIp = address.Local curIpPrefix = fmt.Sprintf( "%s/%d", address.Local, address.Prefix) if ste.DhcpIface == ste.DhcpIface6 && address6 != nil { curIpCached6 = false curIp6 = address6.Local curIpPrefix6 = fmt.Sprintf( "%s/%d", address6.Local, address6.Prefix) } } } newIpPrefix = ste.DhcpIp.String() newIp = strings.Split(newIpPrefix, "/")[0] } if ste.DhcpIp6 != nil { if curIp6 == "" { addrStore, ok := store.GetAddress(instId) if ok { curIpCached6 = true curIp6 = addrStore.Addr6 } else { _, address6, e := iproute.AddressGetIfaceMod( namespace, ste.DhcpIface6) if e == nil && address6 != nil { curIpCached6 = false curIp6 = address6.Local curIpPrefix6 = fmt.Sprintf( "%s/%d", address6.Local, address6.Prefix) } } } newIpPrefix6 = ste.DhcpIp6.String() newIp6 = strings.Split(newIpPrefix6, "/")[0] } if newIp != "" && newIp != curIp { if curIpCached { address, address6, e := iproute.AddressGetIfaceMod( namespace, ste.DhcpIface) if e == nil && address != nil { curIpCached = false curIp = address.Local curIpPrefix = fmt.Sprintf( "%s/%d", address.Local, address.Prefix) if ste.DhcpIface == ste.DhcpIface6 && address6 != nil { curIpCached6 = false curIp6 = address6.Local curIpPrefix6 = fmt.Sprintf( "%s/%d", address6.Local, address6.Prefix) } } } if newIp != curIp { logrus.WithFields(logrus.Fields{ "instance": instId.Hex(), "namespace": namespace, "cur_ip": curIpPrefix, "new_ip": newIpPrefix, }).Info("imds: Updating instance DHCP IPv4 address") if curIpPrefix != "" { _, err = pritunlutils.ExecCombinedOutputLogged( []string{"File exists", "Cannot assign"}, "ip", "netns", "exec", namespace, "ip", "addr", "del", curIpPrefix, "dev", ste.DhcpIface, ) if err != nil { return } } _, err = pritunlutils.ExecCombinedOutputLogged( []string{"File exists", "already assigned"}, "ip", "netns", "exec", namespace, "ip", "addr", "add", newIpPrefix, "dev", ste.DhcpIface, ) if err != nil { return } if ste.DhcpGateway != nil { _, err = pritunlutils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", namespace, "ip", "route", "add", "default", "via", ste.DhcpGateway.String(), "dev", ste.DhcpIface, ) if err != nil { return } } clearIpCache = true } } if newIp6 != "" && newIp6 != curIp6 { if curIpCached6 { _, address6, e := iproute.AddressGetIfaceMod( namespace, ste.DhcpIface6) if e == nil && address6 != nil { curIpCached6 = false curIp6 = address6.Local curIpPrefix6 = fmt.Sprintf( "%s/%d", address6.Local, address6.Prefix) } } if newIp6 != curIp6 { logrus.WithFields(logrus.Fields{ "instance": instId.Hex(), "namespace": namespace, "cur_ip6": curIpPrefix6, "new_ip6": newIpPrefix6, }).Info("imds: Updating instance DHCP IPv6 address") if curIpPrefix6 != "" { _, err = pritunlutils.ExecCombinedOutputLogged( []string{"File exists", "Cannot assign"}, "ip", "netns", "exec", namespace, "ip", "addr", "del", curIpPrefix6, "dev", ste.DhcpIface6, ) if err != nil { return } } _, err = pritunlutils.ExecCombinedOutputLogged( []string{"File exists", "already assigned"}, "ip", "netns", "exec", namespace, "ip", "addr", "add", newIpPrefix6, "dev", ste.DhcpIface6, ) if err != nil { return } clearIpCache = true } } if clearIpCache { store.RemAddress(instId) } } return } func Pull(db *database.Database, instId, deplyId bson.ObjectID, imdsHostSecret string, journals []*types.Journal) (err error) { sockPath := paths.GetImdsSockPath(instId) exists, err := utils.Exists(sockPath) if err != nil { return } if !exists { return } client := &http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", sockPath) }, }, Timeout: 6 * time.Second, } req, err := http.NewRequest("GET", "http://unix/sync", nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to create imds request"), } return } req.Header.Set("User-Agent", "pritunl-imds") req.Header.Set("Auth-Token", imdsHostSecret) resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "agent: Imds request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData := &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } if errData != nil && errData.Message != "" { body = errData.Message } err = &errortypes.RequestError{ errors.Newf( "agent: Imds host sync error %d - %s", resp.StatusCode, body), } return } ste := &types.State{} err = json.NewDecoder(resp.Body).Decode(ste) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to decode imds host sync resp"), } return } if ste.Status != "" { coll := db.Instances() data := bson.M{ "guest.status": ste.Status, "guest.timestamp": time.Now(), "guest.heartbeat": ste.Timestamp, "guest.memory": ste.Memory, "guest.hugepages": ste.HugePages, "guest.load1": ste.Load1, "guest.load5": ste.Load5, "guest.load15": ste.Load15, } if ste.DhcpIp != nil { data["dhcp_ip"] = ste.DhcpIp.String() } if ste.DhcpIp6 != nil { data["dhcp_ip6"] = ste.DhcpIp6.String() } if ste.Updates != nil { err = mergeUpdateDetails(db, instId, ste.Updates) if err != nil { return } data["guest.updates"] = ste.Updates } _, err = coll.UpdateOne(db, &bson.M{ "_id": instId, }, bson.M{ "$set": data, }) if err != nil { err = database.ParseError(err) return } var kind int32 var resource bson.ObjectID if !deplyId.IsZero() { kind = journal.DeploymentAgent resource = deplyId } else { kind = journal.InstanceAgent resource = instId } for _, entry := range ste.Output { if entry.Level < 1 || entry.Level > 9 { continue } if len(entry.Message) > 100000 { entry.Message = entry.Message[:100000] } jrnl := &journal.Journal{ Resource: resource, Kind: kind, Level: entry.Level, Timestamp: entry.Timestamp, Count: int32(counter.Add(1) % counterMax), Message: entry.Message, } err = jrnl.Insert(db) if err != nil { return } } if ste.Journals != nil { indexes := map[string]int32{} for _, jrnl := range journals { indexes[jrnl.Key] = jrnl.Index } for key, output := range ste.Journals { index := indexes[key] if index == 0 { continue } for _, entry := range output { if entry.Level < 1 || entry.Level > 9 { continue } if len(entry.Message) > 100000 { entry.Message = entry.Message[:100000] } jrnl := &journal.Journal{ Resource: resource, Kind: index, Level: entry.Level, Timestamp: entry.Timestamp, Count: int32(counter.Add(1) % counterMax), Message: entry.Message, } err = jrnl.Insert(db) if err != nil { return } } } } } return } func State(db *database.Database, instId bson.ObjectID, imdsHostSecret string) (ste *types.State, err error) { sockPath := paths.GetImdsSockPath(instId) exists, err := utils.Exists(sockPath) if err != nil { return } if !exists { return } client := &http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", sockPath) }, }, Timeout: 6 * time.Second, } req, err := http.NewRequest("GET", "http://unix/state", nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to create imds request"), } return } req.Header.Set("User-Agent", "pritunl-imds") req.Header.Set("Auth-Token", imdsHostSecret) resp, e := client.Do(req) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "agent: Imds request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { body := "" data, _ := ioutil.ReadAll(resp.Body) if data != nil { body = string(data) } errData := &errortypes.ErrorData{} err = json.Unmarshal(data, errData) if err != nil || errData.Error == "" { errData = nil } if errData != nil && errData.Message != "" { body = errData.Message } err = &errortypes.RequestError{ errors.Newf( "agent: Imds host sync error %d - %s", resp.StatusCode, body), } return } ste = &types.State{} err = json.NewDecoder(resp.Body).Decode(ste) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "agent: Failed to decode imds host sync resp"), } return } return } ================================================ FILE: imds/resource/resource.go ================================================ package resource import ( "strings" "github.com/pritunl/pritunl-cloud/finder" "github.com/pritunl/pritunl-cloud/imds/server/config" "github.com/pritunl/pritunl-cloud/secret" ) func Query(resrc string, keys ...string) (val string, err error) { var resrcInf interface{} key := "" isJson := false switch resrc { case finder.NodeKind: if len(keys) != 2 || keys[0] != "self" { break } key = keys[1] resrcInf = config.Config.Node break case finder.InstanceKind: if len(keys) != 2 || keys[0] != "self" { break } key = keys[1] resrcInf = config.Config.Instance break case finder.VpcKind: if len(keys) != 2 || keys[0] != "self" { break } key = keys[1] resrcInf = config.Config.Vpc break case finder.SubnetKind: if len(keys) != 2 || keys[0] != "self" { break } key = keys[1] resrcInf = config.Config.Subnet break case finder.SecretKind: if len(keys) != 2 { break } key = keys[1] for _, secr := range config.Config.Secrets { if secr.Name == keys[0] { resrcInf = secr if secr.Type == secret.Json && strings.HasPrefix(key, "data.") { isJson = true } break } } break case finder.CertificateKind: if len(keys) != 2 { break } key = keys[1] for _, cert := range config.Config.Certificates { if cert.Name == keys[0] { resrcInf = cert break } } break case finder.PodKind: if len(keys) == 2 { key = keys[1] } else if len(keys) == 4 { key = keys[3] } else { break } for _, pd := range config.Config.Pods { if pd.Name == keys[0] { if len(keys) == 4 { if keys[1] == finder.UnitKind { for _, unit := range pd.Units { if unit.Name == keys[2] { resrcInf = unit break } } } } else { resrcInf = pd } break } } break case finder.UnitKind: if len(keys) != 2 { break } key = keys[1] for _, pd := range config.Config.Pods { for _, unit := range pd.Units { if unit.Name == keys[0] { resrcInf = unit break } } } break default: return } if resrcInf == nil { return } val, err = selector(resrcInf, key, isJson) if err != nil { return } return } ================================================ FILE: imds/resource/utils.go ================================================ package resource import ( "encoding/json" "fmt" "reflect" "strconv" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) func selector(v interface{}, key string, isJson bool) (val string, err error) { valRef := reflect.ValueOf(v) if valRef.Kind() != reflect.Ptr || valRef.IsNil() { err = &errortypes.ParseError{ errors.New("Selector input invalid"), } return } elm := valRef.Elem() if elm.Kind() != reflect.Struct { err = &errortypes.ParseError{ errors.New("Selector kind invalid"), } return } jsonKey := "" if isJson { keys := strings.SplitN(key, ".", 2) if len(keys) == 2 { key = keys[0] jsonKey = keys[1] } else { isJson = false } } typ := elm.Type() for i := 0; i < elm.NumField(); i++ { field := typ.Field(i) jsonTag := field.Tag.Get("json") if jsonTag == key { fieldVal := elm.Field(i) if fieldVal.Kind() == reflect.Slice { var elements []string for j := 0; j < fieldVal.Len(); j++ { elements = append(elements, selectString(fieldVal.Index(j).Interface())) } val = strings.Join(elements, ",") } else { val = selectString(elm.Field(i).Interface()) if isJson && jsonKey != "" { var jsonData map[string]any err = json.Unmarshal([]byte(val), &jsonData) if err != nil { val = "" return } jsonValue, exists := jsonData[jsonKey] if !exists { val = "" return } val = jsonValString(jsonValue) } } return } } return } func jsonValString(value any) string { switch val := value.(type) { case string: return val case bool: return strconv.FormatBool(val) case float64: return strconv.FormatFloat(val, 'f', -1, 64) case int: return strconv.Itoa(val) case int64: return strconv.FormatInt(val, 10) case nil: return "" default: jsonBytes, _ := json.Marshal(val) return string(jsonBytes) } } func selectString(obj interface{}) string { if oid, ok := obj.(bson.ObjectID); ok { return oid.Hex() } val := reflect.ValueOf(obj) val = reflect.Indirect(val) method := val.MethodByName("String") if method.IsValid() && method.Type().NumIn() == 0 && method.Type().NumOut() == 1 && method.Type().Out(0).Kind() == reflect.String { result := method.Call(nil) return result[0].String() } return fmt.Sprintf("%v", obj) } ================================================ FILE: imds/server/config/config.go ================================================ package config import ( "github.com/pritunl/pritunl-cloud/imds/types" ) var ( Config = &types.Config{} ) ================================================ FILE: imds/server/constants/constants.go ================================================ package constants import ( "time" ) const ( Version = "1.0.3229.20" ConfRefresh = 500 * time.Millisecond ) var ( Sock = "" Host = "127.0.0.1" Port = 80 Client = "127.0.0.1" ClientSecret = "" DhcpSecret = "" HostSecret = "" Interrupt = false ) ================================================ FILE: imds/server/errortypes/errortypes.go ================================================ package errortypes import ( "github.com/dropbox/godropbox/errors" ) type UnknownError struct { errors.DropboxError } type NotFoundError struct { errors.DropboxError } type ReadError struct { errors.DropboxError } type WriteError struct { errors.DropboxError } type ParseError struct { errors.DropboxError } type AuthenticationError struct { errors.DropboxError } type VerificationError struct { errors.DropboxError } type ApiError struct { errors.DropboxError } type DatabaseError struct { errors.DropboxError } type RequestError struct { errors.DropboxError } type ConnectionError struct { errors.DropboxError } type TimeoutError struct { errors.DropboxError } type ExecError struct { errors.DropboxError } type NetworkError struct { errors.DropboxError } type TypeError struct { errors.DropboxError } type ErrorData struct { Error string `json:"error"` Message string `json:"error_msg"` } ================================================ FILE: imds/server/handlers/certificate.go ================================================ package handlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/server/config" ) func certificatesGet(c *gin.Context) { c.JSON(200, config.Config.Certificates) } ================================================ FILE: imds/server/handlers/dhcp.go ================================================ package handlers import ( "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/dhcpc" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" "github.com/pritunl/pritunl-cloud/imds/server/state" "github.com/pritunl/pritunl-cloud/utils" ) func dhcpPut(c *gin.Context) { data := &dhcpc.Lease{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } state.Global.State.DhcpIface = data.Iface state.Global.State.DhcpIface6 = data.Iface6 state.Global.State.DhcpIp = data.Address state.Global.State.DhcpGateway = data.Gateway state.Global.State.DhcpIp6 = data.Address6 c.JSON(200, map[string]string{}) } ================================================ FILE: imds/server/handlers/handlers.go ================================================ package handlers import ( "crypto/subtle" "net/http" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/server/constants" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" "github.com/pritunl/tools/logger" ) type AuthenticationError struct { Error string `json:"error"` Message string `json:"message"` } func Recovery(c *gin.Context) { defer func() { if r := recover(); r != nil { err := &errortypes.UnknownError{ errors.Newf("handlers: Handler panic %s", r), } logger.WithFields(logger.Fields{ "error": err, }).Error("handlers: Handler panic") c.Writer.WriteHeader(http.StatusInternalServerError) } }() c.Next() } func Errors(c *gin.Context) { c.Next() for _, err := range c.Errors { logger.WithFields(logger.Fields{ "error": err, }).Error("handlers: Handler error") } } func AuthVirt(c *gin.Context) { token := c.Request.Header.Get("Auth-Token") if token == "" { token = c.Query("token") } // TODO config.Config.ClientIps not loaded // addr := utils.StripPort(c.Request.RemoteAddr) // if len(config.Config.ClientIps) != 0 && config.Config.ClientIps[0] == "" && // !utils.StringsContains(config.Config.ClientIps, addr) { // c.AbortWithStatusJSON(401, &AuthenticationError{ // Error: "authentication", // Message: "Source IP address invalid", // }) // return // } if c.Request.Header.Get("Origin") != "" || c.Request.Header.Get("Referer") != "" || c.Request.Header.Get("User-Agent") != "pritunl-imds" || constants.ClientSecret == "" || (subtle.ConstantTimeCompare([]byte(token), []byte(constants.ClientSecret)) != 1) { c.AbortWithStatus(401) return } c.Next() } func AuthDhcp(c *gin.Context) { token := c.Request.Header.Get("Auth-Token") if token == "" { token = c.Query("token") } if c.Request.Header.Get("Origin") != "" || c.Request.Header.Get("Referer") != "" || c.Request.Header.Get("User-Agent") != "pritunl-dhcp" || constants.DhcpSecret == "" || (subtle.ConstantTimeCompare([]byte(token), []byte(constants.DhcpSecret)) != 1) { c.AbortWithStatus(401) return } c.Next() } func AuthHost(c *gin.Context) { token := c.Request.Header.Get("Auth-Token") if token == "" { token = c.Query("token") } if c.Request.Header.Get("Origin") != "" || c.Request.Header.Get("Referer") != "" || c.Request.Header.Get("User-Agent") != "pritunl-imds" || constants.HostSecret == "" || (subtle.ConstantTimeCompare([]byte(token), []byte(constants.HostSecret)) != 1) { c.AbortWithStatus(401) return } c.Next() } func RegisterVirt(engine *gin.Engine) { engine.Use(Recovery) engine.Use(Errors) virtGroup := engine.Group("") virtGroup.Use(AuthVirt) dhcpGroup := engine.Group("") dhcpGroup.Use(AuthDhcp) virtGroup.GET("/query/:resource", queryGet) virtGroup.GET("/query/:resource/:key1", queryGet) virtGroup.GET("/query/:resource/:key1/:key2", queryGet) virtGroup.GET("/query/:resource/:key1/:key2/:key3", queryGet) virtGroup.GET("/query/:resource/:key1/:key2/:key3/:key4", queryGet) virtGroup.GET("/node", nodeGet) virtGroup.GET("/instance", instanceGet) virtGroup.GET("/vpc", vpcGet) virtGroup.GET("/subnet", subnetGet) virtGroup.GET("/certificate", certificatesGet) virtGroup.GET("/secret", secretsGet) virtGroup.PUT("/sync", syncPut) dhcpGroup.PUT("/dhcp", dhcpPut) } func RegisterHost(engine *gin.Engine) { engine.Use(AuthHost) engine.Use(Recovery) engine.Use(Errors) engine.PUT("/sync", hostSyncPut) engine.GET("/sync", hostSyncGet) engine.GET("/state", hostStateGet) } ================================================ FILE: imds/server/handlers/instance.go ================================================ package handlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/server/config" ) func instanceGet(c *gin.Context) { c.JSON(200, config.Config.Instance) } ================================================ FILE: imds/server/handlers/node.go ================================================ package handlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/server/config" ) func nodeGet(c *gin.Context) { c.JSON(200, config.Config.Node) } ================================================ FILE: imds/server/handlers/query.go ================================================ package handlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/resource" ) func queryGet(c *gin.Context) { resrc := c.Param("resource") key1 := c.Param("key1") key2 := c.Param("key2") key3 := c.Param("key3") key4 := c.Param("key4") keys := []string{} if key1 != "" { keys = append(keys, key1) if key2 != "" { keys = append(keys, key2) if key3 != "" { keys = append(keys, key3) if key4 != "" { keys = append(keys, key4) } } } } val, err := resource.Query(resrc, keys...) if err != nil { c.AbortWithError(500, err) return } c.String(200, val) } ================================================ FILE: imds/server/handlers/secret.go ================================================ package handlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/server/config" ) func secretsGet(c *gin.Context) { c.JSON(200, config.Config.Secrets) } ================================================ FILE: imds/server/handlers/sync.go ================================================ package handlers import ( "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/dnss" "github.com/pritunl/pritunl-cloud/imds/server/config" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" "github.com/pritunl/pritunl-cloud/imds/server/state" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/telemetry" "github.com/pritunl/pritunl-cloud/utils" ) var ( lastSecurity = time.Now().Add(-7 * time.Minute) lastSecurityLock sync.Mutex ) type syncRespData struct { Spec string `json:"spec"` Hash uint32 `json:"hash"` Journals []*types.Journal `json:"journals"` } func syncPut(c *gin.Context) { data := &types.State{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } if !state.Global.State.Final() { state.Global.State.Status = data.Status } state.Global.State.Timestamp = time.Now() state.Global.State.Memory = data.Memory state.Global.State.HugePages = data.HugePages state.Global.State.Load1 = data.Load1 state.Global.State.Load5 = data.Load5 state.Global.State.Load15 = data.Load15 if data.Updates != nil { telemetry.Updates.Set(data.Updates) } if data.Output != nil { for _, entry := range data.Output { state.Global.AppendOutput(entry) } } if data.Journals != nil { for key, output := range data.Journals { for _, entry := range output { state.Global.AppendJournalOutput(key, entry) } } } if data.Hash != config.Config.Hash { c.JSON(200, &syncRespData{ Spec: config.Config.SpecData, Hash: config.Config.Hash, Journals: config.Config.Journals, }) } else { c.JSON(200, &syncRespData{ Hash: config.Config.Hash, Journals: config.Config.Journals, }) } } func hostSyncPut(c *gin.Context) { data := &types.Config{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } if data.Hash != 0 { config.Config = data dnss.LoadConfig(data.Domains) } ste := state.Global.State.Copy() ste.Hash = config.Config.Hash ste.Output = state.Global.GetOutput() ste.Journals = state.Global.GetJournals() updates, ok := telemetry.Updates.Get() if ok { ste.Updates = updates } else { ste.Updates = nil } c.JSON(200, ste) } func hostSyncGet(c *gin.Context) { ste := state.Global.State.Copy() ste.Output = state.Global.GetOutput() ste.Journals = state.Global.GetJournals() updates, ok := telemetry.Updates.Get() if ok { ste.Updates = updates } else { ste.Updates = nil } c.JSON(200, ste) } func hostStateGet(c *gin.Context) { ste := state.Global.State.Copy() c.JSON(200, ste) } ================================================ FILE: imds/server/handlers/vpc.go ================================================ package handlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/imds/server/config" ) func vpcGet(c *gin.Context) { c.JSON(200, config.Config.Vpc) } func subnetGet(c *gin.Context) { c.JSON(200, config.Config.Subnet) } ================================================ FILE: imds/server/router/router.go ================================================ package router import ( "context" "fmt" "net" "net/http" "os" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/dnss" "github.com/pritunl/pritunl-cloud/imds/server/constants" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" "github.com/pritunl/pritunl-cloud/imds/server/handlers" "github.com/pritunl/tools/logger" ) type Router struct { virtServer *http.Server hostServer *http.Server dnsServer *dnss.Server } func (r *Router) Run() (err error) { logger.WithFields(logger.Fields{ "host": constants.Host, "port": constants.Port, "sock": constants.Sock, "version": constants.Version, }).Info("main: Starting imds server") waiters := &sync.WaitGroup{} waiters.Add(1) go func() { defer waiters.Done() e := r.virtServer.ListenAndServe() if e != nil { e = &errortypes.WriteError{ errors.Wrap(e, "main: Server listen error"), } if err == nil { err = e } r.Shutdown() return } }() waiters.Add(1) go func() { defer waiters.Done() _ = os.Remove(constants.Sock) listener, e := net.Listen("unix", constants.Sock) if e != nil { e = &errortypes.WriteError{ errors.Wrap(e, "main: Failed to create unix socket"), } if err == nil { err = e } r.Shutdown() return } e = r.hostServer.Serve(listener) if e != nil { e = &errortypes.WriteError{ errors.Wrap(e, "main: Server listen error"), } if err == nil { err = e } r.Shutdown() return } }() waiters.Add(1) go func() { defer waiters.Done() e := r.dnsServer.ListenUdp() if e != nil { if err == nil { err = e } r.Shutdown() return } }() waiters.Add(1) go func() { defer waiters.Done() e := r.dnsServer.ListenTcp() if e != nil { if err == nil { err = e } r.Shutdown() return } }() waiters.Wait() if err != nil { return } return } func (r *Router) Shutdown() { defer func() { recover() }() webCtx, webCancel := context.WithTimeout( context.Background(), 1*time.Second, ) defer webCancel() _ = r.virtServer.Shutdown(webCtx) _ = r.virtServer.Close() _ = r.hostServer.Shutdown(webCtx) _ = r.hostServer.Close() _ = r.dnsServer.Shutdown() } func (r *Router) Init() { gin.SetMode(gin.ReleaseMode) virtRouter := gin.New() handlers.RegisterVirt(virtRouter) r.virtServer = &http.Server{ Addr: fmt.Sprintf( "%s:%d", constants.Host, constants.Port, ), Handler: virtRouter, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 4096, } hostRouter := gin.New() handlers.RegisterHost(hostRouter) r.hostServer = &http.Server{ Addr: "127.0.0.1:99999", Handler: hostRouter, ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 4096, } r.dnsServer = dnss.NewServer(fmt.Sprintf( "%s:53", constants.Host, )) return } ================================================ FILE: imds/server/server.go ================================================ package server import ( "flag" "fmt" "os" "strings" "github.com/pritunl/pritunl-cloud/imds/server/constants" "github.com/pritunl/pritunl-cloud/imds/server/router" "github.com/pritunl/pritunl-cloud/imds/server/state" "github.com/pritunl/tools/logger" ) func Main() (err error) { constants.ClientSecret = os.Getenv("CLIENT_SECRET") constants.DhcpSecret = os.Getenv("DHCP_SECRET") constants.HostSecret = os.Getenv("HOST_SECRET") os.Unsetenv("CLIENT_SECRET") os.Unsetenv("DHCP_SECRET") os.Unsetenv("HOST_SECRET") logger.Init( logger.SetTimeFormat(""), ) logger.AddHandler(func(record *logger.Record) { fmt.Print(record.String()) }) host := "" flag.StringVar(&host, "host", "127.0.0.1", "Server bind address") port := 0 flag.IntVar(&port, "port", 80, "Server bind port") client := "" flag.StringVar(&client, "client", "127.0.0.1", "Client address") sockPath := "" flag.StringVar(&sockPath, "sock", "", "Socket path") flag.Parse() constants.Host = strings.Split(host, "/")[0] constants.Port = port constants.Sock = sockPath constants.Client = client routr := &router.Router{} routr.Init() err = state.Init() if err != nil { return } err = routr.Run() if err != nil { return } return } ================================================ FILE: imds/server/state/state.go ================================================ package state import ( "sync" "github.com/pritunl/pritunl-cloud/imds/types" ) var Global = &Store{ State: &types.State{}, output: make(chan *types.Entry, 10000), journals: map[string]chan *types.Entry{}, } type Store struct { State *types.State output chan *types.Entry journals map[string]chan *types.Entry lock sync.RWMutex } func (s *Store) AppendOutput(entry *types.Entry) { if len(s.output) > 9000 { return } s.output <- entry } func (s *Store) GetOutput() (entries []*types.Entry) { for { select { case entry := <-s.output: entries = append(entries, entry) default: return } } } func (s *Store) AppendJournalOutput(key string, entry *types.Entry) { s.lock.Lock() output, exists := s.journals[key] if !exists { output = make(chan *types.Entry, 10000) s.journals[key] = output } s.lock.Unlock() if len(output) > 9000 { return } output <- entry } func (s *Store) GetJournals() (journals map[string][]*types.Entry) { journals = map[string][]*types.Entry{} s.lock.RLock() keys := make([]string, 0, len(s.journals)) outputs := make(map[string]chan *types.Entry) for key, output := range s.journals { keys = append(keys, key) outputs[key] = output } s.lock.RUnlock() for _, key := range keys { output := outputs[key] if output == nil { continue } var entries []*types.Entry for { select { case entry := <-output: entries = append(entries, entry) default: if len(entries) > 0 { journals[key] = entries } goto nextKey } } nextKey: } return } func Init() (err error) { return } ================================================ FILE: imds/server/utils/files.go ================================================ package utils import ( "bufio" "io/ioutil" "os" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var invalidPaths = set.NewSet("/", "", ".", "./") func Chmod(pth string, mode os.FileMode) (err error) { err = os.Chmod(pth, mode) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to chmod %s", pth), } return } return } func Exists(pth string) (exists bool, err error) { _, err = os.Stat(pth) if err == nil { exists = true return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to stat %s", pth), } return } func ExistsDir(pth string) (exists bool, err error) { stat, err := os.Stat(pth) if err == nil { exists = stat.IsDir() return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to stat %s", pth), } return } func ExistsFile(pth string) (exists bool, err error) { stat, err := os.Stat(pth) if err == nil { exists = !stat.IsDir() return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to stat %s", pth), } return } func ExistsMkdir(pth string, perm os.FileMode) (err error) { exists, err := ExistsDir(pth) if err != nil { return } if !exists { err = os.MkdirAll(pth, perm) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to mkdir %s", pth), } return } } return } func ExistsRemove(pth string) (err error) { exists, err := Exists(pth) if err != nil { return } if exists { err = os.RemoveAll(pth) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to rm %s", pth), } return } } return } func Remove(path string) (err error) { if invalidPaths.Contains(path) { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Invalid remove path '%s'", path), } return } err = os.Remove(path) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to remove '%s'", path), } return } return } func RemoveAll(path string) (err error) { if invalidPaths.Contains(path) { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Invalid remove path '%s'", path), } return } err = os.RemoveAll(path) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to remove '%s'", path), } return } return } func ContainsDir(pth string) (hasDir bool, err error) { exists, err := ExistsDir(pth) if !exists { return } entries, err := ioutil.ReadDir(pth) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "queue: Failed to read dir %s", pth), } return } for _, entry := range entries { if entry.IsDir() { hasDir = true return } } return } func Open(path string, perm os.FileMode) (file *os.File, err error) { file, err = os.OpenFile(path, os.O_RDWR|os.O_TRUNC, perm) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to open '%s'", path), } return } return } func Read(path string) (data string, err error) { dataByt, err := ioutil.ReadFile(path) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", path), } return } data = string(dataByt) return } func ReadLines(path string) (lines []string, err error) { file, err := os.Open(path) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to open '%s'", path), } return } defer func() { err = file.Close() if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", path), } return } }() lines = []string{} reader := bufio.NewReader(file) for { line, e := reader.ReadString('\n') if e != nil { break } lines = append(lines, strings.Trim(line, "\n")) } return } func Write(path string, data string, perm os.FileMode) (err error) { file, err := Open(path, perm) if err != nil { return } defer func() { err = file.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write '%s'", path), } return } }() _, err = file.WriteString(data) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write to file '%s'", path), } return } return } func Create(path string, perm os.FileMode) (file *os.File, err error) { file, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to create '%s'", path), } return } return } func CreateWrite(path string, data string, perm os.FileMode) (err error) { file, err := Create(path, perm) if err != nil { return } defer func() { err = file.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write '%s'", path), } return } }() _, err = file.WriteString(data) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write to file '%s'", path), } return } return } ================================================ FILE: imds/server/utils/misc.go ================================================ package utils func StringsContains(val []string, str string) bool { if val == nil { return false } for _, v := range val { if v == str { return true } } return false } ================================================ FILE: imds/server/utils/request.go ================================================ package utils import ( "strings" ) func StripPort(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { return hostport } n := strings.Count(hostport, ":") if n > 1 { if i := strings.IndexByte(hostport, ']'); i != -1 { return strings.TrimPrefix(hostport[:i], "[") } return hostport } return hostport[:colon] } ================================================ FILE: imds/systemd.go ================================================ package imds import ( "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/features" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) const systemdNamespaceTemplate = `[Unit] Description=Pritunl Cloud IMDS Server After=network.target [Service] Type=simple User=%s Environment="CLIENT_SECRET=%s" Environment="DHCP_SECRET=%s" Environment="HOST_SECRET=%s" ExecStart=/usr/bin/pritunl-cloud -sock=%s -host=%s -port=%d imds-server TimeoutStopSec=5 Restart=always RestartSec=3 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true NetworkNamespacePath=/var/run/netns/%s AmbientCapabilities=CAP_NET_BIND_SERVICE ` const systemdTemplate = `[Unit] Description=Pritunl Cloud IMDS Server After=network.target [Service] Type=simple User=root Environment="CLIENT_SECRET=%s" Environment="DHCP_SECRET=%s" Environment="HOST_SECRET=%s" ExecStart=/usr/sbin/ip netns exec %s /usr/bin/pritunl-cloud -sock=%s -host=%s -port=%d imds-server TimeoutStopSec=5 Restart=always RestartSec=3 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true AmbientCapabilities=CAP_NET_BIND_SERVICE ` func WriteService(vmId bson.ObjectID, namespace, clientSecret, dhcpSecret, hostSecret string, systemdNamespace bool) (err error) { unitPath := paths.GetUnitPathImds(vmId) sockPath := paths.GetImdsSockPath(vmId) if clientSecret == "" || dhcpSecret == "" || hostSecret == "" { err = &errortypes.ParseError{ errors.New("imds: Cannot start imds with empty secret"), } return } output := "" if systemdNamespace { output = fmt.Sprintf( systemdNamespaceTemplate, permission.GetUserName(vmId), clientSecret, dhcpSecret, hostSecret, sockPath, settings.Hypervisor.ImdsAddress, settings.Hypervisor.ImdsPort, namespace, ) } else { output = fmt.Sprintf( systemdTemplate, clientSecret, dhcpSecret, hostSecret, namespace, sockPath, settings.Hypervisor.ImdsAddress, settings.Hypervisor.ImdsPort, ) } err = utils.CreateWrite(unitPath, output, 0600) if err != nil { return } return } func Start(db *database.Database, virt *vm.VirtualMachine) (err error) { namespace := vm.GetNamespace(virt.Id, 0) hasSystemdNamespace := features.HasSystemdNamespace() unit := paths.GetUnitNameImds(virt.Id) logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), "systemd_unit": unit, }).Info("imds: Starting virtual machine imds server") _ = systemd.Stop(unit) err = WriteService(virt.Id, namespace, virt.ImdsClientSecret, virt.ImdsDhcpSecret, virt.ImdsHostSecret, hasSystemdNamespace) if err != nil { return } err = systemd.Reload() if err != nil { return } err = systemd.Start(unit) if err != nil { return } return } func Restart(instId bson.ObjectID) (err error) { unit := paths.GetUnitNameImds(instId) _ = systemd.Restart(unit) return } func Stop(virt *vm.VirtualMachine) (err error) { unit := paths.GetUnitNameImds(virt.Id) _ = systemd.Stop(unit) return } ================================================ FILE: imds/types/certificate.go ================================================ package types import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/certificate" ) type Certificate struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Type string `json:"type"` Key string `json:"key"` Certificate string `json:"certificate"` } func NewCertificates(certs []*certificate.Certificate) []*Certificate { datas := []*Certificate{} for _, cert := range certs { if cert == nil { continue } data := &Certificate{ Id: cert.Id, Name: cert.Name, Type: cert.Type, Key: cert.Key, Certificate: cert.Certificate, } datas = append(datas, data) } return datas } ================================================ FILE: imds/types/config.go ================================================ package types import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/utils" ) // Cannot contain maps for encode order type Config struct { Spec bson.ObjectID `json:"spec"` SpecData string `json:"spec_data" gob:"-"` ImdsHostSecret string `json:"-"` ClientIps []string `json:"client_ips"` Node *Node `json:"node"` Instance *Instance `json:"instance"` Vpc *Vpc `json:"vpc"` Subnet *Subnet `json:"subnet"` Certificates []*Certificate `json:"certificates"` Secrets []*Secret `json:"secrets"` Pods []*Pod `json:"pods"` Journals []*Journal `json:"journals"` Domains []*Domain `json:"domains"` Hash uint32 `json:"hash"` } func (c *Config) ComputeHash() (err error) { c.Hash = 0 confHash, err := utils.CrcHash(c) if err != nil { return } c.Hash = confHash return } ================================================ FILE: imds/types/constants.go ================================================ package types const ( Initializing = "initializing" ReloadingClean = "reloading_clean" ReloadingFault = "reloading_fault" Running = "running" Fault = "fault" Offline = "offline" Imaged = "imaged" ) ================================================ FILE: imds/types/domain.go ================================================ package types import ( "net" ) type Domain struct { Domain string `json:"domain"` Type string `json:"type"` Ip net.IP `json:"ip"` Target string `json:"target"` } ================================================ FILE: imds/types/instance.go ================================================ package types import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" ) type Instance struct { Id bson.ObjectID `json:"id"` Organization bson.ObjectID `json:"organization"` Zone bson.ObjectID `json:"zone"` Vpc bson.ObjectID `json:"vpc"` Subnet bson.ObjectID `json:"subnet"` CloudSubnet string `json:"cloud_subnet"` CloudVnic string `json:"cloud_vnic"` Image bson.ObjectID `json:"image"` State string `json:"state"` Timestamp time.Time `json:"timestamp"` Action string `json:"action"` Uefi bool `json:"uefi"` SecureBoot bool `json:"secure_boot"` Tpm bool `json:"tpm"` DhcpServer bool `json:"dhcp_server"` CloudType string `json:"cloud_type"` SystemKind string `json:"system_kind"` DeleteProtection bool `json:"delete_protection"` SkipSourceDestCheck bool `json:"skip_source_dest_check"` QemuVersion string `json:"qemu_version"` PublicIps []string `json:"public_ips"` PublicIps6 []string `json:"public_ips6"` PrivateIps []string `json:"private_ips"` PrivateIps6 []string `json:"private_ips6"` GatewayIps []string `json:"gateway_ips"` GatewayIps6 []string `json:"gateway_ips6"` CloudPrivateIps []string `json:"cloud_private_ips"` CloudPublicIps []string `json:"cloud_public_ips"` CloudPublicIps6 []string `json:"cloud_public_ips6"` HostIps []string `json:"host_ips"` NodePortIps []string `json:"node_port_ips"` NetworkNamespace string `json:"network_namespace"` NoPublicAddress bool `json:"no_public_address"` NoPublicAddress6 bool `json:"no_public_address6"` NoHostAddress bool `json:"no_host_address"` Node bson.ObjectID `json:"node"` Shape bson.ObjectID `json:"shape"` Name string `json:"name"` RootEnabled bool `json:"root_enabled"` Memory int `json:"memory"` Processors int `json:"processors"` Roles []string `json:"roles"` Vnc bool `json:"vnc"` Spice bool `json:"spice"` Gui bool `json:"gui"` Deployment bson.ObjectID `json:"deployment"` } func NewInstance(inst *instance.Instance) *Instance { if inst == nil { return &Instance{} } return &Instance{ Id: inst.Id, Organization: inst.Organization, Zone: inst.Zone, Vpc: inst.Vpc, Subnet: inst.Subnet, CloudSubnet: inst.CloudSubnet, CloudVnic: inst.CloudVnic, Image: inst.Image, State: inst.State, Timestamp: inst.Timestamp, Action: inst.Action, Uefi: inst.Uefi, SecureBoot: inst.SecureBoot, Tpm: inst.Tpm, DhcpServer: inst.DhcpServer, CloudType: inst.CloudType, SystemKind: inst.SystemKind, DeleteProtection: inst.DeleteProtection, SkipSourceDestCheck: inst.SkipSourceDestCheck, QemuVersion: inst.QemuVersion, PublicIps: inst.PublicIps, PublicIps6: inst.PublicIps6, PrivateIps: inst.PrivateIps, PrivateIps6: inst.PrivateIps6, GatewayIps: inst.GatewayIps, GatewayIps6: inst.GatewayIps6, CloudPrivateIps: inst.CloudPrivateIps, CloudPublicIps: inst.CloudPublicIps, CloudPublicIps6: inst.CloudPublicIps6, HostIps: inst.HostIps, NodePortIps: inst.NodePortIps, NetworkNamespace: inst.NetworkNamespace, NoPublicAddress: inst.NoPublicAddress, NoPublicAddress6: inst.NoPublicAddress6, NoHostAddress: inst.NoHostAddress, Node: inst.Node, Shape: inst.Shape, Name: inst.Name, RootEnabled: inst.RootEnabled, Memory: inst.Memory, Processors: inst.Processors, Roles: inst.Roles, Vnc: inst.Vnc, Spice: inst.Spice, Gui: inst.Gui, Deployment: inst.Deployment, } } ================================================ FILE: imds/types/journal.go ================================================ package types import ( "github.com/pritunl/pritunl-cloud/spec" ) type Journal struct { Index int32 `json:"index"` Key string `json:"key"` Type string `json:"type"` Unit string `json:"unit"` Path string `json:"path"` } func NewJournals(spc *spec.Spec) []*Journal { if spc == nil || spc.Journal == nil { return nil } jrnls := []*Journal{} for _, jrnl := range spc.Journal.Inputs { jrnls = append(jrnls, &Journal{ Index: jrnl.Index, Key: jrnl.Key, Type: jrnl.Type, Unit: jrnl.Unit, Path: jrnl.Path, }) } return jrnls } ================================================ FILE: imds/types/node.go ================================================ package types import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/node" ) type Node struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` PublicIps []string `json:"public_ips"` PublicIps6 []string `json:"public_ips6"` } func NewNode(nde *node.Node) *Node { if nde == nil { return &Node{} } return &Node{ Id: nde.Id, Name: nde.Name, PublicIps: nde.PublicIps, PublicIps6: nde.PublicIps6, } } ================================================ FILE: imds/types/pod.go ================================================ package types import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/unit" ) type Pod struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Units []*Unit `json:"units"` } type Unit struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Kind string `json:"kind"` Count int `json:"count"` PublicIps []string `json:"public_ips"` PublicIps6 []string `json:"public_ips6"` HealthyPublicIps []string `json:"healthy_public_ips"` HealthyPublicIps6 []string `json:"healthy_public_ips6"` UnhealthyPublicIps []string `json:"unhealthy_public_ips"` UnhealthyPublicIps6 []string `json:"unhealthy_public_ips6"` PrivateIps []string `json:"private_ips"` PrivateIps6 []string `json:"private_ips6"` HealthyPrivateIps []string `json:"healthy_private_ips"` HealthyPrivateIps6 []string `json:"healthy_private_ips6"` UnhealthyPrivateIps []string `json:"unhealthy_private_ips"` UnhealthyPrivateIps6 []string `json:"unhealthy_private_ips6"` CloudPublicIps []string `json:"cloud_public_ips"` CloudPublicIps6 []string `json:"cloud_public_ips6"` CloudPrivateIps []string `json:"cloud_private_ips"` HealthyCloudPublicIps []string `json:"healthy_cloud_public_ips"` HealthyCloudPublicIps6 []string `json:"healthy_cloud_public_ips6"` HealthyCloudPrivateIps []string `json:"healthy_cloud_private_ips"` UnhealthyCloudPublicIps []string `json:"unhealthy_cloud_public_ips"` UnhealthyCloudPublicIps6 []string `json:"unhealthy_cloud_public_ips6"` UnhealthyCloudPrivateIps []string `json:"unhealthy_cloud_private_ips"` } func NewPods(pods []*pod.Pod, podUnitsMap map[bson.ObjectID][]*unit.Unit, deployments map[bson.ObjectID]*deployment.Deployment) []*Pod { datas := []*Pod{} for _, pd := range pods { if pd == nil { continue } units := []*Unit{} for _, pdUnit := range podUnitsMap[pd.Id] { unit := &Unit{ Id: pdUnit.Id, Name: pdUnit.Name, Kind: pdUnit.Kind, Count: pdUnit.Count, PublicIps: []string{}, PublicIps6: []string{}, HealthyPublicIps: []string{}, HealthyPublicIps6: []string{}, UnhealthyPublicIps: []string{}, UnhealthyPublicIps6: []string{}, PrivateIps: []string{}, PrivateIps6: []string{}, HealthyPrivateIps: []string{}, HealthyPrivateIps6: []string{}, UnhealthyPrivateIps: []string{}, UnhealthyPrivateIps6: []string{}, CloudPublicIps: []string{}, CloudPublicIps6: []string{}, CloudPrivateIps: []string{}, HealthyCloudPublicIps: []string{}, HealthyCloudPublicIps6: []string{}, HealthyCloudPrivateIps: []string{}, UnhealthyCloudPublicIps: []string{}, UnhealthyCloudPublicIps6: []string{}, UnhealthyCloudPrivateIps: []string{}, } for _, unitDeplyId := range pdUnit.Deployments { deply := deployments[unitDeplyId] if deply != nil { data := deply.InstanceData if data == nil { data = &deployment.InstanceData{} } publicIps := data.PublicIps if publicIps == nil { publicIps = []string{} } publicIps6 := data.PublicIps6 if publicIps6 == nil { publicIps6 = []string{} } healthyPublicIps := []string{} unhealthyPublicIps := []string{} healthyPublicIps6 := []string{} unhealthyPublicIps6 := []string{} privateIps := data.PrivateIps if privateIps == nil { privateIps = []string{} } privateIps6 := data.PrivateIps6 if privateIps6 == nil { privateIps6 = []string{} } healthyPrivateIps := []string{} unhealthyPrivateIps := []string{} healthyPrivateIps6 := []string{} unhealthyPrivateIps6 := []string{} cloudPublicIps := data.CloudPublicIps if cloudPublicIps == nil { cloudPublicIps = []string{} } cloudPublicIps6 := data.CloudPublicIps6 if cloudPublicIps6 == nil { cloudPublicIps6 = []string{} } cloudPrivateIps := data.CloudPrivateIps if cloudPrivateIps == nil { cloudPrivateIps = []string{} } healthyCloudPublicIps := []string{} unhealthyCloudPublicIps := []string{} healthyCloudPublicIps6 := []string{} unhealthyCloudPublicIps6 := []string{} healthyCloudPrivateIps := []string{} unhealthyCloudPrivateIps := []string{} if deply.IsHealthy() { healthyPublicIps = publicIps healthyPublicIps6 = publicIps6 healthyPrivateIps = privateIps healthyPrivateIps6 = privateIps6 healthyCloudPublicIps = cloudPublicIps healthyCloudPublicIps6 = cloudPublicIps6 healthyCloudPrivateIps = cloudPrivateIps } else { unhealthyPublicIps = publicIps unhealthyPublicIps6 = publicIps6 unhealthyPrivateIps = privateIps unhealthyPrivateIps6 = privateIps6 unhealthyCloudPublicIps = cloudPublicIps unhealthyCloudPublicIps6 = cloudPublicIps6 unhealthyCloudPrivateIps = cloudPrivateIps } unit.PublicIps = append( unit.PublicIps, publicIps...) unit.PublicIps6 = append( unit.PublicIps6, publicIps6...) unit.HealthyPublicIps = append( unit.HealthyPublicIps, healthyPublicIps...) unit.HealthyPublicIps6 = append( unit.HealthyPublicIps6, healthyPublicIps6...) unit.UnhealthyPublicIps = append( unit.UnhealthyPublicIps, unhealthyPublicIps...) unit.UnhealthyPublicIps6 = append( unit.UnhealthyPublicIps6, unhealthyPublicIps6...) unit.PrivateIps = append( unit.PrivateIps, privateIps...) unit.PrivateIps6 = append( unit.PrivateIps6, privateIps6...) unit.HealthyPrivateIps = append( unit.HealthyPrivateIps, healthyPrivateIps...) unit.HealthyPrivateIps6 = append( unit.HealthyPrivateIps6, healthyPrivateIps6...) unit.UnhealthyPrivateIps = append( unit.UnhealthyPrivateIps, unhealthyPrivateIps...) unit.UnhealthyPrivateIps6 = append( unit.UnhealthyPrivateIps6, unhealthyPrivateIps6...) unit.CloudPublicIps = append( unit.CloudPublicIps, cloudPublicIps...) unit.HealthyCloudPublicIps = append( unit.HealthyCloudPublicIps, healthyCloudPublicIps..., ) unit.UnhealthyCloudPublicIps = append( unit.UnhealthyCloudPublicIps, unhealthyCloudPublicIps..., ) unit.CloudPublicIps6 = append( unit.CloudPublicIps6, cloudPublicIps6...) unit.HealthyCloudPublicIps6 = append( unit.HealthyCloudPublicIps6, healthyCloudPublicIps6..., ) unit.UnhealthyCloudPublicIps6 = append( unit.UnhealthyCloudPublicIps6, unhealthyCloudPublicIps6..., ) unit.CloudPrivateIps = append( unit.CloudPrivateIps, cloudPrivateIps...) unit.HealthyCloudPrivateIps = append( unit.HealthyCloudPrivateIps, healthyCloudPrivateIps..., ) unit.UnhealthyCloudPrivateIps = append( unit.UnhealthyCloudPrivateIps, unhealthyCloudPrivateIps..., ) } } units = append(units, unit) } data := &Pod{ Id: pd.Id, Name: pd.Name, Units: units, } datas = append(datas, data) } return datas } ================================================ FILE: imds/types/secret.go ================================================ package types import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/secret" ) type Secret struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Type string `json:"type"` Key string `json:"key"` Value string `json:"value"` Data string `json:"data"` Region string `json:"region"` PublicKey string `json:"public_key"` PrivateKey string `json:"private_key"` } func NewSecrets(secrs []*secret.Secret) []*Secret { datas := []*Secret{} for _, secr := range secrs { if secr == nil { continue } data := &Secret{ Id: secr.Id, Name: secr.Name, Type: secr.Type, Key: secr.Key, Value: secr.Value, Data: secr.Data, Region: secr.Region, PublicKey: secr.PublicKey, PrivateKey: secr.PrivateKey, } datas = append(datas, data) } return datas } ================================================ FILE: imds/types/state.go ================================================ package types import ( "net" "time" "github.com/pritunl/pritunl-cloud/telemetry" ) type State struct { Hash uint32 `json:"hash"` Status string `json:"status"` Memory float64 `json:"memory"` HugePages float64 `json:"hugepages"` Load1 float64 `json:"load1"` Load5 float64 `json:"load5"` Load15 float64 `json:"load15"` DhcpIface string `json:"dhcp_iface"` DhcpIface6 string `json:"dhcp_iface6"` DhcpIp *net.IPNet `json:"dhcp_ip"` DhcpIp6 *net.IPNet `json:"dhcp_ip6"` DhcpGateway net.IP `json:"dhcp_gateway"` Updates []*telemetry.Update `json:"updates"` Timestamp time.Time `json:"timestamp"` Output []*Entry `json:"output,omitempty"` Journals map[string][]*Entry `json:"journals,omitempty"` } func (s *State) Final() bool { if s.Status == Imaged { return true } return false } func (s *State) Copy() *State { return &State{ Hash: s.Hash, Status: s.Status, Memory: s.Memory, HugePages: s.HugePages, Load1: s.Load1, Load5: s.Load5, Load15: s.Load15, DhcpIface: s.DhcpIface, DhcpIface6: s.DhcpIface6, DhcpIp: s.DhcpIp, DhcpIp6: s.DhcpIp6, DhcpGateway: s.DhcpGateway, Updates: s.Updates, Timestamp: s.Timestamp, } } type Entry struct { Timestamp time.Time `json:"t"` Level int32 `json:"l"` Message string `json:"m"` } const ( Error = 3 Info = 5 ) ================================================ FILE: imds/types/vpc.go ================================================ package types import ( "fmt" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/vpc" ) type Vpc struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` VpcId int `json:"vpc_id"` Network string `json:"network"` Network6 string `json:"network6"` Subnets []*Subnet `json:"subnets"` Routes []*Route `json:"routes"` } type Subnet struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Network string `json:"network"` } func (s *Subnet) String() string { return s.Network } type Route struct { Destination string `json:"destination"` Target string `json:"target"` } func (r *Route) String() string { return fmt.Sprintf("%s via %s", r.Destination, r.Target) } func NewSubnet(subnet *vpc.Subnet) *Subnet { if subnet == nil { return &Subnet{} } return &Subnet{ Id: subnet.Id, Name: subnet.Name, Network: subnet.Network, } } func NewRoute(subnet *vpc.Route) *Route { if subnet == nil { return &Route{} } return &Route{ Destination: subnet.Destination, Target: subnet.Target, } } func NewVpc(vpc *vpc.Vpc) *Vpc { if vpc == nil { return &Vpc{} } vpc.Json() subnets := []*Subnet{} if vpc.Subnets != nil { for _, subnet := range vpc.Subnets { subnets = append(subnets, NewSubnet(subnet)) } } routes := []*Route{} if vpc.Routes != nil { for _, route := range vpc.Routes { routes = append(routes, NewRoute(route)) } } return &Vpc{ Id: vpc.Id, Name: vpc.Name, VpcId: vpc.VpcId, Network: vpc.Network, Network6: vpc.Network6, Subnets: subnets, Routes: routes, } } ================================================ FILE: info/instance.go ================================================ package info import ( "fmt" "sort" "strings" "time" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/set" ) func NewInstance(stat *state.State, inst *instance.Instance) ( inf *instance.Info) { inf = &instance.Info{ Timestamp: time.Now().Add(time.Duration( utils.RandInt(0, 10)) * time.Second), Disks: []string{}, FirewallRules: map[string]string{}, Authorities: []string{}, CloudSubnets: []*node.CloudSubnet{}, } nde := stat.Node() if inst.Node != nde.Id { return } inf.Node = nde.Name if len(nde.PublicIps) > 0 { inf.NodePublicIp = nde.PublicIps[0] } inf.Iscsi = nde.Iscsi inf.Isos = nde.LocalIsos inf.CloudSubnets = nde.GetCloudSubnetsName() if nde.UsbPassthrough { inf.UsbDevices = nde.UsbDevices } if nde.PciDevices != nil { inf.PciDevices = nde.PciDevices } if nde.InstanceDrives != nil { inf.DriveDevices = nde.InstanceDrives } dc := stat.NodeDatacenter() if dc != nil { inf.Mtu = dc.GetInstanceMtu() } instDisks := stat.GetInstaceDisks(inst.Id) for _, dsk := range instDisks { inf.Disks = append( inf.Disks, fmt.Sprintf("%s: %s", dsk.Index, dsk.Name), ) } firewallRulesKeys := []string{} firewallRules := map[string]set.Set{} namespaces := stat.GetInstanceNamespaces(inst.Id) firewalls := stat.Firewalls() for _, namespace := range namespaces { for _, rule := range firewalls[namespace] { key := rule.Protocol if rule.Port != "" { key += ":" + rule.Port } rules := firewallRules[key] if rules == nil { rules = set.NewSet() firewallRules[key] = rules firewallRulesKeys = append( firewallRulesKeys, key, ) } for _, sourceIp := range rule.SourceIps { rules.Add(sourceIp) } } } sort.Strings(firewallRulesKeys) for _, key := range firewallRulesKeys { rules := firewallRules[key] vals := []string{} for rule := range rules.Iter() { vals = append(vals, rule.(string)) } sort.Strings(vals) inf.FirewallRules[key] = strings.Join(vals, ", ") } authrs := stat.GetInstaceAuthorities(inst.Organization, inst.Roles) for _, authr := range authrs { inf.Authorities = append(inf.Authorities, authr.Name) } sort.Strings(inf.Authorities) return } ================================================ FILE: instance/constants.go ================================================ package instance import ( "github.com/dropbox/godropbox/container/set" ) const ( Starting = "starting" Running = "running" Stopped = "stopped" Failed = "failed" Updating = "updating" Provisioning = "provisioning" Bridge = "bridge" Vxlan = "vxlan" Start = "start" Stop = "stop" Cleanup = "cleanup" Restart = "restart" Destroy = "destroy" Linux = "linux" LinuxLegacy = "linux_legacy" BSD = "bsd" AlpineLinux = "alpinelinux" ArchLinux = "archlinux" RedHat = "redhat" Fedora = "fedora" Ubuntu = "ubuntu" FreeBSD = "freebsd" HostPath = "host_path" ) var ( ValidStates = set.NewSet( Starting, Running, Stopped, Failed, Updating, Provisioning, Bridge, Vxlan, ) ValidActions = set.NewSet( Start, Stop, Cleanup, Restart, Destroy, ) ValidCloudTypes = set.NewSet( Linux, LinuxLegacy, BSD, ) ValidSystemKinds = set.NewSet( AlpineLinux, ArchLinux, RedHat, Fedora, Ubuntu, FreeBSD, ) ) ================================================ FILE: instance/errortypes.go ================================================ package instance import ( "github.com/dropbox/godropbox/errors" ) type VncDialError struct { errors.DropboxError } ================================================ FILE: instance/instance.go ================================================ package instance import ( "fmt" "math/rand" "net/http" "regexp" "strconv" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gorilla/websocket" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/iscsi" "github.com/pritunl/pritunl-cloud/iso" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/telemetry" "github.com/pritunl/pritunl-cloud/tpm" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) var scriptReg = regexp.MustCompile("^#!") type Instance struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Organization bson.ObjectID `bson:"organization" json:"organization"` UnixId int `bson:"unix_id" json:"unix_id"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Zone bson.ObjectID `bson:"zone" json:"zone"` Vpc bson.ObjectID `bson:"vpc" json:"vpc"` Subnet bson.ObjectID `bson:"subnet" json:"subnet"` Created time.Time `bson:"created" json:"created"` Guest *GuestData `bson:"guest,omitempty" json:"guest"` CloudSubnet string `bson:"cloud_subnet" json:"cloud_subnet"` CloudVnic string `bson:"cloud_vnic" json:"cloud_vnic"` CloudVnicAttach string `bson:"cloud_vnic_attach" json:"cloud_vnic_attach"` Image bson.ObjectID `bson:"image" json:"image"` ImageBacking bool `bson:"image_backing" json:"image_backing"` DiskType string `bson:"disk_type" json:"disk_type"` DiskPool bson.ObjectID `bson:"disk_pool" json:"disk_pool"` Status string `bson:"-" json:"status"` StatusInfo *StatusInfo `bson:"status_info,omitempty" json:"status_info"` Uptime string `bson:"-" json:"uptime"` State string `bson:"state" json:"state"` Action string `bson:"action" json:"action"` PublicMac string `bson:"-" json:"public_mac"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Restart bool `bson:"restart" json:"restart"` RestartReason string `bson:"restart_reason" json:"restart_reason"` RestartBlockIp bool `bson:"restart_block_ip" json:"restart_block_ip"` Uefi bool `bson:"uefi" json:"uefi"` SecureBoot bool `bson:"secure_boot" json:"secure_boot"` Tpm bool `bson:"tpm" json:"tpm"` TpmSecret string `bson:"tpm_secret" json:"-"` DhcpServer bool `bson:"dhcp_server" json:"dhcp_server"` CloudType string `bson:"cloud_type" json:"cloud_type"` CloudScript string `bson:"cloud_script" json:"cloud_script"` SystemKind string `bson:"system_kind" json:"system_kind"` DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` SkipSourceDestCheck bool `bson:"skip_source_dest_check" json:"skip_source_dest_check"` QemuVersion string `bson:"qemu_version" json:"qemu_version"` PublicIps []string `bson:"public_ips" json:"public_ips"` PublicIps6 []string `bson:"public_ips6" json:"public_ips6"` PrivateIps []string `bson:"private_ips" json:"private_ips"` PrivateIps6 []string `bson:"private_ips6" json:"private_ips6"` GatewayIps []string `bson:"gateway_ips" json:"gateway_ips"` GatewayIps6 []string `bson:"gateway_ips6" json:"gateway_ips6"` CloudPrivateIps []string `bson:"cloud_private_ips" json:"cloud_private_ips"` CloudPublicIps []string `bson:"cloud_public_ips" json:"cloud_public_ips"` CloudPublicIps6 []string `bson:"cloud_public_ips6" json:"cloud_public_ips6"` HostIps []string `bson:"host_ips" json:"host_ips"` NodePortIps []string `bson:"node_port_ips" json:"node_port_ips"` NodePorts []*nodeport.Mapping `bson:"node_ports,omitempty" json:"node_ports"` DhcpIp string `bson:"dhcp_ip" json:"dhcp_ip"` DhcpIp6 string `bson:"dhcp_ip6" json:"dhcp_ip6"` NetworkNamespace string `bson:"network_namespace" json:"network_namespace"` NoPublicAddress bool `bson:"no_public_address" json:"no_public_address"` NoPublicAddress6 bool `bson:"no_public_address6" json:"no_public_address6"` NoHostAddress bool `bson:"no_host_address" json:"no_host_address"` Node bson.ObjectID `bson:"node" json:"node"` Shape bson.ObjectID `bson:"shape" json:"shape"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` RootEnabled bool `bson:"root_enabled" json:"root_enabled"` RootPasswd string `bson:"root_passwd" json:"root_passwd"` InitDiskSize int `bson:"init_disk_size" json:"init_disk_size"` Memory int `bson:"memory" json:"memory"` Processors int `bson:"processors" json:"processors"` Roles []string `bson:"roles" json:"roles"` Isos []*iso.Iso `bson:"isos,omitempty" json:"isos"` UsbDevices []*usb.Device `bson:"usb_devices,omitempty" json:"usb_devices"` PciDevices []*pci.Device `bson:"pci_devices,omitempty" json:"pci_devices"` DriveDevices []*drive.Device `bson:"drive_devices,omitempty" json:"drive_devices"` IscsiDevices []*iscsi.Device `bson:"iscsi_devices,omitempty" json:"iscsi_devices"` Mounts []*Mount `bson:"mounts,omitempty" json:"mounts"` Vnc bool `bson:"vnc" json:"vnc"` VncPassword string `bson:"vnc_password" json:"vnc_password"` VncDisplay int `bson:"vnc_display" json:"vnc_display"` Spice bool `bson:"spice" json:"spice"` SpicePassword string `bson:"spice_password" json:"spice_password"` SpicePort int `bson:"spice_port" json:"spice_port"` Gui bool `bson:"gui" json:"gui"` Deployment bson.ObjectID `bson:"deployment" json:"deployment"` Info *Info `bson:"info,omitempty" json:"info"` Virt *vm.VirtualMachine `bson:"-" json:"-"` curVpc bson.ObjectID `bson:"-" json:"-"` curSubnet bson.ObjectID `bson:"-" json:"-"` curDeleteProtection bool `bson:"-" json:"-"` curAction string `bson:"-" json:"-"` curNoPublicAddress bool `bson:"-" json:"-"` curNoHostAddress bool `bson:"-" json:"-"` curNodePorts map[bson.ObjectID]*nodeport.Mapping `bson:"-" json:"-"` removedNodePorts []bson.ObjectID `bson:"-" json:"-"` newId bool `bson:"-" json:"-"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` Zone bson.ObjectID `bson:"zone" json:"zone"` Vpc bson.ObjectID `bson:"vpc" json:"vpc"` Subnet bson.ObjectID `bson:"subnet" json:"subnet"` Node bson.ObjectID `bson:"node" json:"node"` } type Mount struct { Name string `bson:"name" json:"name"` Type string `bson:"type" json:"type"` Path string `bson:"path" json:"path"` HostPath string `bson:"host_path" json:"host_path"` } type StatusInfo struct { DownloadProgress int `bson:"download_progress,omitempty" json:"download_progress"` DownloadSpeed float64 `bson:"download_speed,omitempty" json:"download_speed"` } type GuestData struct { Status string `bson:"status" json:"status"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Heartbeat time.Time `bson:"heartbeat" json:"heartbeat"` Memory float64 `bson:"memory" json:"memory"` HugePages float64 `bson:"hugepages" json:"hugepages"` Load1 float64 `bson:"load1" json:"load1"` Load5 float64 `bson:"load5" json:"load5"` Load15 float64 `bson:"load15" json:"load15"` Updates []*telemetry.Update `bson:"updates" json:"updates"` } type Info struct { Node string `bson:"node" json:"node"` NodePublicIp string `bson:"node_public_ip" json:"node_public_ip"` Mtu int `bson:"mtu" json:"mtu"` Iscsi bool `bson:"iscsi" json:"iscsi"` Disks []string `bson:"disks" json:"disks"` FirewallRules map[string]string `bson:"firewall_rules" json:"firewall_rules"` Authorities []string `bson:"authorities" json:"authorities"` Isos []*iso.Iso `bson:"isos" json:"isos"` UsbDevices []*usb.Device `bson:"usb_devices" json:"usb_devices"` PciDevices []*pci.Device `bson:"pci_devices" json:"pci_devices"` DriveDevices []*drive.Device `bson:"drive_devices" json:"drive_devices"` CloudSubnets []*node.CloudSubnet `bson:"cloud_subnets" json:"cloud_subnets"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` } func (i *Instance) GenerateId() (err error) { if !i.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("instance: Instance already exists"), } return } i.newId = true i.Id = bson.NewObjectID() return } func (i *Instance) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { i.Name = utils.FilterName(i.Name) if i.Action == "" { i.Action = Start } if i.Action != Start { i.Restart = false i.RestartReason = "" i.RestartBlockIp = false } if i.State != "" && !ValidStates.Contains(i.State) { errData = &errortypes.ErrorData{ Error: "invalid_state", Message: "Invalid instance state", } return } if !ValidActions.Contains(i.Action) { errData = &errortypes.ErrorData{ Error: "invalid_action", Message: "Invalid instance action", } return } if i.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "organization_required", Message: "Missing required organization", } return } if i.Zone.IsZero() { errData = &errortypes.ErrorData{ Error: "zone_required", Message: "Missing required zone", } return } if i.Node.IsZero() { if i.Shape.IsZero() { errData = &errortypes.ErrorData{ Error: "node_required", Message: "Missing required node", } return } shpe, e := shape.Get(db, i.Shape) if e != nil { err = e return } if !shpe.Flexible { i.Processors = shpe.Processors i.Memory = shpe.Memory } nde, e := shpe.FindNode(db, i.Processors, i.Memory) if e != nil { err = e return } i.Node = nde.Id i.DiskType = shpe.DiskType i.DiskPool = shpe.DiskPool } if i.Vpc.IsZero() { errData = &errortypes.ErrorData{ Error: "vpc_required", Message: "Missing required VPC", } return } if i.UnixId == 0 { i.GenerateUnixId() } switch i.DiskType { case disk.Lvm: if i.DiskPool.IsZero() { errData = &errortypes.ErrorData{ Error: "pool_required", Message: "Missing required disk pool", } return } break case disk.Qcow2, "": i.DiskType = disk.Qcow2 } vc, err := vpc.Get(db, i.Vpc) if err != nil { return } if i.Subnet.IsZero() { errData = &errortypes.ErrorData{ Error: "vpc_subnet_required", Message: "Missing required VPC subnet", } return } sub := vc.GetSubnet(i.Subnet) if sub == nil { errData = &errortypes.ErrorData{ Error: "vpc_subnet_missing", Message: "VPC subnet does not exist", } return } if i.InitDiskSize != 0 && i.InitDiskSize < 10 { errData = &errortypes.ErrorData{ Error: "init_disk_size_invalid", Message: "Disk size below minimum", } return } if i.Memory < 256 { i.Memory = 256 } if i.Processors < 1 { i.Processors = 1 } if i.Roles == nil { i.Roles = []string{} } if i.PublicIps == nil { i.PublicIps = []string{} } if i.PublicIps6 == nil { i.PublicIps6 = []string{} } if i.PrivateIps == nil { i.PrivateIps = []string{} } if i.PrivateIps6 == nil { i.PrivateIps6 = []string{} } if i.CloudType == "" { i.CloudType = Linux } if !ValidCloudTypes.Contains(i.CloudType) { errData = &errortypes.ErrorData{ Error: "invalid_cloud_type", Message: "Invalid cloud init type", } return } if i.SystemKind != "" && !ValidSystemKinds.Contains(i.SystemKind) { errData = &errortypes.ErrorData{ Error: "invalid_system_kind", Message: "Instance system kind invalid", } return } if i.CloudScript != "" && !scriptReg.MatchString(i.CloudScript) { errData = &errortypes.ErrorData{ Error: "invalid_cloud_script", Message: "Startup script missing shebang on first line", } return } if i.TpmSecret == "" { i.TpmSecret, err = tpm.GenerateSecret() if err != nil { return } } nde, err := node.Get(db, i.Node) if err != nil { return } if i.Datacenter == bson.NilObjectID { i.Datacenter = nde.Datacenter } if i.Datacenter != vc.Datacenter { errData = &errortypes.ErrorData{ Error: "vpc_invalid_datacenter", Message: "VPC must be in same datacenter as instance", } return } if i.CloudSubnet != "" { match := false for _, subnet := range nde.CloudSubnets { if subnet == i.CloudSubnet { match = true break } } if !match { errData = &errortypes.ErrorData{ Error: "cloud_subnet_invalid", Message: "Invalid Cloud subnet", } return } } if i.RootEnabled { if i.RootPasswd == "" { i.RootPasswd, err = utils.RandPasswd(8) if err != nil { return } } } else { i.RootPasswd = "" } if i.Isos == nil { i.Isos = []*iso.Iso{} } else { for _, is := range i.Isos { is.Name = utils.FilterRelPath(is.Name) } } if i.UsbDevices == nil { i.UsbDevices = []*usb.Device{} } else { for _, device := range i.UsbDevices { device.Name = "" device.Vendor = usb.FilterId(device.Vendor) device.Product = usb.FilterId(device.Product) device.Bus = usb.FilterAddr(device.Bus) device.Address = usb.FilterAddr(device.Address) if (device.Vendor == "" || device.Product == "") && (device.Bus == "" || device.Address == "") { errData = &errortypes.ErrorData{ Error: "usb_device_invalid", Message: "Invalid USB device", } return } available, e := usb.Available(db, i.Id, i.Node, device) if e != nil { err = e return } if !available { errData = &errortypes.ErrorData{ Error: "usb_device_unavailable", Message: "USB device in use by another instance", } return } } } if i.PciDevices == nil { i.PciDevices = []*pci.Device{} } else { for _, device := range i.PciDevices { device.Name = "" device.Class = "" device.Driver = "" if !pci.CheckSlot(device.Slot) { errData = &errortypes.ErrorData{ Error: "pci_device_slot_invalid", Message: "Invalid PCI slot", } return } } } instanceDrives := set.NewSet() nodeInstanceDrives := nde.InstanceDrives for _, device := range nodeInstanceDrives { instanceDrives.Add(device.Id) } if i.DriveDevices == nil { i.DriveDevices = []*drive.Device{} } else { for _, device := range i.DriveDevices { if !instanceDrives.Contains(device.Id) { errData = &errortypes.ErrorData{ Error: "drive_invalid", Message: "Instance drive not available", } return } } } iscsiDevices := []*iscsi.Device{} if i.IscsiDevices != nil { for _, device := range i.IscsiDevices { if device.Uri == "" { continue } errData, err = device.Parse() if err != nil || errData != nil { return } iscsiDevices = append(iscsiDevices, device) } } i.IscsiDevices = iscsiDevices newMounts := []*Mount{} for _, mount := range i.Mounts { if mount.Name == "" && mount.HostPath == "" { continue } mount.Name = utils.FilterNameCmd(mount.Name) mount.Type = HostPath mount.Path = utils.FilterPath(mount.Path) mount.HostPath = utils.FilterPath(mount.HostPath) if mount.Name == "" { errData = &errortypes.ErrorData{ Error: "missing_mount_name", Message: "Missing required mount name", } return } if mount.HostPath == "" { errData = &errortypes.ErrorData{ Error: "mount_host_path_invalid", Message: "Mount host path invalid", } return } newMounts = append(newMounts, mount) } i.Mounts = newMounts if i.Vnc { if i.VncPassword == "" { i.VncPassword, err = utils.RandPasswd(32) if err != nil { return } } } else { i.VncPassword = "" } if i.Spice { if i.SpicePassword == "" { i.SpicePassword, err = utils.RandPasswd(32) if err != nil { return } } } else { i.SpicePassword = "" } externalNodePorts := set.NewSet() for _, mapping := range i.NodePorts { extPortKey := fmt.Sprintf("%s:%d", mapping.Protocol, mapping.ExternalPort) if !mapping.Delete { if externalNodePorts.Contains(extPortKey) { errData = &errortypes.ErrorData{ Error: "node_port_external_duplicate", Message: "Duplicate external node port", } return } } externalNodePorts.Add(extPortKey) errData, err = mapping.Validate(db) if err != nil { return } available, e := nodeport.Available(db, i.Datacenter, i.Organization, mapping.Protocol, mapping.ExternalPort) if e != nil { err = e return } if !available { errData = &errortypes.ErrorData{ Error: "node_port_unavailable", Message: "External node port is unavailable", } return } } return } func (i *Instance) GenerateUnixId() { i.UnixId = rand.Intn(55500) + 10000 } func (i *Instance) InitUnixId(db *database.Database) (err error) { if i.UnixId != 0 { return } i.GenerateUnixId() err = i.CommitFields(db, set.NewSet("unix_id")) if err != nil { return } return } func (i *Instance) GenerateSpicePort() { // Spice 15000 - 19999 i.SpicePort = rand.Intn(4999) + 15000 } func (i *Instance) InitSpicePort(db *database.Database) (err error) { if i.SpicePort != 0 { return } i.GenerateSpicePort() coll := db.Instances() for n := 0; n < 10000; n++ { err = coll.CommitFields(i.Id, i, set.NewSet("spice_port")) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { i.GenerateSpicePort() err = nil continue } return } event.PublishDispatch(db, "instance.change") return } err = &errortypes.WriteError{ errors.New("instance: Failed to commit unique spice port"), } return } func (i *Instance) GenerateVncDisplay() { // VNC 10001 - 14999 (+5900) // VNC WebSocket 20001 - 24999 (+15900) i.VncDisplay = rand.Intn(4999) + 4101 } func (i *Instance) InitVncDisplay(db *database.Database) (err error) { if i.VncDisplay != 0 { return } i.GenerateVncDisplay() coll := db.Instances() for n := 0; n < 10000; n++ { err = coll.CommitFields(i.Id, i, set.NewSet("vnc_display")) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { i.GenerateVncDisplay() err = nil continue } return } event.PublishDispatch(db, "instance.change") return } err = &errortypes.WriteError{ errors.New("instance: Failed to commit unique vnc port"), } return } func (i *Instance) Format() { } func (i *Instance) Json(short bool) { switch i.Action { case Start: if i.Restart || i.RestartBlockIp { i.Status = "Restart Required" if i.RestartReason != "" { i.Status += fmt.Sprintf(" (%s)", i.RestartReason) } } else { switch i.State { case vm.Starting: i.Status = "Starting" break case vm.Running: i.Status = "Running" break case vm.Stopped: i.Status = "Starting" break case vm.Failed: i.Status = "Starting" break case vm.Updating: i.Status = "Updating" break case vm.Provisioning: i.Status = "Provisioning" break case "": i.Status = "Provisioning" break } } break case Cleanup: switch i.State { case vm.Starting: i.Status = "Stopping" break case vm.Running: i.Status = "Stopping" break case vm.Stopped: i.Status = "Stopping" break case vm.Failed: i.Status = "Stopping" break case vm.Updating: i.Status = "Updating" break case vm.Provisioning: i.Status = "Stopping" break case "": i.Status = "Stopping" break } break case Stop: switch i.State { case vm.Starting: i.Status = "Stopping" break case vm.Running: i.Status = "Stopping" break case vm.Stopped: i.Status = "Stopped" break case vm.Failed: i.Status = "Failed" break case vm.Updating: i.Status = "Updating" break case vm.Provisioning: i.Status = "Stopped" break case "": i.Status = "Stopped" break } break case Restart: i.Status = "Restarting" break case Destroy: i.Status = "Destroying" break } if !i.IsActive() && i.Guest != nil { i.Guest.Timestamp = time.Time{} i.Guest.Heartbeat = time.Time{} i.Guest.Memory = 0 i.Guest.HugePages = 0 i.Guest.Load1 = 0 i.Guest.Load5 = 0 i.Guest.Load15 = 0 i.Guest.Updates = []*telemetry.Update{} } i.PublicMac = vm.GetMacAddrExternal(i.Id, i.Vpc) if i.Timestamp.IsZero() || !i.IsActive() { i.Uptime = "" } else { if short { i.Uptime = systemd.FormatUptimeShort(i.Timestamp) } else { i.Uptime = systemd.FormatUptime(i.Timestamp) } } if i.IscsiDevices != nil { for _, device := range i.IscsiDevices { device.Json() } } } func (i *Instance) IsActive() bool { return i.Action == Start || i.State == vm.Running || i.State == vm.Starting || i.State == vm.Provisioning } func (i *Instance) IsIpv6Only() bool { return (node.Self.NetworkMode == node.Disabled || i.NoPublicAddress) && (node.Self.NetworkMode6 != node.Disabled && !i.NoPublicAddress6) && (node.Self.NoHostNetwork || i.NoHostAddress) } func (i *Instance) PreCommit() { i.curVpc = i.Vpc i.curSubnet = i.Subnet i.curDeleteProtection = i.DeleteProtection i.curAction = i.Action i.curNoPublicAddress = i.NoPublicAddress i.curNoHostAddress = i.NoHostAddress nodePortMap := map[bson.ObjectID]*nodeport.Mapping{} for _, mapping := range i.NodePorts { nodePortMap[mapping.NodePort] = mapping } i.curNodePorts = nodePortMap } func (i *Instance) UpsertNodePorts(newNodePorts []*nodeport.Mapping) { if len(i.NodePorts) == 0 { i.NodePorts = newNodePorts return } processed := make(map[int]bool) newMappings := []*nodeport.Mapping{} for _, newMapping := range newNodePorts { matched := false if newMapping.ExternalPort != 0 { for x, curMapping := range i.NodePorts { if curMapping.Protocol == newMapping.Protocol && curMapping.InternalPort == newMapping.InternalPort && curMapping.ExternalPort == newMapping.ExternalPort { newMapping.NodePort = curMapping.NodePort newMappings = append(newMappings, newMapping) processed[x] = true matched = true break } } } else { for x, curMapping := range i.NodePorts { if curMapping.Protocol == newMapping.Protocol && curMapping.InternalPort == newMapping.InternalPort && !processed[x] { newMapping.NodePort = curMapping.NodePort newMapping.ExternalPort = curMapping.ExternalPort newMappings = append(newMappings, newMapping) processed[x] = true matched = true break } } } if !matched { newMappings = append(newMappings, newMapping) } } i.NodePorts = newMappings } func (i *Instance) SyncNodePorts(db *database.Database) (err error) { newNodePorts := []*nodeport.Mapping{} newNodePortIds := set.NewSet() externalPorts := set.NewSet() if i.curNodePorts == nil { i.curNodePorts = map[bson.ObjectID]*nodeport.Mapping{} } for _, mapping := range i.NodePorts { if !mapping.NodePort.IsZero() { curMapping := i.curNodePorts[mapping.NodePort] if curMapping == nil { continue } newNodePortIds.Add(curMapping.NodePort) if mapping.Delete { i.removedNodePorts = append( i.removedNodePorts, curMapping.NodePort) continue } curMapping.InternalPort = mapping.InternalPort mapping = curMapping } var errData *errortypes.ErrorData var ndePort *nodeport.NodePort if mapping.ExternalPort != 0 { ndePort, err = nodeport.GetPort(db, i.Datacenter, i.Organization, mapping.Protocol, mapping.ExternalPort) if err != nil { if _, ok := err.(*database.NotFoundError); ok { ndePort = nil err = nil } else { return } } } if ndePort == nil { ndePort, errData, err = nodeport.New(db, i.Datacenter, i.Organization, mapping.Protocol, mapping.ExternalPort) if err != nil { return } if errData != nil { err = errData.GetError() return } } mapping.NodePort = ndePort.Id mapping.ExternalPort = ndePort.Port extPortKey := fmt.Sprintf("%s:%d", mapping.Protocol, mapping.ExternalPort) if externalPorts.Contains(extPortKey) { continue } externalPorts.Add(extPortKey) newNodePorts = append(newNodePorts, mapping) } i.NodePorts = newNodePorts for _, mapping := range i.curNodePorts { if newNodePortIds.Contains(mapping.NodePort) { continue } i.removedNodePorts = append(i.removedNodePorts, mapping.NodePort) } return } func (i *Instance) PostCommit(db *database.Database) ( dskChange bool, err error) { err = i.SyncNodePorts(db) if err != nil { return } if (!i.curVpc.IsZero() && i.curVpc != i.Vpc) || (!i.curSubnet.IsZero() && i.curSubnet != i.Subnet) { i.DhcpIp = "" i.DhcpIp6 = "" err = vpc.RemoveInstanceIp(db, i.Id, i.curVpc) if err != nil { return } } if i.curDeleteProtection != i.DeleteProtection { dskChange = true err = disk.SetDeleteProtection(db, i.Id, i.DeleteProtection) if err != nil { return } } if i.curAction != i.Action && (i.Action == Stop || i.Action == Start || i.Action == Restart) { i.Restart = false i.RestartBlockIp = false } if i.curNoPublicAddress != i.NoPublicAddress && i.NoPublicAddress { err = block.RemoveInstanceIpsType(db, i.Id, block.External) if err != nil { return } } if i.curNoHostAddress != i.NoHostAddress && i.NoHostAddress { err = block.RemoveInstanceIpsType(db, i.Id, block.Host) if err != nil { return } } return } func (i *Instance) Cleanup(db *database.Database) (err error) { for _, mapping := range i.NodePorts { ndePort, e := nodeport.Get(db, mapping.NodePort) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil continue } return } err = ndePort.Sync(db) if err != nil { return } } for _, ndePortId := range i.removedNodePorts { ndePort, e := nodeport.Get(db, ndePortId) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil continue } return } err = ndePort.Sync(db) if err != nil { return } } return } func (i *Instance) Commit(db *database.Database) (err error) { coll := db.Instances() err = coll.Commit(i.Id, i) if err != nil { return } return } func (i *Instance) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Instances() if fields.Contains("unix_id") { for n := 0; n < 10000; n++ { err = coll.CommitFields(i.Id, i, fields) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { i.GenerateUnixId() err = nil continue } return } return } err = &errortypes.WriteError{ errors.New("instance: Failed to commit unique unix id"), } return } else { err = coll.CommitFields(i.Id, i, fields) if err != nil { return } } return } func (i *Instance) Insert(db *database.Database) (err error) { coll := db.Instances() if !i.Id.IsZero() && !i.newId { err = &errortypes.DatabaseError{ errors.New("instance: Instance already exists"), } return } i.Created = time.Now() for n := 0; n < 2000; n++ { resp, e := coll.InsertOne(db, i) if e != nil { err = database.ParseError(e) if _, ok := err.(*database.DuplicateKeyError); ok { i.GenerateUnixId() err = nil continue } return } i.Id = resp.InsertedID.(bson.ObjectID) return } err = &errortypes.WriteError{ errors.New("instance: Failed to insert unique unix id"), } return } func (i *Instance) LoadVirt(poolsMap map[bson.ObjectID]*pool.Pool, disks []*disk.Disk) { i.Virt = &vm.VirtualMachine{ Id: i.Id, Organization: i.Organization, UnixId: i.UnixId, DiskType: i.DiskType, DiskPool: i.DiskPool, Image: i.Image, Processors: i.Processors, Memory: i.Memory, Hugepages: node.Self.Hugepages, Vnc: i.Vnc, VncDisplay: i.VncDisplay, Spice: i.Spice, SpicePort: i.SpicePort, Gui: i.Gui, Disks: []*vm.Disk{}, NetworkAdapters: []*vm.NetworkAdapter{ &vm.NetworkAdapter{ Type: vm.Bridge, MacAddress: vm.GetMacAddr(i.Id, i.Vpc), Vpc: i.Vpc, Subnet: i.Subnet, }, }, CloudSubnet: i.CloudSubnet, CloudVnic: i.CloudVnic, CloudVnicAttach: i.CloudVnicAttach, DhcpIp: i.DhcpIp, DhcpIp6: i.DhcpIp6, Uefi: i.Uefi, SecureBoot: i.SecureBoot, Tpm: i.Tpm, DhcpServer: i.DhcpServer, Deployment: i.Deployment, CloudType: i.CloudType, SystemKind: i.SystemKind, NoPublicAddress: i.NoPublicAddress, NoPublicAddress6: i.NoPublicAddress6, NoHostAddress: i.NoHostAddress, Isos: []*vm.Iso{}, UsbDevices: []*vm.UsbDevice{}, PciDevices: []*vm.PciDevice{}, DriveDevices: []*vm.DriveDevice{}, IscsiDevices: []*vm.IscsiDevice{}, Mounts: []*vm.Mount{}, } if disks != nil { for _, dsk := range disks { switch dsk.Type { case disk.Lvm: if poolsMap == nil { continue } pl := poolsMap[dsk.Pool] if pl == nil { continue } i.Virt.DriveDevices = append( i.Virt.DriveDevices, &vm.DriveDevice{ Id: dsk.Id.Hex(), Type: vm.Lvm, VgName: pl.VgName, LvName: dsk.Id.Hex(), }, ) break case disk.Qcow2, "": index, err := strconv.Atoi(dsk.Index) if err != nil { continue } i.Virt.Disks = append(i.Virt.Disks, &vm.Disk{ Id: dsk.Id, Index: index, Path: paths.GetDiskPath(dsk.Id), }) break } } } for _, is := range i.Isos { i.Virt.Isos = append(i.Virt.Isos, &vm.Iso{ Name: is.Name, }) } if node.Self.UsbPassthrough && i.UsbDevices != nil { for _, device := range i.UsbDevices { usbDevice, _ := usb.GetDevice( device.Bus, device.Address, device.Vendor, device.Product, ) if usbDevice != nil { i.Virt.UsbDevices = append(i.Virt.UsbDevices, &vm.UsbDevice{ Vendor: usbDevice.Vendor, Product: usbDevice.Product, Bus: usbDevice.Bus, Address: usbDevice.Address, }) } } } if node.Self.PciPassthrough && i.PciDevices != nil { for _, device := range i.PciDevices { i.Virt.PciDevices = append(i.Virt.PciDevices, &vm.PciDevice{ Slot: device.Slot, }) } } instanceDrives := set.NewSet() nodeInstanceDrives := node.Self.InstanceDrives if nodeInstanceDrives != nil { for _, device := range nodeInstanceDrives { instanceDrives.Add(device.Id) } } if i.DriveDevices != nil { for _, device := range i.DriveDevices { if instanceDrives.Contains(device.Id) { i.Virt.DriveDevices = append( i.Virt.DriveDevices, &vm.DriveDevice{ Id: device.Id, Type: vm.Physical, }, ) } } } if node.Self.Iscsi && i.IscsiDevices != nil { for _, device := range i.IscsiDevices { i.Virt.IscsiDevices = append( i.Virt.IscsiDevices, &vm.IscsiDevice{ Uri: device.QemuUri(), }, ) } } for _, mount := range i.Mounts { i.Virt.Mounts = append( i.Virt.Mounts, &vm.Mount{ Name: mount.Name, Type: mount.Type, Path: mount.Path, HostPath: mount.HostPath, }, ) } return } func (i *Instance) Changed(curVirt *vm.VirtualMachine) (bool, string) { curCloudType := curVirt.CloudType if curCloudType == "" { curCloudType = Linux } cloudType := i.Virt.CloudType if cloudType == "" { cloudType = Linux } if i.Virt.Memory != curVirt.Memory { return true, "Memory size changed" } if i.Virt.Hugepages != curVirt.Hugepages { return true, "Hugepages changed" } if i.Virt.Processors != curVirt.Processors { return true, "Processor count changed" } if i.Virt.Vnc != curVirt.Vnc { return true, "VNC changed" } if i.Virt.VncDisplay != curVirt.VncDisplay { return true, "VNC display changed" } if i.Virt.Spice != curVirt.Spice { return true, "SPICE changed" } if i.Virt.SpicePort != curVirt.SpicePort { return true, "SPICE port changed" } if i.Virt.Gui != curVirt.Gui { return true, "GUI changed" } if i.Virt.Uefi != curVirt.Uefi { return true, "UEFI changed" } if i.Virt.SecureBoot != curVirt.SecureBoot { return true, "Secure boot changed" } if i.Virt.Tpm != curVirt.Tpm { return true, "TPM changed" } if i.Virt.DhcpServer != curVirt.DhcpServer { return true, "DHCP server changed" } if cloudType != curCloudType { return true, "Cloud type changed" } if i.Virt.NoPublicAddress != curVirt.NoPublicAddress { return true, "Public address changed" } if i.Virt.NoPublicAddress6 != curVirt.NoPublicAddress6 { return true, "Public IPv6 changed" } if i.Virt.NoHostAddress != curVirt.NoHostAddress { return true, "Host address changed" } for i, adapter := range i.Virt.NetworkAdapters { if len(curVirt.NetworkAdapters) <= i { return true, "Network adapters changed" } if adapter.Vpc != curVirt.NetworkAdapters[i].Vpc { return true, "VPC changed" } if adapter.Subnet != curVirt.NetworkAdapters[i].Subnet { return true, "Subnet changed" } } if i.Virt.Isos != nil { if len(i.Virt.Isos) > 0 && curVirt.Isos == nil { return true, "ISO devices changed" } for i, device := range i.Virt.Isos { if len(curVirt.Isos) <= i { return true, "ISO devices changed" } if device.Name != curVirt.Isos[i].Name { return true, "ISO device changed" } } } if i.Virt.PciDevices != nil { if len(i.Virt.PciDevices) > 0 && curVirt.PciDevices == nil { return true, "PCI devices changed" } for i, device := range i.Virt.PciDevices { if len(curVirt.PciDevices) <= i { return true, "PCI devices changed" } if device.Slot != curVirt.PciDevices[i].Slot { return true, "PCI device slot changed" } } } if i.Virt.DriveDevices != nil { if len(i.Virt.DriveDevices) > 0 && curVirt.DriveDevices == nil { return true, "Drive devices changed" } for i, device := range i.Virt.DriveDevices { if len(curVirt.DriveDevices) <= i { return true, "Drive devices changed" } if device.Id != curVirt.DriveDevices[i].Id { return true, "Drive device changed" } } } if i.Virt.IscsiDevices != nil { if len(i.Virt.IscsiDevices) > 0 && curVirt.IscsiDevices == nil { return true, "iSCSI devices changed" } for i, device := range i.Virt.IscsiDevices { if len(curVirt.IscsiDevices) <= i { return true, "iSCSI devices changed" } if device.Uri != curVirt.IscsiDevices[i].Uri { return true, "iSCSI URI changed" } } } if i.Virt.Mounts != nil { if len(i.Virt.Mounts) > 0 && curVirt.Mounts == nil { return true, "Mounts changed" } for i, mount := range i.Virt.Mounts { if len(curVirt.Mounts) <= i { return true, "Mounts changed" } if mount.Name != curVirt.Mounts[i].Name { return true, "Mount name changed" } if mount.Type != curVirt.Mounts[i].Type { return true, "Mount type changed" } if mount.Path != curVirt.Mounts[i].Path { return true, "Mount path changed" } if mount.HostPath != curVirt.Mounts[i].HostPath { return true, "Mount host path changed" } } } return false, "" } func (i *Instance) DiskChanged(curVirt *vm.VirtualMachine) ( addDisks, remDisks []*vm.Disk) { addDisks = []*vm.Disk{} remDisks = []*vm.Disk{} if !curVirt.DisksAvailable { logrus.WithFields(logrus.Fields{ "instance_id": curVirt.Id.Hex(), }).Warn("qemu: Ignoring disk state") return } disks := map[bson.ObjectID]*vm.Disk{} curDisks := set.NewSet() for _, dsk := range i.Virt.Disks { disks[dsk.Id] = dsk } for _, dsk := range curVirt.Disks { newDsk := disks[dsk.Id] if newDsk == nil || dsk.Index != newDsk.Index { remDisks = append(remDisks, dsk) } else { curDisks.Add(dsk.Id) } } for _, dsk := range i.Virt.Disks { if !curDisks.Contains(dsk.Id) { addDisks = append(addDisks, dsk) } } return } func (i *Instance) UsbChanged(curVirt *vm.VirtualMachine) ( addUsbs, remUsbs []*vm.UsbDevice) { addUsbs = []*vm.UsbDevice{} remUsbs = []*vm.UsbDevice{} if !node.Self.UsbPassthrough { return } if !curVirt.UsbDevicesAvailable { logrus.WithFields(logrus.Fields{ "instance_id": curVirt.Id.Hex(), }).Warn("qemu: Ignoring USB state") return } usbs := set.NewSet() usbsMap := map[string]*vm.UsbDevice{} curUsbs := set.NewSet() curUsbsMap := map[string]*vm.UsbDevice{} if curVirt.UsbDevices != nil { for _, device := range curVirt.UsbDevices { key := device.Key() curUsbs.Add(key) curUsbsMap[key] = device } } if i.Virt.UsbDevices != nil { for _, device := range i.Virt.UsbDevices { key := device.Key() usbs.Add(key) usbsMap[key] = device } } addUsbsSet := usbs.Copy() addUsbsSet.Subtract(curUsbs) remUsbsSet := curUsbs.Copy() remUsbsSet.Subtract(usbs) for deviceInf := range addUsbsSet.Iter() { device := usbsMap[deviceInf.(string)] addUsbs = append(addUsbs, device) } for deviceInf := range remUsbsSet.Iter() { device := curUsbsMap[deviceInf.(string)] remUsbs = append(remUsbs, device) } return } func (i *Instance) VncConnect(db *database.Database, rw http.ResponseWriter, r *http.Request) (err error) { nde, err := node.Get(db, i.Node) if err != nil { return } vncHost := "" if nde.Id == node.Self.Id { vncHost = "127.0.0.1" } if vncHost == "" && len(nde.PrivateIps) > 0 { vncHost = nde.PrivateIps[nde.DefaultInterface] if vncHost == "" { for _, privIp := range nde.PrivateIps { if privIp != "" { vncHost = privIp break } } } } if vncHost == "" && len(nde.PublicIps) > 0 { vncHost = nde.PublicIps[0] } if vncHost == "" { err = &errortypes.NotFoundError{ errors.New("instance: Node missing IP for VNC"), } return } wsUrl := fmt.Sprintf( "ws://%s:%d", vncHost, i.VncDisplay+15900, ) var backConn *websocket.Conn var backResp *http.Response dialer := &websocket.Dialer{ HandshakeTimeout: 10 * time.Second, } header := http.Header{} header.Set( "Sec-Websocket-Protocol", r.Header.Get("Sec-Websocket-Protocol"), ) backConn, backResp, err = dialer.Dial(wsUrl, header) if err != nil { if backResp != nil { err = &VncDialError{ errors.Wrapf(err, "instance: WebSocket dial error %d", backResp.StatusCode), } } else { err = &VncDialError{ errors.Wrap(err, "instance: WebSocket dial error"), } } return } defer backConn.Close() wsUpgrader := &websocket.Upgrader{ HandshakeTimeout: time.Duration( settings.Router.HandshakeTimeout) * time.Second, ReadBufferSize: 2048, WriteBufferSize: 2048, CheckOrigin: func(r *http.Request) bool { return true }, } upgradeHeader := http.Header{} val := backResp.Header.Get("Sec-Websocket-Protocol") if val != "" { upgradeHeader.Set("Sec-Websocket-Protocol", val) } frontConn, err := wsUpgrader.Upgrade(rw, r, upgradeHeader) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "instance: WebSocket upgrade error"), } return } defer frontConn.Close() wait := make(chan bool, 4) go func() { defer func() { rec := recover() if rec != nil { logrus.WithFields(logrus.Fields{ "panic": rec, }).Error("instance: WebSocket VNC back panic") wait <- true } }() for { msgType, msg, err := frontConn.ReadMessage() if err != nil { closeMsg := websocket.FormatCloseMessage( websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) if e, ok := err.(*websocket.CloseError); ok { if e.Code != websocket.CloseNoStatusReceived { closeMsg = websocket.FormatCloseMessage(e.Code, e.Text) } } _ = backConn.WriteMessage(websocket.CloseMessage, closeMsg) break } err = backConn.WriteMessage(msgType, msg) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "instance: WebSocket VNC write error"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("instance: WebSocket VNC back write error") break } } wait <- true }() go func() { defer func() { rec := recover() if rec != nil { logrus.WithFields(logrus.Fields{ "panic": rec, }).Error("instance: WebSocket VNC front panic") wait <- true } }() for { msgType, msg, err := backConn.ReadMessage() if err != nil { closeMsg := websocket.FormatCloseMessage( websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) if e, ok := err.(*websocket.CloseError); ok { if e.Code != websocket.CloseNoStatusReceived { closeMsg = websocket.FormatCloseMessage(e.Code, e.Text) } } _ = frontConn.WriteMessage(websocket.CloseMessage, closeMsg) break } err = frontConn.WriteMessage(msgType, msg) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "instance: WebSocket VNC write error"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("instance: WebSocket VNC back write error") break } } wait <- true }() <-wait return } ================================================ FILE: instance/utils.go ================================================ package instance import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) func Get(db *database.Database, instId bson.ObjectID) ( inst *Instance, err error) { coll := db.Instances() inst = &Instance{} err = coll.FindOneId(instId, inst) if err != nil { return } return } func GetOrg(db *database.Database, orgId, instId bson.ObjectID) ( inst *Instance, err error) { coll := db.Instances() inst = &Instance{} err = coll.FindOne(db, &bson.M{ "_id": instId, "organization": orgId, }).Decode(inst) if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (inst *Instance, err error) { coll := db.Instances() inst = &Instance{} err = coll.FindOne(db, query).Decode(inst) if err != nil { err = database.ParseError(err) return } return } func ExistsIp(db *database.Database, addr string) (exists bool, err error) { coll := db.Instances() n, err := coll.CountDocuments(db, &bson.M{ "$or": []bson.M{ {"public_ips": addr}, {"public_ips6": addr}, {"cloud_private_ips": addr}, {"cloud_public_ips": addr}, {"cloud_public_ips6": addr}, {"host_ips": addr}, {"node_port_ips": addr}, }, }) if err != nil { err = database.ParseError(err) return } if n > 0 { exists = true } return } func ExistsOrg(db *database.Database, orgId, instId bson.ObjectID) ( exists bool, err error) { coll := db.Instances() n, err := coll.CountDocuments(db, &bson.M{ "_id": instId, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } if n > 0 { exists = true } return } func GetAll(db *database.Database, query *bson.M) ( insts []*Instance, err error) { coll := db.Instances() insts = []*Instance{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { inst := &Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } insts = append(insts, inst) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllRoles(db *database.Database, query *bson.M) ( insts []*Instance, rolesSet set.Set, err error) { coll := db.Instances() insts = []*Instance{} rolesSet = set.NewSet() cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { inst := &Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } insts = append(insts, inst) for _, role := range inst.Roles { rolesSet.Add(role) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllVirt(db *database.Database, query *bson.M, pools []*pool.Pool, disks []*disk.Disk) ( insts []*Instance, err error) { poolsMap := map[bson.ObjectID]*pool.Pool{} for _, pl := range pools { poolsMap[pl.Id] = pl } instanceDisks := map[bson.ObjectID][]*disk.Disk{} if disks != nil { for _, dsk := range disks { if dsk.Action == disk.Destroy { if dsk.DeleteProtection { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Info("instance: Delete protection ignore disk detach") } else { continue } } else if dsk.State != disk.Available && dsk.State != disk.Attached { continue } dsks := instanceDisks[dsk.Instance] if dsks == nil { dsks = []*disk.Disk{} } instanceDisks[dsk.Instance] = append(dsks, dsk) } } coll := db.Instances() insts = []*Instance{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { inst := &Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } inst.LoadVirt(poolsMap, instanceDisks[inst.Id]) insts = append(insts, inst) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllVirtMapped(db *database.Database, query *bson.M, pools []*pool.Pool, instanceDisks map[bson.ObjectID][]*disk.Disk) ( insts []*Instance, err error) { coll := db.Instances() insts = []*Instance{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) poolsMap := map[bson.ObjectID]*pool.Pool{} for _, pl := range pools { poolsMap[pl.Id] = pl } for cursor.Next(db) { inst := &Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } virtDsks := []*disk.Disk{} dsks := instanceDisks[inst.Id] if dsks != nil { for _, dsk := range dsks { if dsk.Action == disk.Destroy { if dsk.DeleteProtection { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Info("instance: Delete protection ignore disk detach") } else { continue } } else if dsk.State != disk.Available && dsk.State != disk.Attached { continue } virtDsks = append(virtDsks, dsk) } } inst.LoadVirt(poolsMap, virtDsks) insts = append(insts, inst) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func LoadAllVirt(insts []*Instance, pools []*pool.Pool, instanceDisks map[bson.ObjectID][]*disk.Disk) []*Instance { poolsMap := map[bson.ObjectID]*pool.Pool{} for _, pl := range pools { poolsMap[pl.Id] = pl } for _, inst := range insts { virtDsks := []*disk.Disk{} dsks := instanceDisks[inst.Id] for _, dsk := range dsks { if dsk.Action == disk.Destroy { if dsk.DeleteProtection { logrus.WithFields(logrus.Fields{ "disk_id": dsk.Id.Hex(), }).Info("instance: Delete protection ignore disk detach") } else { continue } } else if dsk.State != disk.Available && dsk.State != disk.Attached { continue } virtDsks = append(virtDsks, dsk) } inst.LoadVirt(poolsMap, virtDsks) } return insts } func GetAllName(db *database.Database, query *bson.M) ( instances []*Instance, err error) { coll := db.Instances() instances = []*Instance{} cursor, err := coll.Find( db, query, options.Find(). SetProjection(&bson.D{ {"name", 1}, }), ) defer cursor.Close(db) for cursor.Next(db) { inst := &Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } instances = append(instances, inst) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (insts []*Instance, count int64, err error) { coll := db.Instances() insts = []*Instance{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { inst := &Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } insts = append(insts, inst) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, instId bson.ObjectID) (err error) { inst, err := Get(db, instId) if err != nil { return } if inst.DeleteProtection { logrus.WithFields(logrus.Fields{ "instance_id": instId.Hex(), }).Info("instance: Delete protection ignore instance remove") return } err = block.RemoveInstanceIps(db, instId) if err != nil { return } err = vpc.RemoveInstanceIps(db, instId) if err != nil { return } err = journal.RemoveAll(db, instId) if err != nil { return } coll := db.Instances() _, err = coll.DeleteOne(db, &bson.M{ "_id": instId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } _ = inst.Cleanup(db) return } func Delete(db *database.Database, instId bson.ObjectID) (err error) { coll := db.Instances() err = coll.UpdateId(instId, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func DeleteOrg(db *database.Database, orgId, instId bson.ObjectID) ( err error) { coll := db.Instances() err = coll.UpdateId(instId, &bson.M{ "$set": &bson.M{ "action": Destroy, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } func DeleteMulti(db *database.Database, instIds []bson.ObjectID) ( err error) { coll := db.Instances() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": instIds, }, "delete_protection": &bson.M{ "$ne": true, }, }, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func DeleteMultiOrg(db *database.Database, orgId bson.ObjectID, instIds []bson.ObjectID) (err error) { coll := db.Instances() _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": instIds, }, "organization": orgId, "delete_protection": &bson.M{ "$ne": true, }, }, &bson.M{ "$set": &bson.M{ "action": Destroy, }, }) if err != nil { err = database.ParseError(err) return } return } func UpdateMulti(db *database.Database, instIds []bson.ObjectID, doc *bson.M) (err error) { coll := db.Instances() query := &bson.M{ "_id": &bson.M{ "$in": instIds, }, "action": &bson.M{ "$ne": Destroy, }, } if (*doc)["action"] == Destroy { (*query)["delete_protection"] = &bson.M{ "$ne": true, } } _, err = coll.UpdateMany(db, query, &bson.M{ "$set": doc, }) if err != nil { err = database.ParseError(err) return } return } func UpdateMultiOrg(db *database.Database, orgId bson.ObjectID, instIds []bson.ObjectID, doc *bson.M) (err error) { coll := db.Instances() query := &bson.M{ "_id": &bson.M{ "$in": instIds, }, "organization": orgId, "action": &bson.M{ "$ne": Destroy, }, } if (*doc)["action"] == Destroy { (*query)["delete_protection"] = &bson.M{ "$ne": true, } } _, err = coll.UpdateMany(db, query, &bson.M{ "$set": doc, }) if err != nil { err = database.ParseError(err) return } return } func SetAction(db *database.Database, instId bson.ObjectID, action string) (err error) { coll := db.Instances() _, err = coll.UpdateOne(db, &bson.M{ "_id": instId, "action": &bson.M{ "$ne": Destroy, }, }, &bson.M{ "$set": &bson.M{ "action": action, }, }) if err != nil { err = database.ParseError(err) return } return } func SetDownloadProgress(db *database.Database, instId bson.ObjectID, progress int, speedMb float64) (err error) { coll := db.Instances() _, err = coll.UpdateOne(db, &bson.M{ "_id": instId, }, &bson.M{ "$set": &bson.M{ "status_info.download_progress": progress, "status_info.download_speed": speedMb, }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: interfaces/interfaces.go ================================================ package interfaces import ( "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) var ( curVxlan = false ifaces = map[string]set.Set{} ifacesLock = sync.Mutex{} lastChange time.Time ) func getIfaces(bridge string) (ifacesSet set.Set, err error) { ifacesSet = set.NewSet() ifaces, err := iproute.IfaceGetBridgeIfaces("", bridge) if err != nil { return } for _, iface := range ifaces { ifacesSet.Add(iface.Name) } return } func SyncIfaces(vxlan bool) { nde := node.Self if vxlan == curVxlan && time.Since(lastChange) < 10*time.Second { return } curVxlan = vxlan ifacesNew := map[string]set.Set{} externalIfaces := nde.ExternalInterfaces internalIfaces := nde.InternalInterfaces blocks := nde.Blocks for _, iface := range externalIfaces { ifaceSet, err := getIfaces(iface) if err != nil { logrus.WithFields(logrus.Fields{ "bridge": iface, "error": err, }).Error("interfaces: Bridge ifaces get failed") } else { ifacesNew[iface] = ifaceSet } } for _, iface := range internalIfaces { ifaceSet, err := getIfaces(iface) if err != nil { logrus.WithFields(logrus.Fields{ "bridge": iface, "error": err, }).Error("interfaces: Bridge ifaces get failed") } else { ifacesNew[iface] = ifaceSet } brIface := vm.GetHostBridgeIface(iface) ifaceSet, err = getIfaces(brIface) if err != nil { logrus.WithFields(logrus.Fields{ "bridge": brIface, "error": err, }).Error("interfaces: Bridge ifaces get failed") } else { ifacesNew[brIface] = ifaceSet } } for _, blck := range blocks { if blck.Interface == "" { continue } ifaceSet, err := getIfaces(blck.Interface) if err != nil { logrus.WithFields(logrus.Fields{ "bridge": blck.Interface, "error": err, }).Error("interfaces: Bridge ifaces get failed") } else { ifacesNew[blck.Interface] = ifaceSet } } ifacesLock.Lock() lastChange = time.Now() ifaces = ifacesNew ifacesLock.Unlock() return } func GetExternal(virtIface string) (externalIface string) { externalIfaces := node.Self.ExternalInterfaces if externalIfaces != nil { curLen := 0 for _, iface := range externalIfaces { ifacesSetLen := 0 ifacesLock.Lock() ifacesSet := ifaces[iface] if ifacesSet != nil { ifacesSetLen = ifacesSet.Len() } ifacesLock.Unlock() if ifacesSet == nil { continue } if externalIface == "" || ifacesSetLen < curLen { curLen = ifacesSetLen externalIface = iface } } if externalIface != "" { ifacesLock.Lock() lastChange = time.Now() ifacesSet := ifaces[externalIface] if ifacesSet != nil { ifacesSet.Add(virtIface) } ifacesLock.Unlock() } } return } func HasExternal() (exists bool) { externalIfaces := node.Self.ExternalInterfaces externalIface := "" if len(externalIfaces) > 0 { externalIface = externalIfaces[0] } if externalIface != "" { exists = true } return } func GetInternal(virtIface string, vxlan bool) (internalIface string) { internalIfaces := node.Self.InternalInterfaces if internalIfaces != nil { curLen := 0 for _, iface := range internalIfaces { if vxlan { iface = vm.GetHostBridgeIface(iface) } ifacesSetLen := 0 ifacesLock.Lock() ifacesSet := ifaces[iface] if ifacesSet != nil { ifacesSetLen = ifacesSet.Len() } ifacesLock.Unlock() if ifacesSet == nil { continue } if internalIface == "" || ifacesSetLen < curLen { curLen = ifacesSetLen internalIface = iface } } if internalIface != "" { ifacesLock.Lock() lastChange = time.Now() ifacesSet := ifaces[internalIface] if ifacesSet != nil { ifacesSet.Add(virtIface) } ifacesLock.Unlock() } } return } func GetBridges(nde *node.Node) (bridges set.Set) { bridges = set.NewSet() externalIfaces := nde.ExternalInterfaces for _, iface := range externalIfaces { bridges.Add(iface) } internalIfaces := nde.InternalInterfaces for _, iface := range internalIfaces { bridges.Add(iface) } ndeBlocks := nde.Blocks for _, blck := range ndeBlocks { if blck.Interface == "" { continue } bridges.Add(blck.Interface) } ndeBlocks6 := nde.Blocks6 for _, blck := range ndeBlocks6 { if blck.Interface == "" { continue } bridges.Add(blck.Interface) } return } func GetBridgesInternal(nde *node.Node) (bridges set.Set) { bridges = set.NewSet() internalIfaces := nde.InternalInterfaces for _, iface := range internalIfaces { bridges.Add(iface) } return } func GetBridgesExternal(nde *node.Node) (bridges set.Set) { bridges = set.NewSet() externalIfaces := nde.ExternalInterfaces for _, iface := range externalIfaces { bridges.Add(iface) } ndeBlocks := nde.Blocks for _, blck := range ndeBlocks { if blck.Interface == "" { continue } bridges.Add(blck.Interface) } ndeBlocks6 := nde.Blocks6 for _, blck := range ndeBlocks6 { if blck.Interface == "" { continue } bridges.Add(blck.Interface) } return } func RemoveVirtIface(virtIface string) { if virtIface == "" { return } ifacesLock.Lock() lastChange = time.Now() for iface, ifaceSet := range ifaces { ifaceSet.Remove(virtIface) ifaces[iface] = ifaceSet } ifacesLock.Unlock() } ================================================ FILE: ip/interface.go ================================================ package ip type Interface struct { Name string `bson:"name" json:"name"` Address string `bson:"address" json:"address"` } ================================================ FILE: ip/ip.go ================================================ package ip import ( "encoding/json" "net" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) var ( cache = map[string]map[string]*Iface{} cacheTime = map[string]time.Time{} cacheLock = sync.Mutex{} ) type Iface struct { Ifindex int `json:"ifindex"` Ifname string `json:"ifname"` Flags []string `json:"flags"` Mtu int `json:"mtu"` Qdisc string `json:"qdisc"` Operstate string `json:"operstate"` Group string `json:"group"` Txqlen int `json:"txqlen"` LinkType string `json:"link_type"` Address string `json:"address"` Broadcast string `json:"broadcast"` AddrInfo []struct { Family string `json:"family"` Local string `json:"local"` Prefixlen int `json:"prefixlen"` Scope string `json:"scope"` Label string `json:"label,omitempty"` ValidLifeTime int64 `json:"valid_life_time"` PreferredLifeTime int64 `json:"preferred_life_time"` Broadcast string `json:"broadcast,omitempty"` Dynamic bool `json:"dynamic,omitempty"` Mngtmpaddr bool `json:"mngtmpaddr,omitempty"` } `json:"addr_info"` Link string `json:"link,omitempty"` Master string `json:"master,omitempty"` LinkIndex int `json:"link_index,omitempty"` LinkNetnsid int `json:"link_netnsid,omitempty"` } func (iface *Iface) GetAddress() string { var addrs []string for _, addr := range iface.AddrInfo { if addr.Family != "inet" { continue } ip := net.ParseIP(addr.Local) if ip == nil || ip.IsLoopback() { continue } switch addr.Scope { case "global": addrs = append([]string{addr.Local}, addrs...) case "link": addrs = append(addrs, addr.Local) } } if len(addrs) > 0 { return addrs[0] } return "" } func (iface *Iface) GetAddress6() string { var addrs []string for _, addr := range iface.AddrInfo { if addr.Family != "inet6" { continue } ip := net.ParseIP(addr.Local) if ip == nil || ip.IsLoopback() || ip.IsLinkLocalUnicast() { continue } switch addr.Scope { case "global": addrs = append([]string{addr.Local}, addrs...) case "link": addrs = append(addrs, addr.Local) } } if len(addrs) > 0 { return addrs[0] } return "" } func GetIfaces(namespace string) (ifaces []*Iface, err error) { output := "" if namespace == "" { output, err = utils.ExecOutput( "", "ip", "-j", "address", ) } else { output, err = utils.ExecOutput( "", "ip", "netns", "exec", namespace, "ip", "-j", "address", ) } if err != nil { return } ifaces = []*Iface{} err = json.Unmarshal([]byte(output), &ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "ip: Failed to parse ip json output"), } return } return } func GetIfacesCached(namespace string) (ifacesMap map[string]*Iface, err error) { cacheLock.Lock() if time.Since(cacheTime[namespace]) < 5*time.Minute { ifacesMap = cache[namespace] cacheLock.Unlock() return } cacheLock.Unlock() ifaces, err := GetIfaces(namespace) if err != nil { return } ifacesMap = map[string]*Iface{} for _, iface := range ifaces { ifacesMap[iface.Ifname] = iface } cacheLock.Lock() cache[namespace] = ifacesMap cacheTime[namespace] = time.Now() cacheLock.Unlock() return } func ClearIfacesCache(namespace string) { cacheLock.Lock() cache[namespace] = map[string]*Iface{} cacheTime[namespace] = time.Time{} cacheLock.Unlock() } ================================================ FILE: iproute/address.go ================================================ package iproute import ( "encoding/json" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Address struct { Family string `json:"family"` Local string `json:"local"` Prefix int `json:"prefixlen"` Scope string `json:"scope"` Label string `json:"label"` Dynamic bool `json:"dynamic"` Deprecated bool `json:"deprecated"` } type AddressIface struct { Name string `json:"ifname"` State string `json:"operstate"` Addresses []*Address `json:"addr_info"` } func AddressGetIface(namespace, name string) ( address, address6 *Address, err error) { ifaces := []*AddressIface{} label := "" if strings.Contains(name, ":") { label = name name = strings.Split(name, ":")[0] } var output string if namespace != "" { output, err = utils.ExecOutputLogged( []string{ "No such file or directory", "does not exist", "setting the network namespace", }, "ip", "netns", "exec", namespace, "ip", "--json", "addr", "show", "dev", name, ) } else { output, err = utils.ExecOutputLogged( []string{ "No such file or directory", "does not exist", "setting the network namespace", }, "ip", "--json", "addr", "show", "dev", name, ) } if err != nil { return } if output == "" { return } err = json.Unmarshal([]byte(output), &ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "iproute: Failed to parse interface address"), } return } dynamic6 := false if label != "" { for _, iface := range ifaces { if iface.Name == name && iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Label == label && addr.Scope == "global" && !addr.Deprecated { if address == nil && addr.Family == "inet" { address = addr } else if addr.Family == "inet6" { if addr.Dynamic && !dynamic6 { address6 = addr dynamic6 = true } else if address6 == nil { address6 = addr } } } } } } } for _, iface := range ifaces { if iface.Name == name && iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Scope == "global" && !addr.Deprecated { if address == nil && addr.Family == "inet" { address = addr } else if addr.Family == "inet6" { if addr.Dynamic && !dynamic6 { address6 = addr dynamic6 = true } else if address6 == nil { address6 = addr } } } } } } return } func AddressGetIfaceMod(namespace, name string) ( address, address6 *Address, err error) { ifaces := []*AddressIface{} label := "" if strings.Contains(name, ":") { label = name name = strings.Split(name, ":")[0] } nameMod := name + "x" nameMod6 := name + "y" var addressMod *Address var address6Mod *Address var addressMod6 *Address var address6Mod6 *Address var output string if namespace != "" { output, err = utils.ExecOutputLogged( []string{ "No such file or directory", "does not exist", "setting the network namespace", }, "ip", "netns", "exec", namespace, "ip", "--json", "addr", "show", ) } else { output, err = utils.ExecOutputLogged( []string{ "No such file or directory", "does not exist", "setting the network namespace", }, "ip", "--json", "addr", "show", ) } if err != nil { return } if output == "" { return } err = json.Unmarshal([]byte(output), &ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "iproute: Failed to parse interface address"), } return } dynamic6 := false if label != "" { for _, iface := range ifaces { if strings.HasPrefix(iface.Name, name) && iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Label == label && addr.Scope == "global" && !addr.Deprecated { if address == nil && addr.Family == "inet" { address = addr } else if addr.Family == "inet6" { if addr.Dynamic && !dynamic6 { address6 = addr dynamic6 = true } else if address6 == nil { address6 = addr } } } } } } } for _, iface := range ifaces { if iface.Name == name && iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Scope == "global" && !addr.Deprecated { if address == nil && addr.Family == "inet" { address = addr } else if addr.Family == "inet6" { if addr.Dynamic && !dynamic6 { address6 = addr dynamic6 = true } else if address6 == nil { address6 = addr } } } } } if iface.Name == nameMod && iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Scope == "global" && !addr.Deprecated { if addressMod == nil && addr.Family == "inet" { addressMod = addr } else if addr.Family == "inet6" { if addr.Dynamic && !dynamic6 { addressMod6 = addr dynamic6 = true } else if addressMod6 == nil { addressMod6 = addr } } } } } if iface.Name == nameMod6 && iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Scope == "global" && !addr.Deprecated { if address6Mod == nil && addr.Family == "inet" { address6Mod = addr } else if addr.Family == "inet6" { if addr.Dynamic && !dynamic6 { address6Mod6 = addr dynamic6 = true } else if address6Mod6 == nil { address6Mod6 = addr } } } } } } if address6Mod6 != nil { address6 = address6Mod6 } else if addressMod6 != nil { address6 = addressMod6 } if addressMod != nil { address = addressMod } else if address6Mod != nil { address = address6Mod } return } ================================================ FILE: iproute/bridge.go ================================================ package iproute import ( "github.com/pritunl/pritunl-cloud/utils" ) func BridgeAdd(namespace, name string) (err error) { if namespace != "" { _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "netns", "exec", namespace, "ip", "link", "add", name, "type", "bridge", "stp_state", "0", "forward_delay", "0", ) } else { _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "link", "add", name, "type", "bridge", "stp_state", "0", "forward_delay", "0", ) } if err != nil { return } return } func BridgeDelete(namespace, name string) (err error) { if namespace != "" { _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "netns", "exec", namespace, "ip", "link", "delete", name, "type", "bridge", ) } else { _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "link", "delete", name, "type", "bridge", ) } if err != nil { return } return } ================================================ FILE: iproute/iface.go ================================================ package iproute import ( "encoding/json" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Iface struct { Name string `json:"ifname"` State string `json:"operstate"` } func IfaceGetAll(namespace string) (ifaces []*Iface, err error) { ifaces = []*Iface{} var output string if namespace != "" { output, err = utils.ExecOutputLogged( nil, "ip", "netns", "exec", namespace, "ip", "--json", "--brief", "link", "show", ) } else { output, err = utils.ExecOutputLogged( nil, "ip", "--json", "--brief", "link", "show", ) } if err != nil { return } err = json.Unmarshal([]byte(output), &ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "iproute: Failed to prase ifaces"), } return } return } func IfaceGetBridges(namespace string) (ifaces []*Iface, err error) { ifaces = []*Iface{} var output string if namespace != "" { output, err = utils.ExecOutputLogged( nil, "ip", "netns", "exec", namespace, "ip", "--json", "--brief", "link", "show", "type", "bridge", ) } else { output, err = utils.ExecOutputLogged( nil, "ip", "--json", "--brief", "link", "show", "type", "bridge", ) } if err != nil { return } err = json.Unmarshal([]byte(output), &ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "iproute: Failed to prase bridges"), } return } return } func IfaceGetBridgeIfaces(namespace, bridge string) ( ifaces []*Iface, err error) { ifaces = []*Iface{} var output string if namespace != "" { output, err = utils.ExecCombinedOutputLogged( []string{ "does not exist", }, "ip", "netns", "exec", namespace, "ip", "--json", "--brief", "link", "show", "master", bridge, ) } else { output, err = utils.ExecCombinedOutputLogged( []string{ "does not exist", }, "ip", "--json", "--brief", "link", "show", "master", bridge, ) } if err != nil { return } if output == "" { return } err = json.Unmarshal([]byte(output), &ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "iproute: Failed to prase bridges"), } return } return } ================================================ FILE: ipset/names.go ================================================ package ipset import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/utils" ) type Names struct { Namespace string Sets set.Set } func (n *Names) Apply(curNames *Names) (err error) { if curNames != nil { for nameInf := range curNames.Sets.Iter() { name := nameInf.(string) if !n.Sets.Contains(name) { if n.Namespace == "0" { _, err = utils.ExecCombinedOutputLogged( []string{"not exist"}, "ipset", "destroy", name, ) if err != nil { return } } else { _, err = utils.ExecCombinedOutputLogged( []string{"not exist"}, "ip", "netns", "exec", n.Namespace, "ipset", "destroy", name, ) if err != nil { return } } } } } return } ================================================ FILE: ipset/sets.go ================================================ package ipset import ( "strings" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/utils" ) type Sets struct { Namespace string Sets map[string]set.Set } func (s *Sets) Apply(curSets *Sets) (err error) { namesSet := set.NewSet() for name, ipSet := range s.Sets { namesSet.Add(name) var curIpSet set.Set if curSets != nil { curIpSet = curSets.Sets[name] } if curIpSet == nil { curIpSet = set.NewSet() } created := false for ipInf := range ipSet.Iter() { ip := ipInf.(string) if curIpSet.Contains(ip) { continue } if !created { family := "inet" if strings.HasPrefix(name, "pr6") { family = "inet6" } if s.Namespace == "0" { _, err = utils.ExecCombinedOutputLogged( []string{"already exists"}, "ipset", "create", name, "hash:net", "family", family, ) if err != nil { return } } else { _, err = utils.ExecCombinedOutputLogged( []string{"already exists"}, "ip", "netns", "exec", s.Namespace, "ipset", "create", name, "hash:net", "family", family, ) if err != nil { return } } created = true } if s.Namespace == "0" { _, err = utils.ExecCombinedOutputLogged( []string{"already added"}, "ipset", "add", name, ip, ) if err != nil { return } } else { _, err = utils.ExecCombinedOutputLogged( []string{"already added"}, "ip", "netns", "exec", s.Namespace, "ipset", "add", name, ip, ) if err != nil { return } } } delIpSet := curIpSet.Copy() delIpSet.Subtract(ipSet) for ipInf := range delIpSet.Iter() { ip := ipInf.(string) if s.Namespace == "0" { _, err = utils.ExecCombinedOutputLogged( []string{ "not exist", "not added", }, "ipset", "del", name, ip, ) if err != nil { return } } else { _, err = utils.ExecCombinedOutputLogged( []string{ "not exist", "not added", }, "ip", "netns", "exec", s.Namespace, "ipset", "del", name, ip, ) if err != nil { return } } } } return } ================================================ FILE: ipset/state.go ================================================ package ipset import ( "strings" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/firewall" ) type State struct { Namespaces map[string]*Sets } func (s *State) AddIngress(namespace string, ingress []*firewall.Rule) { sets := s.Namespaces[namespace] if sets == nil { sets = &Sets{ Namespace: namespace, Sets: map[string]set.Set{}, } s.Namespaces[namespace] = sets } for _, rule := range ingress { name := rule.SetName(false) name6 := rule.SetName(true) if name == "" || name6 == "" || rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { continue } for _, sourceIp := range rule.SourceIps { if sourceIp == "0.0.0.0/0" || sourceIp == "::/0" { continue } ruleName := "" ipv6 := strings.Contains(sourceIp, ":") if ipv6 { sourceIp = strings.Replace(sourceIp, "/128", "", 1) ruleName = name6 } else { sourceIp = strings.Replace(sourceIp, "/32", "", 1) ruleName = name } ruleSet := sets.Sets[ruleName] if ruleSet == nil { ruleSet = set.NewSet() sets.Sets[ruleName] = ruleSet } ruleSet.Add(sourceIp) } } } func (s *State) AddSourceDestCheck(namespace, addr6 string) { sets := s.Namespaces[namespace] if sets == nil { sets = &Sets{ Namespace: namespace, Sets: map[string]set.Set{}, } s.Namespaces[namespace] = sets } sdcSet := set.NewSet() if addr6 != "" { sdcSet.Add(addr6) } sdcSet.Add("fe80::/10") sets.Sets["pr6_sdc"] = sdcSet } func (s *State) AddMember(namespace string, ruleName, member string) { if strings.HasPrefix(ruleName, "prx") { return } sets := s.Namespaces[namespace] if sets == nil { sets = &Sets{ Namespace: namespace, Sets: map[string]set.Set{}, } s.Namespaces[namespace] = sets } ruleSet := sets.Sets[ruleName] if ruleSet == nil { ruleSet = set.NewSet() sets.Sets[ruleName] = ruleSet } ruleSet.Add(member) } type NamesState struct { Namespaces map[string]*Names } func (n *NamesState) AddIngress(namespace string, ingress []*firewall.Rule) { sets := n.Namespaces[namespace] if sets == nil { sets = &Names{ Namespace: namespace, Sets: set.NewSet(), } n.Namespaces[namespace] = sets } for _, rule := range ingress { name := rule.SetName(false) name6 := rule.SetName(true) if name == "" || name6 == "" || rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { continue } for _, sourceIp := range rule.SourceIps { if sourceIp == "0.0.0.0/0" || sourceIp == "::/0" { continue } ipv6 := strings.Contains(sourceIp, ":") if ipv6 { sets.Sets.Add(name6) } else { sets.Sets.Add(name) } } } } func (n *NamesState) AddSourceDestCheck(namespace string) { sets := n.Namespaces[namespace] if sets == nil { sets = &Names{ Namespace: namespace, Sets: set.NewSet(), } n.Namespaces[namespace] = sets } sets.Sets.Add("pr6_sdc") } func (n *NamesState) AddName(namespace string, ruleName string) { if strings.HasPrefix(ruleName, "prx") { return } sets := n.Namespaces[namespace] if sets == nil { sets = &Names{ Namespace: namespace, Sets: set.NewSet(), } n.Namespaces[namespace] = sets } sets.Sets.Add(ruleName) } ================================================ FILE: ipset/utils.go ================================================ package ipset import ( "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) var ( curState *State curNamesState *NamesState stateLock = utils.NewTimeoutLock(3 * time.Minute) ) func UpdateState(instances []*instance.Instance, namespaces []string, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule) ( err error) { lockId := stateLock.Lock() defer stateLock.Unlock(lockId) newState := &State{ Namespaces: map[string]*Sets{}, } if nodeFirewall != nil { newState.AddIngress("0", nodeFirewall) } for _, inst := range instances { if !inst.IsActive() { continue } for i := range inst.Virt.NetworkAdapters { namespace := vm.GetNamespace(inst.Id, i) ingress := firewalls[namespace] if ingress == nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "namespace": namespace, }).Warn("ipset: Failed to load instance firewall rules") continue } addr6 := "" if inst.PrivateIps6 != nil && len(inst.PrivateIps6) != 0 { addr6 = inst.PrivateIps6[0] } newState.AddIngress(namespace, ingress) if !inst.SkipSourceDestCheck { newState.AddSourceDestCheck(namespace, addr6) } } } err = applyState(curState, newState, namespaces) if err != nil { return } curState = newState return } func applyState(oldState, newState *State, namespaces []string) (err error) { namespacesSet := set.NewSet() for _, namespace := range namespaces { namespacesSet.Add(namespace) } for _, ipSet := range newState.Namespaces { if ipSet.Namespace != "0" && !namespacesSet.Contains( ipSet.Namespace) { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "add", ipSet.Namespace, ) if err != nil { return } } curSet := oldState.Namespaces[ipSet.Namespace] err = ipSet.Apply(curSet) if err != nil { return } } return } func UpdateNamesState(instances []*instance.Instance, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule) ( err error) { lockId := stateLock.Lock() defer stateLock.Unlock(lockId) newNamesState := &NamesState{ Namespaces: map[string]*Names{}, } if nodeFirewall != nil { newNamesState.AddIngress("0", nodeFirewall) } for _, inst := range instances { if !inst.IsActive() { continue } for i := range inst.Virt.NetworkAdapters { namespace := vm.GetNamespace(inst.Id, i) ingress := firewalls[namespace] if ingress == nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "namespace": namespace, }).Warn("ipset: Failed to load instance firewall rules") continue } newNamesState.AddIngress(namespace, ingress) if !inst.SkipSourceDestCheck { newNamesState.AddSourceDestCheck(namespace) } } } err = applyNamesState(curNamesState, newNamesState) if err != nil { return } curNamesState = newNamesState return } func applyNamesState(oldNamesState, newNamesState *NamesState) (err error) { for _, ipSet := range newNamesState.Namespaces { curSet := oldNamesState.Namespaces[ipSet.Namespace] err = ipSet.Apply(curSet) if err != nil { return } } return } func loadIpset(namespace string, state *State, namesState *NamesState) ( err error) { output := "" if namespace == "0" { output, err = utils.ExecOutput("", "ipset", "list") if err != nil { return } } else { output, err = utils.ExecOutput("", "ip", "netns", "exec", namespace, "ipset", "list") if err != nil { return } } curName := "" isMembers := false for _, line := range strings.Split(output, "\n") { if strings.HasPrefix(line, "Name:") { curName = strings.TrimSpace(strings.SplitN(line, ":", 2)[1]) isMembers = false } else if isMembers { if line == "" { isMembers = false } else { member := strings.TrimSpace(line) state.AddMember(namespace, curName, member) namesState.AddName(namespace, curName) } } else if strings.HasPrefix(line, "Members:") { isMembers = true } } return } func Init(namespaces []string, instances []*instance.Instance, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule) ( err error) { state := &State{ Namespaces: map[string]*Sets{}, } namesState := &NamesState{ Namespaces: map[string]*Names{}, } err = loadIpset("0", state, namesState) if err != nil { return } for _, namespace := range namespaces { err = loadIpset(namespace, state, namesState) if err != nil { return } } curState = state curNamesState = namesState err = UpdateState(instances, namespaces, nodeFirewall, firewalls) if err != nil { return } return } func InitNames(namespaces []string, instances []*instance.Instance, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule) ( err error) { err = UpdateNamesState(instances, nodeFirewall, firewalls) if err != nil { return } return } ================================================ FILE: iptables/iptables.go ================================================ package iptables import ( "fmt" "sort" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/ipvs" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) var ( curState *State stateLock = utils.NewTimeoutLock(3 * time.Minute) ) func (r *Rules) newCommand() (cmd []string) { chain := "" if r.Interface == "host" { chain = "INPUT" } else { chain = "FORWARD" } cmd = []string{ chain, } return } func (r *Rules) newCommandNatPre() (cmd []string) { cmd = []string{ "PREROUTING", } return } func (r *Rules) newCommandNatPost() (cmd []string) { cmd = []string{ "POSTROUTING", } return } func (r *Rules) newCommandMap() (cmd []string) { cmd = []string{ "PREROUTING", } return } func (r *Rules) newCommandMapPost() (cmd []string) { cmd = []string{ "POSTROUTING", } return } func (r *Rules) commentCommand(inCmd []string, hold bool) (cmd []string) { comment := "" if hold { comment = "pritunl_cloud_hold" } else { comment = "pritunl_cloud_rule" } cmd = append(inCmd, "-m", "comment", "--comment", comment, ) return } func (r *Rules) commentCommandHeader(inCmd []string) (cmd []string) { cmd = append(inCmd, "-m", "comment", "--comment", "pritunl_cloud_head", ) return } func (r *Rules) commentCommandSdc(inCmd []string) (cmd []string) { cmd = append(inCmd, "-m", "comment", "--comment", "pritunl_cloud_sdc", ) return } func (r *Rules) commentCommandNat(inCmd []string) (cmd []string) { cmd = append(inCmd, "-m", "comment", "--comment", "pritunl_cloud_nat", ) return } func (r *Rules) commentCommandMap(inCmd []string) (cmd []string) { cmd = append(inCmd, "-m", "comment", "--comment", "pritunl_cloud_map", ) return } func (r *Rules) run(table string, cmds [][]string, ipCmd string, ipv6 bool) (err error) { iptablesCmd := getIptablesCmd(ipv6) for _, cmd := range cmds { cmd = append([]string{ipCmd}, cmd...) if table != "" { cmd = append([]string{"-t", table}, cmd...) } if r.Namespace != "0" { cmd = append([]string{ "netns", "exec", r.Namespace, iptablesCmd, }, cmd...) } for i := 0; i < 3; i++ { output := "" if r.Namespace == "0" { Lock() output, err = utils.ExecCombinedOutputLogged( []string{ "matching rule exist", }, iptablesCmd, cmd...) Unlock() } else { Lock() output, err = utils.ExecCombinedOutputLogged( []string{ "matching rule exist", "Cannot open network namespace", }, "ip", cmd...) Unlock() } if err != nil { if i < 2 { err = nil time.Sleep(250 * time.Millisecond) continue } else if cmd[len(cmd)-1] == "ACCEPT" { err = nil logrus.WithFields(logrus.Fields{ "ipv6": ipv6, "command": cmd, "output": output, }).Error("iptables: Ignoring invalid iptables command") } else { logrus.WithFields(logrus.Fields{ "ipv6": ipv6, "command": cmd, "output": output, }).Warn("iptables: Failed to run iptables command") return } } break } } return } func (r *Rules) Apply(diff *RulesDiff) (err error) { if diff == nil || diff.HeaderDiff { err = r.run("", r.Header, "-A", false) if err != nil { return } } if diff == nil || diff.Header6Diff { err = r.run("", r.Header6, "-A", true) if err != nil { return } } if diff == nil || diff.SourceDestCheckDiff { err = r.run("", r.SourceDestCheck, "-A", false) if err != nil { return } } if diff == nil || diff.SourceDestCheck6Diff { err = r.run("", r.SourceDestCheck6, "-A", true) if err != nil { return } } if diff == nil || diff.IngressDiff { err = r.run("", r.Ingress, "-A", false) if err != nil { return } } if diff == nil || diff.Ingress6Diff { err = r.run("", r.Ingress6, "-A", true) if err != nil { return } } if diff == nil || diff.NatsDiff { err = r.run("nat", r.Nats, "-A", false) if err != nil { return } } if diff == nil || diff.Nats6Diff { err = r.run("nat", r.Nats6, "-A", true) if err != nil { return } } if diff == nil || diff.MapsDiff { err = r.run("nat", r.Maps, "-A", false) if err != nil { return } } if diff == nil || diff.Maps6Diff { err = r.run("nat", r.Maps6, "-A", true) if err != nil { return } } if diff == nil || diff.IngressDiff { err = r.run("", r.Holds, "-D", false) if err != nil { return } r.Holds = [][]string{} } if diff == nil || diff.Ingress6Diff { err = r.run("", r.Holds6, "-D", true) if err != nil { return } r.Holds6 = [][]string{} } return } func (r *Rules) Hold() (err error) { cmd := r.newCommand() if r.Interface != "host" { if strings.HasPrefix(r.Interface, "e") { cmd = append(cmd, "-i", r.Interface+"+", ) } else if strings.HasPrefix(r.Interface, "h") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "m") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "i") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "o") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "p") { cmd = append(cmd, "-m", "physdev", "--physdev-out", r.Interface, "--physdev-is-bridged", ) } else { err = &errortypes.ParseError{ errors.Newf("iptables: Unknown interface type %s", r.Interface), } return } } cmd = r.commentCommand(cmd, true) cmd = append(cmd, "-j", "DROP", ) r.Holds = append(r.Holds, cmd) cmd = r.newCommand() if r.Interface != "host" { if strings.HasPrefix(r.Interface, "e") { cmd = append(cmd, "-i", r.Interface+"+", ) } else if strings.HasPrefix(r.Interface, "h") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "m") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "i") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "o") { cmd = append(cmd, "-i", r.Interface, ) } else if strings.HasPrefix(r.Interface, "p") { cmd = append(cmd, "-m", "physdev", "--physdev-out", r.Interface, "--physdev-is-bridged", ) } else { err = &errortypes.ParseError{ errors.Newf("iptables: Unknown interface type %s", r.Interface), } return } } cmd = r.commentCommand(cmd, true) cmd = append(cmd, "-j", "DROP", ) r.Holds6 = append(r.Holds6, cmd) err = r.run("", r.Holds, "-A", false) if err != nil { return } err = r.run("", r.Holds6, "-A", true) if err != nil { return } return } func (r *Rules) Remove(diff *RulesDiff) (err error) { if diff == nil || diff.HeaderDiff { err = r.run("", r.Header, "-D", false) if err != nil { return } r.Header = [][]string{} } if diff == nil || diff.Header6Diff { err = r.run("", r.Header6, "-D", true) if err != nil { return } r.Header6 = [][]string{} } if diff == nil || diff.SourceDestCheckDiff { err = r.run("", r.SourceDestCheck, "-D", false) if err != nil { return } r.SourceDestCheck = [][]string{} } if diff == nil || diff.SourceDestCheck6Diff { err = r.run("", r.SourceDestCheck6, "-D", true) if err != nil { return } r.SourceDestCheck6 = [][]string{} } if diff == nil || diff.IngressDiff { err = r.run("", r.Ingress, "-D", false) if err != nil { return } r.Ingress = [][]string{} } if diff == nil || diff.Ingress6Diff { err = r.run("", r.Ingress6, "-D", true) if err != nil { return } r.Ingress6 = [][]string{} } if diff == nil || diff.NatsDiff { err = r.run("nat", r.Nats, "-D", false) if err != nil { return } r.Nats = [][]string{} } if diff == nil || diff.Nats6Diff { err = r.run("nat", r.Nats6, "-D", true) if err != nil { return } r.Nats6 = [][]string{} } if diff == nil || diff.MapsDiff { err = r.run("nat", r.Maps, "-D", false) if err != nil { return } r.Maps = [][]string{} } if diff == nil || diff.Maps6Diff { err = r.run("nat", r.Maps6, "-D", true) if err != nil { return } r.Maps6 = [][]string{} } err = r.run("", r.Holds, "-D", false) if err != nil { return } r.Holds = [][]string{} err = r.run("", r.Holds6, "-D", true) if err != nil { return } r.Holds6 = [][]string{} return } func generateVirt(vc *vpc.Vpc, namespace, iface, addr, addr6 string, sourceDestCheck bool, ingress []*firewall.Rule) (rules *Rules) { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } cmd := rules.newCommand() cmd = append(cmd, "-p", "ipv6-icmp", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "icmp6", "--icmpv6-type", "133", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() cmd = append(cmd, "-p", "ipv6-icmp", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "icmp6", "--icmpv6-type", "134", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() cmd = append(cmd, "-p", "ipv6-icmp", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "icmp6", "--icmpv6-type", "135", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() cmd = append(cmd, "-p", "ipv6-icmp", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "icmp6", "--icmpv6-type", "136", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) if sourceDestCheck { if addr != "" { cmd := rules.newCommand() cmd = append(cmd, "!", "-s", addr+"/32", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-in", rules.Interface, "--physdev-is-bridged", ) } cmd = rules.commentCommandSdc(cmd) cmd = append(cmd, "-j", "DROP", ) rules.SourceDestCheck = append(rules.SourceDestCheck, cmd) } cmd := rules.newCommand() cmd = append(cmd, "-m", "set", "!", "--match-set", "pr6_sdc", "src", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-in", rules.Interface, "--physdev-is-bridged", ) } cmd = rules.commentCommandSdc(cmd) cmd = append(cmd, "-j", "DROP", ) rules.SourceDestCheck6 = append(rules.SourceDestCheck6, cmd) if addr != "" { cmd = rules.newCommand() cmd = append(cmd, "!", "-d", addr+"/32", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = rules.commentCommandSdc(cmd) cmd = append(cmd, "-j", "DROP", ) rules.SourceDestCheck = append(rules.SourceDestCheck, cmd) } cmd = rules.newCommand() cmd = append(cmd, "-m", "set", "!", "--match-set", "pr6_sdc", "dst", ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = rules.commentCommandSdc(cmd) cmd = append(cmd, "-j", "DROP", ) rules.SourceDestCheck6 = append(rules.SourceDestCheck6, cmd) } cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress6 = append(rules.Ingress6, cmd) for _, rule := range ingress { all4 := false all6 := false set4 := false set6 := false setName := rule.SetName(false) setName6 := rule.SetName(true) if setName == "" || setName6 == "" { continue } if rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { cmd = rules.newCommand() cmd = append(cmd, "-p", "udp", "-m", "pkttype", "--pkt-type", rule.Protocol, ) if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "udp", "--dport", strings.Replace(rule.Port, "-", ":", 1), ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) continue } for _, sourceIp := range rule.SourceIps { ipv6 := strings.Contains(sourceIp, ":") if sourceIp == "0.0.0.0/0" { if all4 { continue } all4 = true } else if sourceIp == "::/0" { if all6 { continue } all6 = true } else { if ipv6 { if set6 { continue } set6 = true } else { if set4 { continue } set4 = true } } cmd = rules.newCommand() switch rule.Protocol { case firewall.All: break case firewall.Icmp: if ipv6 { cmd = append(cmd, "-p", "ipv6-icmp", ) } else { cmd = append(cmd, "-p", "icmp", ) } break case firewall.Multicast, firewall.Broadcast: cmd = append(cmd, "-p", "udp", "-m", "pkttype", "--pkt-type", rule.Protocol, ) break case firewall.Tcp, firewall.Udp: cmd = append(cmd, "-p", rule.Protocol, ) break default: continue } if sourceIp != "0.0.0.0/0" && sourceIp != "::/0" && rule.Protocol != firewall.Multicast && rule.Protocol != firewall.Broadcast { if ipv6 { cmd = append(cmd, "-m", "set", "--match-set", setName6, "src", ) } else { cmd = append(cmd, "-m", "set", "--match-set", setName, "src", ) } } if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } switch rule.Protocol { case firewall.Multicast, firewall.Broadcast: cmd = append(cmd, "-m", "udp", "--dport", strings.Replace(rule.Port, "-", ":", 1), ) break case firewall.Tcp, firewall.Udp: cmd = append(cmd, "-m", rule.Protocol, "--dport", strings.Replace(rule.Port, "-", ":", 1), "-m", "conntrack", "--ctstate", "NEW", ) break } if rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) if ipv6 { rules.Header6 = append(rules.Header6, cmd) } else { rules.Header = append(rules.Header, cmd) } } else { cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) if ipv6 { rules.Ingress6 = append(rules.Ingress6, cmd) } else { rules.Ingress = append(rules.Ingress, cmd) } } } } cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress6 = append(rules.Ingress6, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-m", "physdev", "--physdev-out", rules.Interface, "--physdev-is-bridged", ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress6 = append(rules.Ingress6, cmd) if vc != nil && vc.Maps != nil { for _, mp := range vc.Maps { if mp.Type != vpc.Destination { continue } if strings.Contains(mp.Target, ":") { if addr6 != "" { cmd = rules.newCommandMap() cmd = append(cmd, "-s", addr+"/128", "-d", mp.Destination, ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", mp.Target, ) rules.Maps6 = append(rules.Maps6, cmd) } } else { if addr != "" { cmd = rules.newCommandMap() cmd = append(cmd, "-s", addr+"/32", "-d", mp.Destination, ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", mp.Target, ) rules.Maps = append(rules.Maps, cmd) } } } } return } func generateInternal(namespace, iface string, nat, nat6, dhcp, dhcp6 bool, natAddr, natPubAddr, natAddr6, natPubAddr6 string, ingress []*firewall.Rule) (rules *Rules) { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } if strings.HasPrefix(iface, "e") { iface = iface + "+" } if nat && natAddr != "" && natPubAddr != "" { cmd := rules.newCommandNatPre() cmd = append(cmd, "-d", natPubAddr+"/32", ) cmd = rules.commentCommandNat(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", natAddr, ) rules.Nats = append(rules.Nats, cmd) cmd = rules.newCommandNatPost() cmd = append(cmd, "-s", natAddr+"/32", "-d", natAddr+"/32", ) cmd = rules.commentCommandNat(cmd) cmd = append(cmd, "-j", "SNAT", "--to-source", natPubAddr, ) rules.Nats = append(rules.Nats, cmd) cmd = rules.newCommandNatPost() cmd = append(cmd, "-s", natAddr+"/32", "-o", iface, ) cmd = rules.commentCommandNat(cmd) cmd = append(cmd, "-j", "MASQUERADE", ) rules.Nats = append(rules.Nats, cmd) } if nat6 && natAddr6 != "" && natPubAddr6 != "" { cmd := rules.newCommandNatPre() cmd = append(cmd, "-d", natPubAddr6+"/128", ) cmd = rules.commentCommandNat(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", natAddr6, ) rules.Nats6 = append(rules.Nats6, cmd) cmd = rules.newCommandNatPost() cmd = append(cmd, "-s", natAddr6+"/128", "-d", natAddr6+"/128", ) cmd = rules.commentCommandNat(cmd) cmd = append(cmd, "-j", "SNAT", "--to-source", natPubAddr6, ) rules.Nats6 = append(rules.Nats6, cmd) cmd = rules.newCommandNatPost() cmd = append(cmd, "-s", natAddr6+"/128", "-o", iface, ) cmd = rules.commentCommandNat(cmd) cmd = append(cmd, "-j", "MASQUERADE", ) rules.Nats6 = append(rules.Nats6, cmd) } cmd := rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "133", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "134", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "135", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "136", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress6 = append(rules.Ingress6, cmd) for _, rule := range ingress { all4 := false all6 := false set4 := false set6 := false setName := rule.SetName(false) setName6 := rule.SetName(true) if setName == "" || setName6 == "" { continue } if rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-p", "udp", "-m", "pkttype", "--pkt-type", rule.Protocol, ) cmd = append(cmd, "-m", "udp", "--dport", strings.Replace(rule.Port, "-", ":", 1), ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) continue } for _, sourceIp := range rule.SourceIps { ipv6 := strings.Contains(sourceIp, ":") if sourceIp == "0.0.0.0/0" { if all4 { continue } all4 = true } else if sourceIp == "::/0" { if all6 { continue } all6 = true } else { if ipv6 { if set6 { continue } set6 = true } else { if set4 { continue } set4 = true } } cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } switch rule.Protocol { case firewall.All: break case firewall.Icmp: if ipv6 { cmd = append(cmd, "-p", "ipv6-icmp", ) } else { cmd = append(cmd, "-p", "icmp", ) } break case firewall.Multicast, firewall.Broadcast: cmd = append(cmd, "-p", "udp", "-m", "pkttype", "--pkt-type", rule.Protocol, ) break case firewall.Tcp, firewall.Udp: cmd = append(cmd, "-p", rule.Protocol, ) break default: continue } if sourceIp != "0.0.0.0/0" && sourceIp != "::/0" && rule.Protocol != firewall.Multicast && rule.Protocol != firewall.Broadcast { if ipv6 { cmd = append(cmd, "-m", "set", "--match-set", setName6, "src", ) } else { cmd = append(cmd, "-m", "set", "--match-set", setName, "src", ) } } switch rule.Protocol { case firewall.Multicast, firewall.Broadcast: cmd = append(cmd, "-m", "udp", "--dport", strings.Replace(rule.Port, "-", ":", 1), ) break case firewall.Tcp, firewall.Udp: cmd = append(cmd, "-m", rule.Protocol, "--dport", strings.Replace(rule.Port, "-", ":", 1), "-m", "conntrack", "--ctstate", "NEW", ) break } if rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) if ipv6 { rules.Header6 = append(rules.Header6, cmd) } else { rules.Header = append(rules.Header, cmd) } } else { cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) if ipv6 { rules.Ingress6 = append(rules.Ingress6, cmd) } else { rules.Ingress = append(rules.Ingress, cmd) } } } } cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress6 = append(rules.Ingress6, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if iface != "host" { cmd = append(cmd, "-i", iface, ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress6 = append(rules.Ingress6, cmd) return } func generateNodePort(namespace, iface string, addr, nodePortGateway string, firewallMaps []*firewall.Mapping) (rules *Rules) { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } cmd := rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-i", rules.Interface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) for _, mapping := range firewallMaps { if mapping.Protocol != firewall.Tcp && mapping.Protocol != firewall.Udp { continue } cmd = rules.newCommandMap() cmd = append(cmd, "-i", iface, "-p", mapping.Protocol, "-m", mapping.Protocol, "--dport", fmt.Sprintf("%d", mapping.ExternalPort), ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", fmt.Sprintf( "%s:%d", addr, mapping.InternalPort, ), ) rules.Maps = append(rules.Maps, cmd) cmd = rules.newCommand() cmd = append(cmd, "-s", nodePortGateway+"/32", ) if rules.Interface != "host" { cmd = append(cmd, "-i", rules.Interface, ) } cmd = append(cmd, "-p", mapping.Protocol, "-m", mapping.Protocol, "--dport", fmt.Sprintf("%d", mapping.InternalPort), "-m", "conntrack", "--ctstate", "NEW", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) } cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-i", rules.Interface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-i", rules.Interface, ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) return } func generateHost(namespace, iface string, nodePortNetwork bool, nodePortGateway, defaultIface string, externalIfaces, puiblicIps []string, ingress []*firewall.Rule, nodePortMappings map[string][]*firewall.Mapping) (rules *Rules) { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, Ipvs: ipvs.New(), } if rules.Interface == "host" { cmd := rules.newCommand() cmd = append(cmd, "-i", "lo", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) } if rules.Interface == "host" { cmd := rules.newCommand() cmd = append(cmd, "-i", "lo", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress6 = append(rules.Ingress6, cmd) } cmd := rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "133", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "134", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "135", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-p", "ipv6-icmp", "-m", "icmp6", "--icmpv6-type", "136", ) cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) rules.Header6 = append(rules.Header6, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress6 = append(rules.Ingress6, cmd) for _, rule := range ingress { all4 := false all6 := false set4 := false set6 := false setName := rule.SetName(false) setName6 := rule.SetName(true) if setName == "" || setName6 == "" { continue } if rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-p", "udp", "-m", "pkttype", "--pkt-type", rule.Protocol, ) cmd = append(cmd, "-m", "udp", "--dport", strings.Replace(rule.Port, "-", ":", 1), ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) rules.Ingress = append(rules.Ingress, cmd) continue } for _, sourceIp := range rule.SourceIps { ipv6 := strings.Contains(sourceIp, ":") if sourceIp == "0.0.0.0/0" { if all4 { continue } all4 = true } else if sourceIp == "::/0" { if all6 { continue } all6 = true } else { if ipv6 { if set6 { continue } set6 = true } else { if set4 { continue } set4 = true } } cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } switch rule.Protocol { case firewall.All: break case firewall.Icmp: if ipv6 { cmd = append(cmd, "-p", "ipv6-icmp", ) } else { cmd = append(cmd, "-p", "icmp", ) } break case firewall.Multicast, firewall.Broadcast: cmd = append(cmd, "-p", "udp", "-m", "pkttype", "--pkt-type", rule.Protocol, ) break case firewall.Tcp, firewall.Udp: cmd = append(cmd, "-p", rule.Protocol, ) break default: continue } if sourceIp != "0.0.0.0/0" && sourceIp != "::/0" && rule.Protocol != firewall.Multicast && rule.Protocol != firewall.Broadcast { if ipv6 { cmd = append(cmd, "-m", "set", "--match-set", setName6, "src", ) } else { cmd = append(cmd, "-m", "set", "--match-set", setName, "src", ) } } switch rule.Protocol { case firewall.Multicast, firewall.Broadcast: cmd = append(cmd, "-m", "udp", "--dport", strings.Replace(rule.Port, "-", ":", 1), ) break case firewall.Tcp, firewall.Udp: cmd = append(cmd, "-m", rule.Protocol, "--dport", strings.Replace(rule.Port, "-", ":", 1), "-m", "conntrack", "--ctstate", "NEW", ) break } if rule.Protocol == firewall.Multicast || rule.Protocol == firewall.Broadcast { cmd = rules.commentCommandHeader(cmd) cmd = append(cmd, "-j", "ACCEPT", ) if ipv6 { rules.Header6 = append(rules.Header6, cmd) } else { rules.Header = append(rules.Header, cmd) } } else { cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "ACCEPT", ) if ipv6 { rules.Ingress6 = append(rules.Ingress6, cmd) } else { rules.Ingress = append(rules.Ingress, cmd) } } } } cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = append(cmd, "-m", "conntrack", "--ctstate", "INVALID", ) cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress6 = append(rules.Ingress6, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress = append(rules.Ingress, cmd) cmd = rules.newCommand() if rules.Interface != "host" { cmd = append(cmd, "-o", rules.Interface, ) } cmd = rules.commentCommand(cmd, false) cmd = append(cmd, "-j", "DROP", ) rules.Ingress6 = append(rules.Ingress6, cmd) if nodePortNetwork { nodePortKeys := make([]string, 0, len(nodePortMappings)) for k := range nodePortMappings { nodePortKeys = append(nodePortKeys, k) } sort.Strings(nodePortKeys) for _, nodePortAddr := range nodePortKeys { mappings := nodePortMappings[nodePortAddr] for _, mapping := range mappings { if mapping.Protocol != firewall.Tcp && mapping.Protocol != firewall.Udp { continue } if mapping.Ipvs { ipvsProtocol := "" if mapping.Protocol == firewall.Udp { ipvsProtocol = ipvs.Udp } else { ipvsProtocol = ipvs.Tcp } for _, publicIp := range puiblicIps { rules.Ipvs.AddTarget(publicIp, mapping.Address, mapping.ExternalPort, ipvsProtocol) } // Alternative to full SNAT // cmd = rules.newCommandMapPost() // cmd = append(cmd, // "-m", "ipvs", // "--vproto", protocolIndex(mapping.Protocol), // "--vport", fmt.Sprintf("%d", mapping.ExternalPort), // "--vdir", "ORIGINAL", // ) // cmd = rules.commentCommandMap(cmd) // cmd = append(cmd, // "-j", "SNAT", // "--to-source", nodePortGateway, // ) // rules.Maps = append(rules.Maps, cmd) } else { nodePortIfaces := []string{} if defaultIface != "" { nodePortIfaces = append(nodePortIfaces, defaultIface) } for _, iface := range externalIfaces { if iface != defaultIface { nodePortIfaces = append(nodePortIfaces, iface) } } for _, externalIface := range nodePortIfaces { cmd = rules.newCommandMap() cmd = append(cmd, "-i", externalIface, "-p", mapping.Protocol, "-m", mapping.Protocol, "--dport", fmt.Sprintf("%d", mapping.ExternalPort), ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", fmt.Sprintf( "%s:%d", nodePortAddr, mapping.ExternalPort, ), ) rules.Maps = append(rules.Maps, cmd) } } } } cmd = rules.newCommandMapPost() cmd = append(cmd, "-o", settings.Hypervisor.NodePortNetworkName, ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "SNAT", "--to-source", nodePortGateway, ) rules.Maps = append(rules.Maps, cmd) } return } func generateHostNodePort(namespace, iface string, nodePortGateway, defaultIface string, externalIfaces, puiblicIps []string, nodePortMappings map[string][]*firewall.Mapping) (rules *Rules) { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, Ipvs: ipvs.New(), } nodePortKeys := make([]string, 0, len(nodePortMappings)) for k := range nodePortMappings { nodePortKeys = append(nodePortKeys, k) } sort.Strings(nodePortKeys) for _, nodePortAddr := range nodePortKeys { mappings := nodePortMappings[nodePortAddr] for _, mapping := range mappings { if mapping.Protocol != firewall.Tcp && mapping.Protocol != firewall.Udp { continue } if mapping.Ipvs { ipvsProtocol := "" if mapping.Protocol == firewall.Udp { ipvsProtocol = ipvs.Udp } else { ipvsProtocol = ipvs.Tcp } for _, publicIp := range puiblicIps { rules.Ipvs.AddTarget(publicIp, mapping.Address, mapping.ExternalPort, ipvsProtocol) } // Alternative to full SNAT // cmd = rules.newCommandMapPost() // cmd = append(cmd, // "-m", "ipvs", // "--vproto", protocolIndex(mapping.Protocol), // "--vport", fmt.Sprintf("%d", mapping.ExternalPort), // "--vdir", "ORIGINAL", // ) // cmd = rules.commentCommandMap(cmd) // cmd = append(cmd, // "-j", "SNAT", // "--to-source", nodePortGateway, // ) // rules.Maps = append(rules.Maps, cmd) } else { nodePortIfaces := []string{} if defaultIface != "" { nodePortIfaces = append(nodePortIfaces, defaultIface) } for _, iface := range externalIfaces { if iface != defaultIface { nodePortIfaces = append(nodePortIfaces, iface) } } for _, externalIface := range nodePortIfaces { cmd := rules.newCommandMap() cmd = append(cmd, "-i", externalIface, "-p", mapping.Protocol, "-m", mapping.Protocol, "--dport", fmt.Sprintf("%d", mapping.ExternalPort), ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "DNAT", "--to-destination", fmt.Sprintf( "%s:%d", nodePortAddr, mapping.ExternalPort, ), ) rules.Maps = append(rules.Maps, cmd) } } } } cmd := rules.newCommandMapPost() cmd = append(cmd, "-o", settings.Hypervisor.NodePortNetworkName, ) cmd = rules.commentCommandMap(cmd) cmd = append(cmd, "-j", "SNAT", "--to-source", nodePortGateway, ) rules.Maps = append(rules.Maps, cmd) return } ================================================ FILE: iptables/lock.go ================================================ package iptables import ( "sync" ) var lock = sync.Mutex{} func Lock() { lock.Lock() } func Unlock() { lock.Unlock() } ================================================ FILE: iptables/rules.go ================================================ package iptables import ( "github.com/pritunl/pritunl-cloud/ipvs" ) type Rules struct { Namespace string Interface string Header [][]string Header6 [][]string SourceDestCheck [][]string SourceDestCheck6 [][]string Ingress [][]string Ingress6 [][]string Nats [][]string Nats6 [][]string Maps [][]string Maps6 [][]string Holds [][]string Holds6 [][]string Ipvs *ipvs.State } type RulesDiff struct { HeaderDiff bool Header6Diff bool SourceDestCheckDiff bool SourceDestCheck6Diff bool IngressDiff bool Ingress6Diff bool NatsDiff bool Nats6Diff bool MapsDiff bool Maps6Diff bool HoldsDiff bool Holds6Diff bool } ================================================ FILE: iptables/state.go ================================================ package iptables import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) type State struct { Interfaces map[string]*Rules } func LoadState(nodeSelf *node.Node, vpcs []*vpc.Vpc, instances []*instance.Instance, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule, firewallMaps map[string][]*firewall.Mapping) (state *State) { vpcsMap := map[bson.ObjectID]*vpc.Vpc{} for _, vc := range vpcs { vpcsMap[vc.Id] = vc } nodeNetworkMode := node.Self.NetworkMode if nodeNetworkMode == "" { nodeNetworkMode = node.Dhcp } nodeNetworkMode6 := node.Self.NetworkMode6 if nodeNetworkMode6 == "" { nodeNetworkMode6 = node.Dhcp } nodePortNetwork := !node.Self.NoNodePortNetwork state = &State{ Interfaces: map[string]*Rules{}, } hostNodePortMappings := map[string][]*firewall.Mapping{} nodePortGateway, err := block.GetNodePortGateway() if err != nil { return } for _, inst := range instances { if !inst.IsActive() { continue } namespace := vm.GetNamespace(inst.Id, 0) iface := vm.GetIface(inst.Id, 0) ifaceHost := vm.GetIfaceHost(inst.Id, 1) ifaceNodePort := vm.GetIfaceNodePort(inst.Id, 1) ifaceExternal := vm.GetIfaceExternal(inst.Id, 0) addr := "" addr6 := "" pubAddr := "" pubAddr6 := "" if len(inst.PrivateIps) != 0 { addr = inst.PrivateIps[0] } if len(inst.PrivateIps6) != 0 { addr6 = inst.PrivateIps6[0] } if len(inst.PublicIps) != 0 { pubAddr = inst.PublicIps[0] } if len(inst.PublicIps6) != 0 { pubAddr6 = inst.PublicIps6[0] } else if len(inst.CloudPublicIps6) != 0 { pubAddr6 = inst.CloudPublicIps6[0] } nodePortAddr := "" if len(inst.NodePortIps) != 0 { nodePortAddr = inst.NodePortIps[0] } hostNodePortMappings[nodePortAddr] = firewallMaps[namespace] cloudAddr := "" cloudIface := vm.GetIfaceCloud(inst.Id, 0) if len(inst.CloudPrivateIps) != 0 { cloudAddr = inst.CloudPrivateIps[0] } _, ok := state.Interfaces[namespace+"-"+iface] if ok { logrus.WithFields(logrus.Fields{ "namespace": namespace, "interface": iface, }).Error("iptables: Virtual interface conflict") continue } ingress := firewalls[namespace] if ingress == nil { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "namespace": namespace, }).Warn("iptables: Failed to load instance firewall rules") continue } dhcp := false dhcp6 := false if nodeNetworkMode == node.Dhcp { dhcp = true } if nodeNetworkMode6 == node.Dhcp { dhcp6 = true } nat6 := false if nodeNetworkMode != node.Disabled && nodeNetworkMode != node.Cloud { if nodeNetworkMode6 != node.Disabled && nodeNetworkMode6 != node.Cloud { nat6 = true } rules := generateInternal(namespace, ifaceExternal, true, nat6, dhcp, dhcp6, addr, pubAddr, addr6, pubAddr6, ingress) state.Interfaces[namespace+"-"+ifaceExternal] = rules } else if nodeNetworkMode6 != node.Disabled && nodeNetworkMode6 != node.Cloud { rules := generateInternal(namespace, ifaceExternal, false, true, dhcp, dhcp6, addr, pubAddr, addr6, pubAddr6, ingress) state.Interfaces[namespace+"-"+ifaceExternal] = rules } if nodeNetworkMode == node.Cloud { if nodeNetworkMode6 == node.Cloud { nat6 = true } rules := generateInternal(namespace, cloudIface, true, nat6, false, false, addr, cloudAddr, addr6, pubAddr6, ingress) state.Interfaces[namespace+"-"+cloudIface] = rules } rules := generateInternal(namespace, ifaceHost, false, false, false, false, "", "", "", "", ingress) state.Interfaces[namespace+"-"+ifaceHost] = rules if nodePortNetwork { rules := generateNodePort(namespace, ifaceNodePort, addr, nodePortGateway, firewallMaps[namespace]) state.Interfaces[namespace+"-"+ifaceNodePort] = rules } rules = generateVirt(vpcsMap[inst.Vpc], namespace, iface, addr, addr6, !inst.SkipSourceDestCheck, ingress) state.Interfaces[namespace+"-"+iface] = rules } if nodeFirewall != nil { state.Interfaces["0-host"] = generateHost("0", "host", !nodeSelf.NoNodePortNetwork, nodePortGateway, nodeSelf.DefaultInterface, nodeSelf.ExternalInterfaces, nodeSelf.PublicIps, nodeFirewall, hostNodePortMappings) } else if !nodeSelf.NoNodePortNetwork { state.Interfaces["0-host"] = generateHostNodePort("0", "host", nodePortGateway, nodeSelf.DefaultInterface, nodeSelf.ExternalInterfaces, nodeSelf.PublicIps, hostNodePortMappings) } return } ================================================ FILE: iptables/update.go ================================================ package iptables import ( "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/ipvs" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) type Update struct { OldState *State NewState *State Namespaces []string FailedNamespaces set.Set } func (u *Update) Apply() { changed := false var removed []string oldIfaces := set.NewSet() newIfaces := set.NewSet() namespacesSet := set.NewSet() for _, namespace := range u.Namespaces { namespacesSet.Add(namespace) } for iface := range u.OldState.Interfaces { oldIfaces.Add(iface) } for iface := range u.NewState.Interfaces { newIfaces.Add(iface) } oldIfaces.Subtract(newIfaces) for iface := range oldIfaces.Iter() { removed = append(removed, iface.(string)) err := u.OldState.Interfaces[iface.(string)].Remove(nil) if err != nil { logrus.WithFields(logrus.Fields{ "iface": iface, "error": err, }).Error("iptables: Failed to delete removed interface iptables") } } if removed != nil { logrus.WithFields(logrus.Fields{ "ifaces": removed, }).Info("iptables: Removed iptables") } for _, rules := range u.NewState.Interfaces { if u.FailedNamespaces.Contains(rules.Namespace) { logrus.WithFields(logrus.Fields{ "namespace": rules.Namespace, }).Warn("iptables: Skipping failed namespace") continue } if rules.Namespace != "0" && !namespacesSet.Contains(rules.Namespace) { _, err := utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "add", rules.Namespace, ) if err != nil { logrus.WithFields(logrus.Fields{ "namespace": rules.Namespace, "error": err, }).Error("iptables: Namespace add error") u.FailedNamespaces.Add(rules.Namespace) continue } } var diff *RulesDiff oldRules := u.OldState.Interfaces[rules.Namespace+"-"+rules.Interface] if oldRules != nil { diff = diffRules(oldRules, rules) if diff == nil { continue } if !changed { changed = true logrus.WithFields(logrus.Fields{ "ingress": diff.IngressDiff, "ingress6": diff.Ingress6Diff, "nats": diff.NatsDiff, "nats6": diff.Nats6Diff, "maps": diff.MapsDiff, "maps6": diff.Maps6Diff, "holds": diff.HoldsDiff, "holds6": diff.Holds6Diff, }).Info("iptables: Updating iptables") } if (diff.IngressDiff || diff.Ingress6Diff) && rules.Interface != "host" { err := rules.Hold() if err != nil { logrus.WithFields(logrus.Fields{ "namespace": rules.Namespace, "error": err, }).Error("iptables: Namespace hold error") u.FailedNamespaces.Add(rules.Namespace) continue } } err := oldRules.Remove(diff) if err != nil { logrus.WithFields(logrus.Fields{ "namespace": rules.Namespace, "error": err, }).Error("iptables: Namespace remove error") u.FailedNamespaces.Add(rules.Namespace) continue } } if !changed { changed = true logrus.Info("iptables: Updating iptables") } err := rules.Apply(diff) if err != nil { logrus.WithFields(logrus.Fields{ "namespace": rules.Namespace, "error": err, }).Error("iptables: Namespace apply error") u.FailedNamespaces.Add(rules.Namespace) continue } } hostIface := u.NewState.Interfaces["0-host"] if hostIface != nil && hostIface.Ipvs != nil { err := ipvs.UpdateState(u.NewState.Interfaces["0-host"].Ipvs) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("storage: Failed to update ipvs state") } } return } func (u *Update) Recover() { if u.FailedNamespaces.Contains("0") { err := RecoverNode() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to recover node iptables, retrying") time.Sleep(10 * time.Second) } } if u.FailedNamespaces.Len() > 0 { logrus.Error("deploy: Failed to update iptables, " + "reloading state") time.Sleep(10 * time.Second) err := u.reload() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to recover iptables") } } } func (u *Update) reload() (err error) { db := database.GetDatabase() defer db.Close() nodeDatacenter, err := node.Self.GetDatacenter(db) if err != nil { return } vpcs := []*vpc.Vpc{} if !nodeDatacenter.IsZero() { vpcs, err = vpc.GetDatacenter(db, nodeDatacenter) if err != nil { return } } namespaces, err := utils.GetNamespaces() if err != nil { return } instances, err := instance.GetAllVirt(db, &bson.M{ "node": node.Self.Id, }, nil, nil) if err != nil { return } specRules, nodePortsMap, err := firewall.GetSpecRulesSlow( db, node.Self.Id, instances) if err != nil { return } nodeFirewall, firewalls, firewallMaps, _, err := firewall.GetAllIngress( db, node.Self, instances, specRules, nodePortsMap) if err != nil { return } err = Init(namespaces, vpcs, instances, nodeFirewall, firewalls, firewallMaps) if err != nil { return } return } func ApplyUpdate(newState *State, namespaces []string, recover bool) { lockId := stateLock.Lock() update := &Update{ OldState: curState, NewState: newState, Namespaces: namespaces, FailedNamespaces: set.NewSet(), } update.Apply() curState = newState stateLock.Unlock(lockId) if recover { update.Recover() } return } func UpdateState(nodeSelf *node.Node, vpcs []*vpc.Vpc, instances []*instance.Instance, namespaces []string, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule, firewallMaps map[string][]*firewall.Mapping) { newState := LoadState(nodeSelf, vpcs, instances, nodeFirewall, firewalls, firewallMaps) ApplyUpdate(newState, namespaces, false) return } func UpdateStateRecover(nodeSelf *node.Node, vpcs []*vpc.Vpc, instances []*instance.Instance, namespaces []string, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule, firewallMaps map[string][]*firewall.Mapping) { newState := LoadState(nodeSelf, vpcs, instances, nodeFirewall, firewalls, firewallMaps) ApplyUpdate(newState, namespaces, true) return } ================================================ FILE: iptables/utils.go ================================================ package iptables import ( "fmt" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) func diffCmd(a, b []string) bool { if len(a) != len(b) { return true } for i := range a { if a[i] != b[i] { return true } } return false } func diffRules(a, b *Rules) *RulesDiff { diff := &RulesDiff{} changed := false if len(a.Header) != len(b.Header) { diff.HeaderDiff = true changed = true } else { for i := range a.Header { if diffCmd(a.Header[i], b.Header[i]) { diff.HeaderDiff = true changed = true break } } } if len(a.Header6) != len(b.Header6) { diff.Header6Diff = true changed = true } else { for i := range a.Header6 { if diffCmd(a.Header6[i], b.Header6[i]) { diff.Header6Diff = true changed = true break } } } if len(a.SourceDestCheck) != len(b.SourceDestCheck) { diff.SourceDestCheckDiff = true changed = true } else { for i := range a.SourceDestCheck { if diffCmd(a.SourceDestCheck[i], b.SourceDestCheck[i]) { diff.SourceDestCheckDiff = true changed = true break } } } if len(a.SourceDestCheck6) != len(b.SourceDestCheck6) { diff.SourceDestCheck6Diff = true changed = true } else { for i := range a.SourceDestCheck6 { if diffCmd(a.SourceDestCheck6[i], b.SourceDestCheck6[i]) { diff.SourceDestCheck6Diff = true changed = true break } } } if len(a.Ingress) != len(b.Ingress) { diff.IngressDiff = true changed = true } else { for i := range a.Ingress { if diffCmd(a.Ingress[i], b.Ingress[i]) { diff.IngressDiff = true changed = true break } } } if len(a.Ingress6) != len(b.Ingress6) { diff.Ingress6Diff = true changed = true } else { for i := range a.Ingress6 { if diffCmd(a.Ingress6[i], b.Ingress6[i]) { diff.Ingress6Diff = true changed = true break } } } if len(a.Nats) != len(b.Nats) { diff.NatsDiff = true changed = true } else { for i := range a.Nats { if diffCmd(a.Nats[i], b.Nats[i]) { diff.NatsDiff = true changed = true break } } } if len(a.Nats6) != len(b.Nats6) { diff.Nats6Diff = true changed = true } else { for i := range a.Nats6 { if diffCmd(a.Nats6[i], b.Nats6[i]) { diff.Nats6Diff = true changed = true break } } } if len(a.Maps) != len(b.Maps) { diff.MapsDiff = true changed = true } else { for i := range a.Maps { if diffCmd(a.Maps[i], b.Maps[i]) { diff.MapsDiff = true changed = true break } } } if len(a.Maps6) != len(b.Maps6) { diff.Maps6Diff = true changed = true } else { for i := range a.Maps6 { if diffCmd(a.Maps6[i], b.Maps6[i]) { diff.Maps6Diff = true changed = true break } } } if len(a.Holds) != len(b.Holds) { diff.HoldsDiff = true changed = true } else { for i := range a.Holds { if diffCmd(a.Holds[i], b.Holds[i]) { diff.HoldsDiff = true changed = true break } } } if len(a.Holds6) != len(b.Holds6) { diff.Holds6Diff = true changed = true } else { for i := range a.Holds6 { if diffCmd(a.Holds6[i], b.Holds6[i]) { diff.Holds6Diff = true changed = true break } } } if !changed { return nil } return diff } func getIptablesCmd(ipv6 bool) string { if ipv6 { return "ip6tables" } else { return "iptables" } } func loadIptables(namespace, instIface string, state *State, ipv6 bool) (err error) { Lock() defer Unlock() iptablesCmd := getIptablesCmd(ipv6) output := "" if namespace == "0" { output, err = utils.ExecOutput("", iptablesCmd, "-S") if err != nil { return } } else { output, err = utils.ExecOutput("", "ip", "netns", "exec", namespace, iptablesCmd, "-S") if err != nil { return } } for _, line := range strings.Split(output, "\n") { ruleComment := strings.Contains(line, "pritunl_cloud_rule") holdComment := strings.Contains(line, "pritunl_cloud_hold") headComment := strings.Contains(line, "pritunl_cloud_head") sdcComment := strings.Contains(line, "pritunl_cloud_sdc") if !ruleComment && !holdComment && !headComment && !sdcComment { continue } cmd := strings.Fields(line) if len(cmd) < 3 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables state") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables state"), } return } cmd = cmd[1:] iface := "" if sdcComment { if cmd[0] != "FORWARD" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables sdc chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables sdc chain"), } return } for i, item := range cmd { if item == "--physdev-in" || item == "--physdev-out" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables sdc interface") err = &errortypes.ParseError{ errors.New( "iptables: Invalid iptables sdc interface"), } return } iface = strings.Trim(cmd[i+1], "+") break } } } else if namespace != "0" { if cmd[0] != "FORWARD" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables chain"), } return } for i, item := range cmd { if item == "--physdev-out" || item == "-o" || item == "-i" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables interface") err = &errortypes.ParseError{ errors.New( "iptables: Invalid iptables interface"), } return } iface = strings.Trim(cmd[i+1], "+") break } } } else { iface = "host" if cmd[0] != "INPUT" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables chain"), } return } } if iface == "" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Missing iptables interface") err = &errortypes.ParseError{ errors.New("iptables: Missing iptables interface"), } return } rules := state.Interfaces[namespace+"-"+iface] if rules == nil { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } state.Interfaces[namespace+"-"+iface] = rules } if holdComment { if ipv6 { rules.Holds6 = append(rules.Holds6, cmd) } else { rules.Holds = append(rules.Holds, cmd) } } else if sdcComment { if ipv6 { rules.SourceDestCheck6 = append(rules.SourceDestCheck6, cmd) } else { rules.SourceDestCheck = append(rules.SourceDestCheck, cmd) } } else { if headComment { if ipv6 { rules.Header6 = append(rules.Header6, cmd) } else { rules.Header = append(rules.Header, cmd) } } else { if ipv6 { rules.Ingress6 = append(rules.Ingress6, cmd) } else { rules.Ingress = append(rules.Ingress, cmd) } } } } if namespace == "0" { output, err = utils.ExecOutput("", iptablesCmd, "-S", "-t", "nat") if err != nil { return } } else { output, err = utils.ExecOutput("", "ip", "netns", "exec", namespace, iptablesCmd, "-S", "-t", "nat") if err != nil { return } } postIface := "" natRules := [][]string{} for _, line := range strings.Split(output, "\n") { natComment := strings.Contains(line, "pritunl_cloud_nat") mapComment := strings.Contains(line, "pritunl_cloud_map") if !natComment && !mapComment { continue } cmd := strings.Fields(line) if len(cmd) < 3 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables state") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables state"), } return } cmd = cmd[1:] if mapComment && namespace == "0" { if cmd[0] != "PREROUTING" && cmd[0] != "POSTROUTING" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables map chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables map chain"), } return } rules := state.Interfaces[namespace+"-host"] if rules == nil { rules = &Rules{ Namespace: namespace, Interface: "host", Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } state.Interfaces[namespace+"-host"] = rules } if ipv6 { rules.Maps6 = append(rules.Maps6, cmd) } else { rules.Maps = append(rules.Maps, cmd) } } else if mapComment { iface := instIface for i, item := range cmd { if item == "-i" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables interface") err = &errortypes.ParseError{ errors.New( "iptables: Invalid iptables interface"), } return } iface = strings.Trim(cmd[i+1], "+") break } } if iface == "" { logrus.WithFields(logrus.Fields{ "namespace": namespace, "iface": iface, "iptables_rule": line, }).Error("iptables: Missing instance iface for map") } else { if cmd[0] != "PREROUTING" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables map chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables map chain"), } return } rules := state.Interfaces[namespace+"-"+iface] if rules == nil { rules = &Rules{ Namespace: namespace, Interface: iface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Maps: [][]string{}, Maps6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } state.Interfaces[namespace+"-"+iface] = rules } if ipv6 { rules.Maps6 = append(rules.Maps6, cmd) } else { rules.Maps = append(rules.Maps, cmd) } } } else if natComment { if cmd[0] != "PREROUTING" && cmd[0] != "POSTROUTING" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables map chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables map chain"), } return } if cmd[0] == "POSTROUTING" { for i, item := range cmd { if item == "-o" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables addr") err = &errortypes.ParseError{ errors.New( "iptables: Invalid iptables addr"), } return } postIface = strings.Trim(cmd[i+1], "+") } } } natRules = append(natRules, cmd) } } cloudPostIface := "" cloudNatRules := [][]string{} for _, line := range strings.Split(output, "\n") { if !strings.Contains(line, "pritunl_cloud_cloud_nat") { continue } cmd := strings.Fields(line) if len(cmd) < 3 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables state") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables state"), } return } cmd = cmd[1:] if cmd[0] != "PREROUTING" && cmd[0] != "POSTROUTING" { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables map chain") err = &errortypes.ParseError{ errors.New("iptables: Invalid iptables map chain"), } return } if cmd[0] == "POSTROUTING" { for i, item := range cmd { if item == "-o" { if len(cmd) < i+2 { logrus.WithFields(logrus.Fields{ "iptables_rule": line, }).Error("iptables: Invalid iptables addr") err = &errortypes.ParseError{ errors.New( "iptables: Invalid iptables addr"), } return } cloudPostIface = strings.Trim(cmd[i+1], "+") } } } cloudNatRules = append(cloudNatRules, cmd) } if postIface != "" { rules := state.Interfaces[namespace+"-"+postIface] if rules == nil { rules = &Rules{ Namespace: namespace, Interface: postIface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } state.Interfaces[namespace+"-"+postIface] = rules } if ipv6 { rules.Nats6 = append(rules.Nats6, natRules...) } else { rules.Nats = append(rules.Nats, natRules...) } } if cloudPostIface != "" { rules := state.Interfaces[namespace+"-"+cloudPostIface] if rules == nil { rules = &Rules{ Namespace: namespace, Interface: cloudPostIface, Header: [][]string{}, Header6: [][]string{}, SourceDestCheck: [][]string{}, SourceDestCheck6: [][]string{}, Ingress: [][]string{}, Ingress6: [][]string{}, Holds: [][]string{}, Holds6: [][]string{}, } state.Interfaces[namespace+"-"+cloudPostIface] = rules } if ipv6 { rules.Nats6 = append(rules.Nats6, cloudNatRules...) } else { rules.Nats = append(rules.Nats, cloudNatRules...) } } return } func RecoverNode() (err error) { cmds := [][]string{} if !node.Self.Firewall { return } cmds = append(cmds, []string{ "-I", "INPUT", "1", "-m", "comment", "--comment", "pritunl_cloud_rule", "-j", "DROP", }) cmds = append(cmds, []string{ "-I", "INPUT", "1", "-m", "conntrack", "--ctstate", "INVALID", "-m", "comment", "--comment", "pritunl_cloud_rule", "-j", "DROP", }) cmds = append(cmds, []string{ "-I", "INPUT", "1", "-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-m", "comment", "--comment", "pritunl_cloud_rule", "-j", "ACCEPT", }) for _, cmd := range cmds { Lock() output, e := utils.ExecCombinedOutput("", "iptables", cmd...) Unlock() if e != nil { err = e logrus.WithFields(logrus.Fields{ "command": cmd, "output": output, "error": err, }).Error("iptables: Failed to add iptables recover rule") return } } for _, cmd := range cmds { Lock() output, e := utils.ExecCombinedOutput("", "ip6tables", cmd...) Unlock() if e != nil { err = e logrus.WithFields(logrus.Fields{ "command": cmd, "output": output, "error": err, }).Error("iptables: Failed to add ip6tables recover rule") return } } return } func Init(namespaces []string, vpcs []*vpc.Vpc, instances []*instance.Instance, nodeFirewall []*firewall.Rule, firewalls map[string][]*firewall.Rule, firewallMaps map[string][]*firewall.Mapping) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", "net.ipv6.conf.all.accept_ra=2", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", "net.ipv6.conf.default.accept_ra=2", ) if err != nil { return } interfaces, err := utils.GetInterfaces() if err != nil { return } for _, iface := range interfaces { if len(iface) == 14 && (strings.HasPrefix(iface, "v") || strings.HasPrefix(iface, "x")) { continue } utils.ExecCombinedOutput("", "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.accept_ra=2", iface), ) } utils.ExecCombinedOutput( "", "sysctl", "-w", "net.bridge.bridge-nf-call-iptables=1", ) utils.ExecCombinedOutput( "", "sysctl", "-w", "net.bridge.bridge-nf-call-ip6tables=1", ) _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", "net.ipv4.ip_forward=1", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", "net.ipv6.conf.all.forwarding=1", ) if err != nil { return } state := &State{ Interfaces: map[string]*Rules{}, } err = loadIptables("0", "", state, false) if err != nil { return } err = loadIptables("0", "", state, true) if err != nil { return } namespaceMap := map[string]*instance.Instance{} for _, inst := range instances { namespaceMap[vm.GetNamespace(inst.Id, 0)] = inst } for _, namespace := range namespaces { instIface := "" inst := namespaceMap[namespace] if inst != nil { instIface = vm.GetIface(inst.Id, 0) } err = loadIptables(namespace, instIface, state, false) if err != nil { return } err = loadIptables(namespace, instIface, state, true) if err != nil { return } } curState = state UpdateState(node.Self, vpcs, instances, namespaces, nodeFirewall, firewalls, firewallMaps) return } func protocolIndex(proto string) string { switch proto { case "icmp": return "1" case "tcp": return "6" case "udp": return "17" default: return "0" } } ================================================ FILE: ipvs/constants.go ================================================ package ipvs const ( Tcp = "-t" Udp = "-u" RoundRobin = "rr" ) ================================================ FILE: ipvs/ipvs.go ================================================ package ipvs import ( "fmt" "sort" "strconv" "strings" "time" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) var ( curState *State ) type State struct { Services map[string]*Service } func (s *State) Print() string { var output strings.Builder output.WriteString("IPVS Configuration:\n") output.WriteString("=================\n\n") if len(s.Services) == 0 { output.WriteString("No services configured.\n") return output.String() } serviceKeys := make([]string, 0, len(s.Services)) for key := range s.Services { serviceKeys = append(serviceKeys, key) } sort.Strings(serviceKeys) for _, key := range serviceKeys { service := s.Services[key] output.WriteString(fmt.Sprintf("Service: %s%s\n", service.Protocol, service.Key())) output.WriteString(fmt.Sprintf(" Key: %s\n", key)) output.WriteString(fmt.Sprintf(" Scheduler: %s\n", service.Scheduler)) if len(service.Targets) == 0 { output.WriteString(" No targets configured.\n") } else { output.WriteString(" Targets:\n") sort.Slice(service.Targets, func(i, j int) bool { if service.Targets[i].Address != service.Targets[j].Address { return service.Targets[i].Address < service.Targets[j].Address } return service.Targets[i].Port < service.Targets[j].Port }) for _, target := range service.Targets { masq := "No" if target.Masquerade { masq = "Yes" } output.WriteString(fmt.Sprintf(" - %s (Weight: %d, Masquerade: %s)\n", target.Key(), target.Weight, masq)) } } output.WriteString("\n") } return output.String() } func (s *State) AddTarget(serviceAddr, targetAddr string, port int, protocol string) (err error) { serviceKey := fmt.Sprintf("%s%s:%d", protocol, serviceAddr, port) service := s.Services[serviceKey] if service == nil { service = &Service{ Scheduler: RoundRobin, Protocol: protocol, Address: serviceAddr, Port: port, } s.Services[serviceKey] = service } target := &Target{ Service: service, Address: targetAddr, Port: port, Weight: 1, Masquerade: true, } service.Targets = append(service.Targets, target) return } func UpdateState(newState *State) (err error) { updated := false if curState == nil { var state *State state, err = LoadState() if err != nil { return } curState = state } for serviceKey, service := range curState.Services { newService := newState.Services[serviceKey] if newService == nil { if !updated { logrus.WithFields(logrus.Fields{ "reason": "unknown_service", }).Info("ipvs: Updating ipvs state") updated = true } err = service.Delete() if err != nil { return } continue } for _, target := range service.Targets { found := false for _, newTarget := range newService.Targets { if target.Address == newTarget.Address && target.Port == newTarget.Port { if target.Weight != newTarget.Weight || target.Masquerade != newTarget.Masquerade { if !updated { logrus.WithFields(logrus.Fields{ "reason": "weight_masquerade", }).Info("ipvs: Updating ipvs state") updated = true } err = target.Delete() if err != nil { return } found = false } else { found = true } break } } if !found { if !updated { logrus.WithFields(logrus.Fields{ "reason": "target_unknown", }).Info("ipvs: Updating ipvs state") updated = true } err = target.Delete() if err != nil { return } } } if service.Scheduler != newService.Scheduler { if !updated { logrus.WithFields(logrus.Fields{ "reason": "scheduler", }).Info("ipvs: Updating ipvs state") updated = true } err = service.Delete() if err != nil { return } err = newService.Add() if err != nil { return } for _, target := range newService.Targets { target.Service = newService err = target.Add() if err != nil { return } } } } for serviceKey, newService := range newState.Services { service := curState.Services[serviceKey] if service == nil { if !updated { logrus.WithFields(logrus.Fields{ "reason": "new_service", }).Info("ipvs: Updating ipvs state") updated = true } err = newService.Add() if err != nil { return } for _, target := range newService.Targets { target.Service = newService err = target.Add() if err != nil { return } } } else if service.Scheduler == newService.Scheduler { for _, newTarget := range newService.Targets { found := false needsUpdate := false for _, target := range service.Targets { if target.Address == newTarget.Address && target.Port == newTarget.Port { found = true if target.Weight != newTarget.Weight || target.Masquerade != newTarget.Masquerade { needsUpdate = true } break } } if !found || needsUpdate { newTarget.Service = service if !updated { logrus.WithFields(logrus.Fields{ "reason": "new_target", }).Info("ipvs: Updating ipvs state") updated = true } err = newTarget.Add() if err != nil { return } } } } } curState = newState return } func LoadState() (state *State, err error) { resp, err := commander.Exec(&commander.Opt{ Name: "ipvsadm-save", Args: []string{ "-n", }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error("ipvs: Failed to load state") return } state = &State{ Services: map[string]*Service{}, } for _, line := range strings.Split(string(resp.Output), "\n") { line = strings.TrimSpace(line) if line == "" { continue } parts := strings.Fields(line) if len(parts) == 0 { continue } if parts[0] == "-A" { serviceKey := "" service := &Service{ Targets: []*Target{}, } for i := 0; i < len(parts); i++ { switch parts[i] { case "-t": if i+1 < len(parts) { serviceKey = parts[i+1] addrPort := strings.Split(serviceKey, ":") serviceKey = Tcp + serviceKey service.Protocol = Tcp if len(addrPort) == 2 { service.Address = addrPort[0] service.Port, _ = strconv.Atoi(addrPort[1]) } i++ } case "-u": if i+1 < len(parts) { serviceKey = parts[i+1] addrPort := strings.Split(serviceKey, ":") serviceKey = Udp + serviceKey service.Protocol = Udp if len(addrPort) == 2 { service.Address = addrPort[0] service.Port, _ = strconv.Atoi(addrPort[1]) } i++ } case "-s": if i+1 < len(parts) { switch parts[i+1] { case "rr": service.Scheduler = RoundRobin break } i++ } } } if serviceKey != "" { state.Services[serviceKey] = service } } else if parts[0] == "-a" { target := &Target{} for i := 0; i < len(parts); i++ { switch parts[i] { case "-t": if i+1 < len(parts) { target.Service = state.Services[Tcp+parts[i+1]] i++ } case "-u": if i+1 < len(parts) { target.Service = state.Services[Udp+parts[i+1]] i++ } case "-r": if i+1 < len(parts) { addrPort := strings.Split(parts[i+1], ":") if len(addrPort) == 2 { target.Address = addrPort[0] target.Port, _ = strconv.Atoi(addrPort[1]) } i++ } case "-w": if i+1 < len(parts) { target.Weight, _ = strconv.Atoi(parts[i+1]) i++ } case "-m": target.Masquerade = true } } if target.Service != nil { target.Service.Targets = append(target.Service.Targets, target) } } } return } func New() *State { return &State{ Services: map[string]*Service{}, } } ================================================ FILE: ipvs/service.go ================================================ package ipvs import ( "fmt" "time" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) var ( hasSysctl = false ) type Service struct { Scheduler string Protocol string Address string Port int Targets []*Target } func (s *Service) Key() string { return fmt.Sprintf("%s:%d", s.Address, s.Port) } func (s *Service) Add() (err error) { if s.Scheduler == "" { s.Scheduler = RoundRobin } if !hasSysctl { resp, err := commander.Exec(&commander.Opt{ Name: "sysctl", Args: []string{ "-w", "net.ipv4.vs.conntrack=1", }, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error( "ipvs: Failed to set ipvs sysctl") err = nil } hasSysctl = true } resp, err := commander.Exec(&commander.Opt{ Name: "ipvsadm", Args: []string{ "-A", s.Protocol, s.Key(), "-s", s.Scheduler, }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error("ipvs: Failed to add service") return } return } func (s *Service) Delete() (err error) { resp, err := commander.Exec(&commander.Opt{ Name: "ipvsadm", Args: []string{ "-D", s.Protocol, s.Key(), }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error("ipvs: Failed to remove service") return } return } ================================================ FILE: ipvs/target.go ================================================ package ipvs import ( "fmt" "time" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) type Target struct { Service *Service Address string Port int Weight int Masquerade bool } func (t *Target) Key() string { return fmt.Sprintf("%s:%d", t.Address, t.Port) } func (t *Target) Add() (err error) { if t.Weight == 0 { t.Weight = 1 } args := []string{ "-a", t.Service.Protocol, t.Service.Key(), "-r", t.Key(), } if t.Masquerade { args = append(args, "-m") } args = append(args, "-w", fmt.Sprintf("%d", t.Weight)) resp, err := commander.Exec(&commander.Opt{ Name: "ipvsadm", Args: args, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error("ipvs: Failed to add target") return } return } func (t *Target) Delete() (err error) { resp, err := commander.Exec(&commander.Opt{ Name: "ipvsadm", Args: []string{ "-d", t.Service.Protocol, t.Service.Key(), "-r", t.Key(), }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error("ipvs: Failed to remove target") return } return } ================================================ FILE: iscsi/iscsi.go ================================================ package iscsi import ( "fmt" "net/url" "strconv" "strings" "github.com/pritunl/pritunl-cloud/errortypes" ) type Device struct { Host string `bson:"host" json:"host"` Port int `bson:"port" json:"port"` Iqn string `bson:"iqn" json:"iqn"` Lun string `bson:"lun" json:"lun"` Username string `bson:"username" json:"username"` Password string `bson:"password" json:"password"` Uri string `bson:"-" json:"uri"` } func (d *Device) Json() { host := "" if d.Port != 0 { host = fmt.Sprintf("%s:%d", d.Host, d.Port) } else { host = fmt.Sprintf("%s", d.Host) } uri := url.URL{ Scheme: "iscsi", Host: host, Path: fmt.Sprintf("%s/%s", d.Iqn, d.Lun), } if d.Username != "" && d.Password != "" { uri.User = url.UserPassword(d.Username, d.Password) } d.Uri = uri.String() } func (d *Device) QemuUri() (uriStr string) { host := "" if d.Port != 0 { host = fmt.Sprintf("%s:%d", d.Host, d.Port) } else { host = fmt.Sprintf("%s", d.Host) } uri := url.URL{ Scheme: "iscsi", Host: host, Path: fmt.Sprintf("%s/%s", d.Iqn, d.Lun), } if d.Username != "" && d.Password != "" { uri.User = url.UserPassword(d.Username, d.Password) } uriStr = uri.String() uriStr = strings.Replace(uriStr, "%", "", -1) if d.Username != "" && d.Password != "" && len(uriStr) > 8 { uriStr = uriStr[:8] + strings.Replace(uriStr[8:], ":", "%%", 1) } return } func (d *Device) Parse() (errData *errortypes.ErrorData, err error) { if d.Uri == "" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_uri", Message: "Invalid iSCSI URI", } return } uri, err := url.Parse(d.Uri) if err != nil { err = nil errData = &errortypes.ErrorData{ Error: "invalid_iscsi_uri", Message: "Invalid iSCSI URI", } return } if uri.Scheme != "iscsi" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_uri", Message: "Invalid iSCSI URI", } return } port := 0 portStr := uri.Port() if portStr != "" { port, err = strconv.Atoi(portStr) if err != nil { err = nil errData = &errortypes.ErrorData{ Error: "invalid_iscsi_port", Message: "Invalid iSCSI port", } return } } host := uri.Hostname() if host == "" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_uri", Message: "Invalid iSCSI URI", } return } username := "" password := "" if uri.User != nil { username = uri.User.Username() password, _ = uri.User.Password() if username != "" || password != "" { if username == "" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_username", Message: "Missing iSCSI username", } return } if password == "" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_password", Message: "Missing iSCSI password", } return } } } path := strings.Split(uri.Path, "/") if len(path) != 3 { errData = &errortypes.ErrorData{ Error: "missing_iscsi_iqn_lun", Message: "Missing iSCSI IQN and LUN", } return } iqn := path[1] if iqn == "" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_iqn", Message: "Invalid iSCSI IQN", } return } lun := path[2] if lun == "" { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_lun", Message: "Invalid iSCSI LUN", } return } d.Host = host d.Port = port d.Iqn = iqn d.Lun = lun d.Username = username d.Password = password d.Uri = "" d.Json() if strings.Contains(d.Uri, "%") { errData = &errortypes.ErrorData{ Error: "invalid_iscsi_uri", Message: "Invalid iSCSI URI, cannot contain % character", } return } d.Uri = "" return } ================================================ FILE: iso/iso.go ================================================ package iso import ( "io/ioutil" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var ( syncLast time.Time syncLock sync.Mutex syncCache []*Iso ) type Iso struct { Name string `bson:"name" json:"name"` } func GetIsos(isoDir string) (isos []*Iso, err error) { if time.Since(syncLast) < 30*time.Second { isos = syncCache return } syncLock.Lock() defer syncLock.Unlock() err = utils.ExistsMkdir(isoDir, 0755) if err != nil { return } isoFiles, err := ioutil.ReadDir(isoDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "backup: Failed to read isos directory"), } return } for _, item := range isoFiles { filename := item.Name() filenameFilt := utils.FilterRelPath(filename) if filenameFilt != filename { logrus.WithFields(logrus.Fields{ "name": filename, }).Error("iso: Invalid ISO filename") } iso := &Iso{ Name: filenameFilt, } isos = append(isos, iso) } syncCache = isos syncLast = time.Now() return } ================================================ FILE: journal/constants.go ================================================ package journal const ( InstanceAgent = 1 DeploymentAgent = 2 ) const ( Panic = 1 Critical = 2 Error = 3 Warning = 4 Info = 5 Debug = 6 Trace = 7 ) ================================================ FILE: journal/journal.go ================================================ package journal import ( "fmt" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) type Journal struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Resource bson.ObjectID `bson:"r" json:"r"` Kind int32 `bson:"k" json:"k"` Level int32 `bson:"l" json:"l"` Timestamp time.Time `bson:"t" json:"t"` Count int32 `bson:"c" json:"-"` Message string `bson:"m" json:"m"` Fields map[string]string `bson:"f,omitempty" json:"f"` } func (j *Journal) String() string { return fmt.Sprintf( "[%s] %s\n", j.Timestamp.Format("2006-01-02 15:04:05"), j.Message, ) } func (j *Journal) Insert(db *database.Database) (err error) { coll := db.Journal() if j.Level == 0 { j.Level = Info } _, err = coll.InsertOne(db, j) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: journal/store.go ================================================ package journal import ( "github.com/pritunl/pritunl-cloud/database" ) type KindGenerator interface { GetKind(db *database.Database, key string) (kind int32, err error) } ================================================ FILE: journal/utils.go ================================================ package journal import ( "context" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" ) func GetOutput(c context.Context, db *database.Database, resource bson.ObjectID, kind int32) (output []string, err error) { coll := db.Journal() limit := int64(settings.Hypervisor.JournalDisplayLimit) cursor, err := coll.Find( c, &bson.M{ "r": resource, "k": kind, }, options.Find(). SetLimit(limit). SetSort(&bson.D{ {"t", -1}, {"c", -1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(c) outputRevrse := []string{} for cursor.Next(c) { jrnl := &Journal{} err = cursor.Decode(jrnl) if err != nil { err = database.ParseError(err) return } outputRevrse = append(outputRevrse, jrnl.String()) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } for i := len(outputRevrse) - 1; i >= 0; i-- { if i == 0 { output = append(output, strings.TrimSuffix(outputRevrse[i], "\n")) } else { output = append(output, outputRevrse[i]) } } return } func Remove(db *database.Database, resource bson.ObjectID, kind int) (err error) { coll := db.Journal() _, err = coll.DeleteMany(db, &bson.M{ "r": resource, "k": kind, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveAll(db *database.Database, resource bson.ObjectID) (err error) { coll := db.Journal() _, err = coll.DeleteMany(db, &bson.M{ "r": resource, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } ================================================ FILE: lock/lvm.go ================================================ package lock import ( "fmt" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" ) type LvmLocker struct { Id string `bson:"_id" json:"_id"` Node bson.ObjectID `bson:"node" json:"node"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` } func LvmLock(db *database.Database, vgName, lvName string) ( acquired bool, err error) { coll := db.LvmLock() doc := &LvmLocker{ Id: fmt.Sprintf("%s/%s", vgName, lvName), Node: node.Self.Id, Timestamp: time.Now(), } _, err = coll.InsertOne(db, doc) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { err = nil return } return } acquired = true return } func LvmRelock(db *database.Database, vgName, lvName string) (err error) { coll := db.LvmLock() _, err = coll.UpdateOne(db, &bson.M{ "id": fmt.Sprintf("%s/%s", vgName, lvName), "node": node.Self.Id, }, &bson.M{ "timestamp": time.Now(), }) if err != nil { err = database.ParseError(err) return } return } func LvmUnlock(db *database.Database, vgName, lvName string) (err error) { coll := db.LvmLock() _, err = coll.DeleteOne(db, &bson.M{ "id": fmt.Sprintf("%s/%s", vgName, lvName), "node": node.Self.Id, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: log/constants.go ================================================ package log const ( Debug = "debug" Info = "info" Warning = "warning" Error = "error" Fatal = "fatal" Panic = "panic" Unknown = "unknown" ) ================================================ FILE: log/log.go ================================================ package log import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/requires" ) var published = false type Entry struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Level string `bson:"level" json:"level"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Message string `bson:"message" json:"message"` Stack string `bson:"stack" json:"stack"` Fields map[string]interface{} `bson:"fields" json:"fields"` } func (e *Entry) Insert(db *database.Database) (err error) { coll := db.Logs() if !e.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("log: Entry already exists"), } return } _, err = coll.InsertOne(db, e) if err != nil { err = database.ParseError(err) return } published = true return } func publish() { db := database.GetDatabase() defer db.Close() event.PublishDispatch(db, "log.change") } func initSender() { for { time.Sleep(1500 * time.Millisecond) if published { published = false publish() } } } func init() { module := requires.New("log") module.After("logger") module.Handler = func() (err error) { go initSender() return } } ================================================ FILE: log/utils.go ================================================ package log import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, logId bson.ObjectID) ( entry *Entry, err error) { coll := db.Logs() entry = &Entry{} err = coll.FindOneId(logId, entry) if err != nil { return } return } func GetAll(db *database.Database, query *bson.M, page, pageCount int64) ( entries []*Entry, count int64, err error) { coll := db.Logs() entries = []*Entry{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"$natural", -1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { entry := &Entry{} err = cursor.Decode(entry) if err != nil { err = database.ParseError(err) return } entries = append(entries, entry) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Clear(db *database.Database) (err error) { coll := db.Logs() _, err = coll.DeleteMany(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } event.PublishDispatch(db, "log.change") return } ================================================ FILE: logger/database.go ================================================ package logger import ( "fmt" "strings" "github.com/sirupsen/logrus" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/log" ) var ( databaseBuffer = make(chan *logrus.Entry, 128) ) type databaseSender struct{} func (s *databaseSender) Init() {} func (s *databaseSender) Parse(entry *logrus.Entry) { if len(buffer) <= 32 { databaseBuffer <- entry } } func databaseSend(entry *logrus.Entry) (err error) { level := "" db := database.GetDatabase() if db == nil { return } defer db.Close() switch entry.Level { case logrus.DebugLevel: level = log.Debug break case logrus.WarnLevel: level = log.Warning break case logrus.InfoLevel: level = log.Info break case logrus.ErrorLevel: level = log.Error break case logrus.FatalLevel: level = log.Fatal break case logrus.PanicLevel: level = log.Panic break default: level = log.Unknown } ent := &log.Entry{ Level: level, Timestamp: entry.Time, Message: entry.Message, Fields: map[string]interface{}{}, } for key, val := range entry.Data { if key == "error" { ent.Stack = fmt.Sprintf("%s", val) } else { ent.Fields[key] = val } } err = ent.Insert(db) if err != nil { return } return } func initDatabaseSender() { go func() { for { entry := <-databaseBuffer if constants.Interrupt { return } if strings.HasPrefix(entry.Message, "logger:") { continue } err := databaseSend(entry) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("logger: Database send error") } } }() } func init() { senders = append(senders, &databaseSender{}) } ================================================ FILE: logger/file.go ================================================ package logger import ( "os" "github.com/sirupsen/logrus" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" ) type fileSender struct{} func (s *fileSender) Init() {} func (s *fileSender) Parse(entry *logrus.Entry) { err := s.send(entry) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("logger: File send error") } } func (s *fileSender) send(entry *logrus.Entry) (err error) { msg := formatPlain(entry) file, err := os.OpenFile(constants.LogPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "logger: Failed to open log file"), } return } defer file.Close() stat, err := file.Stat() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "logger: Failed to stat log file"), } return } if stat.Size() >= 5000000 { os.Remove(constants.LogPath2) err = os.Rename(constants.LogPath, constants.LogPath2) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "logger: Failed to rotate log file"), } return } file.Close() file, err = os.OpenFile(constants.LogPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "logger: Failed to open log file"), } return } } _, err = file.Write(msg) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "logger: Failed to write to log file"), } return } return } func init() { senders = append(senders, &fileSender{}) } ================================================ FILE: logger/formatter.go ================================================ package logger import ( "fmt" "reflect" "sort" "time" "github.com/pritunl/pritunl-cloud/colorize" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) var ( blueArrow = colorize.ColorString("▶", colorize.BlueBold, colorize.None) whiteDiamond = colorize.ColorString("◆", colorize.WhiteBold, colorize.None) ) func format(entry *logrus.Entry) (output []byte) { msg := fmt.Sprintf("%s%s %s %s", formatTime(entry.Time), formatLevel(entry.Level), blueArrow, entry.Message, ) keys := []string{} var errStr string for key, val := range entry.Data { if key == "error" { errStr = fmt.Sprintf("%s", val) continue } else if key == "error_data" { if val != nil && !reflect.ValueOf(val).IsNil() { if errData, ok := val.(*errortypes.ErrorData); ok { entry.Data["error_key"] = errData.Error entry.Data["error_msg"] = errData.Message keys = append(keys, "error_key", "error_msg") } } continue } keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { msg += fmt.Sprintf(" %s %s=%v", whiteDiamond, colorize.ColorString(key, colorize.CyanBold, colorize.None), colorize.ColorString(fmt.Sprintf("%#v", entry.Data[key]), colorize.GreenBold, colorize.None)) } if errStr != "" { msg += "\n" + colorize.ColorString(errStr, colorize.Red, colorize.None) } if string(msg[len(msg)-1]) != "\n" { msg += "\n" } output = []byte(msg) return } func formatPlain(entry *logrus.Entry) (output []byte) { msg := fmt.Sprintf("%s%s ▶ %s", entry.Time.Format("[2006-01-02 15:04:05]"), formatLevelPlain(entry.Level), entry.Message, ) keys := []string{} var errStr string for key, val := range entry.Data { if key == "error" { errStr = fmt.Sprintf("%s", val) continue } else if key == "error_data" { if val != nil && !reflect.ValueOf(val).IsNil() { if errData, ok := val.(*errortypes.ErrorData); ok { entry.Data["error_key"] = errData.Error entry.Data["error_msg"] = errData.Message keys = append(keys, "error_key", "error_msg") } } continue } keys = append(keys, key) } sort.Strings(keys) for _, key := range keys { msg += fmt.Sprintf(" ◆ %s=%v", key, fmt.Sprintf("%#v", entry.Data[key])) } if errStr != "" { msg += "\n" + errStr } if string(msg[len(msg)-1]) != "\n" { msg += "\n" } output = []byte(msg) return } func formatTime(timestamp time.Time) (str string) { return colorize.ColorString( timestamp.Format("[2006-01-02 15:04:05]"), colorize.Bold, colorize.None, ) } func formatLevel(lvl logrus.Level) (str string) { var colorBg colorize.Color switch lvl { case logrus.InfoLevel: colorBg = colorize.CyanBg str = "[INFO]" case logrus.WarnLevel: colorBg = colorize.YellowBg str = "[WARN]" case logrus.ErrorLevel: colorBg = colorize.RedBg str = "[ERRO]" case logrus.FatalLevel: colorBg = colorize.RedBg str = "[FATL]" case logrus.PanicLevel: colorBg = colorize.RedBg str = "[PANC]" default: colorBg = colorize.BlackBg } str = colorize.ColorString(str, colorize.WhiteBold, colorBg) return } func formatLevelPlain(lvl logrus.Level) string { switch lvl { case logrus.InfoLevel: return "[INFO]" case logrus.WarnLevel: return "[WARN]" case logrus.ErrorLevel: return "[ERRO]" case logrus.FatalLevel: return "[FATL]" case logrus.PanicLevel: return "[PANC]" default: } return "" } type formatter struct{} func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) { return format(entry), nil } ================================================ FILE: logger/hook.go ================================================ package logger import ( "strings" "github.com/sirupsen/logrus" ) type logHook struct{} func (h *logHook) Fire(entry *logrus.Entry) (err error) { if strings.HasPrefix(entry.Message, "logger:") { return } if len(buffer) <= 32 { buffer <- entry } return } func (h *logHook) Levels() []logrus.Level { return []logrus.Level{ logrus.InfoLevel, logrus.WarnLevel, logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel, } } ================================================ FILE: logger/limiter.go ================================================ package logger import ( "hash/fnv" "time" "github.com/sirupsen/logrus" ) type limiter map[uint32]time.Time func (l limiter) Check(entry *logrus.Entry, limit time.Duration) bool { hash := fnv.New32a() hash.Write([]byte(entry.Message)) key := hash.Sum32() if timestamp, ok := l[key]; ok && time.Since(timestamp) < limit { return false } l[key] = time.Now() return true } ================================================ FILE: logger/logger.go ================================================ package logger import ( "os" "strings" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/requires" "github.com/sirupsen/logrus" ) var ( buffer = make(chan *logrus.Entry, 128) senders = []sender{} ) func initSender() { for _, sndr := range senders { sndr.Init() } go func() { for { entry := <-buffer if constants.Interrupt { return } if strings.HasPrefix(entry.Message, "logger:") { continue } for _, sndr := range senders { sndr.Parse(entry) } } }() } func Init() { logrus.SetFormatter(&formatter{}) logrus.AddHook(&logHook{}) logrus.SetOutput(os.Stderr) logrus.SetLevel(logrus.InfoLevel) } func InitStdout() { logrus.SetFormatter(&formatter{}) logrus.SetOutput(os.Stdout) logrus.SetLevel(logrus.InfoLevel) } func init() { module := requires.New("logger") module.After("config") module.Handler = func() (err error) { initSender() initDatabaseSender() return } } ================================================ FILE: logger/sender.go ================================================ package logger import ( "github.com/sirupsen/logrus" ) type sender interface { Init() Parse(entry *logrus.Entry) } ================================================ FILE: logger/writer.go ================================================ package logger import ( "strings" "github.com/sirupsen/logrus" ) type ErrorWriter struct { Message string Fields logrus.Fields Filters []string } func (w *ErrorWriter) Write(input []byte) (n int, err error) { n = len(input) inputStr := string(input) if w.Filters != nil { for _, filter := range w.Filters { if strings.Contains(inputStr, filter) { return } } } w.Fields["err"] = inputStr logrus.WithFields(w.Fields).Error(w.Message) return } ================================================ FILE: lvm/lv.go ================================================ package lvm import ( "fmt" "math" "path/filepath" "strconv" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) func CreateLv(vgName, lvName string, size int) (err error) { _, err = utils.ExecCombinedOutputLogged(nil, "lvcreate", "-an", "-L", fmt.Sprintf("%d.1G", size), "-n", lvName, vgName) if err != nil { return } return } func RemoveLv(vgName, lvName string) (err error) { _, err = utils.ExecCombinedOutputLogged([]string{ "Failed to find", }, "lvremove", "-y", fmt.Sprintf("%s/%s", vgName, lvName)) if err != nil { return } return } func ActivateLv(vgName, lvName string) (err error) { _, err = utils.ExecCombinedOutputLogged(nil, "lvchange", "-ay", fmt.Sprintf("%s/%s", vgName, lvName)) if err != nil { return } return } func DeactivateLv(vgName, lvName string) (err error) { _, err = utils.ExecCombinedOutputLogged(nil, "lvchange", "-an", fmt.Sprintf("%s/%s", vgName, lvName)) if err != nil { return } return } func WriteLv(vgName, lvName, sourcePth string) (err error) { dstPth := filepath.Join("/dev/mapper", fmt.Sprintf("%s-%s", vgName, lvName)) _, err = utils.ExecCombinedOutputLogged(nil, "qemu-img", "convert", "-f", "qcow2", "-O", "raw", sourcePth, dstPth) if err != nil { return } return } func ExtendLv(vgName, lvName string, addSize int) (err error) { _, err = utils.ExecCombinedOutputLogged(nil, "lvextend", "-L", fmt.Sprintf("+%dG", addSize), fmt.Sprintf("%s/%s", vgName, lvName)) if err != nil { return } return } func GetSizeLv(vgName, lvName string) (size int, err error) { output, err := utils.ExecCombinedOutput("", "lvs", fmt.Sprintf("%s/%s", vgName, lvName), "-o", "LV_SIZE", "--units", "g", "--noheadings") if err != nil { return } output = strings.Trim(strings.TrimSpace(strings.ToLower(output)), "g") number, err := strconv.ParseFloat(output, 64) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "lvm: Failed to parse lvm volume size"), } return } size = int(math.Round(number)) return } func HasLocking(vgName string) (hasLock bool, err error) { output, err := utils.ExecCombinedOutput("", "vgs", vgName, "-o", "vg_lock_type", "--noheadings") if err != nil { return } lockType := strings.TrimSpace(output) hasLock = lockType != "" && lockType != "none" return } func IsLockspaceActive(vgName string) (isLocked bool, err error) { output, err := utils.ExecCombinedOutput("", "lvmlockctl", "-i") if err != nil { return } lines := strings.Split(output, "\n") for _, line := range lines { if strings.Contains(line, vgName) && strings.Contains(line, "sanlock") { isLocked = true return } } return } func InitLock(vgName string) (err error) { hasLock, err := HasLocking(vgName) if err != nil { return } if !hasLock { return } isLocked, err := IsLockspaceActive(vgName) if err != nil { return } if isLocked { return } _, err = utils.ExecCombinedOutputLogged(nil, "vgchange", "--lock-start", vgName) if err != nil { return } return } ================================================ FILE: lvm/vgs.go ================================================ package lvm import ( "encoding/json" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" ) var ( cachedNodePools []*pool.Pool cachedNodePoolsTimestamp time.Time ) type report struct { Report []*vgReport `json:"report"` } type vgReport struct { Vg []*vgDetails `json:"vg"` } type vgDetails struct { VgName string `json:"vg_name"` PvCount string `json:"pv_count"` LvCount string `json:"lv_count"` SnapCount string `json:"snap_count"` VgAttr string `json:"vg_attr"` VgSize string `json:"vg_size"` VgFree string `json:"vg_free"` } func GetAvailablePools(db *database.Database, zoneId bson.ObjectID) ( availablePools []*pool.Pool, err error) { if time.Since(cachedNodePoolsTimestamp) < 30*time.Second { availablePools = cachedNodePools return } availablePools = []*pool.Pool{} vgNames := set.NewSet() output, err := utils.ExecCombinedOutput("", "vgs", "--reportformat", "json") if err != nil { return } reprt := &report{} err = json.Unmarshal([]byte(output), reprt) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "deploy: Failed to unmarshal vgs report"), } return } if reprt.Report != nil { for _, reportGroup := range reprt.Report { if reportGroup.Vg != nil { for _, reportVg := range reportGroup.Vg { vgNames.Add(reportVg.VgName) } } } } if vgNames.Len() > 0 { pools, e := pool.GetAll(db, &bson.M{ "zone": zoneId, }) if e != nil { err = e return } for _, pl := range pools { if vgNames.Contains(pl.VgName) { availablePools = append(availablePools, pl) } } } cachedNodePools = availablePools cachedNodePoolsTimestamp = time.Now() return } ================================================ FILE: main.go ================================================ package main import ( "flag" "fmt" "os" "strings" "time" "github.com/pritunl/pritunl-cloud/cmd" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/logger" "github.com/pritunl/pritunl-cloud/requires" ) const help = ` Usage: pritunl-cloud COMMAND Commands: version Show version mongo Set MongoDB URI set Set a setting unset Unset a setting start Start node clear-logs Clear logs reset-node-web Reset node web server settings optimize Optimize system configuration default-password Get default administrator password reset-password Reset administrator password disable-policies Disable all policies disable-firewall Disable firewall on this node start-instance Start instance by name stop-instance Stop instance by name mtu-check Check and show instance MTUs backup Backup local data ` func Init() { logger.Init() requires.Init(nil) } func InitLimited() { logger.Init() requires.Init([]string{"ahandlers", "uhandlers"}) } func main() { defer time.Sleep(500 * time.Millisecond) command := "" for _, arg := range os.Args[1:] { if !strings.HasPrefix(arg, "-") { command = arg break } } switch command { case "start": flag.Parse() for _, arg := range flag.Args() { switch arg { case "--debug": constants.Production = false break case "--debug-web": constants.DebugWeb = true break case "--fast-exit": constants.FastExit = true break } } Init() err := cmd.Node() if err != nil { panic(err) } return case "version": fmt.Printf("pritunl-cloud v%s\n", constants.Version) return case "mongo": flag.Parse() logger.Init() err := cmd.Mongo() if err != nil { panic(err) } return case "optimize": logger.Init() err := cmd.Optimize() if err != nil { panic(err) } return case "reset-node-web": InitLimited() err := cmd.ResetNodeWeb() if err != nil { panic(err) } return case "default-password": InitLimited() err := cmd.DefaultPassword() if err != nil { panic(err) } return case "reset-password": InitLimited() err := cmd.ResetPassword() if err != nil { panic(err) } return case "disable-policies": InitLimited() err := cmd.DisablePolicies() if err != nil { panic(err) } return case "disable-firewall": InitLimited() err := cmd.DisableFirewall() if err != nil { panic(err) } return case "mtu-check": InitLimited() err := cmd.MtuCheck() if err != nil { panic(err) } return case "set": flag.Parse() InitLimited() err := cmd.SettingsSet() if err != nil { panic(err) } return case "unset": flag.Parse() InitLimited() err := cmd.SettingsUnset() if err != nil { panic(err) } return case "clear-logs": InitLimited() err := cmd.ClearLogs() if err != nil { panic(err) } return case "backup": flag.Parse() InitLimited() err := cmd.Backup() if err != nil { panic(err) } return case "imds-server": err := cmd.ImdsServer() if err != nil { panic(err) } return case "dhcp4-server": err := cmd.Dhcp4Server() if err != nil { panic(err) } return case "dhcp6-server": err := cmd.Dhcp6Server() if err != nil { panic(err) } return case "dhcp-client": err := cmd.DhcpClient() if err != nil { panic(err) } return case "ndp-server": err := cmd.NdpServer() if err != nil { panic(err) } return case "start-instance": flag.Parse() InitLimited() err := cmd.StartInstance(flag.Args()[1]) if err != nil { panic(err) } return case "stop-instance": flag.Parse() InitLimited() err := cmd.StopInstance(flag.Args()[1]) if err != nil { panic(err) } return default: fmt.Printf(help) } } ================================================ FILE: middlewear/gzip.go ================================================ package middlewear import ( "compress/gzip" "net/http" "strings" "github.com/gin-gonic/gin" ) type GzipWriter struct { gzipWriter *gzip.Writer httpWriter http.ResponseWriter } func (g *GzipWriter) Header() http.Header { return g.httpWriter.Header() } func (g *GzipWriter) WriteHeader(statusCode int) { g.httpWriter.WriteHeader(statusCode) } func (g *GzipWriter) Write(b []byte) (int, error) { if g.gzipWriter != nil { return g.gzipWriter.Write(b) } return g.httpWriter.Write(b) } func (g *GzipWriter) Close() { if g.gzipWriter != nil { g.gzipWriter.Close() } } func NewGzipWriter(c *gin.Context) *GzipWriter { if !strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") { return &GzipWriter{ httpWriter: c.Writer, } } c.Writer.Header().Set("Content-Encoding", "gzip") c.Writer.Header().Set("Vary", "Accept-Encoding") gz, _ := gzip.NewWriterLevel(c.Writer, gzip.DefaultCompression) return &GzipWriter{ gzipWriter: gz, httpWriter: c.Writer, } } ================================================ FILE: middlewear/middlewear.go ================================================ package middlewear import ( "fmt" "net/http" "strings" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/auth" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/csrf" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/validator" "github.com/sirupsen/logrus" ) const robots = `User-agent: * Disallow: / ` func Limiter(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1000000) } func Counter(c *gin.Context) { node.Self.AddRequest() } func Database(c *gin.Context) { db := database.GetDatabaseCtx(c.Request.Context()) c.Set("db", db) c.Next() db.Close() } func Headers(c *gin.Context) { headers := c.Writer.Header() headers.Add("Cache-Control", "no-store") headers.Add("X-Frame-Options", "DENY") headers.Add("X-XSS-Protection", "1; mode=block") headers.Add("X-Content-Type-Options", "nosniff") headers.Add("X-Robots-Tag", "noindex") } func SessionAdmin(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr, err := authorizer.AuthorizeAdmin(db, c.Writer, c.Request) if err != nil { switch err.(type) { case *errortypes.AuthenticationError: utils.AbortWithError(c, 401, err) break default: utils.AbortWithError(c, 500, err) } return } if authr.IsValid() { usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } if usr != nil { active, err := auth.SyncUser(db, usr) if err != nil { utils.AbortWithError(c, 500, err) return } if !active { err = authr.Clear(db, c.Writer, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = session.RemoveAll(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } } } } c.Set("authorizer", authr) } func SessionUser(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr, err := authorizer.AuthorizeUser(db, c.Writer, c.Request) if err != nil { switch err.(type) { case *errortypes.AuthenticationError: utils.AbortWithError(c, 401, err) break default: utils.AbortWithError(c, 500, err) } return } if authr.IsValid() { usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } if usr != nil { active, err := auth.SyncUser(db, usr) if err != nil { utils.AbortWithError(c, 500, err) return } if !active { err = authr.Clear(db, c.Writer, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = session.RemoveAll(db, usr.Id) if err != nil { return } } } } c.Set("authorizer", authr) } func AuthAdmin(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { utils.AbortWithStatus(c, 401) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } if usr == nil { utils.AbortWithStatus(c, 401) return } _, _, errAudit, errData, err := validator.ValidateAdmin( db, usr, authr.IsApi(), c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = authr.Clear(db, c.Writer, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "check" err = audit.New( db, c.Request, usr.Id, audit.AdminAuthFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } utils.AbortWithStatus(c, 401) return } } func AuthUser(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { utils.AbortWithStatus(c, 401) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } if usr == nil { utils.AbortWithStatus(c, 401) return } _, _, errAudit, errData, err := validator.ValidateUser( db, usr, authr.IsApi(), c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = authr.Clear(db, c.Writer, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "check" err = audit.New( db, c.Request, usr.Id, audit.UserAuthFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } utils.AbortWithStatus(c, 401) return } } func UserOrg(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { utils.AbortWithStatus(c, 401) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } orgIdStr := "" if strings.ToLower(c.Request.Header.Get("Upgrade")) == "websocket" { orgIdStr = c.Query("organization") } else { orgIdStr = c.GetHeader("Organization") } if orgIdStr == "" { utils.AbortWithStatus(c, 401) return } orgId, ok := utils.ParseObjectId(orgIdStr) if orgId.IsZero() || !ok { utils.AbortWithStatus(c, 400) return } org, err := organization.Get(db, orgId) if err != nil { utils.AbortWithError(c, 500, err) return } match := usr.RolesMatch(org.Roles) if !match { utils.AbortWithStatus(c, 401) return } c.Set("organization", org.Id) } func CsrfToken(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { utils.AbortWithStatus(c, 401) return } if authr.IsApi() { return } token := "" if strings.ToLower(c.Request.Header.Get("Upgrade")) == "websocket" { token = c.Query("csrf_token") } else { token = c.Request.Header.Get("Csrf-Token") } valid, err := csrf.ValidateToken(db, authr.SessionId(), token) if err != nil { switch err.(type) { case *database.NotFoundError: utils.AbortWithStatus(c, 401) break default: utils.AbortWithError(c, 500, err) } return } if !valid { utils.AbortWithStatus(c, 401) return } } func Recovery(c *gin.Context) { defer func() { if r := recover(); r != nil { logrus.WithFields(logrus.Fields{ "client": node.Self.GetRemoteAddr(c.Request), "error": errors.New(fmt.Sprintf("%s", r)), }).Error("middlewear: Handler panic") utils.AbortWithStatus(c, 500) return } }() defer func() { if c.Errors != nil && len(c.Errors) != 0 { logrus.WithFields(logrus.Fields{ "client": node.Self.GetRemoteAddr(c.Request), "error": c.Errors, }).Error("middlewear: Handler error") } }() c.Next() } func RobotsGet(c *gin.Context) { c.String(200, robots) } func NotFound(c *gin.Context) { utils.AbortWithStatus(c, 404) } ================================================ FILE: mtu/mtu.go ================================================ package mtu import ( "fmt" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/ip" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/vm" ) type Check struct { node *node.Node mtuInternal int mtuExternal int mtuInstance int mtuHost int Instances []*instance.Instance } func (c *Check) host(db *database.Database) (err error) { ifaces, e := ip.GetIfaces("") if e != nil { err = e return } internalIfaces := set.NewSet() externalIfaces := set.NewSet() for _, iface := range c.node.InternalInterfaces { internalIfaces.Add(iface) } for _, iface := range c.node.ExternalInterfaces { externalIfaces.Add(iface) } for _, iface := range c.node.ExternalInterfaces6 { externalIfaces.Add(iface) } for _, blck := range c.node.Blocks { externalIfaces.Add(blck.Interface) } for _, blck := range c.node.Blocks6 { externalIfaces.Add(blck.Interface) } fmt.Println("*******************************************") fmt.Printf("host: %s\n", c.node.Name) for _, iface := range ifaces { mtu := 0 if iface.Ifname == settings.Hypervisor.HostNetworkName { mtu = c.mtuHost } else if iface.Ifname == settings.Hypervisor.NodePortNetworkName { mtu = c.mtuHost } else if internalIfaces.Contains(iface.Ifname) { mtu = c.mtuHost } else if externalIfaces.Contains(iface.Ifname) { mtu = c.mtuExternal } else if len(iface.Ifname) != 14 { continue } else if strings.HasPrefix(iface.Ifname, "b") { mtu = c.mtuInternal } else if strings.HasPrefix(iface.Ifname, "k") { mtu = c.mtuInternal } else if strings.HasPrefix(iface.Ifname, "r") { mtu = c.mtuExternal } else if strings.HasPrefix(iface.Ifname, "j") { mtu = c.mtuInternal } else if strings.HasPrefix(iface.Ifname, "k") { mtu = c.mtuInternal } else { continue } if iface.Mtu != mtu { fmt.Printf("◆◆◆ERROR◆◆◆\n%s: %d (%d)\n", iface.Ifname, iface.Mtu, mtu) } else { fmt.Printf("%s: %d\n", iface.Ifname, iface.Mtu) } } fmt.Println("*******************************************") return } func (c *Check) instances(db *database.Database) (err error) { insts, err := instance.GetAll(db, &bson.M{ "node": c.node.Id, }) for _, inst := range insts { if inst.State != vm.Running { continue } namespace := inst.NetworkNamespace if namespace == "" { continue } ifaces, e := ip.GetIfaces(namespace) if e != nil { err = e return } fmt.Println("*******************************************") fmt.Printf("instance: %s\n", inst.Name) for _, iface := range ifaces { mtu := 0 if iface.Ifname == settings.Hypervisor.BridgeIfaceName { mtu = c.mtuInstance } else if iface.Ifname == "lo" { continue } else if strings.HasPrefix(iface.Ifname, "p") { mtu = c.mtuInstance } else if strings.HasPrefix(iface.Ifname, "e") { mtu = c.mtuExternal } else if strings.HasPrefix(iface.Ifname, "i") { mtu = c.mtuInternal } else if strings.HasPrefix(iface.Ifname, "x") { mtu = c.mtuInternal } else if strings.HasPrefix(iface.Ifname, "h") { mtu = c.mtuHost } else if strings.HasPrefix(iface.Ifname, "m") { mtu = c.mtuHost } else { fmt.Println("◆◆◆UNKNOWN IFACE◆◆◆") } if iface.Mtu != mtu { fmt.Printf("◆◆◆ERROR◆◆◆\n%s-%s: %d (%d)\n", namespace, iface.Ifname, iface.Mtu, mtu) } else { fmt.Printf("%s-%s: %d\n", namespace, iface.Ifname, iface.Mtu) } } fmt.Println("*******************************************") } return } func (c *Check) Run() (err error) { db := database.GetDatabase() defer db.Close() ndeId, err := bson.ObjectIDFromHex(config.Config.NodeId) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "backup: Failed to parse ObjectId"), } return } c.node, err = node.Get(db, ndeId) if err != nil { return } dc, err := datacenter.Get(db, c.node.Datacenter) if err != nil { return } c.mtuInternal -= dc.GetOverlayMtu() c.mtuInstance -= dc.GetInstanceMtu() err = c.host(db) if err != nil { return } err = c.instances(db) if err != nil { return } return } func NewCheck() (chk *Check) { return &Check{} } ================================================ FILE: netconf/address.go ================================================ package netconf import ( "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/interfaces" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" ) func (n *NetConf) Address(db *database.Database) (err error) { vc, err := vpc.Get(db, n.VmAdapter.Vpc) if err != nil { return } n.VlanId = vc.VpcId vcNet, err := vc.GetNetwork() if err != nil { return } cidr, _ := vcNet.Mask.Size() addr, gatewayAddr, err := vc.GetIp(db, n.VmAdapter.Subnet, n.Virt.Id) if err != nil { return } n.InternalAddr = addr n.InternalGatewayAddr = gatewayAddr n.InternalGatewayAddrCidr = fmt.Sprintf( "%s/%d", gatewayAddr.String(), cidr) n.InternalAddr6 = vc.GetIp6(n.Virt.Id) n.InternalGatewayAddr6 = vc.GetGatewayIp6(n.Virt.Id) n.ExternalMacAddr = vm.GetMacAddrExternal(n.Virt.Id, vc.Id) n.InternalMacAddr = vm.GetMacAddrInternal(n.Virt.Id, vc.Id) n.HostMacAddr = vm.GetMacAddrHost(n.Virt.Id, vc.Id) n.NodePortMacAddr = vm.GetMacAddrNodePort(n.Virt.Id, vc.Id) if n.NetworkMode == node.Dhcp { n.PhysicalExternalIface = interfaces.GetExternal( n.SystemExternalIface) } else if n.NetworkMode == node.Static { blck, staticAddr, externalIface, e := node.Self.GetStaticAddr( db, n.Virt.Id) if e != nil { err = e return } n.PhysicalExternalIface = externalIface staticGateway := blck.GetGateway() staticMask := blck.GetMask() if staticGateway == nil || staticMask == nil { err = &errortypes.ParseError{ errors.New("qemu: Invalid block gateway cidr"), } return } staticSize, _ := staticMask.Size() staticCidr := fmt.Sprintf( "%s/%d", staticAddr.String(), staticSize) n.ExternalVlan = blck.Vlan if n.ExternalVlan != 0 { n.SpaceExternalIfaceMod = n.SpaceExternalIface + "x" } n.ExternalAddrCidr = staticCidr n.ExternalGatewayAddr = staticGateway } else if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud { n.PhysicalExternalIface = interfaces.GetExternal( n.SystemExternalIface) } if n.NetworkMode6 == node.Static { blck, staticAddr, prefix, iface, e := node.Self.GetStaticAddr6( db, n.Virt.Id, vc.VpcId, n.PhysicalExternalIface) if e != nil { err = e return } if n.PhysicalExternalIface != "" && n.PhysicalExternalIface != iface { err = &errortypes.ParseError{ errors.Newf( "netconf: Unsupported mismatched external "+ "IPv4 and IPv6 iface %s - %s", n.PhysicalExternalIface, iface, ), } return } n.PhysicalExternalIface = iface staticCidr6 := fmt.Sprintf("%s/%d", staticAddr.String(), prefix) gateway6 := blck.GetGateway6() n.ExternalVlan6 = blck.Vlan if n.ExternalVlan6 != 0 { if n.ExternalVlan == n.ExternalVlan6 { n.SpaceExternalIfaceMod6 = n.SpaceExternalIfaceMod } else { n.SpaceExternalIfaceMod6 = n.SpaceExternalIface + "y" } } n.ExternalAddrCidr6 = staticCidr6 n.ExternalGatewayAddr6 = gateway6 } if n.HostNetwork { blck, staticAddr, e := node.Self.GetStaticHostAddr(db, n.Virt.Id) if e != nil { err = e return } n.HostAddr = staticAddr hostStaticGateway := blck.GetGateway() hostStaticMask := blck.GetMask() if hostStaticGateway == nil || hostStaticMask == nil { err = &errortypes.ParseError{ errors.New("qemu: Invalid block gateway cidr"), } return } hostStaticSize, _ := hostStaticMask.Size() hostStaticCidr := fmt.Sprintf( "%s/%d", staticAddr.String(), hostStaticSize) n.HostAddrCidr = hostStaticCidr n.HostGatewayAddr = hostStaticGateway } if n.NodePortNetwork { blck, staticAddr, e := node.Self.GetStaticNodePortAddr(db, n.Virt.Id) if e != nil { err = e return } n.NodePortAddr = staticAddr nodePortStaticMask := blck.GetMask() if nodePortStaticMask == nil { err = &errortypes.ParseError{ errors.New("qemu: Invalid block gateway cidr"), } return } nodePortStaticSize, _ := nodePortStaticMask.Size() nodePortStaticCidr := fmt.Sprintf( "%s/%d", staticAddr.String(), nodePortStaticSize) n.NodePortAddrCidr = nodePortStaticCidr } return } ================================================ FILE: netconf/base.go ================================================ package netconf import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) Base(db *database.Database) (err error) { if n.PhysicalExternalIface != "" { n.PhysicalExternalIfaceBridge, err = utils.IsInterfaceBridge( n.PhysicalExternalIface) if err != nil { return } } n.PhysicalInternalIfaceBridge, err = utils.IsInterfaceBridge( n.PhysicalInternalIface) if err != nil { return } return } ================================================ FILE: netconf/bridge.go ================================================ package netconf import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/iptables" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) bridgeNet(db *database.Database) (err error) { err = iproute.BridgeAdd(n.Namespace, n.SpaceBridgeIface) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.br0.arp_accept=0", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.br0.arp_ignore=2", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.br0.arp_filter=1", ) if err != nil { return } return } func (n *NetConf) bridgeMaster(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", n.BridgeInternalIface, "master", n.SpaceBridgeIface, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", n.VirtIface, "master", n.SpaceBridgeIface, ) if err != nil { return } return } func (n *NetConf) bridgeRoute(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.InternalGatewayAddrCidr, "dev", n.SpaceBridgeIface, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "-6", "addr", "add", n.InternalGatewayAddr6.String()+"/64", "dev", n.SpaceBridgeIface, ) if err != nil { return } return } func (n *NetConf) bridgeIptables(db *database.Database) (err error) { iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ebtables", "-A", "INPUT", "-p", "ARP", "-i", "!", n.VirtIface, "--arp-ip-dst", n.InternalGatewayAddr.String(), "-j", "DROP", ) iptables.Unlock() if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ebtables", "-A", "OUTPUT", "-p", "ARP", "-o", "!", n.VirtIface, "--arp-ip-dst", n.InternalGatewayAddr.String(), "-j", "DROP", ) iptables.Unlock() if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ebtables", "-A", "FORWARD", "-p", "ARP", "-o", "!", n.VirtIface, "--arp-ip-dst", n.InternalGatewayAddr.String(), "-j", "DROP", ) iptables.Unlock() if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ebtables", "-A", "INPUT", "-p", "ARP", "-i", "!", n.VirtIface, "--arp-ip-src", n.InternalGatewayAddr.String(), "-j", "DROP", ) iptables.Unlock() if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ebtables", "-A", "OUTPUT", "-p", "ARP", "-o", "!", n.VirtIface, "--arp-ip-src", n.InternalGatewayAddr.String(), "-j", "DROP", ) iptables.Unlock() if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ebtables", "-A", "FORWARD", "-p", "ARP", "-o", "!", n.VirtIface, "--arp-ip-src", n.InternalGatewayAddr.String(), "-j", "DROP", ) iptables.Unlock() if err != nil { return } return } func (n *NetConf) bridgeUp(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceBridgeIface, "up", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "bridge", "link", "set", "dev", n.VirtIface, "hairpin", "on", ) if err != nil { return } return } func (n *NetConf) Bridge(db *database.Database) (err error) { err = n.bridgeNet(db) if err != nil { return } err = n.bridgeMaster(db) if err != nil { return } err = n.bridgeRoute(db) if err != nil { return } err = n.bridgeIptables(db) if err != nil { return } err = n.bridgeUp(db) if err != nil { return } return } ================================================ FILE: netconf/clear.go ================================================ package netconf import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/interfaces" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/tools/commander" ) func (n *NetConf) Clear(db *database.Database) (err error) { lockId := lock.Lock("clear") defer lock.Unlock("clear", lockId) clearIface("", n.SystemExternalIface) // bridged clearIface("", n.SystemInternalIface) // bridged clearIface("", n.SystemHostIface) // bridged clearIface("", n.SystemNodePortIface) // bridged clearIface("", n.SpaceExternalIface) clearIface("", n.SpaceExternalIfaceMod) clearIface("", n.SpaceExternalIfaceMod6) clearIface("", n.SpaceInternalIface) clearIface("", n.SpaceHostIface) clearIface("", n.SpaceNodePortIface) clearIface(n.Namespace, n.SpaceBridgeIface) clearIface(n.Namespace, n.SpaceImdsIface) interfaces.RemoveVirtIface(n.SystemExternalIface) interfaces.RemoveVirtIface(n.SystemInternalIface) interfaces.RemoveVirtIface(n.SystemNodePortIface) return } func (n *NetConf) ClearAll(db *database.Database) (err error) { if len(n.Virt.NetworkAdapters) == 0 { err = &errortypes.NotFoundError{ errors.New("qemu: Missing network interfaces"), } return } err = n.Clear(db) if err != nil { return } store.RemAddress(n.Virt.Id) store.RemRoutes(n.Virt.Id) store.RemArp(n.Virt.Id) return } func clearIface(namespace, iface string) { if iface == "" { return } if namespace != "" { commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", namespace, "ip", "link", "set", iface, "nomaster", }, PipeOut: true, PipeErr: true, }) time.Sleep(200 * time.Millisecond) commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", namespace, "ip", "link", "set", iface, "down", }, PipeOut: true, PipeErr: true, }) commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", namespace, "ip", "link", "del", iface, }, PipeOut: true, PipeErr: true, }) } else { commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "link", "set", iface, "nomaster", }, PipeOut: true, PipeErr: true, }) time.Sleep(200 * time.Millisecond) commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "link", "set", iface, "down", }, PipeOut: true, PipeErr: true, }) commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "link", "del", iface, }, PipeOut: true, PipeErr: true, }) } } ================================================ FILE: netconf/external.go ================================================ package netconf import ( "fmt" "strconv" "strings" "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) externalNet(db *database.Database) (err error) { if (n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud) { if n.PhysicalExternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", n.SystemExternalIface, "type", "veth", "peer", "name", n.SpaceExternalIface, "addr", n.ExternalMacAddr, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "tc", "qdisc", "replace", "dev", n.SystemExternalIface, "root", "fq_codel", ) if err != nil { return err } } else { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", n.SpaceExternalIface, "addr", n.ExternalMacAddr, "link", n.PhysicalExternalIface, "type", "macvlan", "mode", "bridge", ) if err != nil { return } } } return } func (n *NetConf) externalMtu(db *database.Database) (err error) { if (n.PhysicalExternalIfaceBridge && n.SystemExternalIfaceMtu != "") && ((n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud)) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemExternalIface, "mtu", n.SystemExternalIfaceMtu, ) if err != nil { return } } if n.SpaceExternalIfaceMtu != "" && ((n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud)) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SpaceExternalIface, "mtu", n.SpaceExternalIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) externalUp(db *database.Database) (err error) { if n.PhysicalExternalIfaceBridge && ((n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud)) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemExternalIface, "up", ) if err != nil { return } } return } func (n *NetConf) externalSysctl(db *database.Database) (err error) { if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud { _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.accept_ra=2", strings.ReplaceAll(n.PhysicalExternalIface, ".", "/")), ) if err != nil { return } if n.NetworkMode6 != node.Slaac && n.NetworkMode6 != node.DhcpSlaac { _, err = utils.ExecCombinedOutputLogged( nil, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.addr_gen_mode=1", strings.ReplaceAll(n.PhysicalExternalIface, ".", "/")), ) if err != nil { return } } } return } func (n *NetConf) externalMaster(db *database.Database) (err error) { if n.PhysicalExternalIfaceBridge && ((n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud)) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", n.SystemExternalIface, "master", n.PhysicalExternalIface, ) if err != nil { return } } return } func (n *NetConf) externalSpace(db *database.Database) (err error) { if (n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud) { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "link", "set", "dev", n.SpaceExternalIface, "netns", n.Namespace, ) if err != nil { return } if n.PhysicalExternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "tc", "qdisc", "replace", "dev", n.SpaceExternalIface, "root", "fq_codel", ) if err != nil { return err } } } return } func (n *NetConf) externalSpaceMod(db *database.Database) (err error) { if n.SpaceExternalIfaceMod != "" { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "link", "add", "link", n.SpaceExternalIface, "name", n.SpaceExternalIfaceMod, "type", "vlan", "id", strconv.Itoa(n.ExternalVlan), ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceExternalIfaceMod, "mtu", n.SpaceExternalIfaceMtu, ) if err != nil { return } } if n.SpaceExternalIfaceMod6 != "" && n.SpaceExternalIfaceMod6 != n.SpaceExternalIfaceMod { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "link", "add", "link", n.SpaceExternalIface, "name", n.SpaceExternalIfaceMod6, "type", "vlan", "id", strconv.Itoa(n.ExternalVlan6), ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceExternalIfaceMod6, "mtu", n.SpaceExternalIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) externalSpaceSysctl(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv6.conf.all.accept_ra=0", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv6.conf.default.accept_ra=0", ) if err != nil { return } if n.NetworkMode6 == node.Static { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.autoconf=0", n.SpaceExternalIface), ) if err != nil { return } if n.SpaceExternalIfaceMod6 != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.autoconf=0", n.SpaceExternalIfaceMod6), ) if err != nil { return } } } if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud { if n.SpaceExternalIfaceMod6 == "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.accept_ra=2", n.SpaceExternalIface), ) if err != nil { return } } else { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.accept_ra=2", n.SpaceExternalIfaceMod6), ) if err != nil { return } } } if (n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) && (n.NetworkMode6 == node.Disabled || n.NetworkMode6 == node.Cloud) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.disable_ipv6=1", n.SpaceExternalIface), ) if err != nil { return } } if n.SpaceExternalIfaceMod != n.SpaceExternalIfaceMod6 && n.SpaceExternalIfaceMod != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.disable_ipv6=1", n.SpaceExternalIfaceMod), ) if err != nil { return } } return } func (n *NetConf) externalSpaceUp(db *database.Database) (err error) { if (n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceExternalIface, "up", ) if err != nil { return } } if n.SpaceExternalIfaceMod != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceExternalIfaceMod, "up", ) if err != nil { return } } if n.SpaceExternalIfaceMod6 != "" && n.SpaceExternalIfaceMod6 != n.SpaceExternalIfaceMod { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceExternalIfaceMod6, "up", ) if err != nil { return } } return } func (n *NetConf) External(db *database.Database) (err error) { delay := time.Duration(settings.Hypervisor.ActionRate) * time.Second lockId := lock.Lock("external") defer lock.DelayUnlock("external", lockId, delay) err = n.externalNet(db) if err != nil { return } err = n.externalMtu(db) if err != nil { return } err = n.externalUp(db) if err != nil { return } err = n.externalSysctl(db) if err != nil { return } err = n.externalMaster(db) if err != nil { return } err = n.externalSpace(db) if err != nil { return } err = n.externalSpaceMod(db) if err != nil { return } err = n.externalSpaceSysctl(db) if err != nil { return } err = n.externalSpaceUp(db) if err != nil { return } return } ================================================ FILE: netconf/host.go ================================================ package netconf import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) hostNet(db *database.Database) (err error) { if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", n.SystemHostIface, "type", "veth", "peer", "name", n.SpaceHostIface, "addr", n.HostMacAddr, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "tc", "qdisc", "replace", "dev", n.SystemHostIface, "root", "fq_codel", ) if err != nil { return err } } return } func (n *NetConf) hostMtu(db *database.Database) (err error) { if n.HostNetwork && n.SystemHostIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemHostIface, "mtu", n.SystemHostIfaceMtu, ) if err != nil { return } } if n.HostNetwork && n.SpaceHostIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SpaceHostIface, "mtu", n.SpaceHostIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) hostUp(db *database.Database) (err error) { if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemHostIface, "up", ) if err != nil { return } } return } func (n *NetConf) hostMaster(db *database.Database) (err error) { if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", n.SystemHostIface, "master", n.PhysicalHostIface, ) if err != nil { return } } return } func (n *NetConf) hostSpace(db *database.Database) (err error) { if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "link", "set", "dev", n.SpaceHostIface, "netns", n.Namespace, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "tc", "qdisc", "replace", "dev", n.SpaceHostIface, "root", "fq_codel", ) if err != nil { return err } } return } func (n *NetConf) hostSpaceUp(db *database.Database) (err error) { if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceHostIface, "up", ) if err != nil { return } } return } func (n *NetConf) Host(db *database.Database) (err error) { delay := time.Duration(settings.Hypervisor.ActionRate) * time.Second lockId := lock.Lock("host") defer lock.DelayUnlock("host", lockId, delay) err = n.hostNet(db) if err != nil { return } err = n.hostMtu(db) if err != nil { return } err = n.hostUp(db) if err != nil { return } err = n.hostMaster(db) if err != nil { return } err = n.hostSpace(db) if err != nil { return } err = n.hostSpaceUp(db) if err != nil { return } return } ================================================ FILE: netconf/iface.go ================================================ package netconf import ( "strconv" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/interfaces" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/vm" ) func (n *NetConf) Iface1(db *database.Database) (err error) { n.NetworkMode = node.Self.NetworkMode if n.NetworkMode == "" { n.NetworkMode = node.Dhcp } n.NetworkMode6 = node.Self.NetworkMode6 if n.NetworkMode6 == "" { n.NetworkMode6 = node.Dhcp } if n.NetworkMode == node.Internal || n.Virt.NoPublicAddress { n.NetworkMode = node.Disabled } if n.Virt.NoPublicAddress6 { n.NetworkMode6 = node.Disabled } if !node.Self.NoHostNetwork && !n.Virt.NoHostAddress { n.HostNetwork = true if node.Self.HostNat { n.HostNat = true } blck, e := block.GetNodeBlock(node.Self.Id) if e != nil { err = e return } hostNetwork, e := blck.GetNetwork() if e != nil { err = e return } n.HostSubnet = hostNetwork.String() } if !node.Self.NoNodePortNetwork { n.NodePortNetwork = true blck, e := block.GetNodePortBlock(node.Self.Id) if e != nil { err = e return } nodePortNetwork, e := blck.GetNetwork() if e != nil { err = e return } n.NodePortSubnet = nodePortNetwork.String() } n.CloudSubnets = set.NewSet() if node.Self.CloudSubnets != nil { for _, subnet := range node.Self.CloudSubnets { n.CloudSubnets.Add(subnet) } } n.Namespace = vm.GetNamespace(n.Virt.Id, 0) if n.Virt.NetworkAdapters == nil || len(n.Virt.NetworkAdapters) < 1 { err = &errortypes.ParseError{ errors.New("netconf: Missing virt network adapter"), } return } n.VmAdapter = n.Virt.NetworkAdapters[0] n.VirtIface = vm.GetIface(n.Virt.Id, 0) n.SystemExternalIface = vm.GetIfaceNodeExternal(n.Virt.Id, 0) n.SystemInternalIface = vm.GetIfaceNodeInternal(n.Virt.Id, 0) n.SystemHostIface = vm.GetIfaceHost(n.Virt.Id, 0) n.SystemNodePortIface = vm.GetIfaceNodePort(n.Virt.Id, 0) n.SpaceExternalIface = vm.GetIfaceExternal(n.Virt.Id, 0) n.SpaceInternalIface = vm.GetIfaceInternal(n.Virt.Id, 0) n.SpaceHostIface = vm.GetIfaceHost(n.Virt.Id, 1) n.SpaceNodePortIface = vm.GetIfaceNodePort(n.Virt.Id, 1) n.SpaceCloudIface = vm.GetIfaceCloud(n.Virt.Id, 0) n.SpaceCloudVirtIface = vm.GetIfaceCloudVirt(n.Virt.Id, 0) n.SpaceBridgeIface = settings.Hypervisor.BridgeIfaceName n.SpaceImdsIface = settings.Hypervisor.ImdsIfaceName return } func (n *NetConf) Iface2(db *database.Database, clean bool) (err error) { dc, err := datacenter.Get(db, node.Self.Datacenter) if err != nil { return } n.Vxlan = dc.Vxlan() n.PhysicalHostIface = settings.Hypervisor.HostNetworkName n.PhysicalNodePortIface = settings.Hypervisor.NodePortNetworkName n.BridgeInternalIface = vm.GetIfaceVlan(n.Virt.Id, 0) n.PhysicalInternalIface = interfaces.GetInternal( n.SystemInternalIface, n.Vxlan) mtuSizeExternal := dc.GetBaseExternalMtu() mtuSizeInternal := dc.GetBaseInternalMtu() mtuSizeOverlay := dc.GetOverlayMtu() mtuSizeInstance := dc.GetInstanceMtu() n.SpaceExternalIfaceMtu = strconv.Itoa(mtuSizeExternal) n.SystemExternalIfaceMtu = strconv.Itoa(mtuSizeExternal) n.SpaceHostIfaceMtu = strconv.Itoa(mtuSizeInternal) n.SpaceNodePortIfaceMtu = strconv.Itoa(mtuSizeInternal) n.SystemHostIfaceMtu = strconv.Itoa(mtuSizeInternal) n.SystemNodePortIfaceMtu = strconv.Itoa(mtuSizeInternal) n.ImdsIfaceMtu = strconv.Itoa(mtuSizeInternal) n.SpaceInternalIfaceMtu = strconv.Itoa(mtuSizeOverlay) n.BridgeInternalIfaceMtu = strconv.Itoa(mtuSizeOverlay) n.SystemInternalIfaceMtu = strconv.Itoa(mtuSizeOverlay) n.VirtIfaceMtu = strconv.Itoa(mtuSizeInstance) return } ================================================ FILE: netconf/imds.go ================================================ package netconf import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) imdsNet(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "netns", "exec", n.Namespace, "ip", "link", "add", n.SpaceImdsIface, "type", "dummy", //"addr", n.ImdsMacAddr, ) if err != nil { return } return } func (n *NetConf) imdsMtu(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceImdsIface, "mtu", n.ImdsIfaceMtu, ) if err != nil { return } return } func (n *NetConf) imdsAddr(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", settings.Hypervisor.ImdsAddress, "dev", n.SpaceImdsIface, ) if err != nil { return } return } func (n *NetConf) imdsUp(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceImdsIface, "up", ) if err != nil { return } return } func (n *NetConf) imdsStart(db *database.Database) (err error) { err = imds.Start(db, n.Virt) if err != nil { return } return } func (n *NetConf) Imds(db *database.Database) (err error) { err = n.imdsNet(db) if err != nil { return } err = n.imdsMtu(db) if err != nil { return } err = n.imdsAddr(db) if err != nil { return } err = n.imdsUp(db) if err != nil { return } err = n.imdsStart(db) if err != nil { return } return } ================================================ FILE: netconf/internal.go ================================================ package netconf import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) internalNet(db *database.Database) (err error) { if n.PhysicalInternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", n.SystemInternalIface, "type", "veth", "peer", "name", n.SpaceInternalIface, "addr", n.InternalMacAddr, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "tc", "qdisc", "replace", "dev", n.SystemInternalIface, "root", "fq_codel", ) if err != nil { return err } } else { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", n.SpaceInternalIface, "addr", n.InternalMacAddr, "link", n.PhysicalInternalIface, "type", "macvlan", "mode", "bridge", ) if err != nil { return } } return } func (n *NetConf) internalMtu(db *database.Database) (err error) { if n.SystemInternalIfaceMtu != "" && n.PhysicalInternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemInternalIface, "mtu", n.SystemInternalIfaceMtu, ) if err != nil { return } } if n.SpaceInternalIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SpaceInternalIface, "mtu", n.SpaceInternalIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) internalUp(db *database.Database) (err error) { if n.PhysicalInternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemInternalIface, "up", ) if err != nil { return } } return } func (n *NetConf) internalMaster(db *database.Database) (err error) { if n.PhysicalInternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", n.SystemInternalIface, "master", n.PhysicalInternalIface, ) if err != nil { return } } return } func (n *NetConf) internalSpace(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "link", "set", "dev", n.SpaceInternalIface, "netns", n.Namespace, ) if err != nil { return } if n.PhysicalInternalIfaceBridge { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "tc", "qdisc", "replace", "dev", n.SpaceInternalIface, "root", "fq_codel", ) if err != nil { return err } } return } func (n *NetConf) internalSpaceUp(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceInternalIface, "up", ) if err != nil { return } return } func (n *NetConf) Internal(db *database.Database) (err error) { delay := time.Duration(settings.Hypervisor.ActionRate) * time.Second lockId := lock.Lock("internal") defer lock.DelayUnlock("internal", lockId, delay) err = n.internalNet(db) if err != nil { return } err = n.internalMtu(db) if err != nil { return } err = n.internalUp(db) if err != nil { return } err = n.internalMaster(db) if err != nil { return } err = n.internalSpace(db) if err != nil { return } err = n.internalSpaceUp(db) if err != nil { return } return } ================================================ FILE: netconf/ip.go ================================================ package netconf import ( "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/dhcpc" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/iptables" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) func (n *NetConf) ipExternal(db *database.Database) (err error) { if n.NetworkMode == node.Dhcp || n.NetworkMode6 == node.Dhcp || n.NetworkMode6 == node.DhcpSlaac { err = dhcpc.Start( db, n.Virt, n.SpaceExternalIface, n.SpaceExternalIface, n.NetworkMode == node.Dhcp, n.NetworkMode6 == node.Dhcp || n.NetworkMode6 == node.DhcpSlaac, ) if err != nil { return } var imdsErr error ip4 := false ip6 := false ipTimeout := settings.Hypervisor.IpTimeout * 4 for i := 0; i < ipTimeout; i++ { stat, e := imds.State(db, n.Virt.Id, n.Virt.ImdsHostSecret) if e != nil { imdsErr = e time.Sleep(250 * time.Millisecond) continue } if stat == nil { time.Sleep(250 * time.Millisecond) continue } if stat.DhcpIp != nil && stat.DhcpGateway != nil { _, err = utils.ExecCombinedOutputLogged( []string{"File exists", "already assigned"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", stat.DhcpIp.String(), "dev", n.SpaceExternalIface, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "route", "add", "default", "via", stat.DhcpGateway.String(), "dev", n.SpaceExternalIface, ) if err != nil { return } ip4 = true } if stat.DhcpIp6 != nil { _, err = utils.ExecCombinedOutputLogged( []string{"File exists", "already assigned"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", stat.DhcpIp6.String(), "dev", n.SpaceExternalIface, ) if err != nil { return } ip6 = true } if (n.NetworkMode != node.Dhcp || ip4) && ((n.NetworkMode6 != node.Dhcp && n.NetworkMode6 != node.DhcpSlaac) || ip6) { break } time.Sleep(250 * time.Millisecond) } if !ip4 && n.NetworkMode == node.Dhcp { if imdsErr != nil { logrus.WithFields(logrus.Fields{ "instance": n.Virt.Id.Hex(), "dhcp4": ip4, "dhcp6": ip6, "error": imdsErr, }).Error("netconf: DHCP IPv4 timeout") } else { logrus.WithFields(logrus.Fields{ "instance": n.Virt.Id.Hex(), }).Error("netconf: DHCP IPv4 timeout") } } if !ip6 && (n.NetworkMode6 == node.Dhcp || n.NetworkMode6 == node.DhcpSlaac) { if imdsErr != nil { logrus.WithFields(logrus.Fields{ "instance": n.Virt.Id.Hex(), "dhcp4": ip4, "dhcp6": ip6, "error": imdsErr, }).Error("netconf: DHCP IPv6 timeout") } else { logrus.WithFields(logrus.Fields{ "instance": n.Virt.Id.Hex(), "dhcp4": ip4, "dhcp6": ip6, }).Error("netconf: DHCP IPv6 timeout") } } } if n.NetworkMode == node.Static { if n.SpaceExternalIfaceMod != "" { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.ExternalAddrCidr, "dev", n.SpaceExternalIfaceMod, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "route", "add", "default", "via", n.ExternalGatewayAddr.String(), "dev", n.SpaceExternalIfaceMod, ) if err != nil { return } } else { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.ExternalAddrCidr, "dev", n.SpaceExternalIface, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "route", "add", "default", "via", n.ExternalGatewayAddr.String(), "dev", n.SpaceExternalIface, ) if err != nil { return } } } if n.NetworkMode6 == node.Static { if n.SpaceExternalIfaceMod6 != "" { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.ExternalAddrCidr6, "dev", n.SpaceExternalIfaceMod6, ) if err != nil { return } if n.ExternalGatewayAddr6 != nil { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "-6", "route", "add", "default", "via", n.ExternalGatewayAddr6.String(), "dev", n.SpaceExternalIfaceMod6, ) if err != nil { return } } } else { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.ExternalAddrCidr6, "dev", n.SpaceExternalIface, ) if err != nil { return } if n.ExternalGatewayAddr6 != nil { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "-6", "route", "add", "default", "via", n.ExternalGatewayAddr6.String(), "dev", n.SpaceExternalIface, ) if err != nil { return } } } } return } func (n *NetConf) ipHost(db *database.Database) (err error) { if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.HostAddrCidr, "dev", n.SpaceHostIface, ) if err != nil { return } if n.NetworkMode == node.Disabled { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "route", "add", "default", "via", n.HostGatewayAddr.String(), ) if err != nil { return } } } return } func (n *NetConf) ipNodePort(db *database.Database) (err error) { if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.NodePortAddrCidr, "dev", n.SpaceNodePortIface, ) if err != nil { return } } return } func (n *NetConf) ipDetect(db *database.Database) (err error) { time.Sleep(250 * time.Millisecond) ipTimeout := settings.Hypervisor.IpTimeout * 4 ipTimeout6 := settings.Hypervisor.IpTimeout6 * 4 pubAddr := "" pubAddr6 := "" if n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud { for i := 0; i < ipTimeout; i++ { address, address6, e := iproute.AddressGetIfaceMod( n.Namespace, n.SpaceExternalIface) if e != nil { err = e return } if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud { if address != nil { pubAddr = address.Local } if address != nil && address6 != nil { if address6 != nil { pubAddr6 = address6.Local } break } } else if address != nil { pubAddr = address.Local break } time.Sleep(250 * time.Millisecond) } if pubAddr == "" { err = &errortypes.NetworkError{ errors.New("qemu: Instance missing IPv4 address"), } return } } else if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud { for i := 0; i < ipTimeout6; i++ { _, address6, e := iproute.AddressGetIfaceMod( n.Namespace, n.SpaceExternalIface) if e != nil { err = e return } if address6 != nil { pubAddr6 = address6.Local break } time.Sleep(250 * time.Millisecond) } if pubAddr6 == "" { err = &errortypes.NetworkError{ errors.New("qemu: Instance missing IPv6 address"), } return } } n.PublicAddress = pubAddr if n.ExternalAddrCidr6 != "" { n.PublicAddress6 = n.ExternalAddrCidr6 } else { n.PublicAddress6 = pubAddr6 } return } func (n *NetConf) ipHostIptables(db *database.Database) (err error) { if n.HostNetwork { iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-t", "nat", "-A", "POSTROUTING", "-s", n.InternalAddr.String()+"/32", "-d", n.InternalAddr.String()+"/32", "-m", "comment", "--comment", "pritunl_cloud_host_nat", "-j", "SNAT", "--to", n.HostAddr.String(), ) iptables.Unlock() if err != nil { return } if n.HostNat { iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-t", "nat", "-A", "POSTROUTING", "-s", n.InternalAddr.String()+"/32", "-o", n.SpaceHostIface, "-m", "comment", "--comment", "pritunl_cloud_host_nat", "-j", "MASQUERADE", ) iptables.Unlock() if err != nil { return } } else { iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-t", "nat", "-A", "POSTROUTING", "-s", n.InternalAddr.String()+"/32", "-d", n.HostSubnet, "-o", n.SpaceHostIface, "-m", "comment", "--comment", "pritunl_cloud_host_nat", "-j", "MASQUERADE", ) iptables.Unlock() if err != nil { return } } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-t", "nat", "-A", "PREROUTING", "-d", n.HostAddr.String()+"/32", "-m", "comment", "--comment", "pritunl_cloud_host_nat", "-j", "DNAT", "--to-destination", n.InternalAddr.String(), ) iptables.Unlock() if err != nil { return } } return } func (n *NetConf) ipDatabase(db *database.Database) (err error) { store.RemAddress(n.Virt.Id) store.RemRoutes(n.Virt.Id) store.RemArp(n.Virt.Id) hostIps := []string{} if n.HostAddr != nil { hostIps = append(hostIps, n.HostAddr.String()) } nodePortIps := []string{} if n.NodePortAddr != nil { nodePortIps = append(nodePortIps, n.NodePortAddr.String()) } coll := db.Instances() err = coll.UpdateId(n.Virt.Id, &bson.M{ "$set": &bson.M{ "private_ips": []string{n.InternalAddr.String()}, "private_ips6": []string{n.InternalAddr6.String()}, "gateway_ips": []string{n.InternalGatewayAddrCidr}, "gateway_ips6": []string{ n.InternalGatewayAddr6.String() + "/64"}, "network_namespace": n.Namespace, "host_ips": hostIps, "node_port_ips": nodePortIps, }, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if !n.Virt.Deployment.IsZero() { coll = db.Deployments() hostIps := []string{} if n.HostAddr != nil { hostIps = append(hostIps, n.HostAddr.String()) } privateIps := []string{} if n.InternalAddr != nil { privateIps = append(privateIps, n.InternalAddr.String()) } privateIps6 := []string{} if n.InternalAddr6 != nil { privateIps6 = append(privateIps6, n.InternalAddr6.String()) } err = coll.UpdateId(n.Virt.Deployment, &bson.M{ "$set": &bson.M{ "instance_data.host_ips": hostIps, "instance_data.private_ips": privateIps, "instance_data.private_ips6": privateIps6, }, }) if err != nil { err = database.ParseError(err) return } } return } func (n *NetConf) ipInit6(db *database.Database) (err error) { if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud && n.PublicAddress6 != "" && !settings.Hypervisor.NoIpv6PingInit { for i := 0; i < 3; i++ { time.Sleep(200 * time.Millisecond) resp, e := commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", n.Namespace, "dig", "@" + settings.Hypervisor.DnsServerPrimary6, "app6.pritunl.com", "AAAA", }, Timeout: 5 * time.Second, PipeOut: true, PipeErr: true, }) if e != nil { output := "" if resp != nil { output = string(resp.Output) } logrus.WithFields(logrus.Fields{ "instance_id": n.Virt.Id.Hex(), "namespace": n.Namespace, "address6": n.PublicAddress6, "output": output, }).Warn("netconf: IPv6 network DNS lookup test failed") continue } resp, e = commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", n.Namespace, "ping6", "-c", "3", "-i", "0.5", "-w", "6", settings.Hypervisor.Ipv6PingHost, }, Timeout: 6 * time.Second, PipeOut: true, PipeErr: true, }) if e != nil { output := "" if resp != nil { output = string(resp.Output) } logrus.WithFields(logrus.Fields{ "instance_id": n.Virt.Id.Hex(), "namespace": n.Namespace, "address6": n.PublicAddress6, "output": output, }).Warn("netconf: IPv6 network DNS lookup test failed") continue } break } } return } func (n *NetConf) ipArp(db *database.Database) (err error) { if n.NetworkMode == node.Static { addr := strings.Split(n.ExternalAddrCidr, "/")[0] iface := n.SpaceExternalIfaceMod if iface == "" { iface = n.SpaceExternalIface } _, _ = commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", n.Namespace, "arping", "-U", "-I", iface, "-c", "3", addr, }, Timeout: 6 * time.Second, PipeOut: true, PipeErr: true, }) _, _ = commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", n.Namespace, "arping", "-I", iface, "-c", "3", n.ExternalGatewayAddr.String(), }, Timeout: 6 * time.Second, PipeOut: true, PipeErr: true, }) } if n.NetworkMode6 == node.Static { addr := strings.Split(n.ExternalAddrCidr6, "/")[0] iface := n.SpaceExternalIfaceMod6 if iface == "" { iface = n.SpaceExternalIface } _, _ = commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", n.Namespace, "ndisc6", "-r", "3", addr, iface, }, Timeout: 6 * time.Second, PipeOut: true, PipeErr: true, }) if n.ExternalGatewayAddr6 != nil { _, _ = commander.Exec(&commander.Opt{ Name: "ip", Args: []string{ "netns", "exec", n.Namespace, "ping6", "-c", "3", "-i", "0.5", "-w", "6", "-I", iface, n.ExternalGatewayAddr6.String(), }, Timeout: 8 * time.Second, PipeOut: true, PipeErr: true, }) } } return } func (n *NetConf) ipInit6Alt(db *database.Database) (err error) { if n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud && n.PublicAddress6 != "" && !settings.Hypervisor.NoIpv6PingInit { addrs, e := utils.DnsLookup( settings.Hypervisor.DnsServerPrimary6, "app6.pritunl.com", ) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": n.Virt.Id.Hex(), "namespace": n.Namespace, "address6": n.PublicAddress6, "error": e, }).Warn("netconf: Failed to initialize IPv6 network DNS lookup") } else if addrs != nil && len(addrs) > 0 { output, e := utils.ExecCombinedOutput( "", "ip", "netns", "exec", n.Namespace, "ping6", "-c", "3", "-i", "0.5", "-w", "6", addrs[0], ) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": n.Virt.Id.Hex(), "namespace": n.Namespace, "address6": n.PublicAddress6, "output": output, }).Warn("netconf: Failed to initialize IPv6 network ping") } } else { logrus.WithFields(logrus.Fields{ "instance_id": n.Virt.Id.Hex(), "namespace": n.Namespace, "address6": n.PublicAddress6, "lookup": addrs, }).Warn("netconf: Failed to initialize IPv6 network DNS lookup") } } return } func (n *NetConf) Ip(db *database.Database) (err error) { err = n.ipExternal(db) if err != nil { return } err = n.ipHost(db) if err != nil { return } err = n.ipNodePort(db) if err != nil { return } err = n.ipDetect(db) if err != nil { return } err = n.ipHostIptables(db) if err != nil { return } err = n.ipDatabase(db) if err != nil { return } err = n.ipArp(db) if err != nil { return } err = n.ipInit6(db) if err != nil { return } return } ================================================ FILE: netconf/netconf.go ================================================ package netconf import ( "net" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) var ( lock = utils.NewMultiTimeoutLock(2 * time.Minute) ) type NetConf struct { Virt *vm.VirtualMachine Vxlan bool VlanId int NetworkMode string NetworkMode6 string HostNetwork bool HostNat bool HostSubnet string NodePortNetwork bool NodePortSubnet string CloudSubnets set.Set Namespace string VmAdapter *vm.NetworkAdapter PublicAddress string PublicAddress6 string CloudVlan int CloudAddress string CloudAddressSubnet string CloudRouterAddress string CloudAddress6 string CloudAddressSubnet6 string CloudRouterAddress6 string CloudMetal bool SpaceBridgeIface string VirtIface string SpaceExternalIface string SpaceExternalIfaceMod string SpaceExternalIfaceMod6 string SpaceInternalIface string SpaceHostIface string SpaceNodePortIface string SpaceCloudIface string SpaceCloudVirtIface string SpaceImdsIface string BridgeInternalIface string SystemExternalIface string SystemInternalIface string SystemHostIface string SystemNodePortIface string PhysicalExternalIface string PhysicalExternalIfaceBridge bool PhysicalInternalIface string PhysicalInternalIfaceBridge bool PhysicalHostIface string PhysicalNodePortIface string SpaceExternalIfaceMtu string SystemExternalIfaceMtu string SpaceInternalIfaceMtu string BridgeInternalIfaceMtu string SystemInternalIfaceMtu string SpaceHostIfaceMtu string SystemHostIfaceMtu string ImdsIfaceMtu string SpaceNodePortIfaceMtu string SystemNodePortIfaceMtu string VirtIfaceMtu string InternalAddr net.IP InternalGatewayAddr net.IP InternalGatewayAddrCidr string InternalAddr6 net.IP InternalGatewayAddr6 net.IP ExternalVlan int ExternalAddrCidr string ExternalGatewayAddr net.IP ExternalVlan6 int ExternalAddrCidr6 string ExternalGatewayAddr6 net.IP HostAddr net.IP HostAddrCidr string HostGatewayAddr net.IP NodePortAddr net.IP NodePortAddrCidr string ExternalMacAddr string InternalMacAddr string HostMacAddr string NodePortMacAddr string } func (n *NetConf) Init(db *database.Database) (err error) { err = n.Validate() if err != nil { return } err = n.Iface1(db) if err != nil { return } err = n.Address(db) if err != nil { return } err = n.Iface2(db, false) if err != nil { return } err = n.Clear(db) if err != nil { return } err = n.Base(db) if err != nil { return } err = n.Oracle(db) if err != nil { return } err = n.External(db) if err != nil { return } err = n.Internal(db) if err != nil { return } err = n.Host(db) if err != nil { return } err = n.NodePort(db) if err != nil { return } err = n.Space(db) if err != nil { return } err = n.Vlan(db) if err != nil { return } err = n.Bridge(db) if err != nil { return } err = n.Imds(db) if err != nil { return } err = n.Ip(db) if err != nil { return } return } func (n *NetConf) Clean(db *database.Database) (err error) { err = n.Iface1(db) if err != nil { return } err = n.Iface2(db, true) if err != nil { return } err = n.ClearAll(db) if err != nil { return } return } ================================================ FILE: netconf/nodeport.go ================================================ package netconf import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) nodePortNet(db *database.Database) (err error) { if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", n.SystemNodePortIface, "type", "veth", "peer", "name", n.SpaceNodePortIface, "addr", n.NodePortMacAddr, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "tc", "qdisc", "replace", "dev", n.SystemNodePortIface, "root", "fq_codel", ) if err != nil { return err } } return } func (n *NetConf) nodePortMtu(db *database.Database) (err error) { if n.NodePortNetwork && n.SystemNodePortIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemNodePortIface, "mtu", n.SystemNodePortIfaceMtu, ) if err != nil { return } } if n.NodePortNetwork && n.SpaceNodePortIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SpaceNodePortIface, "mtu", n.SpaceNodePortIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) nodePortUp(db *database.Database) (err error) { if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", n.SystemNodePortIface, "up", ) if err != nil { return } } return } func (n *NetConf) nodePortMaster(db *database.Database) (err error) { if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", n.SystemNodePortIface, "master", n.PhysicalNodePortIface, ) if err != nil { return } } return } func (n *NetConf) nodePortSpace(db *database.Database) (err error) { if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "link", "set", "dev", n.SpaceNodePortIface, "netns", n.Namespace, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "tc", "qdisc", "replace", "dev", n.SpaceNodePortIface, "root", "fq_codel", ) if err != nil { return err } } return } func (n *NetConf) nodePortSpaceUp(db *database.Database) (err error) { if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceNodePortIface, "up", ) if err != nil { return } } return } func (n *NetConf) NodePort(db *database.Database) (err error) { delay := time.Duration(settings.Hypervisor.ActionRate) * time.Second lockId := lock.Lock("nodeport") defer lock.DelayUnlock("nodeport", lockId, delay) err = n.nodePortNet(db) if err != nil { return } err = n.nodePortMtu(db) if err != nil { return } err = n.nodePortUp(db) if err != nil { return } err = n.nodePortMaster(db) if err != nil { return } err = n.nodePortSpace(db) if err != nil { return } err = n.nodePortSpaceUp(db) if err != nil { return } return } ================================================ FILE: netconf/oracle.go ================================================ package netconf import ( "net" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/oracle" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) func (n *NetConf) oracleInitVnic(db *database.Database) (err error) { pv, err := oracle.NewProvider(node.Self.GetOracleAuthProvider()) if err != nil { return } var vnic *oracle.Vnic found := false if n.Virt.CloudVnic != "" { vnic, err = oracle.GetVnic(pv, n.Virt.CloudVnic) if err != nil { if _, ok := err.(*errortypes.NotFoundError); ok { logrus.WithFields(logrus.Fields{ "vnic_id": n.Virt.CloudVnic, "error": err, }).Warn("netconf: Cloud vnic not found, creating new vnic") err = nil } else { return } } if vnic == nil { found = false } else if vnic.SubnetId != n.Virt.CloudSubnet { err = oracle.RemoveVnic(pv, n.Virt.CloudVnicAttach) if err != nil { return } vnic = nil } else if !n.CloudSubnets.Contains(vnic.SubnetId) { err = oracle.RemoveVnic(pv, n.Virt.CloudVnicAttach) if err != nil { return } vnic = nil } else { found = true } } if !n.CloudSubnets.Contains(n.Virt.CloudSubnet) { err = &errortypes.NotFoundError{ errors.New("netconf: Invalid cloud subnet"), } return } if !found { vnicId, vnicAttachId, e := oracle.CreateVnic( pv, n.Virt.Id.Hex(), n.Virt.CloudSubnet, !n.Virt.NoPublicAddress, !n.Virt.NoPublicAddress6) if e != nil { err = e return } n.Virt.CloudVnic = vnicId n.Virt.CloudVnicAttach = vnicAttachId err = n.Virt.CommitCloudVnic(db) if err != nil { _ = oracle.RemoveVnic(pv, vnicAttachId) return } } return } func (n *NetConf) oracleConfVnic(db *database.Database) (err error) { mdata, e := oracle.GetOciMetadata() if e != nil { err = e return } n.CloudMetal = mdata.IsBareMetal() if n.CloudMetal { err = n.oracleConfVnicMetal(db) if err != nil { return } } else { err = n.oracleConfVnicVirt(db) if err != nil { return } } return } func (n *NetConf) oracleConfVnicMetal(db *database.Database) (err error) { found := false nicIndex := 0 macAddr := "" physicalMacAddr := "" pv, err := oracle.NewProvider(node.Self.GetOracleAuthProvider()) if err != nil { return } for i := 0; i < 120; i++ { time.Sleep(2 * time.Second) mdata, e := oracle.GetOciMetadata() if e != nil { err = e return } for _, vnic := range mdata.Vnics { if vnic.Id == n.Virt.CloudVnic { n.Virt.CloudPrivateIp = vnic.PrivateIp n.CloudVlan = vnic.VlanTag n.CloudAddress = vnic.PrivateIp n.CloudAddressSubnet = vnic.SubnetCidrBlock n.CloudRouterAddress = vnic.VirtualRouterIp if len(vnic.Ipv6Addresses) > 0 { n.CloudAddress6 = vnic.Ipv6Addresses[0] n.CloudAddressSubnet6 = vnic.Ipv6SubnetCidrBlock n.CloudRouterAddress6 = vnic.Ipv6VirtualRouterIp } nicIndex = vnic.NicIndex macAddr = strings.ToLower(vnic.MacAddr) found = true break } } if found { break } } if !found { err = &errortypes.NotFoundError{ errors.New("netconf: Failed to find vnic"), } return } mdata, err := oracle.GetOciMetadata() if err != nil { return } found = false for _, vnic := range mdata.Vnics { if vnic.NicIndex == nicIndex && vnic.VlanTag == 0 { physicalMacAddr = strings.ToLower(vnic.MacAddr) found = true break } } if !found { err = &errortypes.NotFoundError{ errors.New("netconf: Failed to find physical nic"), } return } vnic, err := oracle.GetVnic(pv, n.Virt.CloudVnic) if err != nil { return } n.Virt.CloudPublicIp = vnic.PublicIp n.Virt.CloudPublicIp6 = vnic.PublicIp6 err = n.Virt.CommitCloudIps(db) if err != nil { return } ifaces, err := net.Interfaces() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "netconf: Failed get network interfaces"), } return } physicalIface := "" for _, iface := range ifaces { if strings.ToLower(iface.HardwareAddr.String()) == physicalMacAddr { physicalIface = iface.Name break } } if physicalIface == "" { err = &errortypes.NotFoundError{ errors.New("netconf: Failed to find cloud physical interface"), } return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "add", "link", physicalIface, "name", n.SpaceCloudVirtIface, "address", macAddr, "type", "macvlan", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "link", "set", "dev", n.SpaceCloudVirtIface, "netns", n.Namespace, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "link", "add", "link", n.SpaceCloudVirtIface, "name", n.SpaceCloudIface, "type", "vlan", "id", strconv.Itoa(n.CloudVlan), ) if err != nil { return } return } func (n *NetConf) oracleConfVnicVirt(db *database.Database) (err error) { found := false cloudMacAddr := "" pv, err := oracle.NewProvider(node.Self.GetOracleAuthProvider()) if err != nil { return } for i := 0; i < 120; i++ { time.Sleep(2 * time.Second) mdata, e := oracle.GetOciMetadata() if e != nil { err = e return } for _, vnic := range mdata.Vnics { if vnic.Id == n.Virt.CloudVnic { n.Virt.CloudPrivateIp = vnic.PrivateIp n.CloudAddress = vnic.PrivateIp n.CloudAddressSubnet = vnic.SubnetCidrBlock n.CloudRouterAddress = vnic.VirtualRouterIp if len(vnic.Ipv6Addresses) > 0 { n.CloudAddress6 = vnic.Ipv6Addresses[0] n.CloudAddressSubnet6 = vnic.Ipv6SubnetCidrBlock n.CloudRouterAddress6 = vnic.Ipv6VirtualRouterIp } cloudMacAddr = strings.ToLower(vnic.MacAddr) found = true break } } if found { break } } if !found { err = &errortypes.NotFoundError{ errors.New("netconf: Failed to find vnic"), } return } vnic, err := oracle.GetVnic(pv, n.Virt.CloudVnic) if err != nil { return } n.Virt.CloudPublicIp = vnic.PublicIp n.Virt.CloudPublicIp6 = vnic.PublicIp6 err = n.Virt.CommitCloudIps(db) if err != nil { return } ifaces, err := net.Interfaces() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "netconf: Failed get network interfaces"), } return } cloudIface := "" for _, iface := range ifaces { if strings.ToLower(iface.HardwareAddr.String()) == cloudMacAddr { cloudIface = iface.Name break } } if cloudIface == "" { err = &errortypes.NotFoundError{ errors.New("netconf: Failed to find cloud interface"), } return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", cloudIface, "down", ) if err != nil { return } if cloudIface != n.SpaceCloudIface { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", cloudIface, "name", n.SpaceCloudIface, ) if err != nil { return } } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "link", "set", "dev", n.SpaceCloudIface, "netns", n.Namespace, ) if err != nil { return } return } func (n *NetConf) oracleMtu(db *database.Database) (err error) { if n.CloudMetal { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceCloudVirtIface, "mtu", "9000", ) if err != nil { return } } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceCloudIface, "mtu", "9000", ) if err != nil { return } return } func (n *NetConf) oracleIp(db *database.Database) (err error) { subnetSplit := strings.Split(n.CloudAddressSubnet, "/") if len(subnetSplit) != 2 { err = &errortypes.ParseError{ errors.Newf("netconf: Failed to get cloud cidr %s", n.CloudAddressSubnet), } return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.CloudAddress+"/"+subnetSplit[1], "dev", n.SpaceCloudIface, ) if err != nil { return } if n.CloudAddress6 != "" { subnetSplit6 := strings.Split(n.CloudAddressSubnet6, "/") if len(subnetSplit6) != 2 { err = &errortypes.ParseError{ errors.Newf("netconf: Failed to get cloud cidr6 %s", n.CloudAddressSubnet6), } return } _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "addr", "add", n.CloudAddress6+"/"+subnetSplit6[1], "dev", n.SpaceCloudIface, ) if err != nil { return } } return } func (n *NetConf) oracleUp(db *database.Database) (err error) { if n.CloudMetal { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceCloudVirtIface, "up", ) if err != nil { return } } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.SpaceCloudIface, "up", ) if err != nil { return } return } func (n *NetConf) oracleRoute(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "route", "add", "default", "via", n.CloudRouterAddress, "dev", n.SpaceCloudIface, ) if err != nil { return } if n.CloudAddress6 != "" && n.CloudRouterAddress6 != "" { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "-6", "route", "add", "default", "via", n.CloudRouterAddress6, "dev", n.SpaceCloudIface, ) if err != nil { return } } return } func (n *NetConf) Oracle(db *database.Database) (err error) { if n.NetworkMode != node.Cloud || n.Virt.CloudSubnet == "" { return } delay := time.Duration(settings.Hypervisor.ActionRate) * time.Second lockId := lock.Lock("oracle") defer lock.DelayUnlock("oracle", lockId, delay) err = n.oracleInitVnic(db) if err != nil { return } err = n.oracleConfVnic(db) if err != nil { return } err = n.oracleMtu(db) if err != nil { return } err = n.oracleIp(db) if err != nil { return } err = n.oracleUp(db) if err != nil { return } err = n.oracleRoute(db) if err != nil { return } return } ================================================ FILE: netconf/space.go ================================================ package netconf import ( "fmt" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/iptables" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) spaceSysctl(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.all.accept_redirects=0", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.default.accept_redirects=0", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.all.rp_filter=1", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.conf.default.rp_filter=1", ) if err != nil { return } if n.HostNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.disable_ipv6=1", n.SpaceHostIface), ) if err != nil { return } } if n.NodePortNetwork { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", fmt.Sprintf("net.ipv6.conf.%s.disable_ipv6=1", n.SpaceNodePortIface), ) if err != nil { return } } return } func (n *NetConf) spaceForward(db *database.Database) (err error) { if (n.NetworkMode != node.Disabled && n.NetworkMode != node.Cloud) || (n.NetworkMode6 != node.Disabled && n.NetworkMode6 != node.Cloud) { _, err = utils.ExecCombinedOutputLogged( []string{"already exists"}, "ip", "netns", "exec", n.Namespace, "ipset", "create", "prx_inst6", "hash:net", "family", "inet6", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"already added"}, "ip", "netns", "exec", n.Namespace, "ipset", "add", "prx_inst6", "fe80::/64", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( []string{"already added"}, "ip", "netns", "exec", n.Namespace, "ipset", "add", "prx_inst6", n.InternalAddr6.String()+"/128", ) if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-I", "FORWARD", "1", "!", "-d", n.InternalAddr.String()+"/32", "-i", n.SpaceExternalIface+"+", "-m", "comment", "--comment", "pritunl_cloud_base", "-j", "DROP", ) iptables.Unlock() if err != nil { return } iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip6tables", "-I", "FORWARD", "1", "-m", "set", "!", "--match-set", "prx_inst6", "dst", "-i", n.SpaceExternalIface+"+", "-m", "comment", "--comment", "pritunl_cloud_base", "-j", "DROP", ) iptables.Unlock() if err != nil { return } } if n.HostNetwork { iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-I", "FORWARD", "1", "!", "-d", n.InternalAddr.String()+"/32", "-i", n.SpaceHostIface, "-m", "comment", "--comment", "pritunl_cloud_base", "-j", "DROP", ) iptables.Unlock() if err != nil { return } } if n.NodePortNetwork { iptables.Lock() _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "iptables", "-I", "FORWARD", "1", "!", "-d", n.InternalAddr.String()+"/32", "-i", n.SpaceNodePortIface, "-m", "comment", "--comment", "pritunl_cloud_base", "-j", "DROP", ) iptables.Unlock() if err != nil { return } } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv4.ip_forward=1", ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "sysctl", "-w", "net.ipv6.conf.all.forwarding=1", ) if err != nil { return } return } func (n *NetConf) spaceVirt(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", "File exists", }, "ip", "link", "set", "dev", n.VirtIface, "netns", n.Namespace, ) if err != nil { return } return } func (n *NetConf) spaceLoopback(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", "lo", "up", ) if err != nil { return } return } func (n *NetConf) spaceMtu(db *database.Database) (err error) { if n.VirtIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.VirtIface, "mtu", n.VirtIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) spaceUp(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.VirtIface, "up", ) if err != nil { return } return } func (n *NetConf) Space(db *database.Database) (err error) { err = n.spaceSysctl(db) if err != nil { return } err = n.spaceForward(db) if err != nil { return } err = n.spaceVirt(db) if err != nil { return } err = n.spaceLoopback(db) if err != nil { return } err = n.spaceMtu(db) if err != nil { return } err = n.spaceUp(db) if err != nil { return } return } ================================================ FILE: netconf/utils.go ================================================ package netconf import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/oracle" "github.com/pritunl/pritunl-cloud/vm" ) func New(virt *vm.VirtualMachine) *NetConf { return &NetConf{ Virt: virt, } } func Destroy(db *database.Database, virt *vm.VirtualMachine) (err error) { if virt.CloudVnicAttach == "" { return } pv, err := oracle.NewProvider(node.Self.GetOracleAuthProvider()) if err != nil { return } err = oracle.RemoveVnic(pv, virt.CloudVnicAttach) if err != nil { return } return } ================================================ FILE: netconf/validate.go ================================================ package netconf import ( "net" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/vm" ) func (n *NetConf) Validate() (err error) { namespace := vm.GetNamespace(n.Virt.Id, 0) if len(n.Virt.NetworkAdapters) == 0 { err = &errortypes.NotFoundError{ errors.New("netconf: Missing network interfaces"), } return } ifaceNames := set.NewSet() for i := range n.Virt.NetworkAdapters { ifaceNames.Add(vm.GetIface(n.Virt.Id, i)) } for i := range n.Virt.NetworkAdapters { ifaceNames.Add(vm.GetIface(n.Virt.Id, i)) } for i := 0; i < 100; i++ { ifaces, e := net.Interfaces() if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to get network interfaces"), } return } for _, iface := range ifaces { if ifaceNames.Contains(iface.Name) { ifaceNames.Remove(iface.Name) } } if ifaceNames.Len() == 0 { break } ifaces2, e := iproute.IfaceGetAll(namespace) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to get network interfaces"), } return } for _, iface := range ifaces2 { if ifaceNames.Contains(iface.Name) { ifaceNames.Remove(iface.Name) } } if ifaceNames.Len() == 0 { break } time.Sleep(250 * time.Millisecond) } if ifaceNames.Len() != 0 { err = &errortypes.ReadError{ errors.New("qemu: Failed to find network interfaces"), } return } return } ================================================ FILE: netconf/vlan.go ================================================ package netconf import ( "strconv" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func (n *NetConf) vlanNet(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( []string{"File exists"}, "ip", "netns", "exec", n.Namespace, "ip", "link", "add", "link", n.SpaceInternalIface, "name", n.BridgeInternalIface, "type", "vlan", "id", strconv.Itoa(n.VlanId), ) if err != nil { return } return } func (n *NetConf) vlanMtu(db *database.Database) (err error) { if n.SpaceInternalIfaceMtu != "" { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.BridgeInternalIface, "mtu", n.SpaceInternalIfaceMtu, ) if err != nil { return } } return } func (n *NetConf) vlanUp(db *database.Database) (err error) { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", n.Namespace, "ip", "link", "set", "dev", n.BridgeInternalIface, "up", ) if err != nil { return } return } func (n *NetConf) Vlan(db *database.Database) (err error) { err = n.vlanNet(db) if err != nil { return } err = n.vlanMtu(db) if err != nil { return } err = n.vlanUp(db) if err != nil { return } return } ================================================ FILE: node/block.go ================================================ package node import "github.com/pritunl/mongo-go-driver/v2/bson" type BlockAttachment struct { Interface string `bson:"interface" json:"interface"` Block bson.ObjectID `bson:"block" json:"block"` } ================================================ FILE: node/certificate.go ================================================ package node import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "time" "github.com/sirupsen/logrus" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" ) func selfCert(parent *x509.Certificate, parentKey *ecdsa.PrivateKey) ( cert *x509.Certificate, certByt []byte, certKey *ecdsa.PrivateKey, err error) { certKey, err = ecdsa.GenerateKey( elliptic.P384(), rand.Reader, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "certificate: Failed to generate private key"), } return } serialLimit := new(big.Int).Lsh(big.NewInt(1), 128) serial, err := rand.Int(rand.Reader, serialLimit) if err != nil { err = &errortypes.ReadError{ errors.Wrap( err, "certificate: Failed to generate certificate serial", ), } return } certTempl := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ Organization: []string{"Pritunl Cloud"}, }, NotBefore: time.Now().Add(-24 * time.Hour), NotAfter: time.Now().Add(26280 * time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, SignatureAlgorithm: x509.ECDSAWithSHA256, } if parent == nil { parent = certTempl parentKey = certKey } certByt, err = x509.CreateCertificate(rand.Reader, certTempl, parent, certKey.Public(), parentKey) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "certificate: Failed to create certificate"), } return } cert, err = x509.ParseCertificate(certByt) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "certificate: Failed to parse certificate"), } return } return } func SelfCert() (certPem, keyPem []byte, err error) { if Self.SelfCertificate != "" && Self.SelfCertificateKey != "" { certPem = []byte(Self.SelfCertificate) keyPem = []byte(Self.SelfCertificateKey) return } logrus.Info("certificate: Generating self signed certificate") caCert, _, caKey, err := selfCert(nil, nil) if err != nil { return } _, certByt, certKey, err := selfCert(caCert, caKey) if err != nil { return } certKeyByte, err := x509.MarshalECPrivateKey(certKey) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "certificate: Failed to parse private key"), } return } certKeyBlock := &pem.Block{ Type: "EC PRIVATE KEY", Bytes: certKeyByte, } keyPem = pem.EncodeToMemory(certKeyBlock) certBlock := &pem.Block{ Type: "CERTIFICATE", Bytes: certByt, } certPem = pem.EncodeToMemory(certBlock) db := database.GetDatabase() defer db.Close() Self.SelfCertificate = string(certPem) Self.SelfCertificateKey = string(keyPem) err = Self.CommitFields(db, set.NewSet( "self_certificate", "self_certificate_key")) if err != nil { return } return } ================================================ FILE: node/constants.go ================================================ package node import ( "github.com/dropbox/godropbox/container/set" ) const ( Admin = "admin" User = "user" Balancer = "balancer" Hypervisor = "hypervisor" Qemu = "qemu" Kvm = "kvm" // std Std = "std" // vmware Vmware = "vmware" // virtio-vga Virtio = "virtio" // virtio-gpu-pci VirtioPci = "virtio_pci" // virtio-vga-gl VirtioVgaGl = "virtio_vga_gl" // virtio-vga-gl,venus=true VirtioVgaGlVulkan = "virtio_vga_gl_vulkan" // virtio-gpu-gl VirtioGl = "virtio_gl" // virtio-gpu-gl,venus=true VirtioGlVulkan = "virtio_gl_vulkan" // virtio-gpu-gl-pci VirtioPciGl = "virtio_pci_gl" // virtio-gpu-gl-pci,venus=true VirtioPciGlVulkan = "virtio_pci_gl_vulkan" // virtio-vga prime=1 VirtioPrime = "virtio_prime" // virtio-gpu-pci prime=1 VirtioPciPrime = "virtio_pci_prime" // virtio-vga-gl prime=1 VirtioVgaGlPrime = "virtio_vga_gl_prime" // virtio-vga-gl,venus=true prime=1 VirtioVgaGlVulkanPrime = "virtio_vga_gl_vulkan_prime" // virtio-gpu-gl prime=1 VirtioGlPrime = "virtio_gl_prime" // virtio-gpu-gl,venus=true prime=1 VirtioGlVulkanPrime = "virtio_gl_vulkan_prime" // virtio-gpu-gl-pci prime=1 VirtioPciGlPrime = "virtio_pci_gl_prime" // virtio-gpu-gl-pci,venus=true prime=1 VirtioPciGlVulkanPrime = "virtio_pci_gl_vulkan_prime" Sdl = "sdl" Gtk = "gtk" Disabled = "disabled" Dhcp = "dhcp" DhcpSlaac = "dhcp_slaac" Slaac = "slaac" Static = "static" Internal = "internal" Cloud = "cloud" Restart = "restart" HostPath = "host_path" ) var ( VgaModes = set.NewSet( Std, Vmware, Virtio, VirtioPci, VirtioVgaGl, VirtioVgaGlVulkan, VirtioGl, VirtioGlVulkan, VirtioPciGl, VirtioPciGlVulkan, VirtioPrime, VirtioPciPrime, VirtioVgaGlPrime, VirtioVgaGlVulkanPrime, VirtioGlPrime, VirtioGlVulkanPrime, VirtioPciGlPrime, VirtioPciGlVulkanPrime, ) VgaRenderModes = set.NewSet( VirtioPci, VirtioVgaGl, VirtioVgaGlVulkan, VirtioGl, VirtioGlVulkan, VirtioPciGl, VirtioPciGlVulkan, VirtioPciPrime, VirtioVgaGlPrime, VirtioVgaGlVulkanPrime, VirtioGlPrime, VirtioGlVulkanPrime, VirtioPciGlPrime, VirtioPciGlVulkanPrime, ) ) ================================================ FILE: node/interfaces.go ================================================ package node import ( "strings" "sync" "time" "github.com/pritunl/pritunl-cloud/ip" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) var ( netLock = sync.Mutex{} netIfaces = []ip.Interface{} netLastIfacesSync time.Time defaultIface = "" defaultIfaceSync time.Time ) func ClearIfaceCache() { netLastIfacesSync = time.Time{} netIfaces = []ip.Interface{} defaultIfaceSync = time.Time{} defaultIface = "" } func GetInterfaces() (ifaces []ip.Interface, err error) { if time.Since(netLastIfacesSync) < 15*time.Second { ifaces = netIfaces return } ifacesNew := []ip.Interface{} allIfaces, err := utils.GetInterfaces() if err != nil { return } ifacesData, err := ip.GetIfacesCached("") if err != nil { return } for _, iface := range allIfaces { if len(iface) == 14 || iface == "lo" || strings.Contains(iface, "br") || iface == settings.Hypervisor.HostNetworkName || iface == settings.Hypervisor.NodePortNetworkName || iface == "" { continue } ifaceData := ifacesData[iface] if ifaceData != nil { ifacesNew = append(ifacesNew, ip.Interface{ Name: iface, Address: ifaceData.GetAddress(), }) } else { ifacesNew = append(ifacesNew, ip.Interface{ Name: iface, }) } } ifaces = ifacesNew netLastIfacesSync = time.Now() netIfaces = ifacesNew return } func getDefaultIface() (iface string, err error) { if time.Since(defaultIfaceSync) < 900*time.Second { iface = defaultIface return } output, err := utils.ExecCombinedOutput("", "route", "-n") if err != nil { return } outputLines := strings.Split(output, "\n") for _, line := range outputLines { fields := strings.Fields(line) if len(fields) < 2 { continue } if fields[0] == "0.0.0.0" { iface = strings.TrimSpace(fields[len(fields)-1]) _ = strings.TrimSpace(fields[1]) } } defaultIface = iface defaultIfaceSync = time.Now() return } ================================================ FILE: node/node.go ================================================ package node import ( "container/list" "fmt" "net" "net/http" "os" "os/exec" "path" "path/filepath" "runtime/debug" "sort" "strings" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/go-webauthn/webauthn/webauthn" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/advisory" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/bridges" "github.com/pritunl/pritunl-cloud/cloud" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/ip" "github.com/pritunl/pritunl-cloud/iso" "github.com/pritunl/pritunl-cloud/lvm" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/render" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/telemetry" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" ) var ( Self *Node ) type Node struct { Id bson.ObjectID `bson:"_id" json:"id"` Datacenter bson.ObjectID `bson:"datacenter,omitempty" json:"datacenter"` Zone bson.ObjectID `bson:"zone,omitempty" json:"zone"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Types []string `bson:"types" json:"types"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Port int `bson:"port" json:"port"` Http2 bool `bson:"http2" json:"http2"` NoRedirectServer bool `bson:"no_redirect_server" json:"no_redirect_server"` Protocol string `bson:"protocol" json:"protocol"` Hypervisor string `bson:"hypervisor" json:"hypervisor"` Vga string `bson:"vga" json:"vga"` VgaRender string `bson:"vga_render" json:"vga_render"` AvailableRenders []string `bson:"available_renders" json:"available_renders"` Gui bool `bson:"gui" json:"gui"` GuiUser string `bson:"gui_user" json:"gui_user"` GuiMode string `bson:"gui_mode" json:"gui_mode"` Certificates []bson.ObjectID `bson:"certificates" json:"certificates"` SelfCertificate string `bson:"self_certificate_key" json:"-"` SelfCertificateKey string `bson:"self_certificate" json:"-"` AdminDomain string `bson:"admin_domain" json:"admin_domain"` UserDomain string `bson:"user_domain" json:"user_domain"` WebauthnDomain string `bson:"webauthn_domain" json:"webauthn_domain"` RequestsMin int64 `bson:"requests_min" json:"requests_min"` ForwardedForHeader string `bson:"forwarded_for_header" json:"forwarded_for_header"` ForwardedProtoHeader string `bson:"forwarded_proto_header" json:"forwarded_proto_header"` ExternalInterfaces []string `bson:"external_interfaces" json:"external_interfaces"` ExternalInterfaces6 []string `bson:"external_interfaces6" json:"external_interfaces6"` InternalInterfaces []string `bson:"internal_interfaces" json:"internal_interfaces"` AvailableInterfaces []ip.Interface `bson:"available_interfaces" json:"available_interfaces"` AvailableBridges []ip.Interface `bson:"available_bridges" json:"available_bridges"` AvailableVpcs []*cloud.Vpc `bson:"available_vpcs" json:"available_vpcs"` CloudSubnets []string `bson:"cloud_subnets" json:"cloud_subnets"` DefaultInterface string `bson:"default_interface" json:"default_interface"` NetworkMode string `bson:"network_mode" json:"network_mode"` NetworkMode6 string `bson:"network_mode6" json:"network_mode6"` Blocks []*BlockAttachment `bson:"blocks" json:"blocks"` Blocks6 []*BlockAttachment `bson:"blocks6" json:"blocks6"` Pools []bson.ObjectID `bson:"pools" json:"pools"` Shares []*Share `bson:"shares" json:"shares"` AvailableDrives []*drive.Device `bson:"available_drives" json:"available_drives"` InstanceDrives []*drive.Device `bson:"instance_drives" json:"instance_drives"` NoHostNetwork bool `bson:"no_host_network" json:"no_host_network"` NoNodePortNetwork bool `bson:"no_node_port_network" json:"no_node_port_network"` HostNat bool `bson:"host_nat" json:"host_nat"` DefaultNoPublicAddress bool `bson:"default_no_public_address" json:"default_no_public_address"` DefaultNoPublicAddress6 bool `bson:"default_no_public_address6" json:"default_no_public_address6"` JumboFrames bool `bson:"jumbo_frames" json:"jumbo_frames"` JumboFramesInternal bool `bson:"jumbo_frames_internal" json:"jumbo_frames_internal"` Iscsi bool `bson:"iscsi" json:"iscsi"` LocalIsos []*iso.Iso `bson:"local_isos" json:"local_isos"` UsbPassthrough bool `bson:"usb_passthrough" json:"usb_passthrough"` UsbDevices []*usb.Device `bson:"usb_devices" json:"usb_devices"` PciPassthrough bool `bson:"pci_passthrough" json:"pci_passthrough"` PciDevices []*pci.Device `bson:"pci_devices" json:"pci_devices"` Hugepages bool `bson:"hugepages" json:"hugepages"` HugepagesSize int `bson:"hugepages_size" json:"hugepages_size"` Firewall bool `bson:"firewall" json:"firewall"` Roles []string `bson:"roles" json:"roles"` Memory float64 `bson:"memory" json:"memory"` HugePagesUsed float64 `bson:"hugepages_used" json:"hugepages_used"` Load1 float64 `bson:"load1" json:"load1"` Load5 float64 `bson:"load5" json:"load5"` Load15 float64 `bson:"load15" json:"load15"` CpuUnits int `bson:"cpu_units" json:"cpu_units"` MemoryUnits float64 `bson:"memory_units" json:"memory_units"` CpuUnitsRes int `bson:"cpu_units_res" json:"cpu_units_res"` MemoryUnitsRes float64 `bson:"memory_units_res" json:"memory_units_res"` Updates []*telemetry.Update `bson:"updates" json:"updates"` PublicIps []string `bson:"public_ips" json:"public_ips"` PublicIps6 []string `bson:"public_ips6" json:"public_ips6"` PrivateIps map[string]string `bson:"private_ips" json:"private_ips"` SoftwareVersion string `bson:"software_version" json:"software_version"` Hostname string `bson:"hostname" json:"hostname"` Version int `bson:"version" json:"-"` VirtPath string `bson:"virt_path" json:"virt_path"` CachePath string `bson:"cache_path" json:"cache_path"` TempPath string `bson:"temp_path" json:"temp_path"` OracleUser string `bson:"oracle_user" json:"oracle_user"` OracleTenancy string `bson:"oracle_tenancy" json:"oracle_tenancy"` OraclePrivateKey string `bson:"oracle_private_key" json:"-"` OraclePublicKey string `bson:"oracle_public_key" json:"oracle_public_key"` Operation string `bson:"operation" json:"operation"` cloudSubnetsNamed []*CloudSubnet `bson:"-" json:"-"` reqCount *list.List `bson:"-" json:"-"` dcId bson.ObjectID `bson:"-" json:"-"` dcZoneId bson.ObjectID `bson:"-" json:"-"` lock sync.Mutex `bson:"-" json:"-"` } type Completion struct { Id bson.ObjectID `bson:"_id" json:"id"` Name string `bson:"name" json:"name"` Zone bson.ObjectID `bson:"zone,omitempty" json:"zone"` Types []string `bson:"types" json:"types"` } func (n *Completion) IsHypervisor() bool { for _, typ := range n.Types { if typ == Hypervisor { return true } } return false } type Share struct { Type string `bson:"type" json:"type"` Path string `bson:"path" json:"path"` Roles []string `bson:"roles" json:"roles"` } func (s *Share) MatchPath(pth string) bool { sharePath := utils.FilterPath(s.Path) + string(filepath.Separator) pth = utils.FilterPath(pth) + string(filepath.Separator) if sharePath == "" || pth == "" { return false } if sharePath == pth { return true } if !strings.HasPrefix(pth, sharePath) { return false } relPath, err := filepath.Rel(sharePath, pth) if err != nil { return false } return !strings.HasPrefix(relPath, "..") } type CloudSubnet struct { Id string `json:"id"` Name string `json:"name"` } func (n *Node) Copy() *Node { n.lock.Lock() defer n.lock.Unlock() nde := &Node{ Id: n.Id, Datacenter: n.Datacenter, Zone: n.Zone, Name: n.Name, Comment: n.Comment, Types: n.Types, Timestamp: n.Timestamp, Port: n.Port, Http2: n.Http2, NoRedirectServer: n.NoRedirectServer, Protocol: n.Protocol, Hypervisor: n.Hypervisor, Vga: n.Vga, VgaRender: n.VgaRender, Gui: n.Gui, GuiUser: n.GuiUser, GuiMode: n.GuiMode, AvailableRenders: n.AvailableRenders, Certificates: n.Certificates, SelfCertificate: n.SelfCertificate, SelfCertificateKey: n.SelfCertificateKey, AdminDomain: n.AdminDomain, UserDomain: n.UserDomain, WebauthnDomain: n.WebauthnDomain, RequestsMin: n.RequestsMin, ForwardedForHeader: n.ForwardedForHeader, ForwardedProtoHeader: n.ForwardedProtoHeader, ExternalInterfaces: n.ExternalInterfaces, ExternalInterfaces6: n.ExternalInterfaces6, InternalInterfaces: n.InternalInterfaces, AvailableInterfaces: n.AvailableInterfaces, AvailableBridges: n.AvailableBridges, AvailableVpcs: n.AvailableVpcs, CloudSubnets: n.CloudSubnets, DefaultInterface: n.DefaultInterface, NetworkMode: n.NetworkMode, NetworkMode6: n.NetworkMode6, Blocks: n.Blocks, Blocks6: n.Blocks6, Shares: n.Shares, Pools: n.Pools, AvailableDrives: n.AvailableDrives, InstanceDrives: n.InstanceDrives, NoHostNetwork: n.NoHostNetwork, NoNodePortNetwork: n.NoNodePortNetwork, HostNat: n.HostNat, DefaultNoPublicAddress: n.DefaultNoPublicAddress, DefaultNoPublicAddress6: n.DefaultNoPublicAddress6, JumboFrames: n.JumboFrames, JumboFramesInternal: n.JumboFramesInternal, Iscsi: n.Iscsi, LocalIsos: n.LocalIsos, UsbPassthrough: n.UsbPassthrough, UsbDevices: n.UsbDevices, PciPassthrough: n.PciPassthrough, PciDevices: n.PciDevices, Hugepages: n.Hugepages, HugepagesSize: n.HugepagesSize, Firewall: n.Firewall, Roles: n.Roles, Memory: n.Memory, HugePagesUsed: n.HugePagesUsed, Load1: n.Load1, Load5: n.Load5, Load15: n.Load15, CpuUnits: n.CpuUnits, MemoryUnits: n.MemoryUnits, CpuUnitsRes: n.CpuUnitsRes, MemoryUnitsRes: n.MemoryUnitsRes, Updates: n.Updates, PublicIps: n.PublicIps, PublicIps6: n.PublicIps6, PrivateIps: n.PrivateIps, SoftwareVersion: n.SoftwareVersion, Hostname: n.Hostname, Version: n.Version, VirtPath: n.VirtPath, CachePath: n.CachePath, TempPath: n.TempPath, OracleUser: n.OracleUser, OracleTenancy: n.OracleTenancy, OraclePrivateKey: n.OraclePrivateKey, OraclePublicKey: n.OraclePublicKey, Operation: n.Operation, cloudSubnetsNamed: n.cloudSubnetsNamed, dcId: n.dcId, dcZoneId: n.dcZoneId, } return nde } func (n *Node) AddRequest() { n.lock.Lock() back := n.reqCount.Back() back.Value = back.Value.(int) + 1 n.lock.Unlock() } func (n *Node) GetVirtPath() string { if n.VirtPath == "" { return constants.DefaultRoot } return n.VirtPath } func (n *Node) GetCachePath() string { if n.CachePath == "" { return constants.DefaultCache } return n.CachePath } func (n *Node) GetTempPath() string { if n.TempPath == "" { return constants.DefaultTemp } return n.TempPath } func (n *Node) GetDatacenter(db *database.Database) ( dcId bson.ObjectID, err error) { n.lock.Lock() if n.Zone == n.dcZoneId { dcId = n.dcId n.lock.Unlock() return } n.lock.Unlock() zne, err := zone.Get(db, n.Zone) if err != nil { return } dcId = zne.Datacenter n.lock.Lock() n.dcId = zne.Datacenter n.dcZoneId = n.Zone n.lock.Unlock() return } func (n *Node) GetCloudSubnetsName() (subnets []*CloudSubnet) { n.lock.Lock() subnets = n.cloudSubnetsNamed n.lock.Unlock() if subnets != nil { return } names := map[string]string{} if n.AvailableVpcs != nil { for _, vpc := range n.AvailableVpcs { for _, subnet := range vpc.Subnets { names[subnet.Id] = fmt.Sprintf( "%s - %s", vpc.Name, subnet.Name) } } } subnets = []*CloudSubnet{} if n.CloudSubnets != nil { for _, subnetId := range n.CloudSubnets { name := names[subnetId] if name == "" { name = subnetId } subnets = append(subnets, &CloudSubnet{ Id: subnetId, Name: name, }) } } n.lock.Lock() n.cloudSubnetsNamed = subnets n.lock.Unlock() return } func (n *Node) IsAdmin() bool { for _, typ := range n.Types { if typ == Admin { return true } } return false } func (n *Node) IsUser() bool { for _, typ := range n.Types { if typ == User { return true } } return false } func (n *Node) IsBalancer() bool { for _, typ := range n.Types { if typ == Balancer { return true } } return false } func (n *Node) IsHypervisor() bool { for _, typ := range n.Types { if typ == Hypervisor { return true } } return false } func (n *Node) IsOnline() bool { if time.Since(n.Timestamp) > time.Duration( settings.System.NodeTimestampTtl)*time.Second { return false } return true } func (n *Node) IsDhcp() bool { return n.NetworkMode == Dhcp || n.NetworkMode == DhcpSlaac } func (n *Node) IsDhcp6() bool { return n.NetworkMode6 == Dhcp || n.NetworkMode6 == DhcpSlaac } func (n *Node) Usage() int { memoryUsage := float64(n.MemoryUnitsRes) / float64(n.MemoryUnits) if memoryUsage > 1.0 { memoryUsage = 1.0 } cpuUsage := float64(n.CpuUnitsRes) / float64(n.CpuUnits) if cpuUsage > 1.0 { cpuUsage = 1.0 } totalUsage := (memoryUsage * 0.75) + (cpuUsage * 0.25) if totalUsage > 1.0 { totalUsage = 1.0 } return int(totalUsage * 100) } func (n *Node) SizeResource(memory, processors int) bool { memoryUnits := float64(memory) / float64(1024) if memoryUnits+n.MemoryUnitsRes > n.MemoryUnits { return false } if processors+n.CpuUnitsRes > n.CpuUnits*2 { return false } return true } func (n *Node) GetOracleAuthProvider() (pv *NodeOracleAuthProvider) { pv = &NodeOracleAuthProvider{ nde: n, } return } func (n *Node) GetWebauthn(origin string, strict bool) ( web *webauthn.WebAuthn, err error) { webauthnDomain := n.WebauthnDomain if webauthnDomain == "" { if strict { err = &errortypes.ReadError{ errors.New("node: Webauthn domain not configured"), } return } else { userN := strings.Count(n.UserDomain, ".") adminN := strings.Count(n.AdminDomain, ".") if userN <= adminN { webauthnDomain = n.UserDomain } else { webauthnDomain = n.AdminDomain } } } web, err = webauthn.New(&webauthn.Config{ RPDisplayName: "Pritunl Cloud", RPID: webauthnDomain, RPOrigins: []string{origin}, }) if err != nil { err = utils.ParseWebauthnError(err) return } return } func (n *Node) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { n.Name = utils.FilterName(n.Name) if n.Hypervisor == "" { n.Hypervisor = Kvm } if n.Vga == "" { n.Vga = Virtio n.VgaRender = "" } if !VgaModes.Contains(n.Vga) { errData = &errortypes.ErrorData{ Error: "node_vga_invalid", Message: "Invalid VGA type", } return } if VgaRenderModes.Contains(n.Vga) && n.VgaRender != "" { found := false for _, rendr := range n.AvailableRenders { if n.VgaRender == rendr { found = true break } } if !found { errData = &errortypes.ErrorData{ Error: "node_vga_render_invalid", Message: "Invalid EGL render", } return } } else { n.VgaRender = "" } if n.Gui { if n.GuiUser == "" { errData = &errortypes.ErrorData{ Error: "gui_user_missing", Message: "Desktop GUI user must be set", } return } switch n.GuiMode { case Sdl, "": n.GuiMode = Sdl break case Gtk: break default: errData = &errortypes.ErrorData{ Error: "gui_mode_invalid", Message: "Invalid desktop GUI mode", } return } } else { n.GuiUser = "" n.GuiMode = "" } if n.Protocol != "http" && n.Protocol != "https" { errData = &errortypes.ErrorData{ Error: "node_protocol_invalid", Message: "Invalid node server protocol", } return } if n.Port < 1 || n.Port > 65535 { errData = &errortypes.ErrorData{ Error: "node_port_invalid", Message: "Invalid node server port", } return } if n.Certificates == nil || n.Protocol != "https" { n.Certificates = []bson.ObjectID{} } if n.Types == nil { n.Types = []string{} } for _, typ := range n.Types { switch typ { case Admin, User, Balancer, Hypervisor: break default: errData = &errortypes.ErrorData{ Error: "type_invalid", Message: "Invalid node type", } return } } if !n.IsBalancer() && ((n.IsAdmin() && !n.IsUser()) || (n.IsUser() && !n.IsAdmin())) { n.AdminDomain = "" n.UserDomain = "" } else { if !n.IsAdmin() { n.AdminDomain = "" } if !n.IsUser() { n.UserDomain = "" } } if !n.Zone.IsZero() { zne, e := zone.Get(db, n.Zone) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if zne == nil { n.Zone = bson.NilObjectID } else { n.Datacenter = zne.Datacenter n.Zone = zne.Id } } if n.VirtPath == "" { n.VirtPath = constants.DefaultRoot } if n.CachePath == "" { n.CachePath = constants.DefaultCache } if n.Roles == nil { n.Roles = []string{} } if n.Firewall && len(n.Roles) == 0 { errData = &errortypes.ErrorData{ Error: "firewall_empty_roles", Message: "Cannot enable firewall without network roles", } return } if n.ExternalInterfaces == nil { n.ExternalInterfaces = []string{} } if n.InternalInterfaces == nil { n.InternalInterfaces = []string{} } if n.Blocks == nil { n.Blocks = []*BlockAttachment{} } if n.ExternalInterfaces6 == nil { n.ExternalInterfaces6 = []string{} } if n.Blocks6 == nil { n.Blocks6 = []*BlockAttachment{} } if len(n.InternalInterfaces) == 0 { errData = &errortypes.ErrorData{ Error: "internal_interface_invalid", Message: "Missing required internal interface", } return } if n.CloudSubnets == nil { n.CloudSubnets = []string{} } instanceDrives := []*drive.Device{} if n.InstanceDrives != nil { for _, device := range n.InstanceDrives { device.Id = utils.FilterRelPath(device.Id) instanceDrives = append(instanceDrives, device) } } n.InstanceDrives = instanceDrives switch n.NetworkMode { case Static: for _, blckAttch := range n.Blocks { blck, e := block.Get(db, blckAttch.Block) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if blck == nil || blck.Type != block.IPv4 { errData = &errortypes.ErrorData{ Error: "invalid_block", Message: "External IPv4 block invalid", } return } } break case Cloud: n.Blocks = []*BlockAttachment{} break case Dhcp: n.Blocks = []*BlockAttachment{} break case Disabled, "": n.NetworkMode = Disabled n.Blocks = []*BlockAttachment{} break default: errData = &errortypes.ErrorData{ Error: "invalid_network_mode", Message: "Network mode invalid", } return } switch n.NetworkMode6 { case Static: for _, blckAttch := range n.Blocks6 { blck, e := block.Get(db, blckAttch.Block) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if blck == nil || blck.Type != block.IPv6 { errData = &errortypes.ErrorData{ Error: "invalid_block6", Message: "External IPv6 block invalid", } return } } break case Cloud: n.Blocks6 = []*BlockAttachment{} break case Dhcp: n.Blocks6 = []*BlockAttachment{} break case Disabled, "": n.NetworkMode6 = Disabled n.Blocks6 = []*BlockAttachment{} break default: errData = &errortypes.ErrorData{ Error: "invalid_network_mode6", Message: "Network mode6 invalid", } return } if n.NetworkMode == Static && n.NetworkMode6 == Static || n.NetworkMode == Disabled && n.NetworkMode6 == Disabled { n.ExternalInterfaces = []string{} } if n.NetworkMode == Cloud || n.NetworkMode6 == Cloud { if n.OracleUser == "" { errData = &errortypes.ErrorData{ Error: "missing_oracle_user", Message: "Oracle user OCID required for host routing", } return } if n.OracleTenancy == "" { errData = &errortypes.ErrorData{ Error: "missing_oracle_tenancy", Message: "Oracle tenancy OCID required for host routing", } return } } else { n.OracleUser = "" n.OracleTenancy = "" n.CloudSubnets = []string{} n.AvailableVpcs = []*cloud.Vpc{} } newShares := []*Share{} for _, share := range n.Shares { if share.Type == "" && share.Path == "" { continue } share.Type = HostPath share.Path = utils.FilterPath(share.Path) if share.Path == "" { errData = &errortypes.ErrorData{ Error: "share_path_invalid", Message: "Invalid share path", } return } if len(share.Roles) == 0 { errData = &errortypes.ErrorData{ Error: "missing_share_path_roles", Message: "Share path missing required roles", } return } newShares = append(newShares, share) } n.Shares = newShares n.Format() return } func (n *Node) Format() { sort.Strings(n.Types) utils.SortObjectIds(n.Certificates) } func (n *Node) JsonHypervisor() { vpcs := []*cloud.Vpc{} oracleSubnets := set.NewSet() for _, subnet := range n.CloudSubnets { oracleSubnets.Add(subnet) } for _, vpc := range n.AvailableVpcs { subnets := []*cloud.Subnet{} for _, subnet := range vpc.Subnets { if oracleSubnets.Contains(subnet.Id) { subnets = append(subnets, subnet) } } if len(subnets) > 0 { vpcs = append(vpcs, vpc) } } n.AvailableVpcs = vpcs return } func (n *Node) SetActive() { if time.Since(n.Timestamp) > 30*time.Second { n.RequestsMin = 0 n.Memory = 0 n.Load1 = 0 n.Load5 = 0 n.Load15 = 0 n.CpuUnits = 0 n.CpuUnitsRes = 0 n.MemoryUnits = 0 n.MemoryUnitsRes = 0 } } func (n *Node) Commit(db *database.Database) (err error) { coll := db.Nodes() err = coll.Commit(n.Id, n) if err != nil { return } return } func (n *Node) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Nodes() err = coll.CommitFields(n.Id, n, fields) if err != nil { return } return } func (n *Node) GetStaticAddr(db *database.Database, instId bson.ObjectID) (blck *block.Block, ip net.IP, iface string, err error) { blck, blckIp, err := block.GetInstanceIp(db, instId, block.External) if err != nil { return } if blckIp != nil { for _, blckAttch := range n.Blocks { if blckAttch.Block == blck.Id { ip = blckIp.GetIp() iface = blckAttch.Interface return } } err = block.RemoveIp(db, blckIp.Id) if err != nil { return } } for _, blckAttch := range n.Blocks { blck, err = block.Get(db, blckAttch.Block) if err != nil { return } iface = blckAttch.Interface ip, err = blck.GetIp(db, instId, block.External) if err != nil { if _, ok := err.(*block.BlockFull); ok { err = nil continue } else { return } } break } if ip == nil { err = &errortypes.NotFoundError{ errors.New("node: No external block addresses available"), } return } return } func (n *Node) GetStaticAddr6(db *database.Database, instId bson.ObjectID, vlan int, matchIface string) ( blck *block.Block, ip net.IP, cidr int, iface string, err error) { mismatch := false if matchIface != "" { for _, blckAttch := range n.Blocks6 { if blckAttch.Interface != matchIface { mismatch = true continue } blck, err = block.Get(db, blckAttch.Block) if err != nil { return } iface = blckAttch.Interface ip, cidr, err = blck.GetIp6(db, instId, vlan) if err != nil { if _, ok := err.(*block.BlockFull); ok { err = nil continue } else { return } } break } } else { for _, blckAttch := range n.Blocks6 { blck, err = block.Get(db, blckAttch.Block) if err != nil { return } iface = blckAttch.Interface ip, cidr, err = blck.GetIp6(db, instId, vlan) if err != nil { if _, ok := err.(*block.BlockFull); ok { err = nil continue } else { return } } break } } if ip == nil { if mismatch { err = &errortypes.NotFoundError{ errors.New("node: No external block6 with matching " + "block interface available"), } } else { err = &errortypes.NotFoundError{ errors.New("node: No external block6 addresses available"), } } return } return } func (n *Node) GetStaticHostAddr(db *database.Database, instId bson.ObjectID) (blck *block.Block, ip net.IP, err error) { blck, err = block.GetNodeBlock(n.Id) if err != nil { return } blckIp, err := block.GetInstanceHostIp(db, instId) if err != nil { return } if blckIp != nil { contains, e := blck.Contains(blckIp) if e != nil { err = e return } if contains { ip = blckIp.GetIp() return } err = block.RemoveIp(db, blckIp.Id) if err != nil { return } } ip, err = blck.GetIp(db, instId, block.Host) if err != nil { if _, ok := err.(*block.BlockFull); ok { err = nil } else { return } } if ip == nil { err = &errortypes.NotFoundError{ errors.New("node: No host addresses available"), } return } return } func (n *Node) GetStaticNodePortAddr(db *database.Database, instId bson.ObjectID) (blck *block.Block, ip net.IP, err error) { blck, err = block.GetNodePortBlock(n.Id) if err != nil { return } blckIp, err := block.GetInstanceNodePortIp(db, instId) if err != nil { return } if blckIp != nil { contains, e := blck.Contains(blckIp) if e != nil { err = e return } if contains { ip = blckIp.GetIp() return } err = block.RemoveIp(db, blckIp.Id) if err != nil { return } } ip, err = blck.GetIp(db, instId, block.NodePort) if err != nil { if _, ok := err.(*block.BlockFull); ok { err = nil } else { return } } if ip == nil { err = &errortypes.NotFoundError{ errors.New("node: No node port addresses available"), } return } return } func (n *Node) GetRemoteAddr(r *http.Request) (addr string) { if n.ForwardedForHeader != "" { addr = strings.TrimSpace( strings.SplitN(r.Header.Get(n.ForwardedForHeader), ",", 2)[0]) if addr != "" { return } } addr = utils.StripPort(r.RemoteAddr) return } func (n *Node) SyncNetwork(clearCache bool) { netLock.Lock() defer netLock.Unlock() if clearCache { ClearIfaceCache() bridges.ClearCache() } defaultIface, err := getDefaultIface() if err != nil { logrus.WithFields(logrus.Fields{ "default_interface": defaultIface, "error": err, }).Error("node: Failed to get public address") } if defaultIface != "" { n.DefaultInterface = defaultIface pubAddr, pubAddr6, err := bridges.GetIpAddrs(defaultIface) if err != nil { logrus.WithFields(logrus.Fields{ "default_interface": defaultIface, "error": err, }).Error("node: Failed to get public address") } if pubAddr != "" { n.PublicIps = []string{ pubAddr, } } if pubAddr6 != "" { n.PublicIps6 = []string{ pubAddr6, } } } privateIps := map[string]string{} internalInterfaces := n.InternalInterfaces for _, iface := range internalInterfaces { addr, _, err := bridges.GetIpAddrs(iface) if err != nil { logrus.WithFields(logrus.Fields{ "internal_interface": iface, "error": err, }).Error("node: Failed to get private address") } if addr != "" { privateIps[iface] = addr } } n.PrivateIps = privateIps ifaces, err := GetInterfaces() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get interfaces") } if ifaces != nil { n.AvailableInterfaces = ifaces } else { n.AvailableInterfaces = []ip.Interface{} } brdgs, err := bridges.GetBridges() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get bridge interfaces") } if brdgs != nil { n.AvailableBridges = brdgs } else { n.AvailableBridges = []ip.Interface{} } if n.JumboFrames { n.JumboFramesInternal = true } if n.NetworkMode == Cloud || n.NetworkMode6 == Cloud { oracleVpcs, e := cloud.GetOracleVpcs(n.GetOracleAuthProvider()) if e != nil { logrus.WithFields(logrus.Fields{ "error": e, }).Error("node: Failed to get oracle vpcs") } if oracleVpcs != nil { n.AvailableVpcs = oracleVpcs } else { n.AvailableVpcs = []*cloud.Vpc{} } } else { n.AvailableVpcs = []*cloud.Vpc{} } } func (n *Node) getUpdateDetails(db *database.Database) ( updates []*telemetry.Update, ok bool) { updates, ok = telemetry.Updates.Get() if !ok { return } if len(updates) == 0 { return } detailsMap := map[string][]*advisory.Advisory{} for _, upd := range n.Updates { if upd.Advisory != "" && len(upd.Details) > 0 { detailsMap[upd.Advisory] = upd.Details } } for _, upd := range updates { if details, ok := detailsMap[upd.Advisory]; ok { upd.Details = details } } return } func (n *Node) update(db *database.Database) (err error) { coll := db.Nodes() fields := bson.M{ "timestamp": n.Timestamp, "requests_min": n.RequestsMin, "memory": n.Memory, "hugepages_used": n.HugePagesUsed, "load1": n.Load1, "load5": n.Load5, "load15": n.Load15, "cpu_units": n.CpuUnits, "memory_units": n.MemoryUnits, "cpu_units_res": n.CpuUnitsRes, "memory_units_res": n.MemoryUnitsRes, "public_ips": n.PublicIps, "public_ips6": n.PublicIps6, "private_ips": n.PrivateIps, "hostname": n.Hostname, "local_isos": n.LocalIsos, "usb_devices": n.UsbDevices, "pci_devices": n.PciDevices, "available_renders": n.AvailableRenders, "available_interfaces": n.AvailableInterfaces, "available_bridges": n.AvailableBridges, "available_vpcs": n.AvailableVpcs, "default_interface": n.DefaultInterface, "pools": n.Pools, "available_drives": n.AvailableDrives, } updates, ok := n.getUpdateDetails(db) if ok { fields["updates"] = updates } nde := &Node{} err = coll.FindOneAndUpdate( db, &bson.M{ "_id": n.Id, }, &bson.M{ "$set": fields, }, options.FindOneAndUpdate().SetReturnDocument(options.After), ).Decode(nde) if err != nil { err = database.ParseError(err) return } n.Id = nde.Id n.Datacenter = nde.Datacenter n.Zone = nde.Zone n.Name = nde.Name n.Comment = nde.Comment n.Types = nde.Types n.Port = nde.Port n.Http2 = nde.Http2 n.NoRedirectServer = nde.NoRedirectServer n.Protocol = nde.Protocol n.Hypervisor = nde.Hypervisor n.Vga = nde.Vga n.VgaRender = nde.VgaRender n.Gui = nde.Gui n.GuiUser = nde.GuiUser n.GuiMode = nde.GuiMode n.Certificates = nde.Certificates n.SelfCertificate = nde.SelfCertificate n.SelfCertificateKey = nde.SelfCertificateKey n.AdminDomain = nde.AdminDomain n.UserDomain = nde.UserDomain n.WebauthnDomain = nde.WebauthnDomain n.ForwardedForHeader = nde.ForwardedForHeader n.ForwardedProtoHeader = nde.ForwardedProtoHeader n.ExternalInterfaces = nde.ExternalInterfaces n.ExternalInterfaces6 = nde.ExternalInterfaces6 n.InternalInterfaces = nde.InternalInterfaces n.CloudSubnets = nde.CloudSubnets n.NetworkMode = nde.NetworkMode n.NetworkMode6 = nde.NetworkMode6 n.Blocks = nde.Blocks n.Blocks6 = nde.Blocks6 n.Shares = nde.Shares n.InstanceDrives = nde.InstanceDrives n.NoHostNetwork = nde.NoHostNetwork n.NoNodePortNetwork = nde.NoNodePortNetwork n.HostNat = nde.HostNat n.DefaultNoPublicAddress = nde.DefaultNoPublicAddress n.DefaultNoPublicAddress6 = nde.DefaultNoPublicAddress6 n.JumboFrames = nde.JumboFrames n.JumboFramesInternal = nde.JumboFramesInternal n.Iscsi = nde.Iscsi n.UsbPassthrough = nde.UsbPassthrough n.PciPassthrough = nde.PciPassthrough n.Hugepages = nde.Hugepages n.HugepagesSize = nde.HugepagesSize n.Firewall = nde.Firewall n.Roles = nde.Roles n.Updates = nde.Updates n.VirtPath = nde.VirtPath n.CachePath = nde.CachePath n.TempPath = nde.TempPath n.OracleUser = nde.OracleUser n.OracleTenancy = nde.OracleTenancy n.OraclePrivateKey = nde.OraclePrivateKey n.OraclePublicKey = nde.OraclePublicKey n.Operation = nde.Operation return } func (n *Node) sync() (nde *Node) { db := database.GetDatabase() defer db.Close() nde = n.Copy() n = nde n.Timestamp = time.Now() mem, err := utils.GetMemInfo() if err != nil { n.Memory = 0 n.HugePagesUsed = 0 n.MemoryUnits = 0 logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get memory") } else { n.Memory = utils.ToFixed(mem.UsedPercent, 2) n.HugePagesUsed = utils.ToFixed(mem.HugePagesUsedPercent, 2) n.MemoryUnits = utils.ToFixed( float64(mem.Total)/float64(1048576), 2) } load, err := utils.LoadAverage() if err != nil { n.CpuUnits = 0 n.Load1 = 0 n.Load5 = 0 n.Load15 = 0 logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get load") } else { n.CpuUnits = load.CpuUnits n.Load1 = load.Load1 n.Load5 = load.Load5 n.Load15 = load.Load15 } n.SyncNetwork(false) pools, err := lvm.GetAvailablePools(db, n.Zone) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get pools") } poolIds := []bson.ObjectID{} for _, pl := range pools { poolIds = append(poolIds, pl.Id) } n.Pools = poolIds drives, err := drive.GetDevices() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get drive devices") n.AvailableDrives = []*drive.Device{} } else { n.AvailableDrives = drives } renders, err := render.GetRenders() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get renders") n.AvailableRenders = []string{} } else { n.AvailableRenders = renders } hostname, err := os.Hostname() if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "node: Failed to get hostname"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get hostname") } n.Hostname = hostname isos, err := iso.GetIsos(path.Join(n.GetVirtPath(), "isos")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get isos") n.LocalIsos = []*iso.Iso{} } else { n.LocalIsos = isos } if n.UsbPassthrough { devices, err := usb.GetDevices() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get usb devices") n.UsbDevices = []*usb.Device{} } else { n.UsbDevices = devices } } else { n.UsbDevices = []*usb.Device{} } if n.PciPassthrough { pciDevices, err := pci.GetVfioAll() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to get vfio devices") n.PciDevices = []*pci.Device{} } else { n.PciDevices = pciDevices } } else { n.PciDevices = []*pci.Device{} } err = n.update(db) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to update node") } if n.Operation == Restart { logrus.Info("node: Restarting node") n.Operation = "" err = n.CommitFields(db, set.NewSet("operation")) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to commit node operation") } else { cmd := exec.Command("systemctl", "restart", "pritunl-cloud") err = cmd.Start() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("node: Failed to start node restart") } } } var reqCount *list.List if Self != nil { Self.lock.Lock() reqCount = utils.CopyList(Self.reqCount) Self.lock.Unlock() } else { reqCount = list.New() for i := 0; i < 60; i++ { reqCount.PushBack(0) } } var count int64 for elm := reqCount.Front(); elm != nil; elm = elm.Next() { count += int64(elm.Value.(int)) } n.RequestsMin = count reqCount.Remove(reqCount.Front()) reqCount.PushBack(0) n.reqCount = reqCount Self = n return } func (n *Node) Init() (err error) { db := database.GetDatabase() defer db.Close() coll := db.Nodes() err = coll.FindOneId(n.Id, n) if err != nil { switch err.(type) { case *database.NotFoundError: err = nil default: return } } n.SoftwareVersion = constants.Version if n.Name == "" { n.Name = utils.RandName() } if n.Types == nil { n.Types = []string{Admin, Hypervisor} } if n.Protocol == "" { n.Protocol = "https" } if n.Port == 0 { n.Port = 443 } if n.Hypervisor == "" { n.Hypervisor = Kvm } bsonSet := bson.M{ "_id": n.Id, "name": n.Name, "types": n.Types, "timestamp": time.Now(), "protocol": n.Protocol, "port": n.Port, "hypervisor": n.Hypervisor, "vga": n.Vga, "software_version": n.SoftwareVersion, } if n.OraclePublicKey == "" || n.OraclePrivateKey == "" { privKey, pubKey, e := utils.GenerateRsaKey() if e != nil { err = e return } bsonSet["oracle_public_key"] = strings.TrimSpace(string(pubKey)) bsonSet["oracle_private_key"] = strings.TrimSpace(string(privKey)) } _, err = coll.UpdateOne( db, &bson.M{ "_id": n.Id, }, &bson.M{ "$set": bsonSet, }, options.UpdateOne().SetUpsert(true), ) if err != nil { err = database.ParseError(err) return } n.sync() event.PublishDispatch(db, "node.change") go func() { nde := n for { if constants.Shutdown { return } nde = nde.sync() time.Sleep(1 * time.Second) } }() go func() { defer func() { panc := recover() if panc != nil { logrus.WithFields(logrus.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("node: Panic in telemetry") } }() for { telemetry.Refresh() time.Sleep(1 * time.Minute) } }() return } ================================================ FILE: node/oracle.go ================================================ package node type NodeOracleAuthProvider struct { nde *Node } func (n *NodeOracleAuthProvider) OracleUser() string { return n.nde.OracleUser } func (n *NodeOracleAuthProvider) OracleTenancy() string { return n.nde.OracleTenancy } func (n *NodeOracleAuthProvider) OraclePrivateKey() string { return n.nde.OraclePrivateKey } ================================================ FILE: node/utils.go ================================================ package node import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, nodeId bson.ObjectID) ( nde *Node, err error) { coll := db.Nodes() nde = &Node{} err = coll.FindOneId(nodeId, nde) if err != nil { return } return } func GetAll(db *database.Database) (nodes []*Node, err error) { coll := db.Nodes() nodes = []*Node{} cursor, err := coll.Find(db, bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } nde.SetActive() nodes = append(nodes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (nde *Node, err error) { coll := db.Nodes() nde = &Node{} err = coll.FindOne(db, query).Decode(nde) if err != nil { err = database.ParseError(err) return } return } func GetAllNamesMap(db *database.Database, query *bson.M) ( nodeNames map[bson.ObjectID]string, err error) { coll := db.Nodes() nodeNames = map[bson.ObjectID]string{} cursor, err := coll.Find( db, query, options.Find().SetProjection(&bson.D{ {"name", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } nodeNames[nde.Id] = nde.Name } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllHypervisors(db *database.Database, query *bson.M) ( nodes []*Node, err error) { coll := db.Nodes() nodes = []*Node{} cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetProjection(&bson.D{ {"name", 1}, {"types", 1}, {"gui", 1}, {"pools", 1}, {"available_vpcs", 1}, {"cloud_subnets", 1}, {"default_no_public_address", 1}, {"default_no_public_address6", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } if !nde.IsHypervisor() { continue } nde.JsonHypervisor() nodes = append(nodes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPool(db *database.Database, poolId bson.ObjectID) ( nodes []*Node, err error) { coll := db.Nodes() nodes = []*Node{} cursor, err := coll.Find( db, &bson.M{ "pools": poolId, }, options.Find().SetProjection(&bson.D{ {"name", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } nodes = append(nodes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (nodes []*Node, count int64, err error) { coll := db.Nodes() nodes = []*Node{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } nde.SetActive() nodes = append(nodes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllShape(db *database.Database, zones []bson.ObjectID, roles []string) (nodes []*Node, err error) { coll := db.Nodes() nodes = []*Node{} query := &bson.M{ "zone": &bson.M{ "$in": zones, }, "roles": &bson.M{ "$in": roles, }, } cursor, err := coll.Find( db, query, options.Find(), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } if !nde.IsHypervisor() || !nde.IsOnline() { continue } nodes = append(nodes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNet(db *database.Database) (nodes []*Node, err error) { coll := db.Nodes() nodes = []*Node{} cursor, err := coll.Find(db, bson.M{}, options.Find().SetProjection(&bson.D{ {"datacenter", 1}, {"zone", 1}, {"private_ips", 1}, })) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Node{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } nodes = append(nodes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, nodeId bson.ObjectID) (err error) { coll := db.Nodes() _, err = coll.DeleteOne(db, &bson.M{ "_id": nodeId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } ================================================ FILE: nodeport/constants.go ================================================ package nodeport const ( Tcp = "tcp" Udp = "udp" ) ================================================ FILE: nodeport/mapping.go ================================================ package nodeport import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" ) type Mapping struct { NodePort bson.ObjectID `bson:"node_port" json:"node_port"` Protocol string `bson:"protocol" json:"protocol"` ExternalPort int `bson:"external_port" json:"external_port"` InternalPort int `bson:"internal_port" json:"internal_port"` Delete bool `bson:"-" json:"delete"` } func (m *Mapping) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { switch m.Protocol { case Tcp, Udp: break default: errData = &errortypes.ErrorData{ Error: "invalid_protocol", Message: "Invalid node port protocol", } return } if m.ExternalPort != 0 { portRanges, e := GetPortRanges() if e != nil { err = e return } matched := false for _, ports := range portRanges { if ports.Contains(m.ExternalPort) { matched = true break } } if !matched { errData = &errortypes.ErrorData{ Error: "invalid_external_port", Message: "Invalid external node port", } return } } if m.InternalPort <= 0 || m.InternalPort > 65535 { errData = &errortypes.ErrorData{ Error: "invalid_internal_port", Message: "Invalid internal node port", } return } return } func (m *Mapping) Diff(mapping *Mapping) bool { if m.Protocol != mapping.Protocol { return true } if m.ExternalPort != mapping.ExternalPort { return true } if m.InternalPort != mapping.InternalPort { return true } return false } ================================================ FILE: nodeport/network.go ================================================ package nodeport import ( "net" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/settings" ) var ( network *net.IPNet ) func init() { module := requires.New("nodeport") module.After("settings") module.Handler = func() (err error) { _, network, err = net.ParseCIDR(settings.Hypervisor.NodePortNetwork) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "nodeport: Failed to parse node port network"), } return } return } } ================================================ FILE: nodeport/nodeport.go ================================================ package nodeport import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" ) type NodePort struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Organization bson.ObjectID `bson:"organization" json:"organization"` Protocol string `bson:"protocol" json:"protocol"` Port int `bson:"port" json:"port"` } func (n *NodePort) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { switch n.Protocol { case Tcp, Udp: break default: errData = &errortypes.ErrorData{ Error: "invalid_protocol", Message: "Invalid node port protocol", } return } if n.Port != 0 { portRanges, e := GetPortRanges() if e != nil { err = e return } matched := false for _, ports := range portRanges { if ports.Contains(n.Port) { matched = true break } } if !matched { errData = &errortypes.ErrorData{ Error: "invalid_port", Message: "Invalid node port", } return } } return } func (n *NodePort) Sync(db *database.Database) (err error) { coll := db.Instances() count, err := coll.CountDocuments(db, &bson.M{ "node_ports.node_port": n.Id, }) if err != nil { err = database.ParseError(err) return } if count == 0 { err = Remove(db, n.Id) if err != nil { return } } return } func (n *NodePort) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.NodePorts() err = coll.CommitFields(n.Id, n, fields) if err != nil { return } return } func (n *NodePort) Insert(db *database.Database) (err error) { coll := db.NodePorts() resp, err := coll.InsertOne(db, n) if err != nil { err = database.ParseError(err) return } n.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: nodeport/utils.go ================================================ package nodeport import ( "strconv" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/set" ) type PortRange struct { Start int End int } func (r *PortRange) Contains(port int) bool { if port >= r.Start && port <= r.End { return true } return false } func Get(db *database.Database, ndePrtId bson.ObjectID) ( ndePrt *NodePort, err error) { coll := db.NodePorts() ndePrt = &NodePort{} err = coll.FindOne(db, &bson.M{ "_id": ndePrtId, }).Decode(ndePrt) if err != nil { err = database.ParseError(err) return } return } func GetOrg(db *database.Database, orgId, ndePrtId bson.ObjectID) ( ndePrt *NodePort, err error) { coll := db.NodePorts() ndePrt = &NodePort{} err = coll.FindOne(db, &bson.M{ "_id": ndePrtId, "organization": orgId, }).Decode(ndePrt) if err != nil { err = database.ParseError(err) return } return } func GetPort(db *database.Database, dcId, orgId bson.ObjectID, protocol string, port int) (ndePrt *NodePort, err error) { coll := db.NodePorts() ndePrt = &NodePort{} err = coll.FindOne(db, &bson.M{ "datacenter": dcId, "port": port, }).Decode(ndePrt) if err != nil { err = database.ParseError(err) return } return } func Available(db *database.Database, datacenterId, orgId bson.ObjectID, protocol string, port int) (available bool, err error) { ndePrt, err := GetPort(db, datacenterId, orgId, protocol, port) if err != nil { if _, ok := err.(*database.NotFoundError); ok { available = true err = nil return } return } if ndePrt.Organization == orgId { available = true return } return } func GetPortRanges() (ranges []*PortRange, err error) { ranges = []*PortRange{} parts := strings.Split(settings.Hypervisor.NodePortRanges, ",") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } bounds := strings.Split(part, "-") if len(bounds) != 2 { err = &errortypes.ParseError{ errors.New("nodeport: Invalid port range format"), } return } start, e := strconv.Atoi(strings.TrimSpace(bounds[0])) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "nodeport: Invalid start port"), } return } end, e := strconv.Atoi(strings.TrimSpace(bounds[1])) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "nodeport: Invalid end port"), } return } if start >= end { err = &errortypes.ParseError{ errors.New("nodeport: Start port larger than end port"), } return } ranges = append(ranges, &PortRange{ Start: start, End: end, }) } if len(ranges) == 0 { err = &errortypes.NotFoundError{ errors.New("nodeport: No node ports configured"), } return } return } func New(db *database.Database, dcId, orgId bson.ObjectID, protocol string, requestPort int) ( ndePrt *NodePort, errData *errortypes.ErrorData, err error) { maxAttempts := settings.Hypervisor.NodePortMaxAttempts ranges, err := GetPortRanges() if err != nil { return } ndPt := &NodePort{ Datacenter: dcId, Organization: orgId, Protocol: protocol, Port: requestPort, } errData, err = ndPt.Validate(db) if err != nil || errData != nil { return } if ndPt.Port != 0 { err = ndPt.Insert(db) if err != nil { return } ndePrt = ndPt return } attempted := set.NewSet() for i := 0; i < maxAttempts; i++ { selectedRange := ranges[utils.RandInt(0, len(ranges)-1)] ndPt.Port = utils.RandInt(selectedRange.Start, selectedRange.End) if attempted.Contains(ndPt.Port) { i-- continue } attempted.Add(ndPt.Port) err = ndPt.Insert(db) if err != nil { if _, ok := err.(*database.DuplicateKeyError); ok { err = nil continue } return } ndePrt = ndPt break } if ndePrt == nil { err = &errortypes.NotFoundError{ errors.New("nodeport: No available node ports found"), } return } return } func Remove(db *database.Database, ndePrtId bson.ObjectID) ( err error) { coll := db.NodePorts() _, err = coll.DeleteOne(db, &bson.M{ "_id": ndePrtId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } ================================================ FILE: nonce/nonce.go ================================================ package nonce import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" ) type nonce struct { Id string `bson:"_id"` Timestamp time.Time `bson:"timestamp"` } func Validate(db *database.Database, nce string) (err error) { doc := &nonce{ Id: nce, Timestamp: time.Now(), } coll := db.Nonces() _, err = coll.InsertOne(db, doc) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.DuplicateKeyError: err = &errortypes.AuthenticationError{ errors.New("nonce: Duplicate authentication nonce"), } break } return } return } ================================================ FILE: notification/notification.go ================================================ package notification import ( "context" "crypto/tls" "encoding/json" "fmt" "net/http" "net/url" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var ( clientTransport = &http.Transport{ TLSHandshakeTimeout: 10 * time.Second, TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, }, } client = &http.Client{ Transport: clientTransport, Timeout: 15 * time.Second, } ) type notificationResp struct { Web bool `json:"web"` Message string `json:"message"` } func Check() (err error) { u := &url.URL{ Scheme: "https", Host: "app.pritunl.com", Path: fmt.Sprintf( "/notification/cloud/%d", utils.GetIntVer(constants.Version), ), } req, err := http.NewRequestWithContext( context.Background(), "GET", u.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "notification: Request init error"), } return } req.Header.Set("User-Agent", "pritunl-cloud") res, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "notification: Request get error"), } return } defer func() { _ = res.Body.Close() }() if res.StatusCode != 200 { err = &errortypes.RequestError{ errors.Newf("notification: Bad status %d", res.StatusCode), } return } data := ¬ificationResp{} err = json.NewDecoder(res.Body).Decode(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "notification: Failed to parse response body"), } return } if data.Web { settings.Local.DisableMsg = utils.FilterStr(data.Message, 256) logrus.WithFields(logrus.Fields{ "message": settings.Local.DisableMsg, }).Error("notification: Disabling web server from vulnerability report") settings.Local.DisableWeb = true } else { settings.Local.DisableWeb = false settings.Local.DisableMsg = "" } return } ================================================ FILE: oracle/iface.go ================================================ package oracle import ( "strings" "sync" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Iface struct { Name string Address string Namespace string MacAddress string VnicId string } var ( ifaceLock = sync.Mutex{} ) func GetIfaces(logOutput bool) (ifaces []*Iface, err error) { ifaceLock.Lock() defer ifaceLock.Unlock() output, err := utils.ExecCombinedOutputLogged( []string{ "does not have", "invalid metadata", "cannot locate", }, "/usr/bin/bash", "/home/opc/secondary_vnic_all_configure.sh", ) if err != nil { return } if logOutput { logrus.WithFields(logrus.Fields{ "output": output, }).Warn("oracle: Oracle iface output") } found := false for _, line := range strings.Split(output, "\n") { fields := strings.Fields(line) if len(fields) < 13 { if found { break } else { continue } } if found && fields[0] == "-" && fields[5] != "-" { iface := &Iface{ Name: fields[7], Address: fields[1], Namespace: fields[5], MacAddress: fields[11], VnicId: fields[12], } ifaces = append(ifaces, iface) } else if fields[0] == "CONFIG" && fields[1] == "ADDR" && fields[5] == "NS" && fields[7] == "IFACE" && fields[11] == "MAC" && fields[12] == "VNIC" { found = true continue } } return } func ConfIfaces(logOutput bool) (err error) { ifaceLock.Lock() defer ifaceLock.Unlock() output, err := utils.ExecCombinedOutputLogged( []string{ "does not have", "invalid metadata", "cannot locate", }, "/usr/bin/bash", "/home/opc/secondary_vnic_all_configure.sh", "-c", "-n", ) if err != nil { return } if logOutput { logrus.WithFields(logrus.Fields{ "output": output, }).Warn("oracle: Oracle iface config output") } return } ================================================ FILE: oracle/metadata.go ================================================ package oracle import ( "encoding/json" "strings" "sync" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) var ( ociLock sync.Mutex ) type Metadata struct { UserOcid string PrivateKey string RegionName string TenancyOcid string CompartmentOcid string InstanceOcid string VnicOcid string } type OciMetaVnic struct { Id string `json:"vnicId"` VlanTag int `json:"vlanTag"` MacAddr string `json:"macAddr"` PrivateIp string `json:"privateIp"` VirtualRouterIp string `json:"virtualRouterIp"` SubnetCidrBlock string `json:"subnetCidrBlock"` Ipv6Addresses []string `json:"ipv6Addresses"` Ipv6SubnetCidrBlock string `json:"ipv6SubnetCidrBlock"` Ipv6VirtualRouterIp string `json:"ipv6VirtualRouterIp"` NicIndex int `json:"nicIndex"` } type OciMetaInstance struct { Id string `json:"id"` DisplayName string `json:"displayName"` CompartmentId string `json:"compartmentId"` RegionName string `json:"canonicalRegionName"` Shape string `json:"shape"` } type OciMeta struct { Instance OciMetaInstance `json:"instance"` Vnics []OciMetaVnic `json:"vnics"` } func (o *OciMeta) IsBareMetal() bool { if strings.Contains(o.Instance.Shape, "BM.") { return true } return false } func GetMetadata(authPv AuthProvider) (mdata *Metadata, err error) { userOcid := authPv.OracleUser() tenancyOcid := authPv.OracleTenancy() privateKey := authPv.OraclePrivateKey() output, err := utils.ExecOutput("", "oci-metadata", "--json") if err != nil { return } data := &OciMeta{} err = json.Unmarshal([]byte(output), data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Failed to parse metadata"), } return } vnicOcid := "" if data.Vnics != nil { for _, vnic := range data.Vnics { vnicOcid = vnic.Id break } } if vnicOcid == "" { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Failed to get vnic in metadata"), } return } mdata = &Metadata{ UserOcid: userOcid, PrivateKey: privateKey, RegionName: data.Instance.RegionName, TenancyOcid: tenancyOcid, CompartmentOcid: data.Instance.CompartmentId, InstanceOcid: data.Instance.Id, VnicOcid: vnicOcid, } return } func GetOciMetadata() (mdata *OciMeta, err error) { ociLock.Lock() defer ociLock.Unlock() output, err := utils.ExecOutput("", "oci-metadata", "--json") if err != nil { return } mdata = &OciMeta{} err = json.Unmarshal([]byte(output), mdata) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Failed to parse metadata"), } return } return } ================================================ FILE: oracle/oracle.go ================================================ package oracle type AuthProvider interface { OracleUser() string OracleTenancy() string OraclePrivateKey() string } ================================================ FILE: oracle/provider.go ================================================ package oracle import ( "crypto/rsa" "fmt" "github.com/dropbox/godropbox/errors" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type Provider struct { Metadata *Metadata privateKey *rsa.PrivateKey tenancy string user string fingerprint string region string compartment string netClient *core.VirtualNetworkClient computeClient *core.ComputeClient } func (p *Provider) LogInfo() { logrus.WithFields(logrus.Fields{ "region": p.Metadata.RegionName, "tenancy": p.Metadata.TenancyOcid, "compartment": p.Metadata.CompartmentOcid, "instance": p.Metadata.InstanceOcid, "instance_vnic": p.Metadata.VnicOcid, "user": p.Metadata.UserOcid, "fingerprint": p.fingerprint, }).Info("oracle: Oracle provider data") } func (p *Provider) AuthType() (common.AuthConfig, error) { return common.AuthConfig{ AuthType: common.UserPrincipal, IsFromConfigFile: false, OboToken: nil, }, nil } func (p *Provider) PrivateRSAKey() (*rsa.PrivateKey, error) { return p.privateKey, nil } func (p *Provider) KeyID() (string, error) { return fmt.Sprintf("%s/%s/%s", p.tenancy, p.user, p.fingerprint), nil } func (p *Provider) TenancyOCID() (string, error) { return p.tenancy, nil } func (p *Provider) UserOCID() (string, error) { return p.user, nil } func (p *Provider) KeyFingerprint() (string, error) { return p.fingerprint, nil } func (p *Provider) Region() (string, error) { return p.region, nil } func (p *Provider) CompartmentOCID() (string, error) { return p.compartment, nil } func (p *Provider) GetNetworkClient() ( netClient *core.VirtualNetworkClient, err error) { if p.netClient != nil { netClient = p.netClient return } client, err := core.NewVirtualNetworkClientWithConfigurationProvider(p) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to create oracle client"), } return } p.netClient = &client netClient = p.netClient return } func (p *Provider) GetComputeClient() ( computeClient *core.ComputeClient, err error) { if p.computeClient != nil { computeClient = p.computeClient return } client, err := core.NewComputeClientWithConfigurationProvider(p) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to create oracle client"), } return } p.computeClient = &client computeClient = p.computeClient return } func NewProvider(authPv AuthProvider) (prov *Provider, err error) { mdata, err := GetMetadata(authPv) if err != nil { return } privateKey, fingerprint, err := loadPrivateKey(mdata) if err != nil { return } prov = &Provider{ Metadata: mdata, privateKey: privateKey, tenancy: mdata.TenancyOcid, user: mdata.UserOcid, fingerprint: fingerprint, region: mdata.RegionName, compartment: mdata.CompartmentOcid, } return } ================================================ FILE: oracle/routetable.go ================================================ package oracle import ( "context" "github.com/dropbox/godropbox/errors" "github.com/oracle/oci-go-sdk/v65/core" "github.com/pritunl/pritunl-cloud/errortypes" ) type RouteTable struct { Id string VcnId string Routes map[string]string routeRules []core.RouteRule } func (r *RouteTable) RouteExists(dest string, nextHopId string) bool { if r.Routes[dest] == nextHopId { return true } return false } func (r *RouteTable) RouteUpsert(dest string, nextHopId string) bool { for i, routeRule := range r.routeRules { if routeRule.Destination != nil && *routeRule.Destination == dest { if routeRule.NetworkEntityId != nil && *routeRule.NetworkEntityId != nextHopId { routeRule.NetworkEntityId = &nextHopId r.routeRules[i] = routeRule return true } else { return false } } } routeRule := core.RouteRule{ Destination: &dest, NetworkEntityId: &nextHopId, } r.routeRules = append(r.routeRules, routeRule) return true } func (r *RouteTable) CommitRouteRules(pv *Provider) (err error) { client, err := pv.GetNetworkClient() if err != nil { return } req := core.UpdateRouteTableRequest{ RtId: &r.Id, UpdateRouteTableDetails: core.UpdateRouteTableDetails{ RouteRules: r.routeRules, }, } _, err = client.UpdateRouteTable(context.Background(), req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to update route table"), } return } return } func GetRouteTables(pv *Provider, vcnId string) ( tables []*RouteTable, err error) { limit := 100 compartmentId, err := pv.CompartmentOCID() if err != nil { return } client, err := pv.GetNetworkClient() if err != nil { return } vnicReq := core.ListRouteTablesRequest{ CompartmentId: &compartmentId, VcnId: &vcnId, Limit: &limit, } orcTables, err := client.ListRouteTables(context.Background(), vnicReq) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to list route tables"), } return } tables = []*RouteTable{} if orcTables.Items != nil { for _, orcTable := range orcTables.Items { table := &RouteTable{} if orcTable.Id != nil { table.Id = *orcTable.Id } if orcTable.VcnId != nil { table.VcnId = *orcTable.VcnId } if orcTable.RouteRules != nil { table.routeRules = orcTable.RouteRules } else { table.routeRules = []core.RouteRule{} } routes := map[string]string{} for _, rule := range table.routeRules { if rule.Destination == nil || rule.NetworkEntityId == nil { continue } routes[*rule.Destination] = *rule.NetworkEntityId } table.Routes = routes tables = append(tables, table) } } return } ================================================ FILE: oracle/subnet.go ================================================ package oracle import ( "context" "github.com/dropbox/godropbox/errors" "github.com/oracle/oci-go-sdk/v65/core" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Vcn struct { Id string Name string Network string Subnets []*Subnet } type Subnet struct { Id string VcnId string Name string Network string } func GetSubnet(pv *Provider, subnetId string) (subnet *Subnet, err error) { client, err := pv.GetNetworkClient() if err != nil { return } subReq := core.GetSubnetRequest{ SubnetId: &subnetId, } orcSubnet, err := client.GetSubnet(context.Background(), subReq) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to get subnet"), } return } subnet = &Subnet{} if orcSubnet.Id != nil { subnet.Id = *orcSubnet.Id } if orcSubnet.VcnId != nil { subnet.VcnId = *orcSubnet.VcnId } if orcSubnet.DisplayName != nil { subnet.Name = *orcSubnet.DisplayName } if orcSubnet.CidrBlock != nil { subnet.Network = *orcSubnet.CidrBlock } return } func GetVcns(pv *Provider) (vcns []*Vcn, err error) { client, err := pv.GetNetworkClient() if err != nil { return } compartmentId, err := pv.CompartmentOCID() if err != nil { return } req := core.ListVcnsRequest{ CompartmentId: &compartmentId, Limit: utils.PointerInt(100), } orcVcns, err := client.ListVcns(context.Background(), req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to get VCNs"), } return } vcns = []*Vcn{} for _, orcVcn := range orcVcns.Items { vcn := &Vcn{} if orcVcn.Id != nil { vcn.Id = *orcVcn.Id } if orcVcn.DisplayName != nil { vcn.Name = *orcVcn.DisplayName } if orcVcn.CidrBlock != nil { vcn.Network = *orcVcn.CidrBlock } subnets, e := GetSubnets(pv, vcn.Id) if e != nil { err = e return } vcn.Subnets = subnets vcns = append(vcns, vcn) } return } func GetSubnets(pv *Provider, vcnId string) (subnets []*Subnet, err error) { client, err := pv.GetNetworkClient() if err != nil { return } compartmentId, err := pv.CompartmentOCID() if err != nil { return } req := core.ListSubnetsRequest{ CompartmentId: &compartmentId, VcnId: utils.PointerString(vcnId), Limit: utils.PointerInt(256), } orcSubnets, err := client.ListSubnets(context.Background(), req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to get subnets"), } return } subnets = []*Subnet{} for _, orcSubnet := range orcSubnets.Items { subnet := &Subnet{} if orcSubnet.Id != nil { subnet.Id = *orcSubnet.Id } if orcSubnet.VcnId != nil { subnet.VcnId = *orcSubnet.VcnId } if orcSubnet.DisplayName != nil { subnet.Name = *orcSubnet.DisplayName } if orcSubnet.CidrBlock != nil { subnet.Network = *orcSubnet.CidrBlock } subnets = append(subnets, subnet) } return } ================================================ FILE: oracle/utils.go ================================================ package oracle import ( "bytes" "crypto/md5" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) func loadPrivateKey(mdata *Metadata) ( key *rsa.PrivateKey, fingerprint string, err error) { block, _ := pem.Decode([]byte(mdata.PrivateKey)) if block == nil { err = &errortypes.ParseError{ errors.New("oracle: Failed to decode private key"), } return } if block.Type != "RSA PRIVATE KEY" { err = &errortypes.ParseError{ errors.New("oracle: Invalid private key type"), } return } key, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Failed to parse rsa key"), } return } pubKey, err := x509.MarshalPKIXPublicKey(key.Public()) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Failed to marshal public key"), } return } keyHash := md5.New() keyHash.Write(pubKey) fingerprint = fmt.Sprintf("%x", keyHash.Sum(nil)) fingerprintBuf := bytes.Buffer{} for i, run := range fingerprint { fingerprintBuf.WriteRune(run) if i%2 == 1 && i != len(fingerprint)-1 { fingerprintBuf.WriteRune(':') } } fingerprint = fingerprintBuf.String() return } ================================================ FILE: oracle/vnic.go ================================================ package oracle import ( "context" "time" "github.com/dropbox/godropbox/errors" "github.com/oracle/oci-go-sdk/v65/core" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) type Vnic struct { Id string SubnetId string IsPrimary bool MacAddress string PrivateIp string PrivateIpId string PublicIp string PublicIp6 string SkipSourceDestCheck bool } func (v *Vnic) SetSkipSourceDestCheck(pv *Provider, val bool) (err error) { client, err := pv.GetNetworkClient() if err != nil { return } req := core.UpdateVnicRequest{ VnicId: &v.Id, UpdateVnicDetails: core.UpdateVnicDetails{ SkipSourceDestCheck: &val, }, } _, err = client.UpdateVnic(context.Background(), req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to update vnic"), } return } return } func GetVnic(pv *Provider, vnicId string) (vnic *Vnic, err error) { client, err := pv.GetNetworkClient() if err != nil { return } req := core.GetVnicRequest{ VnicId: utils.PointerString(vnicId), } orcVnic, err := client.GetVnic(context.Background(), req) if err != nil { if orcVnic.RawResponse != nil && orcVnic.RawResponse.StatusCode == 404 { err = &errortypes.NotFoundError{ errors.Wrap(err, "oracle: Failed to find vnic"), } return } err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to get vnic"), } return } vnic = &Vnic{} if orcVnic.Id != nil { vnic.Id = *orcVnic.Id } if orcVnic.SubnetId != nil { vnic.SubnetId = *orcVnic.SubnetId } if orcVnic.IsPrimary != nil { vnic.IsPrimary = *orcVnic.IsPrimary } if orcVnic.MacAddress != nil { vnic.MacAddress = *orcVnic.MacAddress } if orcVnic.PrivateIp != nil { vnic.PrivateIp = *orcVnic.PrivateIp } if orcVnic.PublicIp != nil { vnic.PublicIp = *orcVnic.PublicIp } if len(orcVnic.Ipv6Addresses) > 0 { vnic.PublicIp6 = orcVnic.Ipv6Addresses[0] } if orcVnic.SkipSourceDestCheck != nil { vnic.SkipSourceDestCheck = *orcVnic.SkipSourceDestCheck } limit := 10 ipReq := core.ListPrivateIpsRequest{ VnicId: &vnic.Id, Limit: &limit, } orcIps, err := client.ListPrivateIps(context.Background(), ipReq) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to get vnic ips"), } return } if orcIps.Items != nil { for _, orcIp := range orcIps.Items { if orcIp.IsPrimary != nil && *orcIp.IsPrimary && orcIp.Id != nil { vnic.PrivateIpId = *orcIp.Id break } } } return } func getVnicAttachment(pv *Provider, attachmentId string) ( vnicId string, err error) { client, err := pv.GetComputeClient() if err != nil { return } req := core.GetVnicAttachmentRequest{ VnicAttachmentId: utils.PointerString(attachmentId), } resp, err := client.GetVnicAttachment(context.Background(), req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to create vnic"), } return } if resp.VnicId != nil { vnicId = *resp.VnicId } return } func CreateVnic(pv *Provider, name, subnetId string, publicIp, publicIp6 bool) (vnicId, vnicAttachId string, err error) { client, err := pv.GetComputeClient() if err != nil { return } req := core.AttachVnicRequest{ AttachVnicDetails: core.AttachVnicDetails{ InstanceId: utils.PointerString(pv.Metadata.InstanceOcid), DisplayName: utils.PointerString(name), CreateVnicDetails: &core.CreateVnicDetails{ AssignPublicIp: utils.PointerBool(publicIp), AssignIpv6Ip: utils.PointerBool(publicIp6), DisplayName: utils.PointerString(name), SubnetId: utils.PointerString(subnetId), }, }, } var resp core.AttachVnicResponse retryCount := settings.System.OracleApiRetryCount retryRate := time.Duration( settings.System.OracleApiRetryRate) * time.Second for i := 0; i < retryCount; i++ { resp, err = client.AttachVnic(context.Background(), req) if err != nil { if i != retryCount-1 && resp.RawResponse != nil && resp.RawResponse.StatusCode == 409 { time.Sleep(retryRate) continue } err = &errortypes.RequestError{ errors.Wrap(err, "oracle: Failed to create vnic"), } return } break } if resp.Id == nil { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Nil vnic attachment id"), } return } vnicAttachId = *resp.Id for i := 0; i < 60; i++ { vnicId, err = getVnicAttachment(pv, vnicAttachId) if err != nil { time.Sleep(500 * time.Millisecond) if i == 59 { return } err = nil continue } if vnicId == "" { time.Sleep(500 * time.Millisecond) continue } break } if vnicId == "" { err = &errortypes.ParseError{ errors.Wrap(err, "oracle: Nil vnic id"), } return } return } func RemoveVnic(pv *Provider, vnicAttachId string) (err error) { client, err := pv.GetComputeClient() if err != nil { return } req := core.DetachVnicRequest{ VnicAttachmentId: utils.PointerString(vnicAttachId), } retryCount := settings.System.OracleApiRetryCount retryRate := time.Duration( settings.System.OracleApiRetryRate) * time.Second for i := 0; i < retryCount; i++ { resp, e := client.DetachVnic(context.Background(), req) if e != nil { if i != retryCount-1 && resp.RawResponse != nil && resp.RawResponse.StatusCode == 409 { time.Sleep(retryRate) continue } err = &errortypes.RequestError{ errors.Wrap(e, "oracle: Failed to remove vnic"), } return } break } return } ================================================ FILE: organization/organization.go ================================================ package organization import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Organization struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Roles []string `bson:"roles" json:"roles"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` } func (d *Organization) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { d.Name = utils.FilterName(d.Name) if d.Roles == nil { d.Roles = []string{} } return } func (d *Organization) Commit(db *database.Database) (err error) { coll := db.Organizations() err = coll.Commit(d.Id, d) if err != nil { return } return } func (d *Organization) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Organizations() err = coll.CommitFields(d.Id, d, fields) if err != nil { return } return } func (c *Organization) Insert(db *database.Database) (err error) { coll := db.Organizations() if !c.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("organization: Organization already exists"), } return } resp, err := coll.InsertOne(db, c) if err != nil { err = database.ParseError(err) return } c.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: organization/utils.go ================================================ package organization import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, dcId bson.ObjectID) ( dc *Organization, err error) { coll := db.Organizations() dc = &Organization{} err = coll.FindOneId(dcId, dc) if err != nil { return } return } func GetAll(db *database.Database, query *bson.M) ( orgs []*Organization, err error) { coll := db.Organizations() orgs = []*Organization{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { org := &Organization{} err = cursor.Decode(org) if err != nil { err = database.ParseError(err) return } orgs = append(orgs, org) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllName(db *database.Database) (orgs []*Organization, err error) { coll := db.Organizations() orgs = []*Organization{} cursor, err := coll.Find( db, &bson.M{}, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{{"name", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { org := &Organization{} err = cursor.Decode(org) if err != nil { err = database.ParseError(err) return } orgs = append(orgs, org) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNameRoles(db *database.Database, roles []string) ( orgs []*Organization, err error) { coll := db.Organizations() orgs = []*Organization{} cursor, err := coll.Find( db, &bson.M{ "roles": &bson.M{ "$in": roles, }, }, options.Find(). SetProjection(bson.D{{"name", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { org := &Organization{} err = cursor.Decode(org) if err != nil { err = database.ParseError(err) return } orgs = append(orgs, org) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (orgs []*Organization, count int64, err error) { coll := db.Organizations() orgs = []*Organization{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { org := &Organization{} err = cursor.Decode(org) if err != nil { err = database.ParseError(err) return } orgs = append(orgs, org) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, dcId bson.ObjectID) (err error) { coll := db.Organizations() _, err = coll.DeleteOne(db, &bson.M{ "_id": dcId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func Count(db *database.Database) (count int64, err error) { coll := db.Organizations() count, err = coll.CountDocuments(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: paths/paths.go ================================================ package paths import ( "crypto/md5" "encoding/base32" "encoding/hex" "fmt" "path" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" ) func GetVmUuid(instId bson.ObjectID) string { idHash := md5.New() idHash.Write(instId[:]) uuid := idHash.Sum(nil) uuid[6] = (uuid[6] & 0x0f) | uint8((3&0xf)<<4) uuid[8] = (uuid[8] & 0x3f) | 0x80 buffer := [36]byte{} hex.Encode(buffer[:], uuid[:4]) buffer[8] = '-' hex.Encode(buffer[9:13], uuid[4:6]) buffer[13] = '-' hex.Encode(buffer[14:18], uuid[6:8]) buffer[18] = '-' hex.Encode(buffer[19:23], uuid[8:10]) buffer[23] = '-' hex.Encode(buffer[24:], uuid[10:]) return string(buffer[:]) } func GetVmPath(instId bson.ObjectID) string { return path.Join(node.Self.GetVirtPath(), "instances", instId.Hex()) } func GetDisksPath() string { return path.Join(node.Self.GetVirtPath(), "disks") } func GetLocalIsosPath() string { return path.Join(node.Self.GetVirtPath(), "isos") } func GetBackingPath() string { return path.Join(node.Self.GetVirtPath(), "backing") } func GetTpmsPath() string { return path.Join(node.Self.GetVirtPath(), "tpms") } func GetTpmPath(virtId bson.ObjectID) string { return path.Join(GetTpmsPath(), virtId.Hex()) } func GetTpmSockPath(virtId bson.ObjectID) string { return path.Join(GetTpmsPath(), virtId.Hex(), "sock") } func GetTpmPwdPath(virtId bson.ObjectID) string { return path.Join(GetTpmsPath(), virtId.Hex(), "pwd") } func GetTempPath() string { return node.Self.GetTempPath() } func GetTempDir() string { return path.Join(GetTempPath(), bson.NewObjectID().Hex()) } func GetDrivePath(driveId string) string { return path.Join("/dev/disk/by-id", driveId) } func GetCachesDir() string { return path.Join(node.Self.GetVirtPath(), "caches") } func GetCacheDir(virtId bson.ObjectID) string { return path.Join(GetCachesDir(), virtId.Hex()) } func GetOvmfDir() string { return path.Join(node.Self.GetVirtPath(), "ovmf") } func GetDiskPath(diskId bson.ObjectID) string { return path.Join(GetDisksPath(), fmt.Sprintf("%s.qcow2", diskId.Hex())) } func GetOvmfVarsPath(virtId bson.ObjectID) string { return path.Join(GetOvmfDir(), fmt.Sprintf("%s_vars.fd", virtId.Hex())) } func GetDiskTempPath() string { return path.Join(GetTempPath(), fmt.Sprintf("disk-%s", bson.NewObjectID().Hex())) } func GetImageTempPath() string { return path.Join(GetTempPath(), fmt.Sprintf("image-%s", bson.NewObjectID().Hex())) } func GetImdsPath() string { return path.Join(node.Self.GetVirtPath(), "imds") } func GetImdsConfPath(instId bson.ObjectID) string { return path.Join(GetImdsPath(), fmt.Sprintf("%s-conf.json", instId.Hex())) } func GetInstRunPath(instId bson.ObjectID) string { return path.Join(settings.Hypervisor.RunPath, instId.Hex()) } func GetImdsSockPath(instId bson.ObjectID) string { return path.Join(GetInstRunPath(instId), "imds.sock") } func GetDiskMountPath() string { return path.Join(GetTempPath(), bson.NewObjectID().Hex()) } func GetInitsPath() string { return path.Join(node.Self.GetVirtPath(), "inits") } func GetInitPath(instId bson.ObjectID) string { return path.Join(GetInitsPath(), fmt.Sprintf("%s.iso", instId.Hex())) } func GetUnitName(virtId bson.ObjectID) string { return fmt.Sprintf("pritunl_cloud_%s.service", virtId.Hex()) } func GetUnitPath(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitName(virtId)) } func GetUnitNameDhcp4(virtId bson.ObjectID, n int) string { return fmt.Sprintf("pritunl_dhcp4_%s_%d.service", virtId.Hex(), n) } func GetUnitPathDhcp4(virtId bson.ObjectID, n int) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameDhcp4(virtId, n)) } func GetUnitNameDhcp6(virtId bson.ObjectID, n int) string { return fmt.Sprintf("pritunl_dhcp6_%s_%d.service", virtId.Hex(), n) } func GetUnitPathDhcp6(virtId bson.ObjectID, n int) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameDhcp6(virtId, n)) } func GetUnitNameNdp(virtId bson.ObjectID, n int) string { return fmt.Sprintf("pritunl_ndp_%s_%d.service", virtId.Hex(), n) } func GetUnitPathNdp(virtId bson.ObjectID, n int) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameNdp(virtId, n)) } func GetUnitNameTpm(virtId bson.ObjectID) string { return fmt.Sprintf("pritunl_tpm_%s.service", virtId.Hex()) } func GetUnitPathTpm(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameTpm(virtId)) } func GetUnitNameImds(virtId bson.ObjectID) string { return fmt.Sprintf("pritunl_imds_%s.service", virtId.Hex()) } func GetUnitNameDhcpc(virtId bson.ObjectID) string { return fmt.Sprintf("pritunl_dhcpc_%s.service", virtId.Hex()) } func GetShareId(virtId bson.ObjectID, shareName string) string { hash := md5.New() hash.Write([]byte(virtId.Hex())) hash.Write([]byte(shareName)) return strings.ToLower(base32.StdEncoding.EncodeToString( hash.Sum(nil))[:12]) } func GetUnitNameShare(virtId bson.ObjectID, shareId string) string { return fmt.Sprintf("pritunl_share_%s_%s.service", virtId.Hex(), shareId) } func GetUnitNameShares(virtId bson.ObjectID) string { return fmt.Sprintf("pritunl_share_%s_*.service", virtId.Hex()) } func GetUnitPathImds(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameImds(virtId)) } func GetUnitPathDhcpc(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameDhcpc(virtId)) } func GetUnitPathShare(virtId bson.ObjectID, shareId string) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameShare(virtId, shareId)) } func GetUnitPathShares(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.SystemdPath, GetUnitNameShares(virtId)) } func GetPidPath(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.RunPath, fmt.Sprintf("%s.pid", virtId.Hex())) } func GetShareSockPath(virtId bson.ObjectID, shareId string) string { return path.Join(GetInstRunPath(virtId), fmt.Sprintf("virtiofs_%s.sock", shareId)) } func GetHugepagePath(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.HugepagesPath, virtId.Hex()) } func GetSockPath(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.RunPath, fmt.Sprintf("%s.sock", virtId.Hex())) } func GetQmpSockPath(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.RunPath, fmt.Sprintf("%s.qmp.sock", virtId.Hex())) } func GetGuestPath(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.RunPath, fmt.Sprintf("%s.guest", virtId.Hex())) } // TODO Backward compatibility func GetPidPathOld(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.LibPath, fmt.Sprintf("%s.pid", virtId.Hex())) } // TODO Backward compatibility func GetSockPathOld(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.LibPath, fmt.Sprintf("%s.sock", virtId.Hex())) } // TODO Backward compatibility func GetQmpSockPathOld(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.LibPath, fmt.Sprintf("%s.qmp.sock", virtId.Hex())) } // TODO Backward compatibility func GetGuestPathOld(virtId bson.ObjectID) string { return path.Join(settings.Hypervisor.LibPath, fmt.Sprintf("%s.guest", virtId.Hex())) } func GetNamespacesPath() string { return "/etc/netns" } func GetNamespacePath(namespace string) string { return path.Join(GetNamespacesPath(), namespace) } ================================================ FILE: paths/utils.go ================================================ package paths import ( "os" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" ) var ( ovmfCodePaths = []string{ "/usr/share/edk2/ovmf/OVMF_CODE.fd", "/usr/share/edk2/ovmf/OVMF_CODE.cc.fd", "/usr/share/OVMF/OVMF_CODE.pure-efi.fd", "/usr/share/OVMF/OVMF_CODE.fd", } ovmfVarsPaths = []string{ "/usr/share/edk2/ovmf/OVMF_VARS.fd", "/usr/share/OVMF/OVMF_VARS.pure-efi.fd", "/usr/share/OVMF/OVMF_VARS.fd", } ovmfSecureCodePaths = []string{ "/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd", "/usr/share/OVMF/OVMF_CODE.secboot.fd", } ovmfSecureVarsPaths = []string{ "/usr/share/edk2/ovmf/OVMF_VARS.secboot.fd", "/usr/share/OVMF/OVMF_VARS.secboot.fd", "/usr/share/OVMF/OVMF_VARS.fd", } ) func existsFile(pth string) (exists bool, err error) { _, err = os.Stat(pth) if err == nil { exists = true return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "paths: Failed to stat %s", pth), } return } func FindOvmfCodePath(secureBoot bool) (pth string, err error) { if secureBoot { pth = settings.Hypervisor.OvmfSecureCodePath if pth != "" { return } for _, pth = range ovmfSecureCodePaths { exists, e := existsFile(pth) if e != nil { err = e return } if exists { return } } } else { pth = settings.Hypervisor.OvmfCodePath if pth != "" { return } for _, pth = range ovmfCodePaths { exists, e := existsFile(pth) if e != nil { err = e return } if exists { return } } } pth = "" err = &errortypes.NotFoundError{ errors.New("paths: Failed to find OVMF code file"), } return } func FindOvmfVarsPath(secureBoot bool) (pth string, err error) { if secureBoot { pth = settings.Hypervisor.OvmfSecureVarsPath if pth != "" { return } for _, pth = range ovmfSecureVarsPaths { exists, e := existsFile(pth) if e != nil { err = e return } if exists { return } } } else { pth = settings.Hypervisor.OvmfVarsPath if pth != "" { return } for _, pth = range ovmfVarsPaths { exists, e := existsFile(pth) if e != nil { err = e return } if exists { return } } } pth = "" err = &errortypes.NotFoundError{ errors.New("paths: Failed to find OVMF vars file"), } return } ================================================ FILE: pci/pci.go ================================================ package pci import ( "sync" "time" ) var ( syncLast time.Time syncLock sync.Mutex syncCache []*Device ) type Device struct { Slot string `bson:"slot" json:"slot"` Class string `bson:"class" json:"class"` Name string `bson:"name" json:"name"` Driver string `bson:"driver" json:"driver"` } ================================================ FILE: pci/utils.go ================================================ package pci import ( "regexp" "strings" "time" "github.com/pritunl/pritunl-cloud/utils" ) var ( reg = regexp.MustCompile( "[a-fA-F0-9][a-fA-F0-9]:[a-fA-F0-9][a-fA-F0-9].[0-9]") ) func CheckSlot(slot string) bool { return reg.MatchString(slot) } func GetVfio(slot string) (dev *Device, err error) { devices, err := GetVfioAll() if err != nil { return } for _, device := range devices { if device.Slot == slot { dev = device return } } return } func GetVfioAll() (devices []*Device, err error) { if time.Since(syncLast) < 30*time.Second { devices = syncCache return } syncLock.Lock() defer syncLock.Unlock() devices = []*Device{} output, err := utils.ExecOutput("", "lspci", "-v") if err != nil { return } dev := &Device{} outputLines := strings.Split(output, "\n") for _, line := range outputLines { if strings.TrimSpace(line) == "" { if dev.Slot != "" && dev.Name != "" && dev.Driver == "vfio-pci" && CheckSlot(dev.Slot) { devices = append(devices, dev) } dev = &Device{} continue } if dev.Slot == "" { lines := strings.SplitN(line, " ", 2) if len(lines) != 2 { continue } names := strings.SplitN(lines[1], ":", 2) if len(names) != 2 { continue } dev.Slot = strings.TrimSpace(lines[0]) dev.Class = strings.TrimSpace(names[0]) dev.Name = strings.TrimSpace(names[1]) } else if strings.Contains(line, "Kernel driver in use:") { lines := strings.SplitN(line, ":", 2) if len(lines) != 2 { continue } dev.Driver = strings.TrimSpace(lines[1]) } } syncCache = devices syncLast = time.Now() return } ================================================ FILE: permission/permission.go ================================================ package permission import ( "fmt" "os" "path/filepath" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) func chown(virt *vm.VirtualMachine, path string) (err error) { err = os.Chown(path, virt.UnixId, 0) if err != nil { err = &errortypes.WriteError{ errors.Newf( "permission: Failed to set owner of '%s' to '%d'", path, virt.UnixId, ), } return } return } func touchChown(virt *vm.VirtualMachine, path string) (err error) { _, err = utils.ExecCombinedOutputLogged(nil, "touch", path, ) if err != nil { return } err = chown(virt, path) if err != nil { return } return } func mkdirChown(virt *vm.VirtualMachine, path string) (err error) { _, err = utils.ExecCombinedOutputLogged(nil, "mkdir", "-p", path, ) if err != nil { return } err = chown(virt, path) if err != nil { return } return } func Restore(pth string) (err error) { err = os.Chown(pth, 0, 0) if err != nil { err = &errortypes.WriteError{ errors.Newf( "permission: Failed to set owner of '%s' to '0'", pth, ), } return } return } func Chown(virt *vm.VirtualMachine, pth string) (err error) { err = chown(virt, pth) if err != nil { return } return } func InitVirt(virt *vm.VirtualMachine) (err error) { err = UserAdd(virt) if err != nil { return } if virt.Uefi { err = chown(virt, paths.GetOvmfVarsPath(virt.Id)) if err != nil { return } } err = chown(virt, paths.GetInitPath(virt.Id)) if err != nil { return } for _, disk := range virt.Disks { err = chown(virt, disk.Path) if err != nil { return } } for _, device := range virt.DriveDevices { drivePth := "" if device.Type == vm.Lvm { drivePth = filepath.Join("/dev/mapper", fmt.Sprintf("%s-%s", device.VgName, device.LvName)) } else { drivePth = paths.GetDrivePath(device.Id) } err = chown(virt, drivePth) if err != nil { return } } for _, device := range virt.UsbDevices { usbDevice, _ := device.GetDevice() if usbDevice != nil { err = Chown(virt, usbDevice.BusPath) if err != nil { return } } } err = chown(virt, paths.GetCacheDir(virt.Id)) if err != nil { return } return } func InitDisk(virt *vm.VirtualMachine, dsk *vm.Disk) (err error) { err = UserAdd(virt) if err != nil { return } err = chown(virt, dsk.Path) if err != nil { return } return } func InitTpm(virt *vm.VirtualMachine) (err error) { tpmPath := paths.GetTpmPath(virt.Id) err = chown(virt, tpmPath) if err != nil { return } return } func InitTpmPwd(virt *vm.VirtualMachine) (err error) { tpmPath := paths.GetTpmPwdPath(virt.Id) err = chown(virt, tpmPath) if err != nil { return } return } func InitImds(virt *vm.VirtualMachine) (err error) { runPath := paths.GetInstRunPath(virt.Id) err = chown(virt, runPath) if err != nil { return } return } func InitMount(virt *vm.VirtualMachine, shareId string) (err error) { sockPath := paths.GetShareSockPath(virt.Id, shareId) err = chown(virt, sockPath) if err != nil { return } return } ================================================ FILE: permission/user.go ================================================ package permission import ( "fmt" "os/user" "path" "strconv" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) func GetUserName(vmId bson.ObjectID) string { return fmt.Sprintf("pritunl-%s", vmId.Hex()) } func UserAdd(virt *vm.VirtualMachine) (err error) { name := GetUserName(virt.Id) usr, e := user.LookupId(strconv.Itoa(virt.UnixId)) if usr != nil && e == nil { return } if virt.UnixId == 0 { err = &errortypes.ParseError{ errors.New("permission: Virt missing unix id"), } return } _, err = utils.ExecCombinedOutputLogged(nil, "useradd", "--no-user-group", "--no-create-home", "--uid", strconv.Itoa(virt.UnixId), name, ) if err != nil { return } mailPath := path.Join("/var/mail", name) _ = utils.RemoveAll(mailPath) return } func UserDelete(virt *vm.VirtualMachine) (err error) { name := GetUserName(virt.Id) _, _ = utils.ExecCombinedOutput("", "userdel", name, ) _, _ = utils.ExecCombinedOutput("", "groupdel", name, ) return } func UserGroupAdd(virtId bson.ObjectID, group string) (err error) { name := GetUserName(virtId) _, err = utils.ExecCombinedOutputLogged( []string{ "does not exist", }, "gpasswd", "-a", name, group, ) return } func UserGroupDelete(virtId bson.ObjectID, group string) (err error) { name := GetUserName(virtId) _, err = utils.ExecCombinedOutputLogged( []string{ "not a member", "does not exist", }, "gpasswd", "-d", name, group, ) return } ================================================ FILE: plan/constants.go ================================================ package plan import ( "github.com/dropbox/godropbox/container/set" ) const ( Start = "start" Stop = "stop" Restart = "restart" Destroy = "destroy" ) var actions = set.NewSet( Start, Stop, Restart, Destroy, ) ================================================ FILE: plan/data.go ================================================ package plan import ( "encoding/json" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/eval" ) type Data struct { Unit Unit `json:"unit"` Instance Instance `json:"instance"` } type Unit struct { Name string `json:"name"` Count int `json:"count"` } type Instance struct { Name string `json:"name"` State string `json:"state"` Action string `json:"action"` Processors int `json:"processors"` Memory int `json:"memory"` LastTimestamp int `json:"last_timestamp"` LastHeartbeat int `json:"last_heartbeat"` } func (d *Data) Export() (data eval.Data, err error) { dataByt, err := json.Marshal(d) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "plan: Failed to marshal"), } return } data = eval.Data{} err = json.Unmarshal(dataByt, &data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "plan: Failed to unmarshal"), } return } return } func GetEmtpyData() (data eval.Data, err error) { dataStrct := Data{} data, err = dataStrct.Export() if err != nil { return } return } ================================================ FILE: plan/plan.go ================================================ package plan import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/eval" "github.com/pritunl/pritunl-cloud/utils" ) type Plan struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Statements []*Statement `bson:"statements" json:"statements"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` } type Statement struct { Id bson.ObjectID `bson:"id" json:"id"` Statement string `bson:"statement" json:"statement"` } func (p *Plan) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { p.Name = utils.FilterName(p.Name) if p.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "organization_required", Message: "Missing required organization", } return } emptyData, err := GetEmtpyData() if err != nil { return } if p.Statements == nil { p.Statements = []*Statement{} } for _, statement := range p.Statements { if statement.Id.IsZero() { statement.Id = bson.NewObjectID() } err = eval.Validate(statement.Statement) if err != nil { return } _, _, err = eval.Eval(emptyData, statement.Statement) if err != nil { return } } return } func (p *Plan) UpdateStatements(inStatements []*Statement) (err error) { curStatements := map[bson.ObjectID]*Statement{} for _, statement := range p.Statements { curStatements[statement.Id] = statement } newStatements := []*Statement{} for _, statement := range inStatements { curStatement := curStatements[statement.Id] if curStatement != nil { if statement.Statement == curStatement.Statement { newStatements = append(newStatements, curStatement) } else { newStatement := &Statement{ Id: bson.NewObjectID(), Statement: statement.Statement, } newStatements = append(newStatements, newStatement) } } else { newStatement := &Statement{ Id: bson.NewObjectID(), Statement: statement.Statement, } newStatements = append(newStatements, newStatement) } } p.Statements = newStatements return } func (p *Plan) Commit(db *database.Database) (err error) { coll := db.Plans() err = coll.Commit(p.Id, p) if err != nil { return } return } func (p *Plan) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Plans() err = coll.CommitFields(p.Id, p, fields) if err != nil { return } return } func (p *Plan) Insert(db *database.Database) (err error) { coll := db.Plans() if !p.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("domain: Plan already exists"), } return } _, err = coll.InsertOne(db, p) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: plan/utils.go ================================================ package plan import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, plnId bson.ObjectID) ( pln *Plan, err error) { coll := db.Plans() pln = &Plan{} err = coll.FindOneId(plnId, pln) if err != nil { return } return } func GetOrg(db *database.Database, orgId, plnId bson.ObjectID) ( pln *Plan, err error) { coll := db.Plans() pln = &Plan{} err = coll.FindOne(db, &bson.M{ "_id": plnId, "organization": orgId, }).Decode(pln) if err != nil { err = database.ParseError(err) return } return } func ExistsOrg(db *database.Database, orgId, plnId bson.ObjectID) ( exists bool, err error) { coll := db.Plans() n, err := coll.CountDocuments(db, &bson.M{ "_id": plnId, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } if n > 0 { exists = true } return } func GetOne(db *database.Database, query *bson.M) (pln *Plan, err error) { coll := db.Plans() pln = &Plan{} err = coll.FindOne(db, query).Decode(pln) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( plns []*Plan, err error) { coll := db.Plans() plns = []*Plan{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dmn := &Plan{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } plns = append(plns, dmn) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (plns []*Plan, count int64, err error) { coll := db.Plans() plns = []*Plan{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) defer cursor.Close(db) for cursor.Next(db) { dmn := &Plan{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } plns = append(plns, dmn) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllName(db *database.Database, query *bson.M) ( plns []*Plan, err error) { coll := db.Plans() plns = []*Plan{} cursor, err := coll.Find( db, query, options.Find(). SetProjection(bson.D{ {"name", 1}, {"organization", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dmn := &Plan{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } plns = append(plns, dmn) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, plnId bson.ObjectID) (err error) { coll := db.Plans() _, err = coll.DeleteOne(db, &bson.M{ "_id": plnId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, plnId bson.ObjectID) ( err error) { coll := db.Plans() _, err = coll.DeleteOne(db, &bson.M{ "_id": plnId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, plnIds []bson.ObjectID) (err error) { coll := db.Plans() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": plnIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, plnIds []bson.ObjectID) (err error) { coll := db.Plans() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": plnIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: planner/planner.go ================================================ package planner import ( "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/eval" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Planner struct { unitsMap map[bson.ObjectID]*unit.Unit } func (p *Planner) setInstanceAction(db *database.Database, deply *deployment.Deployment, inst *instance.Instance, statement *plan.Statement, threshold int, action string) (err error) { disks, e := disk.GetInstance(db, inst.Id) if e != nil { err = e return } for _, dsk := range disks { if dsk.Action != "" { logrus.WithFields(logrus.Fields{ "instance_id": inst.Id.Hex(), "disk_id": dsk.Id.Hex(), "disk_action": dsk.Action, }).Info("deploy: Ignoring instance plan action, " + "disk action pending") return } } if inst.Action == action { return } logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), "statement": statement.Statement, "threshold": threshold, "action": action, }).Info("scheduler: Handling plan action") inst.Action = action errData, e := inst.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } err = inst.CommitFields(db, set.NewSet("action")) if err != nil { return } return } func (p *Planner) checkInstance(db *database.Database, deply *deployment.Deployment) (err error) { if deply.State == deployment.Reserved { return } inst, err := instance.Get(db, deply.Instance) if err != nil { if _, ok := err.(*database.NotFoundError); ok { inst = nil err = nil } else { return } } if inst == nil && deply.Kind == deployment.Instance { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), }).Info("scheduler: Removing deployment for destroyed instance") err = deployment.Remove(db, deply.Id) if err != nil { return } return } unt := p.unitsMap[deply.Unit] if unt == nil { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), }).Error("scheduler: Failed to find unit for deployment") // err = deployment.Remove(db, deply.Id) // if err != nil { // return // } return } if inst == nil { return } if deply.Action == deployment.Restore && inst.IsActive() { deply.Action = "" deply.State = deployment.Deployed err = deply.CommitFields(db, set.NewSet("state", "action")) if err != nil { return } } status := deployment.Unhealthy if inst.Guest != nil { if inst.Guest.Status == types.Running || inst.Guest.Status == types.ReloadingClean { now := time.Now() heartbeatTtl := time.Duration( settings.System.InstanceTimestampTtl) * time.Second if now.Sub(inst.Guest.Heartbeat) <= heartbeatTtl { status = deployment.Healthy } else if now.Sub(inst.Guest.Timestamp) > heartbeatTtl { status = deployment.Unknown } } } if deply.Status != status { deply.Status = status err = deply.CommitFields(db, set.NewSet("status")) if err != nil { return } } if deply.Action != "" { return } switch deply.State { case deployment.Archived: return } if deply.State == deployment.Deployed && !unt.HasDeployment(deply.Id) { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), }).Info("scheduler: Restoring deployment") err = unt.RestoreDeployment(db, deply.Id) if err != nil { return } } spc, err := spec.Get(db, deply.Spec) if err != nil { return } if spc.Instance == nil { return } if deply.State != deployment.Deployed { return } pln, err := plan.Get(db, spc.Instance.Plan) if pln == nil { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), }).Info("scheduler: Failed to find plan for deployment") return } data, err := buildEvalData(unt, inst) if err != nil { return } var statement *plan.Statement action := "" threshold := 0 for _, statement = range pln.Statements { action, threshold, err = eval.Eval(data, statement.Statement) if err != nil { return } threshold = utils.Max(deployment.ThresholdMin, threshold) action, err = deply.HandleStatement( db, statement.Id, threshold, action) if err != nil { return } if action != "" { break } } if action != "" { switch action { case plan.Start: err = p.setInstanceAction(db, deply, inst, statement, threshold, instance.Start) if err != nil { return } break case plan.Stop: err = p.setInstanceAction(db, deply, inst, statement, threshold, instance.Stop) if err != nil { return } break case plan.Restart: err = p.setInstanceAction(db, deply, inst, statement, threshold, instance.Restart) if err != nil { return } break case plan.Destroy: err = p.setInstanceAction(db, deply, inst, statement, threshold, instance.Destroy) if err != nil { return } break default: logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), "statement": statement.Statement, "threshold": threshold, "action": action, }).Error("scheduler: Unknown plan action") } } return } func (p *Planner) ApplyPlans(db *database.Database) (err error) { deployments, err := deployment.GetAll(db, &bson.M{}) if err != nil { return } p.unitsMap, err = unit.GetAllMap(db, &bson.M{}) if err != nil { return } var waiters sync.WaitGroup batch := make(chan struct{}, settings.System.PlannerBatchSize) for _, deply := range deployments { waiters.Add(1) batch <- struct{}{} go func(deply *deployment.Deployment) { defer func() { <-batch waiters.Done() }() switch deply.Kind { case deployment.Instance, deployment.Image: e := p.checkInstance(db, deply) if e != nil { logrus.WithFields(logrus.Fields{ "deployment": deply.Id.Hex(), "instance": deply.Instance.Hex(), "pod": deply.Pod.Hex(), "unit": deply.Unit.Hex(), "error": e, }).Error("scheduler: Failed to check instance deployment") } break } if deply.State == deployment.Reserved && deply.Action == deployment.Destroy && time.Since(deply.Timestamp) > 300*time.Second { err := deployment.Remove(db, deply.Id) if err != nil { logrus.WithFields(logrus.Fields{ "deployment_id": deply.Id.Hex(), "error": err, }).Error("deploy: Failed to remove deployment") return } event.PublishDispatch(db, "pod.change") } }(deply) } waiters.Wait() return } ================================================ FILE: planner/utils.go ================================================ package planner import ( "time" "github.com/pritunl/pritunl-cloud/eval" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" ) func buildEvalData(unt *unit.Unit, inst *instance.Instance) (data eval.Data, err error) { lastTimestamp := 0 lastHeartbeat := 0 if inst.IsActive() { now := time.Now() uptime := int(now.Sub(inst.Timestamp).Seconds()) if inst.Guest != nil { lastTimestamp = int(now.Sub(inst.Guest.Timestamp).Seconds()) lastHeartbeat = int(now.Sub(inst.Guest.Heartbeat).Seconds()) } lastTimestamp = utils.Min(lastTimestamp, uptime) lastHeartbeat = utils.Min(lastHeartbeat, uptime) } dataStrct := plan.Data{ Unit: plan.Unit{ Name: unt.Name, Count: unt.Count, }, Instance: plan.Instance{ Name: inst.Name, State: inst.State, Action: inst.Action, LastTimestamp: lastTimestamp, LastHeartbeat: lastHeartbeat, }, } data, err = dataStrct.Export() if err != nil { return } return } ================================================ FILE: pod/pod.go ================================================ package pod import ( "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" ) type Pod struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` UserDrafts map[bson.ObjectID][]*UnitDraft `bson:"drafts" json:"-"` Drafts []*UnitDraft `bson:"-" json:"drafts"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` } type UnitDraft struct { Id bson.ObjectID `bson:"id" json:"id"` Name string `bson:"name" json:"name"` Spec string `bson:"spec" json:"spec"` Delete bool `bson:"delete" json:"delete"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` New bool `bson:"new" json:"new"` } func (p *Pod) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { p.Name = utils.FilterName(p.Name) if p.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "missing_organization", Message: "Missing organization", } return } if p.UserDrafts == nil { p.UserDrafts = map[bson.ObjectID][]*UnitDraft{} } return } func (p *Pod) Json(usrId bson.ObjectID) { if p.UserDrafts != nil && p.UserDrafts[usrId] != nil { p.Drafts = p.UserDrafts[usrId] } else { p.Drafts = []*UnitDraft{} } } func (p *Pod) InitUnits(db *database.Database, units []*unit.UnitInput) ( errData *errortypes.ErrorData, err error) { newUnits := []*unit.Unit{} newSpecs := []*spec.Spec{} updateSpecs := []*spec.Spec{} for _, unitData := range units { if unitData.Delete { continue } unt := &unit.Unit{ Id: bson.NewObjectID(), Pod: p.Id, Organization: p.Organization, Name: unitData.Name, Spec: unitData.Spec, SpecIndex: 1, Deployments: []bson.ObjectID{}, } newSpec, updateSpec, ed, e := unt.Parse(db, true) if e != nil { err = e return } if ed != nil { errData = ed return } newUnits = append(newUnits, unt) if newSpec != nil { newSpecs = append(newSpecs, newSpec) } if updateSpec != nil { updateSpecs = append(updateSpecs, updateSpec) } } for _, unt := range newUnits { err = unt.Insert(db) if err != nil { return } } for _, spc := range newSpecs { err = spc.Insert(db) if err != nil { return } } for _, spc := range updateSpecs { err = spc.CommitData(db) if err != nil { return } } return } func (p *Pod) CommitFieldsUnits(db *database.Database, units []*unit.UnitInput, fields set.Set) ( errData *errortypes.ErrorData, err error) { curUnitsMap, err := unit.GetAllMap(db, &bson.M{ "pod": p.Id, }) if err != nil { return } unitsName := set.NewSet() parsedUnits := []*unit.Unit{} parsedUnitsNew := []*unit.Unit{} parsedUnitsDel := []*unit.Unit{} newSpecs := []*spec.Spec{} updateSpecs := []*spec.Spec{} for _, unitData := range units { curUnit := curUnitsMap[unitData.Id] if unitData.Delete { if curUnit == nil { continue } parsedUnitsDel = append(parsedUnitsDel, curUnit) } else if curUnit == nil { curUnit := curUnitsMap[unitData.Id] if curUnit != nil { continue } unt := &unit.Unit{ Id: bson.NewObjectID(), Pod: p.Id, Organization: p.Organization, Name: unitData.Name, Spec: unitData.Spec, SpecIndex: 1, Deployments: []bson.ObjectID{}, } newSpec, updateSpec, ed, e := unt.Parse(db, true) if e != nil { err = e return } if ed != nil { errData = ed return } if newSpec != nil { newSpecs = append(newSpecs, newSpec) } if updateSpec != nil { updateSpecs = append(updateSpecs, updateSpec) } if unitsName.Contains(unt.Name) { errData = &errortypes.ErrorData{ Error: "unit_duplicate_name", Message: "Duplicate unit name", } return } unitsName.Add(unt.Name) parsedUnitsNew = append(parsedUnitsNew, unt) } else { curUnit.Name = unitData.Name curUnit.Spec = unitData.Spec if !unitData.DeploySpec.IsZero() { deploySpec, e := spec.Get(db, unitData.DeploySpec) if e != nil || deploySpec.Unit != curUnit.Id { errData = &errortypes.ErrorData{ Error: "unit_deploy_spec_invalid", Message: "Invalid unit deployment commit", } return } curUnit.DeploySpec = deploySpec.Id } newSpec, updateSpec, ed, e := curUnit.Parse(db, false) if e != nil { err = e return } if ed != nil { errData = ed return } if newSpec != nil { newSpecs = append(newSpecs, newSpec) } if updateSpec != nil { updateSpecs = append(updateSpecs, updateSpec) } if unitsName.Contains(curUnit.Name) { errData = &errortypes.ErrorData{ Error: "unit_duplicate_name", Message: "Duplicate unit name", } return } unitsName.Add(curUnit.Name) parsedUnits = append(parsedUnits, curUnit) } } for _, unt := range parsedUnitsDel { deplys, e := deployment.GetAll(db, &bson.M{ "pod": p.Id, "unit": unt.Id, "organization": p.Organization, }) if e != nil { err = e return } if len(deplys) > 0 { errData = &errortypes.ErrorData{ Error: "unit_delete_active_deployments", Message: "Cannot delete unit with active deployments", } return } err = unit.RemoveOrg(db, p.Organization, unt.Id) if err != nil { return } } for _, unt := range parsedUnits { err = unt.CommitFields(db, set.NewSet( "name", "kind", "count", "spec", "last_spec", "deploy_spec", "hash", )) if err != nil { return } } for _, unt := range parsedUnitsNew { err = unt.Insert(db) if err != nil { return } } for _, spc := range newSpecs { err = spc.Insert(db) if err != nil { return } } for _, spc := range updateSpecs { err = spc.CommitData(db) if err != nil { return } } err = p.CommitFields(db, fields) if err != nil { return } return } func (p *Pod) Commit(db *database.Database) (err error) { coll := db.Pods() err = coll.Commit(p.Id, p) if err != nil { return } return } func (p *Pod) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Pods() err = coll.CommitFields(p.Id, p, fields) if err != nil { return } return } func (p *Pod) Insert(db *database.Database) (err error) { coll := db.Pods() _, err = coll.InsertOne(db, p) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: pod/utils.go ================================================ package pod import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, podId bson.ObjectID) ( pd *Pod, err error) { coll := db.Pods() pd = &Pod{} err = coll.FindOneId(podId, pd) if err != nil { return } return } func GetOrg(db *database.Database, orgId, pdId bson.ObjectID) ( pd *Pod, err error) { coll := db.Pods() pd = &Pod{} err = coll.FindOne(db, &bson.M{ "_id": pdId, "organization": orgId, }).Decode(pd) if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (pd *Pod, err error) { coll := db.Pods() pd = &Pod{} err = coll.FindOne(db, query).Decode(pd) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( pods []*Pod, err error) { coll := db.Pods() pods = []*Pod{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { pd := &Pod{} err = cursor.Decode(pd) if err != nil { err = database.ParseError(err) return } pods = append(pods, pd) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (pods []*Pod, count int64, err error) { coll := db.Pods() pods = []*Pod{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { pd := &Pod{} err = cursor.Decode(pd) if err != nil { err = database.ParseError(err) return } pods = append(pods, pd) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func UpdateDrafts(db *database.Database, podId, usrId bson.ObjectID, drafts []*UnitDraft) (err error) { for _, draft := range drafts { draft.Timestamp = time.Now() } coll := db.Pods() _, err = coll.UpdateOne(db, &bson.M{ "_id": podId, }, &bson.M{ "$set": &bson.M{ "drafts." + usrId.Hex(): drafts, }, }) if err != nil { err = database.ParseError(err) return } return nil } func UpdateDraftsOrg(db *database.Database, orgId, podId, usrId bson.ObjectID, drafts []*UnitDraft) (err error) { for _, draft := range drafts { draft.Timestamp = time.Now() } coll := db.Pods() _, err = coll.UpdateOne(db, &bson.M{ "_id": podId, "organization": orgId, }, &bson.M{ "$set": &bson.M{ "drafts." + usrId.Hex(): drafts, }, }) if err != nil { err = database.ParseError(err) return } return nil } func Remove(db *database.Database, podId bson.ObjectID) (err error) { coll := db.Pods() err = spec.RemoveAll(db, &bson.M{ "pod": podId, }) if err != nil { return } err = unit.RemoveAll(db, &bson.M{ "pod": podId, }) if err != nil { return } _, err = coll.DeleteOne(db, &bson.M{ "_id": podId, "delete_protection": false, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, podId bson.ObjectID) ( err error) { coll := db.Pods() err = spec.RemoveAll(db, &bson.M{ "pod": podId, "organization": orgId, }) if err != nil { return } err = unit.RemoveAll(db, &bson.M{ "pod": podId, "organization": orgId, }) if err != nil { return } _, err = coll.DeleteOne(db, &bson.M{ "_id": podId, "organization": orgId, "delete_protection": false, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, podIds []bson.ObjectID) ( err error) { coll := db.Pods() err = spec.RemoveAll(db, &bson.M{ "pod": &bson.M{ "$in": podIds, }, }) if err != nil { return } err = unit.RemoveAll(db, &bson.M{ "pod": &bson.M{ "$in": podIds, }, }) if err != nil { return } _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": podIds, }, "delete_protection": false, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, podIds []bson.ObjectID) (err error) { coll := db.Pods() err = spec.RemoveAll(db, &bson.M{ "pod": &bson.M{ "$in": podIds, }, "organization": orgId, }) if err != nil { return } err = unit.RemoveAll(db, &bson.M{ "pod": &bson.M{ "$in": podIds, }, "organization": orgId, }) if err != nil { return } _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": podIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: policy/constants.go ================================================ package policy const ( Optional = "optional" Required = "required" Disabled = "disabled" OperatingSystem = "operating_system" Browser = "browser" Location = "location" WhitelistNetworks = "whitelist_networks" BlacklistNetworks = "blacklist_networks" ) ================================================ FILE: policy/policy.go ================================================ package policy import ( "fmt" "net" "net/http" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/subscription" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/useragent" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Rule struct { Type string `bson:"type" json:"type"` Disable bool `bson:"disable" json:"disable"` Values []string `bson:"values" json:"values"` } type Policy struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Disabled bool `bson:"disabled" json:"disabled"` Roles []string `bson:"roles" json:"roles"` Rules map[string]*Rule `bson:"rules" json:"rules"` AdminSecondary bson.ObjectID `bson:"admin_secondary,omitempty" json:"admin_secondary"` UserSecondary bson.ObjectID `bson:"user_secondary,omitempty" json:"user_secondary"` AdminDeviceSecondary bool `bson:"admin_device_secondary" json:"admin_device_secondary"` UserDeviceSecondary bool `bson:"user_device_secondary" json:"user_device_secondary"` } func (p *Policy) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { p.Name = utils.FilterName(p.Name) if p.Roles == nil { p.Roles = []string{} } if p.Rules == nil { p.Rules = map[string]*Rule{} } for _, rule := range p.Rules { switch rule.Type { case OperatingSystem: break case Browser: break case Location: if !subscription.Sub.Active { errData = &errortypes.ErrorData{ Error: "location_subscription_required", Message: "Location policy requires subscription " + "for GeoIP service.", } return } break case WhitelistNetworks: break case BlacklistNetworks: break default: errData = &errortypes.ErrorData{ Error: "invalid_rule_type", Message: "Rule type is invalid", } return } } if !p.AdminSecondary.IsZero() && settings.Auth.GetSecondaryProvider(p.AdminSecondary) == nil { p.AdminSecondary = bson.NilObjectID } if !p.UserSecondary.IsZero() && settings.Auth.GetSecondaryProvider(p.UserSecondary) == nil { p.UserSecondary = bson.NilObjectID } hasWebAuthn := false nodes, err := node.GetAll(db) if err != nil { return } for _, nde := range nodes { if nde.WebauthnDomain != "" { hasWebAuthn = true break } } if (p.AdminDeviceSecondary || p.UserDeviceSecondary) && !hasWebAuthn { errData = &errortypes.ErrorData{ Error: "webauthn_domain_unavailable", Message: "At least one node must have a WebAuthn domain " + "configured to use WebAuthn device authentication", } return } return } func (p *Policy) ValidateUser(db *database.Database, usr *user.User, r *http.Request) (errData *errortypes.ErrorData, err error) { if p.Disabled { return } agnt, err := useragent.Parse(db, r) if err != nil { return } for _, rule := range p.Rules { switch rule.Type { case OperatingSystem: match := false for _, value := range rule.Values { if value == agnt.OperatingSystem { match = true break } } if !match { if rule.Disable { errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { return } } else { errData = &errortypes.ErrorData{ Error: "operating_system_policy", Message: "Operating system not permitted", } } return } break case Browser: match := false for _, value := range rule.Values { if value == agnt.Browser { match = true break } } if !match { if rule.Disable { errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { return } } else { errData = &errortypes.ErrorData{ Error: "browser_policy", Message: "Browser not permitted", } } return } break case Location: match := false regionKey := fmt.Sprintf("%s_%s", agnt.CountryCode, agnt.RegionCode) for _, value := range rule.Values { if value == agnt.CountryCode || value == regionKey { match = true break } } if !match { if rule.Disable { errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { return } } else { errData = &errortypes.ErrorData{ Error: "location_policy", Message: "Location not permitted", } } return } break case WhitelistNetworks: match := false clientIp := net.ParseIP(agnt.Ip) for _, value := range rule.Values { _, network, e := net.ParseCIDR(value) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "policy: Failed to parse network"), } logrus.WithFields(logrus.Fields{ "network": value, "error": err, }).Error("policy: Invalid whitelist network") err = nil continue } if network.Contains(clientIp) { match = true break } } if !match { if rule.Disable { errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { return } } else { errData = &errortypes.ErrorData{ Error: "whitelist_networks_policy", Message: "Network not permitted", } } return } break case BlacklistNetworks: match := false clientIp := net.ParseIP(agnt.Ip) for _, value := range rule.Values { _, network, e := net.ParseCIDR(value) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "policy: Failed to parse network"), } logrus.WithFields(logrus.Fields{ "network": value, "error": err, }).Error("policy: Invalid blacklist network") err = nil continue } if network.Contains(clientIp) { match = true break } } if match { if rule.Disable { errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { return } } else { errData = &errortypes.ErrorData{ Error: "blacklist_networks_policy", Message: "Network not permitted", } } return } break } } return } func (p *Policy) Commit(db *database.Database) (err error) { coll := db.Policies() err = coll.Commit(p.Id, p) if err != nil { return } return } func (p *Policy) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Policies() err = coll.CommitFields(p.Id, p, fields) if err != nil { return } return } func (p *Policy) Insert(db *database.Database) (err error) { coll := db.Policies() if !p.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("policy: Policy already exists"), } return } _, err = coll.InsertOne(db, p) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: policy/utils.go ================================================ package policy import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, policyId bson.ObjectID) ( polcy *Policy, err error) { coll := db.Policies() polcy = &Policy{} err = coll.FindOneId(policyId, polcy) if err != nil { return } return } func GetService(db *database.Database, podId bson.ObjectID) ( policies []*Policy, err error) { coll := db.Policies() policies = []*Policy{} cursor, err := coll.Find( db, &bson.M{ "pods": podId, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { polcy := &Policy{} err = cursor.Decode(polcy) if err != nil { err = database.ParseError(err) return } policies = append(policies, polcy) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetRoles(db *database.Database, roles []string) ( policies []*Policy, err error) { coll := db.Policies() policies = []*Policy{} if roles == nil { roles = []string{} } cursor, err := coll.Find( db, &bson.M{ "roles": &bson.M{ "$in": roles, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { polcy := &Policy{} err = cursor.Decode(polcy) if err != nil { err = database.ParseError(err) return } policies = append(policies, polcy) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database) (policies []*Policy, err error) { coll := db.Policies() policies = []*Policy{} cursor, err := coll.Find( db, &bson.M{}, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { polcy := &Policy{} err = cursor.Decode(polcy) if err != nil { err = database.ParseError(err) return } policies = append(policies, polcy) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (polcies []*Policy, count int64, err error) { coll := db.Policies() polcies = []*Policy{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { polcy := &Policy{} err = cursor.Decode(polcy) if err != nil { err = database.ParseError(err) return } polcies = append(polcies, polcy) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, policyId bson.ObjectID) (err error) { coll := db.Policies() _, err = coll.DeleteMany(db, &bson.M{ "_id": policyId, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMulti(db *database.Database, polcyIds []bson.ObjectID) ( err error) { coll := db.Policies() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": polcyIds, }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: pool/constants.go ================================================ package pool const ( Lvm = "lvm" Active = "active" ) ================================================ FILE: pool/pool.go ================================================ package pool import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Pool struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Zone bson.ObjectID `bson:"zone" json:"zone"` Type string `bson:"type" json:"type"` VgName string `bson:"vg_name" json:"vg_name"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Zone bson.ObjectID `bson:"zone" json:"zone"` } func (p *Pool) Json(nodeNames map[bson.ObjectID]string) { } func (p *Pool) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { p.Name = utils.FilterName(p.Name) if p.Datacenter.IsZero() { errData = &errortypes.ErrorData{ Error: "invalid_datacenter", Message: "Missing required datacenter", } return } if p.Zone.IsZero() { errData = &errortypes.ErrorData{ Error: "invalid_zone", Message: "Missing required zone", } return } return } func (p *Pool) Commit(db *database.Database) (err error) { coll := db.Pools() err = coll.Commit(p.Id, p) if err != nil { return } return } func (p *Pool) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Pools() err = coll.CommitFields(p.Id, p, fields) if err != nil { return } return } func (p *Pool) Insert(db *database.Database) (err error) { coll := db.Pools() _, err = coll.InsertOne(db, p) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: pool/utils.go ================================================ package pool import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, poolId bson.ObjectID) ( pl *Pool, err error) { coll := db.Pools() pl = &Pool{} err = coll.FindOneId(poolId, pl) if err != nil { return } return } func GetOne(db *database.Database, query *bson.M) (pl *Pool, err error) { coll := db.Pools() pl = &Pool{} err = coll.FindOne(db, query).Decode(pl) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( pools []*Pool, err error) { coll := db.Pools() pools = []*Pool{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Pool{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } pools = append(pools, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (pools []*Pool, count int64, err error) { coll := db.Pools() pools = []*Pool{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { pl := &Pool{} err = cursor.Decode(pl) if err != nil { err = database.ParseError(err) return } pools = append(pools, pl) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( pools []*Pool, err error) { coll := db.Pools() pools = []*Pool{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{ {"_id", 1}, {"name", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { pl := &Pool{} err = cursor.Decode(pl) if err != nil { err = database.ParseError(err) return } pools = append(pools, pl) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, poolId bson.ObjectID) (err error) { coll := db.Pools() _, err = coll.DeleteOne(db, &bson.M{ "_id": poolId, "delete_protection": false, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, poolIds []bson.ObjectID) ( err error) { coll := db.Pools() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": poolIds, }, "delete_protection": false, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: proxy/constants.go ================================================ package proxy const ( Online = 5 UnknownHigh = 4 UnknownMid = 3 UnknownLow = 2 Offline = 1 ) ================================================ FILE: proxy/domain.go ================================================ package proxy import ( "crypto/md5" "crypto/tls" "math/rand" "net/http" "strconv" "sync" "sync/atomic" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/balancer" ) type Domain struct { Hash []byte Requests *int32 RequestsPrev [5]int RequestsTotal int Retries *int32 RetriesPrev [5]int RetriesTotal int Lock sync.Mutex ProxyProto string ProxyPort int SkipVerify bool Balancer *balancer.Balancer Domain *balancer.Domain ClientAuthority *authority.Authority ClientCertificate *tls.Certificate OnlineWebFirst []*Handler UnknownHighWebFirst []*Handler UnknownMidWebFirst []*Handler UnknownLowWebFirst []*Handler OfflineWebFirst []*Handler OnlineWebSecond []*Handler UnknownHighWebSecond []*Handler UnknownMidWebSecond []*Handler UnknownLowWebSecond []*Handler OfflineWebSecond []*Handler OnlineWebThird []*Handler UnknownHighWebThird []*Handler UnknownMidWebThird []*Handler UnknownLowWebThird []*Handler OfflineWebThird []*Handler WebSocketConns set.Set WebSocketConnsLock sync.Mutex } func (d *Domain) CalculateHash() { h := md5.New() h.Write([]byte(d.ProxyProto)) h.Write([]byte(strconv.Itoa(d.ProxyPort))) h.Write([]byte(strconv.FormatBool(d.SkipVerify))) h.Write([]byte(d.Balancer.Id.Hex())) h.Write([]byte(d.Balancer.Name)) h.Write([]byte(d.Balancer.CheckPath)) h.Write([]byte(strconv.FormatBool(d.Balancer.WebSockets))) h.Write([]byte(d.Domain.Domain)) h.Write([]byte(d.Domain.Host)) if !d.Balancer.ClientAuthority.IsZero() { h.Write([]byte(d.Balancer.ClientAuthority.Hex())) } for _, backend := range d.Balancer.Backends { h.Write([]byte(backend.Protocol)) h.Write([]byte(backend.Hostname)) h.Write([]byte(strconv.Itoa(backend.Port))) } d.Hash = h.Sum(nil) } func (d *Domain) Init() { d.Lock.Lock() defer d.Lock.Unlock() if !d.Balancer.ClientAuthority.IsZero() { //clientAuthr, err := authority.Get(db, d.Balancer.ClientAuthority) //if err != nil { // if _, ok := err.(*database.NotFoundError); ok { // err = nil // // logrus.WithFields(logrus.Fields{ // "balancer_id": d.Balancer.Id.Hex(), // "client_authority_id": d.Balancer.ClientAuthority.Hex(), // }).Warn("proxy: Service client authority not found") // } else { // return // } //} // // var cert *tls.Certificate //if clientAuthr != nil { // cert, err = clientAuthr.CreateClientCertificate(db) // if err != nil { // return // } //} } unknownHighWebFirst := []*Handler{} unknownHighWebSecond := []*Handler{} unknownHighWebThird := []*Handler{} for i, backend := range d.Balancer.Backends { hand := NewHandler(i, UnknownHigh, d.ProxyProto, d.ProxyPort, d, backend, d.ResponseHandler, d.ErrorHandlerFirst) unknownHighWebFirst = append(unknownHighWebFirst, hand) hand = NewHandler(i, UnknownHigh, d.ProxyProto, d.ProxyPort, d, backend, d.ResponseHandler, d.ErrorHandlerSecond) unknownHighWebSecond = append(unknownHighWebSecond, hand) hand = NewHandler(i, UnknownHigh, d.ProxyProto, d.ProxyPort, d, backend, d.ResponseHandler, d.ErrorHandlerThird) unknownHighWebThird = append(unknownHighWebThird, hand) } d.OnlineWebFirst = []*Handler{} d.UnknownHighWebFirst = unknownHighWebFirst d.UnknownMidWebFirst = []*Handler{} d.UnknownLowWebFirst = []*Handler{} d.OfflineWebFirst = []*Handler{} d.OnlineWebSecond = []*Handler{} d.UnknownHighWebSecond = unknownHighWebSecond d.UnknownMidWebSecond = []*Handler{} d.UnknownLowWebSecond = []*Handler{} d.OfflineWebSecond = []*Handler{} d.OnlineWebThird = []*Handler{} d.UnknownHighWebThird = unknownHighWebThird d.UnknownMidWebThird = []*Handler{} d.UnknownLowWebThird = []*Handler{} d.OfflineWebThird = []*Handler{} d.WebSocketConns = set.NewSet() } func (d *Domain) ServeHTTPFirst(rw http.ResponseWriter, r *http.Request) { atomic.AddInt32(d.Requests, 1) onlineWebFirst := d.OnlineWebFirst l := len(onlineWebFirst) if l != 0 { onlineWebFirst[rand.Intn(l)].Serve(rw, r) return } unknownHighWebFirst := d.UnknownHighWebFirst l = len(unknownHighWebFirst) if l != 0 { unknownHighWebFirst[rand.Intn(l)].Serve(rw, r) return } unknownMidWebFirst := d.UnknownMidWebFirst l = len(unknownMidWebFirst) if l != 0 { unknownMidWebFirst[rand.Intn(l)].Serve(rw, r) return } unknownLowWebFirst := d.UnknownLowWebFirst l = len(unknownLowWebFirst) if l != 0 { unknownLowWebFirst[rand.Intn(l)].Serve(rw, r) return } offlineWebFirst := d.OfflineWebFirst l = len(offlineWebFirst) if l != 0 { offlineWebFirst[rand.Intn(l)].Serve(rw, r) return } rw.WriteHeader(http.StatusBadGateway) } func (d *Domain) ServeHTTPSecond(rw http.ResponseWriter, r *http.Request) { atomic.AddInt32(d.Retries, 1) onlineWebSecond := d.OnlineWebSecond l := len(onlineWebSecond) if l != 0 { onlineWebSecond[rand.Intn(l)].Serve(rw, r) return } unknownHighWebSecond := d.UnknownHighWebSecond l = len(unknownHighWebSecond) if l != 0 { unknownHighWebSecond[rand.Intn(l)].Serve(rw, r) return } unknownMidWebSecond := d.UnknownMidWebSecond l = len(unknownMidWebSecond) if l != 0 { unknownMidWebSecond[rand.Intn(l)].Serve(rw, r) return } unknownLowWebSecond := d.UnknownLowWebSecond l = len(unknownLowWebSecond) if l != 0 { unknownLowWebSecond[rand.Intn(l)].Serve(rw, r) return } offlineWebSecond := d.OfflineWebSecond l = len(offlineWebSecond) if l != 0 { offlineWebSecond[rand.Intn(l)].Serve(rw, r) return } rw.WriteHeader(http.StatusBadGateway) } func (d *Domain) ServeHTTPThird(rw http.ResponseWriter, r *http.Request) { atomic.AddInt32(d.Retries, 1) onlineWebThird := d.OnlineWebThird l := len(onlineWebThird) if l != 0 { onlineWebThird[rand.Intn(l)].Serve(rw, r) return } unknownHighWebThird := d.UnknownHighWebThird l = len(unknownHighWebThird) if l != 0 { unknownHighWebThird[rand.Intn(l)].Serve(rw, r) return } unknownMidWebThird := d.UnknownMidWebThird l = len(unknownMidWebThird) if l != 0 { unknownMidWebThird[rand.Intn(l)].Serve(rw, r) return } unknownLowWebThird := d.UnknownLowWebThird l = len(unknownLowWebThird) if l != 0 { unknownLowWebThird[rand.Intn(l)].Serve(rw, r) return } offlineWebThird := d.OfflineWebThird l = len(offlineWebThird) if l != 0 { offlineWebThird[rand.Intn(l)].Serve(rw, r) return } rw.WriteHeader(http.StatusBadGateway) } func (d *Domain) checkHandler(hand *Handler) { resp, err := hand.CheckClient.Get(hand.CheckUrl) if err != nil || resp.StatusCode < 200 || resp.StatusCode >= 300 { if hand.State != Offline { d.offlineHandler(hand) } } else { if hand.State != Online { d.upgradeHandler(hand) } } } func (d *Domain) Check() { d.Lock.Lock() defer d.Lock.Unlock() for _, hand := range d.OnlineWebFirst { go d.checkHandler(hand) } for _, hand := range d.UnknownHighWebFirst { go d.checkHandler(hand) } for _, hand := range d.UnknownMidWebFirst { go d.checkHandler(hand) } for _, hand := range d.UnknownLowWebFirst { go d.checkHandler(hand) } for _, hand := range d.OfflineWebFirst { go d.checkHandler(hand) } return } func (d *Domain) upgradeHandler(hand *Handler) { d.Lock.Lock() defer d.Lock.Unlock() index := hand.Index state := hand.State switch state { case Online: break case UnknownHigh: if time.Since(hand.LastOnlineState) > 5*time.Second { hand = d.UnknownHighWebFirst[index] d.UnknownHighWebFirst[index] = d.UnknownHighWebFirst[len(d.UnknownHighWebFirst)-1] d.UnknownHighWebFirst[len(d.UnknownHighWebFirst)-1] = nil d.UnknownHighWebFirst = d.UnknownHighWebFirst[:len(d.UnknownHighWebFirst)-1] for i, h := range d.UnknownHighWebFirst { h.Index = i } hand.Index = len(d.OnlineWebFirst) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebFirst = append(d.OnlineWebFirst, hand) hand = d.UnknownHighWebSecond[index] d.UnknownHighWebSecond[index] = d.UnknownHighWebSecond[len(d.UnknownHighWebSecond)-1] d.UnknownHighWebSecond[len(d.UnknownHighWebSecond)-1] = nil d.UnknownHighWebSecond = d.UnknownHighWebSecond[:len(d.UnknownHighWebSecond)-1] for i, h := range d.UnknownHighWebSecond { h.Index = i } hand.Index = len(d.OnlineWebSecond) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebSecond = append(d.OnlineWebSecond, hand) hand = d.UnknownHighWebThird[index] d.UnknownHighWebThird[index] = d.UnknownHighWebThird[len(d.UnknownHighWebThird)-1] d.UnknownHighWebThird[len(d.UnknownHighWebThird)-1] = nil d.UnknownHighWebThird = d.UnknownHighWebThird[:len(d.UnknownHighWebThird)-1] for i, h := range d.UnknownHighWebThird { h.Index = i } hand.Index = len(d.OnlineWebThird) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebThird = append(d.OnlineWebThird, hand) } break case UnknownMid: if time.Since(hand.LastOnlineState) > 5*time.Second { hand = d.UnknownMidWebFirst[index] d.UnknownMidWebFirst[index] = d.UnknownMidWebFirst[len(d.UnknownMidWebFirst)-1] d.UnknownMidWebFirst[len(d.UnknownMidWebFirst)-1] = nil d.UnknownMidWebFirst = d.UnknownMidWebFirst[:len(d.UnknownMidWebFirst)-1] for i, h := range d.UnknownMidWebFirst { h.Index = i } hand.Index = len(d.OnlineWebFirst) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebFirst = append(d.OnlineWebFirst, hand) hand = d.UnknownMidWebSecond[index] d.UnknownMidWebSecond[index] = d.UnknownMidWebSecond[len(d.UnknownMidWebSecond)-1] d.UnknownMidWebSecond[len(d.UnknownMidWebSecond)-1] = nil d.UnknownMidWebSecond = d.UnknownMidWebSecond[:len(d.UnknownMidWebSecond)-1] for i, h := range d.UnknownMidWebSecond { h.Index = i } hand.Index = len(d.OnlineWebSecond) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebSecond = append(d.OnlineWebSecond, hand) hand = d.UnknownMidWebThird[index] d.UnknownMidWebThird[index] = d.UnknownMidWebThird[len(d.UnknownMidWebThird)-1] d.UnknownMidWebThird[len(d.UnknownMidWebThird)-1] = nil d.UnknownMidWebThird = d.UnknownMidWebThird[:len(d.UnknownMidWebThird)-1] for i, h := range d.UnknownMidWebThird { h.Index = i } hand.Index = len(d.OnlineWebThird) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebThird = append(d.OnlineWebThird, hand) } break case UnknownLow: if time.Since(hand.LastOnlineState) > 5*time.Second { hand = d.UnknownLowWebFirst[index] d.UnknownLowWebFirst[index] = d.UnknownLowWebFirst[len(d.UnknownLowWebFirst)-1] d.UnknownLowWebFirst[len(d.UnknownLowWebFirst)-1] = nil d.UnknownLowWebFirst = d.UnknownLowWebFirst[:len(d.UnknownLowWebFirst)-1] for i, h := range d.UnknownLowWebFirst { h.Index = i } hand.Index = len(d.OnlineWebFirst) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebFirst = append(d.OnlineWebFirst, hand) hand = d.UnknownLowWebSecond[index] d.UnknownLowWebSecond[index] = d.UnknownLowWebSecond[len(d.UnknownLowWebSecond)-1] d.UnknownLowWebSecond[len(d.UnknownLowWebSecond)-1] = nil d.UnknownLowWebSecond = d.UnknownLowWebSecond[:len(d.UnknownLowWebSecond)-1] for i, h := range d.UnknownLowWebSecond { h.Index = i } hand.Index = len(d.OnlineWebSecond) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebSecond = append(d.OnlineWebSecond, hand) hand = d.UnknownLowWebThird[index] d.UnknownLowWebThird[index] = d.UnknownLowWebThird[len(d.UnknownLowWebThird)-1] d.UnknownLowWebThird[len(d.UnknownLowWebThird)-1] = nil d.UnknownLowWebThird = d.UnknownLowWebThird[:len(d.UnknownLowWebThird)-1] for i, h := range d.UnknownLowWebThird { h.Index = i } hand.Index = len(d.OnlineWebThird) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebThird = append(d.OnlineWebThird, hand) } break case Offline: if time.Since(hand.LastOnlineState) > 5*time.Second { hand = d.OfflineWebFirst[index] d.OfflineWebFirst[index] = d.OfflineWebFirst[len(d.OfflineWebFirst)-1] d.OfflineWebFirst[len(d.OfflineWebFirst)-1] = nil d.OfflineWebFirst = d.OfflineWebFirst[:len(d.OfflineWebFirst)-1] for i, h := range d.OfflineWebFirst { h.Index = i } hand.Index = len(d.OnlineWebFirst) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebFirst = append(d.OnlineWebFirst, hand) hand = d.OfflineWebSecond[index] d.OfflineWebSecond[index] = d.OfflineWebSecond[len(d.OfflineWebSecond)-1] d.OfflineWebSecond[len(d.OfflineWebSecond)-1] = nil d.OfflineWebSecond = d.OfflineWebSecond[:len(d.OfflineWebSecond)-1] for i, h := range d.OfflineWebSecond { h.Index = i } hand.Index = len(d.OnlineWebSecond) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebSecond = append(d.OnlineWebSecond, hand) hand = d.OfflineWebThird[index] d.OfflineWebThird[index] = d.OfflineWebThird[len(d.OfflineWebThird)-1] d.OfflineWebThird[len(d.OfflineWebThird)-1] = nil d.OfflineWebThird = d.OfflineWebThird[:len(d.OfflineWebThird)-1] for i, h := range d.OfflineWebThird { h.Index = i } hand.Index = len(d.OnlineWebThird) hand.State = Online hand.LastOnlineState = time.Now() d.OnlineWebThird = append(d.OnlineWebThird, hand) } break } } func (d *Domain) downgradeHandler(hand *Handler) { d.Lock.Lock() defer d.Lock.Unlock() index := hand.Index state := hand.State switch state { case Online: hand = d.OnlineWebFirst[index] d.OnlineWebFirst[index] = d.OnlineWebFirst[len(d.OnlineWebFirst)-1] d.OnlineWebFirst[len(d.OnlineWebFirst)-1] = nil d.OnlineWebFirst = d.OnlineWebFirst[:len(d.OnlineWebFirst)-1] for i, h := range d.OnlineWebFirst { h.Index = i } hand.Index = len(d.UnknownMidWebFirst) hand.State = UnknownMid hand.LastState = time.Now() d.UnknownMidWebFirst = append(d.UnknownMidWebFirst, hand) hand = d.OnlineWebSecond[index] d.OnlineWebSecond[index] = d.OnlineWebSecond[len(d.OnlineWebSecond)-1] d.OnlineWebSecond[len(d.OnlineWebSecond)-1] = nil d.OnlineWebSecond = d.OnlineWebSecond[:len(d.OnlineWebSecond)-1] for i, h := range d.OnlineWebSecond { h.Index = i } hand.Index = len(d.UnknownMidWebSecond) hand.State = UnknownMid hand.LastState = time.Now() d.UnknownMidWebSecond = append(d.UnknownMidWebSecond, hand) hand = d.OnlineWebThird[index] d.OnlineWebThird[index] = d.OnlineWebThird[len(d.OnlineWebThird)-1] d.OnlineWebThird[len(d.OnlineWebThird)-1] = nil d.OnlineWebThird = d.OnlineWebThird[:len(d.OnlineWebThird)-1] for i, h := range d.OnlineWebThird { h.Index = i } hand.Index = len(d.UnknownMidWebThird) hand.State = UnknownMid hand.LastState = time.Now() d.UnknownMidWebThird = append(d.UnknownMidWebThird, hand) break case UnknownHigh: hand = d.UnknownHighWebFirst[index] d.UnknownHighWebFirst[index] = d.UnknownHighWebFirst[len(d.UnknownHighWebFirst)-1] d.UnknownHighWebFirst[len(d.UnknownHighWebFirst)-1] = nil d.UnknownHighWebFirst = d.UnknownHighWebFirst[:len(d.UnknownHighWebFirst)-1] for i, h := range d.UnknownHighWebFirst { h.Index = i } hand.Index = len(d.UnknownMidWebFirst) hand.State = UnknownMid hand.LastState = time.Now() d.UnknownMidWebFirst = append(d.UnknownMidWebFirst, hand) hand = d.UnknownHighWebSecond[index] d.UnknownHighWebSecond[index] = d.UnknownHighWebSecond[len(d.UnknownHighWebSecond)-1] d.UnknownHighWebSecond[len(d.UnknownHighWebSecond)-1] = nil d.UnknownHighWebSecond = d.UnknownHighWebSecond[:len(d.UnknownHighWebSecond)-1] for i, h := range d.UnknownHighWebSecond { h.Index = i } hand.Index = len(d.UnknownMidWebSecond) hand.State = UnknownMid hand.LastState = time.Now() d.UnknownMidWebSecond = append(d.UnknownMidWebSecond, hand) hand = d.UnknownHighWebThird[index] d.UnknownHighWebThird[index] = d.UnknownHighWebThird[len(d.UnknownHighWebThird)-1] d.UnknownHighWebThird[len(d.UnknownHighWebThird)-1] = nil d.UnknownHighWebThird = d.UnknownHighWebThird[:len(d.UnknownHighWebThird)-1] for i, h := range d.UnknownHighWebThird { h.Index = i } hand.Index = len(d.UnknownMidWebThird) hand.State = UnknownMid hand.LastState = time.Now() d.UnknownMidWebThird = append(d.UnknownMidWebThird, hand) break case UnknownMid: if time.Since(hand.LastState) > 1*time.Second { hand = d.UnknownMidWebFirst[index] d.UnknownMidWebFirst[index] = d.UnknownMidWebFirst[len(d.UnknownMidWebFirst)-1] d.UnknownMidWebFirst[len(d.UnknownMidWebFirst)-1] = nil d.UnknownMidWebFirst = d.UnknownMidWebFirst[:len(d.UnknownMidWebFirst)-1] for i, h := range d.UnknownMidWebFirst { h.Index = i } hand.Index = len(d.UnknownLowWebFirst) hand.State = UnknownLow hand.LastState = time.Now() d.UnknownLowWebFirst = append(d.UnknownLowWebFirst, hand) hand = d.UnknownMidWebSecond[index] d.UnknownMidWebSecond[index] = d.UnknownMidWebSecond[len(d.UnknownMidWebSecond)-1] d.UnknownMidWebSecond[len(d.UnknownMidWebSecond)-1] = nil d.UnknownMidWebSecond = d.UnknownMidWebSecond[:len(d.UnknownMidWebSecond)-1] for i, h := range d.UnknownMidWebSecond { h.Index = i } hand.Index = len(d.UnknownLowWebSecond) hand.State = UnknownLow hand.LastState = time.Now() d.UnknownLowWebSecond = append(d.UnknownLowWebSecond, hand) hand = d.UnknownMidWebThird[index] d.UnknownMidWebThird[index] = d.UnknownMidWebThird[len(d.UnknownMidWebThird)-1] d.UnknownMidWebThird[len(d.UnknownMidWebThird)-1] = nil d.UnknownMidWebThird = d.UnknownMidWebThird[:len(d.UnknownMidWebThird)-1] for i, h := range d.UnknownMidWebThird { h.Index = i } hand.Index = len(d.UnknownLowWebThird) hand.State = UnknownLow hand.LastState = time.Now() d.UnknownLowWebThird = append(d.UnknownLowWebThird, hand) } break case UnknownLow: if time.Since(hand.LastState) > 2*time.Second { hand = d.UnknownLowWebFirst[index] d.UnknownLowWebFirst[index] = d.UnknownLowWebFirst[len(d.UnknownLowWebFirst)-1] d.UnknownLowWebFirst[len(d.UnknownLowWebFirst)-1] = nil d.UnknownLowWebFirst = d.UnknownLowWebFirst[:len(d.UnknownLowWebFirst)-1] for i, h := range d.UnknownLowWebFirst { h.Index = i } hand.Index = len(d.OfflineWebFirst) hand.State = Offline hand.LastState = time.Now() d.OfflineWebFirst = append(d.OfflineWebFirst, hand) hand = d.UnknownLowWebSecond[index] d.UnknownLowWebSecond[index] = d.UnknownLowWebSecond[len(d.UnknownLowWebSecond)-1] d.UnknownLowWebSecond[len(d.UnknownLowWebSecond)-1] = nil d.UnknownLowWebSecond = d.UnknownLowWebSecond[:len(d.UnknownLowWebSecond)-1] for i, h := range d.UnknownLowWebSecond { h.Index = i } hand.Index = len(d.OfflineWebSecond) hand.State = Offline hand.LastState = time.Now() d.OfflineWebSecond = append(d.OfflineWebSecond, hand) hand = d.UnknownLowWebThird[index] d.UnknownLowWebThird[index] = d.UnknownLowWebThird[len(d.UnknownLowWebThird)-1] d.UnknownLowWebThird[len(d.UnknownLowWebThird)-1] = nil d.UnknownLowWebThird = d.UnknownLowWebThird[:len(d.UnknownLowWebThird)-1] for i, h := range d.UnknownLowWebThird { h.Index = i } hand.Index = len(d.OfflineWebThird) hand.State = Offline hand.LastState = time.Now() d.OfflineWebThird = append(d.OfflineWebThird, hand) } break case Offline: break } } func (d *Domain) offlineHandler(hand *Handler) { d.Lock.Lock() defer d.Lock.Unlock() index := hand.Index state := hand.State switch state { case Online: hand = d.OnlineWebFirst[index] d.OnlineWebFirst[index] = d.OnlineWebFirst[len(d.OnlineWebFirst)-1] d.OnlineWebFirst[len(d.OnlineWebFirst)-1] = nil d.OnlineWebFirst = d.OnlineWebFirst[:len(d.OnlineWebFirst)-1] for i, h := range d.OnlineWebFirst { h.Index = i } hand.Index = len(d.UnknownMidWebFirst) hand.State = Offline hand.LastState = time.Now() d.OfflineWebFirst = append(d.OfflineWebFirst, hand) hand = d.OnlineWebSecond[index] d.OnlineWebSecond[index] = d.OnlineWebSecond[len(d.OnlineWebSecond)-1] d.OnlineWebSecond[len(d.OnlineWebSecond)-1] = nil d.OnlineWebSecond = d.OnlineWebSecond[:len(d.OnlineWebSecond)-1] for i, h := range d.OnlineWebSecond { h.Index = i } hand.Index = len(d.OfflineWebSecond) hand.State = Offline hand.LastState = time.Now() d.OfflineWebSecond = append(d.OfflineWebSecond, hand) hand = d.OnlineWebThird[index] d.OnlineWebThird[index] = d.OnlineWebThird[len(d.OnlineWebThird)-1] d.OnlineWebThird[len(d.OnlineWebThird)-1] = nil d.OnlineWebThird = d.OnlineWebThird[:len(d.OnlineWebThird)-1] for i, h := range d.OnlineWebThird { h.Index = i } hand.Index = len(d.OfflineWebThird) hand.State = Offline hand.LastState = time.Now() d.OfflineWebThird = append(d.OfflineWebThird, hand) break case UnknownHigh: hand = d.UnknownHighWebFirst[index] d.UnknownHighWebFirst[index] = d.UnknownHighWebFirst[len(d.UnknownHighWebFirst)-1] d.UnknownHighWebFirst[len(d.UnknownHighWebFirst)-1] = nil d.UnknownHighWebFirst = d.UnknownHighWebFirst[:len(d.UnknownHighWebFirst)-1] for i, h := range d.UnknownHighWebFirst { h.Index = i } hand.Index = len(d.UnknownMidWebFirst) hand.State = Offline hand.LastState = time.Now() d.OfflineWebFirst = append(d.OfflineWebFirst, hand) hand = d.UnknownHighWebSecond[index] d.UnknownHighWebSecond[index] = d.UnknownHighWebSecond[len(d.UnknownHighWebSecond)-1] d.UnknownHighWebSecond[len(d.UnknownHighWebSecond)-1] = nil d.UnknownHighWebSecond = d.UnknownHighWebSecond[:len(d.UnknownHighWebSecond)-1] for i, h := range d.UnknownHighWebSecond { h.Index = i } hand.Index = len(d.OfflineWebSecond) hand.State = Offline hand.LastState = time.Now() d.OfflineWebSecond = append(d.OfflineWebSecond, hand) hand = d.UnknownHighWebThird[index] d.UnknownHighWebThird[index] = d.UnknownHighWebThird[len(d.UnknownHighWebThird)-1] d.UnknownHighWebThird[len(d.UnknownHighWebThird)-1] = nil d.UnknownHighWebThird = d.UnknownHighWebThird[:len(d.UnknownHighWebThird)-1] for i, h := range d.UnknownHighWebThird { h.Index = i } hand.Index = len(d.OfflineWebThird) hand.State = Offline hand.LastState = time.Now() d.OfflineWebThird = append(d.OfflineWebThird, hand) break case UnknownMid: if time.Since(hand.LastState) > 1*time.Second { hand = d.UnknownMidWebFirst[index] d.UnknownMidWebFirst[index] = d.UnknownMidWebFirst[len(d.UnknownMidWebFirst)-1] d.UnknownMidWebFirst[len(d.UnknownMidWebFirst)-1] = nil d.UnknownMidWebFirst = d.UnknownMidWebFirst[:len(d.UnknownMidWebFirst)-1] for i, h := range d.UnknownMidWebFirst { h.Index = i } hand.Index = len(d.UnknownLowWebFirst) hand.State = Offline hand.LastState = time.Now() d.OfflineWebFirst = append(d.OfflineWebFirst, hand) hand = d.UnknownMidWebSecond[index] d.UnknownMidWebSecond[index] = d.UnknownMidWebSecond[len(d.UnknownMidWebSecond)-1] d.UnknownMidWebSecond[len(d.UnknownMidWebSecond)-1] = nil d.UnknownMidWebSecond = d.UnknownMidWebSecond[:len(d.UnknownMidWebSecond)-1] for i, h := range d.UnknownMidWebSecond { h.Index = i } hand.Index = len(d.OfflineWebSecond) hand.State = Offline hand.LastState = time.Now() d.OfflineWebSecond = append(d.OfflineWebSecond, hand) hand = d.UnknownMidWebThird[index] d.UnknownMidWebThird[index] = d.UnknownMidWebThird[len(d.UnknownMidWebThird)-1] d.UnknownMidWebThird[len(d.UnknownMidWebThird)-1] = nil d.UnknownMidWebThird = d.UnknownMidWebThird[:len(d.UnknownMidWebThird)-1] for i, h := range d.UnknownMidWebThird { h.Index = i } hand.Index = len(d.OfflineWebThird) hand.State = Offline hand.LastState = time.Now() d.OfflineWebThird = append(d.OfflineWebThird, hand) } break case UnknownLow: if time.Since(hand.LastState) > 2*time.Second { hand = d.UnknownLowWebFirst[index] d.UnknownLowWebFirst[index] = d.UnknownLowWebFirst[len(d.UnknownLowWebFirst)-1] d.UnknownLowWebFirst[len(d.UnknownLowWebFirst)-1] = nil d.UnknownLowWebFirst = d.UnknownLowWebFirst[:len(d.UnknownLowWebFirst)-1] for i, h := range d.UnknownLowWebFirst { h.Index = i } hand.Index = len(d.OfflineWebFirst) hand.State = Offline hand.LastState = time.Now() d.OfflineWebFirst = append(d.OfflineWebFirst, hand) hand = d.UnknownLowWebSecond[index] d.UnknownLowWebSecond[index] = d.UnknownLowWebSecond[len(d.UnknownLowWebSecond)-1] d.UnknownLowWebSecond[len(d.UnknownLowWebSecond)-1] = nil d.UnknownLowWebSecond = d.UnknownLowWebSecond[:len(d.UnknownLowWebSecond)-1] for i, h := range d.UnknownLowWebSecond { h.Index = i } hand.Index = len(d.OfflineWebSecond) hand.State = Offline hand.LastState = time.Now() d.OfflineWebSecond = append(d.OfflineWebSecond, hand) hand = d.UnknownLowWebThird[index] d.UnknownLowWebThird[index] = d.UnknownLowWebThird[len(d.UnknownLowWebThird)-1] d.UnknownLowWebThird[len(d.UnknownLowWebThird)-1] = nil d.UnknownLowWebThird = d.UnknownLowWebThird[:len(d.UnknownLowWebThird)-1] for i, h := range d.UnknownLowWebThird { h.Index = i } hand.Index = len(d.OfflineWebThird) hand.State = Offline hand.LastState = time.Now() d.OfflineWebThird = append(d.OfflineWebThird, hand) } break case Offline: break } } func (d *Domain) ResponseHandler(hand *Handler, resp *http.Response) error { if hand.State != Online && resp.StatusCode < 500 { d.upgradeHandler(hand) } return nil } func (d *Domain) ErrorHandlerFirst(hand *Handler, rw http.ResponseWriter, r *http.Request, err error) { if _, ok := err.(*WebSocketBlock); ok { return } d.downgradeHandler(hand) d.ServeHTTPSecond(rw, r) } func (d *Domain) ErrorHandlerSecond(hand *Handler, rw http.ResponseWriter, r *http.Request, err error) { if _, ok := err.(*WebSocketBlock); ok { return } d.downgradeHandler(hand) d.ServeHTTPThird(rw, r) } func (d *Domain) ErrorHandlerThird(hand *Handler, rw http.ResponseWriter, r *http.Request, err error) { if _, ok := err.(*WebSocketBlock); ok { return } d.downgradeHandler(hand) rw.WriteHeader(http.StatusBadGateway) } ================================================ FILE: proxy/errortypes.go ================================================ package proxy import "github.com/dropbox/godropbox/errors" type WebSocketBlock struct { errors.DropboxError } ================================================ FILE: proxy/proxy.go ================================================ package proxy import ( "bytes" "net/http" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Proxy struct { Domains map[string]*Domain lock sync.Mutex } type balancerState struct { Balancer *balancer.Balancer State *balancer.State } func (p *Proxy) ServeHTTP(hst string, rw http.ResponseWriter, r *http.Request) { domain := p.Domains[hst] if domain == nil { utils.WriteStatus(rw, 404) return } domain.ServeHTTPFirst(rw, r) } func (p *Proxy) Update(db *database.Database, balncs []*balancer.Balancer) ( err error) { domains := map[string]*Domain{} domainsName := set.NewSet() remDomains := []*Domain{} states := []*balancerState{} proxyProto := node.Self.Protocol proxyPort := node.Self.Port p.lock.Lock() for _, balnc := range balncs { if !balnc.State { continue } onlineWeb := set.NewSet() unknownHighWeb := set.NewSet() unknownMidWeb := set.NewSet() unknownLowWeb := set.NewSet() offlineWeb := set.NewSet() state := &balancer.State{ Timestamp: time.Now(), Online: []string{}, UnknownHigh: []string{}, UnknownMid: []string{}, UnknownLow: []string{}, Offline: []string{}, } for _, domain := range balnc.Domains { if domains[domain.Domain] != nil { conflictDomain := domains[domain.Domain] logrus.WithFields(logrus.Fields{ "first_balancer_id": conflictDomain.Balancer.Id.Hex(), "first_balancer_name": conflictDomain.Balancer.Name, "second_balancer_id": balnc.Id.Hex(), "second_balancer_name": balnc.Name, "conflict_domain": domain.Domain, }).Error("proxy: Balancer domain conflict") continue } domainsName.Add(domain.Domain) proxyDomain := &Domain{ SkipVerify: settings.Router.SkipVerify, ProxyProto: proxyProto, ProxyPort: proxyPort, Balancer: balnc, Domain: domain, Requests: new(int32), Retries: new(int32), } proxyDomain.CalculateHash() curDomain := p.Domains[domain.Domain] if curDomain != nil && curDomain.Balancer.Id == balnc.Id { state.Requests += curDomain.RequestsTotal state.Retries += curDomain.RetriesTotal state.WebSockets += curDomain.WebSocketConns.Len() curDomain.Lock.Lock() for _, hand := range curDomain.OnlineWebFirst { onlineWeb.Add(hand.Key) } for _, hand := range curDomain.UnknownHighWebFirst { unknownHighWeb.Add(hand.Key) } for _, hand := range curDomain.UnknownMidWebFirst { unknownMidWeb.Add(hand.Key) } for _, hand := range curDomain.UnknownLowWebFirst { unknownLowWeb.Add(hand.Key) } for _, hand := range curDomain.OfflineWebFirst { offlineWeb.Add(hand.Key) } if bytes.Equal(curDomain.Hash, proxyDomain.Hash) { domains[domain.Domain] = curDomain curDomain.Lock.Unlock() continue } else { proxyDomain.Requests = curDomain.Requests proxyDomain.RequestsPrev = curDomain.RequestsPrev proxyDomain.RequestsTotal = curDomain.RequestsTotal proxyDomain.Retries = curDomain.Retries proxyDomain.RetriesPrev = curDomain.RetriesPrev proxyDomain.RetriesTotal = curDomain.RetriesTotal curDomain.Lock.Unlock() remDomains = append(remDomains, curDomain) } } proxyDomain.Init() domains[domain.Domain] = proxyDomain } recorded := set.NewSet() for keyInf := range offlineWeb.Iter() { if recorded.Contains(keyInf) { continue } recorded.Add(keyInf) state.Offline = append(state.Offline, keyInf.(string)) } for keyInf := range unknownLowWeb.Iter() { if recorded.Contains(keyInf) { continue } recorded.Add(keyInf) state.UnknownLow = append(state.UnknownLow, keyInf.(string)) } for keyInf := range unknownMidWeb.Iter() { if recorded.Contains(keyInf) { continue } recorded.Add(keyInf) state.UnknownMid = append(state.UnknownMid, keyInf.(string)) } for keyInf := range unknownHighWeb.Iter() { if recorded.Contains(keyInf) { continue } recorded.Add(keyInf) state.UnknownHigh = append(state.UnknownHigh, keyInf.(string)) } for keyInf := range onlineWeb.Iter() { if recorded.Contains(keyInf) { continue } recorded.Add(keyInf) state.Online = append(state.Online, keyInf.(string)) } states = append(states, &balancerState{ Balancer: balnc, State: state, }) } curDomains := p.Domains for name, domain := range curDomains { if !domainsName.Contains(name) { remDomains = append(remDomains, domain) } } p.Domains = domains p.lock.Unlock() for _, domain := range remDomains { domain.WebSocketConnsLock.Lock() for socketInf := range domain.WebSocketConns.Iter() { func() { socket := socketInf.(*webSocketConn) socket.Close() }() } domain.WebSocketConns = set.NewSet() domain.WebSocketConnsLock.Unlock() } for _, balncState := range states { err = balncState.Balancer.CommitState(db, balncState.State) if err != nil { return } } return } func (p *Proxy) syncCount() { p.lock.Lock() defer p.lock.Unlock() domains := p.Domains for _, dom := range domains { req := dom.Requests dom.Requests = new(int32) reqPrev := dom.RequestsPrev reqTotal := reqPrev[0] + reqPrev[1] + reqPrev[2] + reqPrev[3] + reqPrev[4] reqPrev[0] = reqPrev[1] reqPrev[1] = reqPrev[2] reqPrev[2] = reqPrev[3] reqPrev[3] = reqPrev[4] reqPrev[4] = int(*req) reqTotal += int(*req) dom.RequestsPrev = reqPrev dom.RequestsTotal = reqTotal ret := dom.Retries dom.Retries = new(int32) retPrev := dom.RetriesPrev retTotal := retPrev[0] + retPrev[1] + retPrev[2] + retPrev[3] + retPrev[4] retPrev[0] = retPrev[1] retPrev[1] = retPrev[2] retPrev[2] = retPrev[3] retPrev[3] = retPrev[4] retPrev[4] = int(*ret) retTotal += int(*ret) dom.RetriesPrev = retPrev dom.RetriesTotal = retTotal } } func (p *Proxy) runCounter() { for { time.Sleep(10 * time.Second) p.syncCount() } } func (p *Proxy) healthCheck() { p.lock.Lock() defer p.lock.Unlock() domains := p.Domains for _, dom := range domains { dom.Check() } } func (p *Proxy) runHealthCheck() { for { time.Sleep(5 * time.Second) p.healthCheck() } } func (p *Proxy) Init() { p.Domains = map[string]*Domain{} go p.runCounter() go p.runHealthCheck() } ================================================ FILE: proxy/resolver.go ================================================ package proxy import ( "context" "net" "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/settings" ) var ( ResolverLock sync.RWMutex IpDatabase = set.NewSet() ResolverCache = map[string]*Remote{} HostNetwork *net.IPNet NodePortNetwork *net.IPNet ) type Remote struct { Timestamp time.Time Remote net.IP } func ResolverRefresh(db *database.Database) (err error) { coll := db.Instances() ipDatabase := set.NewSet() ttl := time.Duration(settings.Router.ProxyResolverTtl) * time.Second cursor, err := coll.Find( db, &bson.M{}, options.Find(). SetProjection(&bson.M{ "public_ips": 1, "public_ips6": 1, "cloud_private_ips": 1, "cloud_public_ips": 1, "cloud_public_ips6": 1, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { inst := &instance.Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } for _, ipStr := range inst.PublicIps { ip := net.ParseIP(ipStr) if ip != nil { ipDatabase.Add(ip.String()) } } for _, ipStr := range inst.PublicIps6 { ip := net.ParseIP(ipStr) if ip != nil { ipDatabase.Add(ip.String()) } } for _, ipStr := range inst.CloudPublicIps { ip := net.ParseIP(ipStr) if ip != nil { ipDatabase.Add(ip.String()) } } for _, ipStr := range inst.CloudPublicIps6 { ip := net.ParseIP(ipStr) if ip != nil { ipDatabase.Add(ip.String()) } } for _, ipStr := range inst.CloudPrivateIps { ip := net.ParseIP(ipStr) if ip != nil { ipDatabase.Add(ip.String()) } } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } _, hostNetwork, err := net.ParseCIDR( settings.Hypervisor.HostNetwork) if err != nil { return } _, nodePortNetwork, err := net.ParseCIDR( settings.Hypervisor.NodePortNetwork) if err != nil { return } now := time.Now() ResolverLock.Lock() for hostname, cached := range ResolverCache { if now.Sub(cached.Timestamp) > ttl { delete(ResolverCache, hostname) } } IpDatabase = ipDatabase HostNetwork = hostNetwork NodePortNetwork = nodePortNetwork ResolverLock.Unlock() return } func ResolverValidate(ip net.IP) bool { if IpDatabase.Contains(ip.String()) { return true } if HostNetwork.Contains(ip) { return true } if NodePortNetwork.Contains(ip) { return true } return false } func Resolve(hostname string) (remote net.IP, err error) { ResolverLock.RLock() cached, ok := ResolverCache[hostname] if ok { ResolverLock.RUnlock() remote = cached.Remote return } ResolverLock.RUnlock() ip := net.ParseIP(hostname) if ip != nil { ResolverLock.RLock() contains := ResolverValidate(ip) ResolverLock.RUnlock() if !contains { err = &errortypes.RequestError{ errors.New("proxy: Balancer resolved address not in database"), } return } remote = ip } else { ips, e := net.LookupIP(hostname) if e != nil { err = &errortypes.RequestError{ errors.Wrap(e, "proxy: Balancer resolve error"), } return } ResolverLock.RLock() for _, ip := range ips { if ResolverValidate(ip) { remote = ip break } } ResolverLock.RUnlock() if remote == nil { err = &errortypes.RequestError{ errors.New("proxy: Balancer resolved address not in database"), } return } } ResolverLock.Lock() ResolverCache[hostname] = &Remote{ Timestamp: time.Now(), Remote: remote, } ResolverLock.Unlock() return } type StaticDialer struct { dialer *net.Dialer } func (d *StaticDialer) DialContext(ctx context.Context, network, addr string) ( conn net.Conn, err error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } remote, err := Resolve(host) if err != nil { return } conn, err = d.dialer.DialContext( ctx, network, net.JoinHostPort(remote.String(), port)) if err == nil { return } return } func NewStaticDialer(dialer *net.Dialer) *StaticDialer { return &StaticDialer{ dialer: dialer, } } func init() { _, HostNetwork, _ = net.ParseCIDR("0.0.0.0/32") _, NodePortNetwork, _ = net.ParseCIDR("0.0.0.0/32") } ================================================ FILE: proxy/reverse.go ================================================ package proxy import ( "crypto/tls" "fmt" "log" "net" "net/http" "net/http/httputil" "net/url" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/gorilla/websocket" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/logger" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Handler struct { Key string Index int State int Domain *Domain CheckUrl string CheckClient *http.Client LastState time.Time LastOnlineState time.Time BackendHost string BackendProto string BackendProtoWs string RequestHost string ForwardedProto string ForwardedPort string TlsConfig *tls.Config Dialer *StaticDialer WebSockets bool WebSocketsUpgrader *websocket.Upgrader ErrorHandler ErrorHandler *httputil.ReverseProxy } func (h *Handler) ServeWS(rw http.ResponseWriter, r *http.Request) { header := utils.CloneHeader(r.Header) u := &url.URL{} *u = *r.URL u.Scheme = h.BackendProtoWs u.Host = h.BackendHost if h.RequestHost != "" { r.Host = h.RequestHost } header.Set("X-Forwarded-For", node.Self.GetRemoteAddr(r)) header.Set("X-Forwarded-Host", r.Host) header.Set("X-Forwarded-Proto", h.ForwardedProto) header.Set("X-Forwarded-Port", h.ForwardedPort) header.Del("Upgrade") header.Del("Connection") header.Del("Sec-Websocket-Key") header.Del("Sec-Websocket-Version") header.Del("Sec-Websocket-Extensions") var backConn *websocket.Conn var backResp *http.Response var err error dialer := &websocket.Dialer{ NetDialContext: h.Dialer.DialContext, Proxy: func(req *http.Request) (url *url.URL, err error) { if h.RequestHost != "" { req.Host = h.RequestHost } else { req.Host = r.Host } return }, HandshakeTimeout: 45 * time.Second, TLSClientConfig: h.TlsConfig, } backConn, backResp, err = dialer.Dial(u.String(), header) if err != nil { if backResp != nil { err = &errortypes.RequestError{ errors.Wrapf(err, "proxy: WebSocket dial error %d", backResp.StatusCode), } } else { err = &errortypes.RequestError{ errors.Wrap(err, "proxy: WebSocket dial error"), } } h.ErrorHandler(h, rw, r, err) return } defer backConn.Close() upgradeHeaders := http.Header{} val := backResp.Header.Get("Sec-Websocket-Protocol") if val != "" { upgradeHeaders.Set("Sec-Websocket-Protocol", val) } val = backResp.Header.Get("Set-Cookie") if val != "" { upgradeHeaders.Set("Set-Cookie", val) } frontConn, err := h.WebSocketsUpgrader.Upgrade(rw, r, upgradeHeaders) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "proxy: WebSocket upgrade error"), } h.ErrorHandler(h, rw, r, err) return } defer frontConn.Close() conn := &webSocketConn{ front: frontConn, back: backConn, r: r, } conn.Run(h.Domain) } func (h *Handler) Serve(rw http.ResponseWriter, r *http.Request) { if h.WebSockets && strings.ToLower( r.Header.Get("Upgrade")) == "websocket" { h.ServeWS(rw, r) } else { h.ServeHTTP(rw, r) } } func NewHandler(index, state int, proxyProto string, proxyPort int, domain *Domain, backend *balancer.Backend, respHandler RespHandler, errHandler ErrorHandler) (hand *Handler) { proxyPortStr := strconv.Itoa(proxyPort) reqHost := domain.Domain.Host backendProto := backend.Protocol backendHost := utils.FormatHostPort(backend.Hostname, backend.Port) backendProtoWs := "" if backendProto == "https" { backendProtoWs = "wss" } else { backendProtoWs = "ws" } handUrl := fmt.Sprintf( "%s://%s:%d", backend.Protocol, backend.Hostname, backend.Port, ) checkUrl, err := url.Parse(handUrl) if err != nil { logrus.WithFields(logrus.Fields{ "balancer": domain.Balancer.Name, "domain": domain.Domain.Domain, "protocol": backend.Protocol, "hostname": backend.Hostname, "port": backend.Port, "check_path": domain.Balancer.CheckPath, }).Error("proxy: Error parsing balancer backend URL") checkUrl, _ = url.Parse("http://0.0.0.0") } checkUrl.Path = domain.Balancer.CheckPath dialTimeout := time.Duration( settings.Router.DialTimeout) * time.Second dialKeepAlive := time.Duration( settings.Router.DialKeepAlive) * time.Second maxIdleConns := settings.Router.MaxIdleConns maxIdleConnsPerHost := settings.Router.MaxIdleConnsPerHost idleConnTimeout := time.Duration( settings.Router.IdleConnTimeout) * time.Second handshakeTimeout := time.Duration( settings.Router.HandshakeTimeout) * time.Second continueTimeout := time.Duration( settings.Router.ContinueTimeout) * time.Second tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, ServerName: backend.Hostname, } if domain.SkipVerify || net.ParseIP(backend.Hostname) != nil { tlsConfig.InsecureSkipVerify = true } if domain.ClientCertificate != nil { tlsConfig.Certificates = []tls.Certificate{ *domain.ClientCertificate, } } writer := &logger.ErrorWriter{ Message: "proxy: Balancer server error", Fields: logrus.Fields{ "balancer": domain.Balancer.Name, "domain": domain.Domain.Domain, "server": handUrl, }, Filters: []string{ "context canceled", }, } dialer := NewStaticDialer(&net.Dialer{ Timeout: dialTimeout, KeepAlive: dialKeepAlive, DualStack: true, }) checkClient := &http.Client{ Transport: &http.Transport{ DialContext: dialer.DialContext, DisableKeepAlives: true, TLSHandshakeTimeout: 5 * time.Second, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, }, }, Timeout: 5 * time.Second, } hand = &Handler{ Key: fmt.Sprintf("%s:%d", backend.Hostname, backend.Port), Index: index, State: state, Domain: domain, CheckUrl: checkUrl.String(), CheckClient: checkClient, BackendHost: backendHost, BackendProto: backendProto, BackendProtoWs: backendProtoWs, RequestHost: reqHost, ForwardedProto: proxyProto, ForwardedPort: proxyPortStr, WebSockets: domain.Balancer.WebSockets, TlsConfig: tlsConfig, Dialer: dialer, ErrorHandler: errHandler, ReverseProxy: &httputil.ReverseProxy{ Director: func(req *http.Request) { req.Header.Set("X-Forwarded-Host", req.Host) req.Header.Set("X-Forwarded-Proto", proxyProto) req.Header.Set("X-Forwarded-Port", proxyPortStr) if reqHost != "" { req.Host = reqHost } req.URL.Scheme = backendProto req.URL.Host = backendHost }, Transport: &TransportFix{ transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: dialer.DialContext, MaxIdleConns: maxIdleConns, MaxIdleConnsPerHost: maxIdleConnsPerHost, IdleConnTimeout: idleConnTimeout, TLSHandshakeTimeout: handshakeTimeout, ExpectContinueTimeout: continueTimeout, TLSClientConfig: tlsConfig, }, }, ErrorLog: log.New(writer, "", 0), ModifyResponse: func(resp *http.Response) error { return respHandler(hand, resp) }, ErrorHandler: func(rw http.ResponseWriter, r *http.Request, err error) { errHandler(hand, rw, r, err) }, }, } if hand.WebSockets { hand.WebSocketsUpgrader = &websocket.Upgrader{ HandshakeTimeout: time.Duration( settings.Router.HandshakeTimeout) * time.Second, ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: func(r *http.Request) bool { return true }, } } return } ================================================ FILE: proxy/transport.go ================================================ package proxy import ( "net/http" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/node" ) type TransportFix struct { transport *http.Transport } func (t *TransportFix) RoundTrip(r *http.Request) ( res *http.Response, err error) { r.Header.Set("X-Forwarded-For", node.Self.GetRemoteAddr(r)) res, err = t.transport.RoundTrip(r) if err != nil { return } if res.StatusCode == http.StatusSwitchingProtocols { err = &WebSocketBlock{ errors.New("proxy: Blocking websocket connection"), } return } return } ================================================ FILE: proxy/types.go ================================================ package proxy import ( "net/http" ) type RespHandler func(hand *Handler, resp *http.Response) (err error) type ErrorHandler func(hand *Handler, rw http.ResponseWriter, r *http.Request, err error) ================================================ FILE: proxy/utils.go ================================================ package proxy import ( "net/http" "github.com/sirupsen/logrus" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" ) func WriteError(w http.ResponseWriter, r *http.Request, code int, err error) { http.Error(w, utils.GetStatusMessage(code), code) logrus.WithFields(logrus.Fields{ "client": node.Self.GetRemoteAddr(r), "error": err, }).Error("proxy: Serve error") } ================================================ FILE: proxy/ws.go ================================================ package proxy import ( "fmt" "net/http" "github.com/gorilla/websocket" "github.com/sirupsen/logrus" ) type webSocketConn struct { r *http.Request back *websocket.Conn front *websocket.Conn } func (w *webSocketConn) Run(domain *Domain) { domain.WebSocketConnsLock.Lock() domain.WebSocketConns.Add(w) domain.WebSocketConnsLock.Unlock() defer func() { domain.WebSocketConnsLock.Lock() domain.WebSocketConns.Remove(w) domain.WebSocketConnsLock.Unlock() }() wait := make(chan bool, 4) go func() { defer func() { rec := recover() if rec != nil { logrus.WithFields(logrus.Fields{ "panic": rec, }).Error("proxy: WebSocket back panic") wait <- true } }() for { msgType, msg, err := w.front.ReadMessage() if err != nil { closeMsg := websocket.FormatCloseMessage( websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) if e, ok := err.(*websocket.CloseError); ok { if e.Code != websocket.CloseNoStatusReceived { closeMsg = websocket.FormatCloseMessage(e.Code, e.Text) } } _ = w.back.WriteMessage(websocket.CloseMessage, closeMsg) break } _ = w.back.WriteMessage(msgType, msg) } wait <- true }() go func() { defer func() { rec := recover() if rec != nil { logrus.WithFields(logrus.Fields{ "panic": rec, }).Error("proxy: WebSocket front panic") wait <- true } }() for { msgType, msg, err := w.back.ReadMessage() if err != nil { closeMsg := websocket.FormatCloseMessage( websocket.CloseNormalClosure, fmt.Sprintf("%v", err)) if e, ok := err.(*websocket.CloseError); ok { if e.Code != websocket.CloseNoStatusReceived { closeMsg = websocket.FormatCloseMessage(e.Code, e.Text) } } _ = w.front.WriteMessage(websocket.CloseMessage, closeMsg) break } _ = w.front.WriteMessage(msgType, msg) } wait <- true }() <-wait w.Close() } func (w *webSocketConn) Close() { defer func() { recover() }() if w.back != nil { w.back.Close() } if w.front != nil { w.front.Close() } } ================================================ FILE: qemu/constants.go ================================================ package qemu const systemdTemplate = `# PritunlData=%s [Unit] Description=Pritunl Cloud Virtual Machine After=network.target [Service]%s Environment=XDG_CACHE_HOME=%s Type=simple User=root ExecStart=%s TimeoutStopSec=5 PrivateTmp=%s ProtectHome=%s ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true PrivateIPC=true NetworkNamespacePath=/var/run/netns/%s ` const systemdTemplateExternalNet = `# PritunlData=%s [Unit] Description=Pritunl Cloud Virtual Machine After=network.target [Service]%s Environment=XDG_CACHE_HOME=%s Type=simple User=root ExecStart=%s TimeoutStopSec=5 PrivateTmp=%s ProtectHome=%s ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true PrivateIPC=true ` ================================================ FILE: qemu/data.go ================================================ package qemu import ( "os" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/dhcpc" "github.com/pritunl/pritunl-cloud/dhcps" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/guest" "github.com/pritunl/pritunl-cloud/hugepages" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/qms" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/tpm" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/virtiofs" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) func initDirs(virt *vm.VirtualMachine) (err error) { vmPath := paths.GetVmPath(virt.Id) err = utils.ExistsMkdir(settings.Hypervisor.LibPath, 0755) if err != nil { return } err = utils.ExistsMkdir(settings.Hypervisor.RunPath, 0755) if err != nil { return } err = utils.ExistsMkdir(paths.GetImdsPath(), 0755) if err != nil { return } err = utils.ExistsMkdir(vmPath, 0755) if err != nil { return } return } func initHugepage(virt *vm.VirtualMachine) (err error) { if !virt.Hugepages { return } err = hugepages.UpdateHugepagesSize() if err != nil { return } hugepagesPath := paths.GetHugepagePath(virt.Id) _ = os.Remove(hugepagesPath) return } func cleanRun(virt *vm.VirtualMachine) (err error) { _ = tpm.Stop(virt) _ = dhcpc.Stop(virt) _ = imds.Stop(virt) _ = dhcps.Stop(virt) _ = virtiofs.StopAll(virt) runPath := paths.GetInstRunPath(virt.Id) pidPath := paths.GetPidPath(virt.Id) sockPath := paths.GetSockPath(virt.Id) qmpSockPath := paths.GetQmpSockPath(virt.Id) guestPath := paths.GetGuestPath(virt.Id) err = utils.RemoveAll(runPath) if err != nil { return } err = utils.RemoveAll(pidPath) if err != nil { return } err = utils.RemoveAll(sockPath) if err != nil { return } err = utils.RemoveAll(qmpSockPath) if err != nil { return } err = utils.RemoveAll(guestPath) if err != nil { return } return } func initCache(virt *vm.VirtualMachine) (err error) { err = utils.ExistsMkdir(paths.GetCachesDir(), 0755) if err != nil { return } err = utils.ExistsMkdir(paths.GetCacheDir(virt.Id), 0700) if err != nil { return } return } func initRun(virt *vm.VirtualMachine) (err error) { runPath := paths.GetInstRunPath(virt.Id) err = utils.ExistsMkdir(runPath, 0700) if err != nil { return } return } func initPermissions(virt *vm.VirtualMachine) (err error) { err = permission.InitVirt(virt) if err != nil { return } err = permission.InitImds(virt) if err != nil { return } for _, mount := range virt.Mounts { shareId := paths.GetShareId(virt.Id, mount.Name) err = permission.InitMount(virt, shareId) if err != nil { return } } return } func writeOvmfVars(virt *vm.VirtualMachine) (err error) { if !virt.Uefi { return } ovmfVarsPath := paths.GetOvmfVarsPath(virt.Id) ovmfVarsPathSource, err := paths.FindOvmfVarsPath(virt.SecureBoot) if err != nil { return } err = utils.ExistsMkdir(paths.GetOvmfDir(), 0755) if err != nil { return } err = utils.Exec("", "cp", ovmfVarsPathSource, ovmfVarsPath) if err != nil { return } err = utils.Chmod(ovmfVarsPath, 0600) if err != nil { return } return } func activateDisks(db *database.Database, virt *vm.VirtualMachine) (err error) { for _, virtDsk := range virt.Disks { dsk, e := disk.Get(db, virtDsk.Id) if e != nil { err = e return } err = data.ActivateDisk(db, dsk) if err != nil { return } } return } func deactivateDisks(db *database.Database, virt *vm.VirtualMachine) (err error) { for _, virtDsk := range virt.Disks { dsk, e := disk.Get(db, virtDsk.Id) if e != nil { err = e return } err = data.DeactivateDisk(db, dsk) if err != nil { return } } return } func writeService(virt *vm.VirtualMachine) (err error) { unitPath := paths.GetUnitPath(virt.Id) qm, err := NewQemu(virt) if err != nil { return } output, err := qm.Marshal() if err != nil { return } err = utils.CreateWrite(unitPath, output, 0644) if err != nil { return } err = systemd.Reload() if err != nil { return } return } func Destroy(db *database.Database, virt *vm.VirtualMachine) (err error) { vmPath := paths.GetVmPath(virt.Id) unitName := paths.GetUnitName(virt.Id) unitPath := paths.GetUnitPath(virt.Id) unitPathServer4 := paths.GetUnitPathDhcp4(virt.Id, 0) unitPathServer6 := paths.GetUnitPathDhcp6(virt.Id, 0) unitPathServerNdp := paths.GetUnitPathNdp(virt.Id, 0) tpmPath := paths.GetTpmPath(virt.Id) runPath := paths.GetInstRunPath(virt.Id) unitPathTpm := paths.GetUnitPathTpm(virt.Id) unitPathImds := paths.GetUnitPathImds(virt.Id) unitPathDhcpc := paths.GetUnitPathDhcpc(virt.Id) unitPathShares := paths.GetUnitPathShares(virt.Id) sockPath := paths.GetSockPath(virt.Id) sockQmpPath := paths.GetQmpSockPath(virt.Id) // TODO Backward compatibility sockPathOld := paths.GetSockPath(virt.Id) guestPath := paths.GetGuestPath(virt.Id) // TODO Backward compatibility guestPathOld := paths.GetGuestPathOld(virt.Id) pidPath := paths.GetPidPath(virt.Id) // TODO Backward compatibility pidPathOld := paths.GetPidPathOld(virt.Id) ovmfVarsPath := paths.GetOvmfVarsPath(virt.Id) hugepagesPath := paths.GetHugepagePath(virt.Id) cachePath := paths.GetCacheDir(virt.Id) logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("qemu: Destroying virtual machine") exists, err := utils.Exists(unitPath) if err != nil { return } if exists { vrt, e := GetVmInfo(db, virt.Id, false, true) if e != nil { err = e return } if vrt != nil && vrt.State == vm.Running { guestShutdown := true err = guest.Shutdown(virt.Id) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Warn("qemu: Failed to send shutdown to guest agent") err = nil guestShutdown = false } logged := false for i := 0; i < 10; i++ { err = qmp.Shutdown(virt.Id) if err == nil { break } if guestShutdown { err = nil break } if !logged { logged = true logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Warn( "qemu: Failed to send shutdown to virtual machine") } time.Sleep(500 * time.Millisecond) } shutdown := false if err != nil { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), "error": err, }).Error("qemu: Power off virtual machine error") err = nil } else { for i := 0; i < settings.Hypervisor.StopTimeout; i++ { vrt, err = GetVmInfo(db, virt.Id, false, true) if err != nil { return } if vrt == nil || vrt.State == vm.Stopped || vrt.State == vm.Failed { if vrt != nil { err = vrt.Commit(db) if err != nil { return } } shutdown = true break } time.Sleep(1 * time.Second) if (i+1)%15 == 0 { go func() { qmp.Shutdown(virt.Id) qms.Shutdown(virt.Id) }() } } } if !shutdown { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Warning("qemu: Force power off virtual machine") } } err = systemd.Stop(unitName) if err != nil { return } } time.Sleep(1 * time.Second) _ = tpm.Stop(virt) _ = dhcpc.Stop(virt) _ = imds.Stop(virt) _ = dhcps.Stop(virt) _ = virtiofs.StopAll(virt) err = NetworkConfClear(db, virt) if err != nil { return } time.Sleep(3 * time.Second) for _, dsk := range virt.Disks { ds, e := disk.Get(db, dsk.GetId()) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil continue } return } err = data.DeactivateDisk(db, ds) if err != nil { return } if ds.Index == "0" && ds.SourceInstance == virt.Id { err = disk.Delete(db, ds.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil continue } return } } else { err = disk.Detach(db, dsk.GetId()) if err != nil { return } } } for i, dsk := range virt.DriveDevices { if dsk.Type != vm.Lvm { continue } dskId, ok := utils.ParseObjectId(dsk.Id) if dskId.IsZero() || !ok { err = &errortypes.ParseError{ errors.Newf("qemu: Failed to parse LVM disk ID '%s'", dsk.Id), } return } ds, e := disk.Get(db, dskId) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { err = nil continue } return } if i == 0 && ds.SourceInstance == virt.Id { err = disk.Delete(db, ds.Id) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil continue } return } } else { err = disk.Detach(db, dskId) if err != nil { return } } } err = utils.RemoveAll(vmPath) if err != nil { return } err = utils.RemoveAll(unitPath) if err != nil { return } err = utils.RemoveAll(tpmPath) if err != nil { return } err = utils.RemoveAll(runPath) if err != nil { return } err = utils.RemoveAll(unitPathTpm) if err != nil { return } err = utils.RemoveAll(unitPathImds) if err != nil { return } err = utils.RemoveAll(unitPathDhcpc) if err != nil { return } err = utils.RemoveAll(unitPathServer4) if err != nil { return } err = utils.RemoveAll(unitPathServer6) if err != nil { return } err = utils.RemoveAll(unitPathServerNdp) if err != nil { return } _, err = utils.RemoveWildcard(unitPathShares) if err != nil { return } err = utils.RemoveAll(sockPath) if err != nil { return } err = utils.RemoveAll(sockQmpPath) if err != nil { return } // TODO Backward compatibility err = utils.RemoveAll(sockPathOld) if err != nil { return } err = utils.RemoveAll(guestPath) if err != nil { return } // TODO Backward compatibility err = utils.RemoveAll(guestPathOld) if err != nil { return } err = utils.RemoveAll(pidPath) if err != nil { return } // TODO Backward compatibility err = utils.RemoveAll(pidPathOld) if err != nil { return } err = utils.RemoveAll(paths.GetInitPath(virt.Id)) if err != nil { return } err = utils.RemoveAll(unitPath) if err != nil { return } err = utils.RemoveAll(ovmfVarsPath) if err != nil { return } err = utils.RemoveAll(hugepagesPath) if err != nil { return } err = utils.RemoveAll(cachePath) if err != nil { return } err = permission.UserDelete(virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) store.RemAddress(virt.Id) store.RemRoutes(virt.Id) store.RemArp(virt.Id) return } func Cleanup(db *database.Database, virt *vm.VirtualMachine) { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("qemu: Stopped virtual machine") _ = tpm.Stop(virt) _ = dhcpc.Stop(virt) _ = imds.Stop(virt) _ = dhcps.Stop(virt) _ = virtiofs.StopAll(virt) hugepagesPath := paths.GetHugepagePath(virt.Id) _ = os.Remove(hugepagesPath) err := NetworkConfClear(db, virt) if err != nil { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), "error": err, }).Error("qemu: Failed to cleanup virtual machine network") } time.Sleep(3 * time.Second) err = deactivateDisks(db, virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) return } ================================================ FILE: qemu/disk.go ================================================ package qemu import ( "time" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/vm" ) func UpdateVmDisk(virt *vm.VirtualMachine) (err error) { for i := 0; i < 10; i++ { if virt.State == vm.Running { _, disks, e := qmp.GetDisks(virt.Id) if e != nil { if i < 9 { time.Sleep(300 * time.Millisecond) _ = UpdateState(virt) continue } err = e return } virt.Disks = disks store.SetDisks(virt.Id, disks) } break } return } ================================================ FILE: qemu/manage.go ================================================ package qemu import ( "encoding/json" "fmt" "io/ioutil" "regexp" "strings" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/cloudinit" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/dhcps" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/qms" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/tpm" "github.com/pritunl/pritunl-cloud/virtiofs" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" ) var ( serviceReg = regexp.MustCompile("pritunl_cloud_([a-z0-9]+).service") ) type InfoCache struct { Timestamp time.Time Virt *vm.VirtualMachine } func GetVmInfo(db *database.Database, vmId bson.ObjectID, queryQms, force bool) (virt *vm.VirtualMachine, err error) { refreshRate := time.Duration( settings.Hypervisor.RefreshRate) * time.Second virtStore, ok := store.GetVirt(vmId) if !ok { unitPath := paths.GetUnitPath(vmId) unitData, e := ioutil.ReadFile(unitPath) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to read service"), } _ = ForcePowerOffErr(db, virt, err) return } virt = &vm.VirtualMachine{} for _, line := range strings.Split(string(unitData), "\n") { if !strings.HasPrefix(line, "PritunlData=") && !strings.HasPrefix(line, "# PritunlData=") { continue } lineSpl := strings.SplitN(line, "=", 2) if len(lineSpl) != 2 || len(lineSpl[1]) < 6 { continue } err = json.Unmarshal([]byte(lineSpl[1]), virt) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "qemu: Failed to parse service data"), } _ = ForcePowerOffErr(db, virt, err) return } break } if virt.Id.IsZero() { virt = nil return } _ = UpdateState(virt) } else { virt = &virtStore.Virt if force || virt.State != vm.Running || time.Since(virtStore.Timestamp) > 6*time.Second { _ = UpdateState(virt) } } if virt.State == vm.Running && queryQms { virt.DisksAvailable = true disksUpdated := false disksStore, ok := store.GetDisks(vmId) if !ok || time.Since(disksStore.Timestamp) > refreshRate { for i := 0; i < 10; i++ { if virt.State == vm.Running { info, disks, e := qmp.GetDisks(vmId) if e != nil { if i < 9 { time.Sleep(300 * time.Millisecond) _ = UpdateState(virt) continue } logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "error": e, }).Error("qemu: Failed to get VM disk state") virt.DisksAvailable = false e = nil break } virt.QemuVersion = fmt.Sprintf( "%d.%d.%d", info.VersionMajor, info.VersionMinor, info.VersionMicro, ) virt.Disks = disks store.SetDisks(vmId, disks) disksUpdated = true } break } } if ok && !disksUpdated { disks := []*vm.Disk{} for _, dsk := range disksStore.Disks { disks = append(disks, dsk.Copy()) } virt.Disks = disks } } if virt.State == vm.Running && queryQms && node.Self.UsbPassthrough { virt.UsbDevicesAvailable = true usbsUpdated := false usbsStore, ok := store.GetUsbs(vmId) if !ok || time.Since(usbsStore.Timestamp) > refreshRate { for i := 0; i < 10; i++ { if virt.State == vm.Running { usbs, e := qms.GetUsbDevices(vmId) if e != nil { if i < 9 { time.Sleep(300 * time.Millisecond) _ = UpdateState(virt) continue } logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "error": e, }).Error("qemu: Failed to get VM usb state") virt.UsbDevicesAvailable = false e = nil break } virt.UsbDevices = usbs store.SetUsbs(vmId, usbs) usbsUpdated = true } break } } if ok && !usbsUpdated { usbs := []*vm.UsbDevice{} for _, usb := range usbsStore.Usbs { usbs = append(usbs, usb.Copy()) } virt.UsbDevices = usbs } } addrStore, ok := store.GetAddress(virt.Id) if !ok { addr := "" addr6 := "" namespace := vm.GetNamespace(virt.Id, 0) nodeNetworkMode := node.Self.NetworkMode if nodeNetworkMode == "" { nodeNetworkMode = node.Dhcp } nodeNetworkMode6 := node.Self.NetworkMode6 if nodeNetworkMode6 == "" { nodeNetworkMode6 = node.Dhcp } ifaceExternal := vm.GetIfaceExternal(virt.Id, 0) if nodeNetworkMode != node.Disabled && nodeNetworkMode != node.Cloud { address, address6, e := iproute.AddressGetIfaceMod( namespace, ifaceExternal) if e != nil { if addrStore != nil { if len(virt.NetworkAdapters) > 0 { virt.NetworkAdapters[0].IpAddress = addrStore.Addr virt.NetworkAdapters[0].IpAddress6 = addrStore.Addr6 } } else { err = e _ = ForcePowerOffErr(db, virt, err) } return } if address != nil { addr = address.Local } if address6 != nil { addr6 = address6.Local } } else if nodeNetworkMode6 != node.Disabled && nodeNetworkMode6 != node.Cloud { _, address6, e := iproute.AddressGetIfaceMod( namespace, ifaceExternal) if e != nil { if addrStore != nil { if len(virt.NetworkAdapters) > 0 { virt.NetworkAdapters[0].IpAddress = addrStore.Addr virt.NetworkAdapters[0].IpAddress6 = addrStore.Addr6 } } else { err = e _ = ForcePowerOffErr(db, virt, err) } return } if address6 != nil { addr6 = address6.Local } } if len(virt.NetworkAdapters) > 0 { virt.NetworkAdapters[0].IpAddress = addr virt.NetworkAdapters[0].IpAddress6 = addr6 } store.SetAddress(virt.Id, addr, addr6) } else { if len(virt.NetworkAdapters) > 0 { virt.NetworkAdapters[0].IpAddress = addrStore.Addr virt.NetworkAdapters[0].IpAddress6 = addrStore.Addr6 } } return } func updateState(virt *vm.VirtualMachine, retry bool) (err error) { unitName := paths.GetUnitName(virt.Id) state, timestamp, err := systemd.GetState(unitName) if err != nil { return } switch state { case "active": virt.State = vm.Running break case "deactivating": virt.State = vm.Running break case "inactive": virt.State = vm.Stopped break case "failed": virt.State = vm.Failed break case "unknown": virt.State = vm.Stopped break default: if retry { time.Sleep(2 * time.Second) err = updateState(virt, false) return } else { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "state": state, }).Info("qemu: Unknown virtual machine state") virt.State = vm.Failed } break } virt.Timestamp = timestamp store.SetVirt(virt.Id, virt) return } func UpdateState(virt *vm.VirtualMachine) (err error) { err = updateState(virt, true) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Error("deploy: Error updating VM state") return } return } func SetState(virt *vm.VirtualMachine, state string) { virt.State = state store.SetVirt(virt.Id, virt) } func GetVms(db *database.Database) ( virts []*vm.VirtualMachine, err error) { systemdPath := settings.Hypervisor.SystemdPath virts = []*vm.VirtualMachine{} items, err := ioutil.ReadDir(systemdPath) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to read systemd directory"), } return } units := []string{} for _, item := range items { if strings.HasPrefix(item.Name(), "pritunl_cloud") { units = append(units, item.Name()) } } waiter := sync.WaitGroup{} virtsLock := sync.Mutex{} for _, unit := range units { match := serviceReg.FindStringSubmatch(unit) if match == nil || len(match) != 2 { continue } vmId, err := bson.ObjectIDFromHex(match[1]) if err != nil { continue } waiter.Add(1) go func() { defer waiter.Done() virt, e := GetVmInfo(db, vmId, true, false) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "error": e, }).Error("qemu: Failed to get VM state") return } if virt != nil { virtsLock.Lock() virts = append(virts, virt) virtsLock.Unlock() } }() } waiter.Wait() return } func Wait(db *database.Database, virt *vm.VirtualMachine) (err error) { unitName := paths.GetUnitName(virt.Id) for i := 0; i < settings.Hypervisor.StartTimeout; i++ { err = UpdateState(virt) if err != nil { return } if virt.State == vm.Running { break } time.Sleep(1 * time.Second) } if virt.State != vm.Running { err = systemd.Stop(unitName) if err != nil { return } err = &errortypes.TimeoutError{ errors.New("qemu: Power on timeout"), } return } return } func Create(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine) (err error) { unitName := paths.GetUnitName(virt.Id) if constants.Interrupt { return } logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("qemu: Creating virtual machine") virt.State = vm.Provisioning err = virt.Commit(db) if err != nil { return } err = inst.InitUnixId(db) if err != nil { return } virt.UnixId = inst.UnixId if inst.Vnc { err = inst.InitVncDisplay(db) if err != nil { return } virt.VncDisplay = inst.VncDisplay } if inst.Spice { err = inst.InitSpicePort(db) if err != nil { return } virt.SpicePort = inst.SpicePort } err = initDirs(virt) if err != nil { return } err = cleanRun(virt) if err != nil { return } dsk, err := disk.GetInstanceIndex(db, inst.Id, "0") if err != nil { if _, ok := err.(*database.NotFoundError); ok { dsk = nil err = nil } else { return } } if dsk == nil { dsk = &disk.Disk{ Id: bson.NewObjectID(), Name: inst.Name, State: disk.Available, Type: virt.DiskType, Pool: virt.DiskPool, Node: node.Self.Id, Deployment: inst.Deployment, Organization: inst.Organization, Instance: inst.Id, Datacenter: node.Self.Datacenter, Zone: node.Self.Zone, SourceInstance: inst.Id, Image: virt.Image, Backing: inst.ImageBacking, Index: "0", Size: inst.InitDiskSize, DeleteProtection: inst.DeleteProtection, } errData, e := dsk.Validate(db) if e != nil { err = e return } if errData != nil { err = errData.GetError() return } backingImage := "" newSize := 0 if virt.Image.IsZero() { newSize, backingImage, err = data.CreateDisk(db, dsk) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("deploy: Failed to provision disk") return } } else { img, e := image.Get(db, dsk.Image) if e != nil { err = e return } dsk.SystemType = img.GetSystemType() dsk.SystemKind = img.GetSystemKind() newSize, backingImage, err = data.WriteImage(db, dsk) if err != nil { return } } if newSize != 0 { dsk.Size = newSize } dsk.BackingImage = backingImage err = dsk.Insert(db) if err != nil { return } _ = event.PublishDispatch(db, "disk.change") if virt.DiskType == disk.Lvm { pl, e := pool.Get(db, dsk.Pool) if e != nil { err = e return } virt.DriveDevices = append(virt.DriveDevices, &vm.DriveDevice{ Id: dsk.Id.Hex(), Type: vm.Lvm, VgName: pl.VgName, LvName: dsk.Id.Hex(), }) } else { virt.Disks = append(virt.Disks, &vm.Disk{ Id: dsk.Id, Index: 0, Path: paths.GetDiskPath(dsk.Id), }) } } if len(virt.NetworkAdapters) == 0 { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing network adapters"), } return } adapter := virt.NetworkAdapters[0] if adapter.Vpc.IsZero() { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing VPC"), } return } if adapter.Subnet.IsZero() { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing VPC subnet"), } return } dc, err := datacenter.Get(db, node.Self.Datacenter) if err != nil { return } zne, err := zone.Get(db, node.Self.Zone) if err != nil { return } vc, err := vpc.Get(db, adapter.Vpc) if err != nil { return } err = virt.GenerateImdsSecret() if err != nil { return } err = cloudinit.Write(db, inst, virt, dc, zne, vc, true) if err != nil { return } err = initCache(virt) if err != nil { return } err = initHugepage(virt) if err != nil { return } err = writeOvmfVars(virt) if err != nil { return } err = activateDisks(db, virt) if err != nil { return } err = writeService(virt) if err != nil { return } err = initRun(virt) if err != nil { return } virt.State = vm.Starting err = virt.Commit(db) if err != nil { return } err = virtiofs.StartAll(db, virt) if err != nil { return } err = initPermissions(virt) if err != nil { return } if virt.DhcpServer { err = dhcps.Start(db, virt, dc, zne, vc) if err != nil { return } } else { err = dhcps.Stop(virt) if err != nil { return } } if virt.Tpm { err = tpm.Start(db, virt) if err != nil { return } } else { err = tpm.Stop(virt) if err != nil { return } } err = systemd.Start(unitName) if err != nil { return } err = Wait(db, virt) if err != nil { return } if virt.Vnc { err = qmp.VncPassword(virt.Id, inst.VncPassword) if err != nil { return } } if virt.Spice { err = qmp.SetPassword(virt.Id, qmp.Spice, inst.SpicePassword) if err != nil { return } } err = NetworkConf(db, virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) return } ================================================ FILE: qemu/network.go ================================================ package qemu import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/dhcps" "github.com/pritunl/pritunl-cloud/netconf" "github.com/pritunl/pritunl-cloud/vm" ) func NetworkConfClear(db *database.Database, virt *vm.VirtualMachine) (err error) { err = dhcps.Stop(virt) if err != nil { return } nc := netconf.New(virt) err = nc.Clean(db) if err != nil { return } return } func NetworkConf(db *database.Database, virt *vm.VirtualMachine) (err error) { nc := netconf.New(virt) err = nc.Init(db) if err != nil { return } return } ================================================ FILE: qemu/power.go ================================================ package qemu import ( "os" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/cloudinit" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/dhcpc" "github.com/pritunl/pritunl-cloud/dhcps" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/guest" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/qmp" "github.com/pritunl/pritunl-cloud/qms" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/tpm" "github.com/pritunl/pritunl-cloud/virtiofs" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" ) func PowerOn(db *database.Database, inst *instance.Instance, virt *vm.VirtualMachine) (err error) { unitName := paths.GetUnitName(virt.Id) if constants.Interrupt { return } logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("qemu: Starting virtual machine") err = inst.InitUnixId(db) if err != nil { return } virt.UnixId = inst.UnixId if inst.Vnc { err = inst.InitVncDisplay(db) if err != nil { return } virt.VncDisplay = inst.VncDisplay } if inst.Spice { err = inst.InitSpicePort(db) if err != nil { return } virt.SpicePort = inst.SpicePort } err = initDirs(virt) if err != nil { return } err = cleanRun(virt) if err != nil { return } if len(virt.NetworkAdapters) == 0 { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing network adapters"), } return } adapter := virt.NetworkAdapters[0] if adapter.Vpc.IsZero() { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing VPC"), } return } if adapter.Subnet.IsZero() { err = &errortypes.NotFoundError{ errors.Wrap(err, "cloudinit: Instance missing VPC subnet"), } return } dc, err := datacenter.Get(db, node.Self.Datacenter) if err != nil { return } zne, err := zone.Get(db, node.Self.Zone) if err != nil { return } vc, err := vpc.Get(db, adapter.Vpc) if err != nil { return } err = virt.GenerateImdsSecret() if err != nil { return } err = cloudinit.Write(db, inst, virt, dc, zne, vc, false) if err != nil { return } err = initCache(virt) if err != nil { return } err = initHugepage(virt) if err != nil { return } err = writeOvmfVars(virt) if err != nil { return } err = activateDisks(db, virt) if err != nil { return } err = writeService(virt) if err != nil { return } err = initRun(virt) if err != nil { return } err = virtiofs.StartAll(db, virt) if err != nil { return } err = initPermissions(virt) if err != nil { return } if virt.DhcpServer { err = dhcps.Start(db, virt, dc, zne, vc) if err != nil { return } } else { err = dhcps.Stop(virt) if err != nil { return } } if virt.Tpm { err = tpm.Start(db, virt) if err != nil { return } } else { err = tpm.Stop(virt) if err != nil { return } } err = systemd.Start(unitName) if err != nil { return } err = Wait(db, virt) if err != nil { return } if virt.Vnc { err = qmp.VncPassword(virt.Id, inst.VncPassword) if err != nil { return } } if virt.Spice { err = qmp.SetPassword(virt.Id, qmp.Spice, inst.SpicePassword) if err != nil { return } } err = NetworkConf(db, virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) return } func PowerOff(db *database.Database, virt *vm.VirtualMachine) (err error) { unitName := paths.GetUnitName(virt.Id) if constants.Interrupt { return } logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("qemu: Stopping virtual machine") guestShutdown := true err = guest.Shutdown(virt.Id) if err != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Warn("qemu: Failed to send shutdown to guest agent") err = nil guestShutdown = false } logged := false for i := 0; i < 10; i++ { err = qmp.Shutdown(virt.Id) if err == nil { break } if guestShutdown { err = nil break } if !logged { logged = true logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": err, }).Warn("qemu: Failed to send shutdown to virtual machine") } time.Sleep(500 * time.Millisecond) } shutdown := false if err != nil { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), "error": err, }).Error("qemu: Power off virtual machine error") err = nil } else { for i := 0; i < settings.Hypervisor.StopTimeout; i++ { vrt, e := GetVmInfo(db, virt.Id, false, true) if e != nil { err = e return } if vrt == nil || vrt.State == vm.Stopped || vrt.State == vm.Failed { if vrt != nil { err = vrt.Commit(db) if err != nil { return } } shutdown = true break } time.Sleep(1 * time.Second) if (i+1)%15 == 0 { go func() { qmp.Shutdown(virt.Id) qms.Shutdown(virt.Id) }() } } } if !shutdown { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), }).Warning("qemu: Force power off virtual machine") err = systemd.Stop(unitName) if err != nil { return } } _ = tpm.Stop(virt) _ = dhcpc.Stop(virt) _ = imds.Stop(virt) _ = dhcps.Stop(virt) _ = virtiofs.StopAll(virt) hugepagesPath := paths.GetHugepagePath(virt.Id) _ = os.Remove(hugepagesPath) err = NetworkConfClear(db, virt) if err != nil { return } time.Sleep(3 * time.Second) err = deactivateDisks(db, virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) return } func ForcePowerOffErr(db *database.Database, virt *vm.VirtualMachine, e error) (err error) { unitName := paths.GetUnitName(virt.Id) if constants.Interrupt { return } logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "error": e, }).Error("qemu: Force power off virtual machine") go guest.Shutdown(virt.Id) go qmp.Shutdown(virt.Id) go qms.Shutdown(virt.Id) time.Sleep(15 * time.Second) err = systemd.Stop(unitName) if err != nil { return } _ = tpm.Stop(virt) _ = dhcpc.Stop(virt) _ = imds.Stop(virt) _ = dhcps.Stop(virt) _ = virtiofs.StopAll(virt) err = NetworkConfClear(db, virt) if err != nil { return } time.Sleep(3 * time.Second) err = deactivateDisks(db, virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) return } func ForcePowerOff(db *database.Database, virt *vm.VirtualMachine) ( err error) { unitName := paths.GetUnitName(virt.Id) if constants.Interrupt { return } logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), }).Warning("qemu: Force power off virtual machine") go guest.Shutdown(virt.Id) go qmp.Shutdown(virt.Id) go qms.Shutdown(virt.Id) time.Sleep(5 * time.Second) err = systemd.Stop(unitName) if err != nil { return } _ = tpm.Stop(virt) _ = dhcpc.Stop(virt) _ = imds.Stop(virt) _ = dhcps.Stop(virt) _ = virtiofs.StopAll(virt) err = NetworkConfClear(db, virt) if err != nil { return } time.Sleep(3 * time.Second) err = deactivateDisks(db, virt) if err != nil { return } store.RemVirt(virt.Id) store.RemDisks(virt.Id) return } ================================================ FILE: qemu/qemu.go ================================================ package qemu import ( "crypto/md5" "fmt" "math" "path" "path/filepath" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/compositor" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/features" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/render" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) type Disk struct { Id string Index int File string Format string } type Network struct { Iface string MacAddress string } type Iso struct { Name string } type UsbDevice struct { Id string Vendor string Product string Bus string Address string BusPath string } type PciDevice struct { Slot string Gpu bool } type DriveDevice struct { Id string Type string VgName string LvName string } type Mount struct { Id string Name string Sock string } type IscsiDevice struct { Uri string } type Qemu struct { Id bson.ObjectID Data string Kvm bool Machine string Cpu string Cpus int Cores int Threads int Dies int Sockets int Boot string Uefi bool SecureBoot bool Tpm bool OvmfCodePath string OvmfVarsPath string Memory int Hugepages bool Vnc bool VncDisplay int Spice bool SpicePort int Gui bool GuiUser string GuiMode string ProtectHome bool ProtectTmp bool Namespace string Disks Disks Networks []*Network Isos []*Iso UsbDevices []*UsbDevice PciDevices []*PciDevice DriveDevices []*DriveDevice IscsiDevices []*IscsiDevice Mounts []*Mount } func (q *Qemu) GetDiskQueues() (queues int) { queues = int(math.Ceil(float64(q.Cores) / 2)) if queues > settings.Hypervisor.DiskQueuesMax { queues = settings.Hypervisor.DiskQueuesMax } else if queues < settings.Hypervisor.DiskQueuesMin { queues = settings.Hypervisor.DiskQueuesMin } return } func (q *Qemu) GetNetworkQueues() (queues int) { queues = int(math.Ceil(float64(q.Cores) / 2)) if queues > settings.Hypervisor.NetworkQueuesMax { queues = settings.Hypervisor.NetworkQueuesMax } else if queues < settings.Hypervisor.NetworkQueuesMin { queues = settings.Hypervisor.NetworkQueuesMin } return } func (q *Qemu) GetNetworkVectors() (vectors int) { vectors = int(math.Ceil(float64(q.Cores) / 2)) if vectors > settings.Hypervisor.NetworkQueuesMax { vectors = settings.Hypervisor.NetworkQueuesMax } else if vectors < settings.Hypervisor.NetworkQueuesMin { vectors = settings.Hypervisor.NetworkQueuesMin } vectors = (2 * vectors) + 2 return } func (q *Qemu) Marshal() (output string, err error) { localIsosPath := paths.GetLocalIsosPath() slot := -1 qemuPath, err := features.GetQemuPath() if err != nil { return } cmd := []string{ qemuPath, "-nographic", } cmd = append(cmd, "-uuid") cmd = append(cmd, paths.GetVmUuid(q.Id)) nodeVga := node.Self.Vga nodeVgaRenderPath := "" if nodeVga == "" { nodeVga = node.Virtio } if node.VgaRenderModes.Contains(nodeVga) { nodeVgaRender := node.Self.VgaRender if nodeVgaRender != "" { nodeVgaRenderPath, err = render.GetRender(nodeVgaRender) if err != nil { return } } } memoryBackend, err := features.GetMemoryBackendSupport() if err != nil { return } pciPassthrough := false gpuPassthrough := false if node.Self.PciPassthrough && len(q.PciDevices) > 0 { pciPassthrough = true for i, device := range q.PciDevices { slot += 1 cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf("pcie-root-port,id=pcibus%d,slot=%d", i, slot)) cmd = append(cmd, "-device") if device.Gpu { gpuPassthrough = true cmd = append(cmd, fmt.Sprintf( "vfio-pci,host=0000:%s,bus=pcibus%d,"+ "multifunction=on,x-vga=on", device.Slot, i, )) } else { cmd = append(cmd, fmt.Sprintf( "vfio-pci,host=0000:%s,bus=pcibus%d", device.Slot, i, )) } } if gpuPassthrough { cmd = append(cmd, "-display") cmd = append(cmd, "none") cmd = append(cmd, "-vga") cmd = append(cmd, "none") } } vgaPrime := false if !gpuPassthrough && (q.Vnc || q.Spice || q.Gui) { if q.Gui { cmd = append(cmd, "-display") if q.GuiMode == node.Gtk && !settings.Hypervisor.NoGuiFullscreen { cmd = append(cmd, fmt.Sprintf( "%s,gl=on,window-close=off,full-screen=on", q.GuiMode)) } else { cmd = append(cmd, fmt.Sprintf( "%s,gl=on,window-close=off", q.GuiMode)) } } else if node.VgaRenderModes.Contains(nodeVga) { cmd = append(cmd, "-display") options := "egl-headless" if nodeVgaRenderPath != "" { options += fmt.Sprintf(",rendernode=%s", nodeVgaRenderPath) } cmd = append(cmd, options) } switch nodeVga { case node.Std: cmd = append(cmd, "-vga") cmd = append(cmd, "std") case node.Vmware: cmd = append(cmd, "-vga") cmd = append(cmd, "vmware") case node.Virtio: cmd = append(cmd, "-vga") cmd = append(cmd, "virtio") case node.VirtioPci: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-gpu-pci") cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioPciPrime: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-gpu-pci") cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true case node.VirtioVgaGl: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-vga-gl") cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioVgaGlPrime: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-vga-gl") cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true case node.VirtioVgaGlVulkan: cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-vga-gl,blob=true,hostmem=%dM,venus=true", settings.Hypervisor.GlHostMem, )) cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioVgaGlVulkanPrime: cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-vga-gl,blob=true,hostmem=%dM,venus=true", settings.Hypervisor.GlHostMem, )) cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true case node.VirtioGl: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-gpu-gl") cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioGlPrime: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-gpu-gl") cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true case node.VirtioGlVulkan: cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-gpu-gl,blob=true,hostmem=%dM,venus=true", settings.Hypervisor.GlHostMem, )) cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioGlVulkanPrime: cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-gpu-gl,blob=true,hostmem=%dM,venus=true", settings.Hypervisor.GlHostMem, )) cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true case node.VirtioPciGl: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-gpu-gl-pci") cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioPciGlPrime: cmd = append(cmd, "-device") cmd = append(cmd, "virtio-gpu-gl-pci") cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true case node.VirtioPciGlVulkan: cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-gpu-gl-pci,blob=true,hostmem=%dM,venus=true", settings.Hypervisor.GlHostMem, )) cmd = append(cmd, "-vga") cmd = append(cmd, "none") case node.VirtioPciGlVulkanPrime: cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-gpu-gl-pci,blob=true,hostmem=%dM,venus=true", settings.Hypervisor.GlHostMem, )) cmd = append(cmd, "-vga") cmd = append(cmd, "none") vgaPrime = true default: cmd = append(cmd, "-vga") cmd = append(cmd, nodeVga) } if q.Vnc { cmd = append(cmd, "-vnc") cmd = append(cmd, fmt.Sprintf( ":%d,websocket=%d,password=on,share=allow-exclusive", q.VncDisplay, q.VncDisplay+15900, )) } if q.Spice { cmd = append(cmd, "-spice") cmd = append(cmd, fmt.Sprintf( "ipv4=on,port=%d,image-compression=off", q.SpicePort, )) } } if q.Uefi { cmd = append(cmd, "-drive") cmd = append(cmd, fmt.Sprintf( "if=pflash,format=raw,unit=0,readonly=on,file=%s", q.OvmfCodePath, )) cmd = append(cmd, "-drive") cmd = append(cmd, fmt.Sprintf( "if=pflash,format=raw,unit=1,file=%s", q.OvmfVarsPath, )) } if q.Kvm { cmd = append(cmd, "-enable-kvm") } cmd = append(cmd, "-name") cmd = append(cmd, fmt.Sprintf("pritunl_%s", q.Id.Hex())) if !pciPassthrough { supported, e := features.GetRunWithSupport() if e != nil { err = e return } if supported { cmd = append(cmd, "-run-with") cmd = append(cmd, fmt.Sprintf( "user=%s", permission.GetUserName(q.Id))) } else { cmd = append(cmd, "-runas") cmd = append(cmd, permission.GetUserName(q.Id)) } } for i := 0; i < 10; i++ { slot += 1 cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf("pcie-root-port,id=diskbus%d,slot=%d", i, slot)) } cmd = append(cmd, "-machine") options := ",mem-merge=on" if q.SecureBoot { options += ",smm=on" } if q.Hugepages && memoryBackend { options += ",memory-backend=pc.ram" } if q.Kvm { options += ",accel=kvm" } if gpuPassthrough || (!q.Vnc && !q.Spice && !q.Gui) { options += ",vmport=off" } cmd = append(cmd, fmt.Sprintf("type=%s%s", q.Machine, options)) if q.Kvm { cmd = append(cmd, "-cpu") cmd = append(cmd, q.Cpu) } //cmd = append(cmd, "-no-hpet") cmd = append(cmd, "-rtc", "base=utc,driftfix=slew") cmd = append(cmd, "-msg", "timestamp=on") cmd = append(cmd, "-global", "kvm-pit.lost_tick_policy=delay") cmd = append(cmd, "-global", "ICH9-LPC.disable_s3=1") cmd = append(cmd, "-global", "ICH9-LPC.disable_s4=1") if q.SecureBoot { cmd = append( cmd, "-global", "driver=cfi.pflash01,property=secure,value=on", ) } cmd = append(cmd, "-smp") cmd = append(cmd, fmt.Sprintf( "cores=%d,threads=%d,dies=%d,sockets=%d", q.Cores, q.Threads, q.Dies, q.Sockets, )) if q.Isos != nil && len(q.Isos) > 0 { cmd = append(cmd, "-boot") cmd = append( cmd, fmt.Sprintf( "order=d,menu=on,splash-time=%d", settings.Hypervisor.SplashTime*1000, ), ) } else { cmd = append(cmd, "-boot") cmd = append(cmd, q.Boot) } cmd = append(cmd, "-m") cmd = append(cmd, fmt.Sprintf("%dM", q.Memory)) memShare := "off" if len(q.Mounts) > 0 { memShare = "on" } if q.Hugepages { if memoryBackend { cmd = append(cmd, "-object") cmd = append(cmd, fmt.Sprintf( "memory-backend-file,id=pc.ram,"+ "size=%dM,mem-path=%s,prealloc=on,share=%s,merge=off", q.Memory, paths.GetHugepagePath(q.Id), memShare, )) } else { cmd = append(cmd, "-mem-path") cmd = append(cmd, paths.GetHugepagePath(q.Id)) } } if settings.Hypervisor.VirtRng { cmd = append(cmd, "-object", "rng-random,filename=/dev/random,id=rng0") cmd = append(cmd, "-device", "virtio-rng-pci,rng=rng0") } if pciPassthrough { cmd = append(cmd, "-device", "intel-iommu,intremap=on,caching-mode=on") } diskAio := settings.Hypervisor.DiskAio if diskAio == "" { supported, e := features.GetUringSupport() if e != nil { err = e return } if supported { diskAio = "io_uring" } else { diskAio = "native" } } for _, disk := range q.Disks { dskId := fmt.Sprintf("fd_%s", disk.Id) dskFileId := fmt.Sprintf("fdf_%s", disk.Id) dskDevId := fmt.Sprintf("fdd_%s", disk.Id) cmd = append(cmd, "-blockdev") cmd = append(cmd, fmt.Sprintf( "driver=file,node-name=%s,filename=%s,aio=%s,"+ "discard=unmap,cache.direct=on,cache.no-flush=off", dskFileId, disk.File, diskAio, )) cmd = append(cmd, "-blockdev") cmd = append(cmd, fmt.Sprintf( "driver=%s,node-name=%s,file=%s,"+ "cache.direct=on,cache.no-flush=off", disk.Format, dskId, dskFileId, )) cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-blk-pci,drive=%s,num-queues=%d,id=%s,"+ "bus=diskbus%d,write-cache=on,packed=on", dskId, q.GetDiskQueues(), dskDevId, disk.Index, )) } for _, device := range q.DriveDevices { drivePth := "" if device.Type == vm.Lvm { drivePth = filepath.Join("/dev/mapper", fmt.Sprintf("%s-%s", device.VgName, device.LvName)) } else { drivePth = paths.GetDrivePath(device.Id) } dskHashId := drive.GetDriveHashId(device.Id) dskId := fmt.Sprintf("pd_%s", dskHashId) dskFileId := fmt.Sprintf("pdf_%s", dskHashId) dskDevId := fmt.Sprintf("pdd_%s", dskHashId) dskBusId := fmt.Sprintf("pdb_%s", dskHashId) slot += 1 cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "pcie-root-port,id=%s,slot=%d", dskBusId, slot, )) cmd = append(cmd, "-blockdev") cmd = append(cmd, fmt.Sprintf( "driver=file,node-name=%s,filename=%s,aio=%s,"+ "discard=unmap,cache.direct=on,cache.no-flush=off", dskFileId, drivePth, diskAio, )) cmd = append(cmd, "-blockdev") cmd = append(cmd, fmt.Sprintf( "driver=raw,node-name=%s,file=%s,"+ "cache.direct=on,cache.no-flush=off", dskId, dskFileId, )) cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-blk-pci,drive=%s,num-queues=%d,id=%s,"+ "bus=%s,write-cache=on,packed=on", dskId, q.GetDiskQueues(), dskDevId, dskBusId, )) } hasIscsi := false if node.Self.Iscsi { for _, device := range q.IscsiDevices { if !hasIscsi { cmd = append(cmd, "-iscsi") cmd = append(cmd, fmt.Sprintf( "initiator-name=iqn.2008-11.org.linux-kvm:%s", q.Id.Hex(), )) hasIscsi = true } iscsiHash := md5.New() iscsiHash.Write([]byte(device.Uri)) iscsiId := fmt.Sprintf("%x", iscsiHash.Sum(nil)) dskId := fmt.Sprintf("id_%s", iscsiId) dskFileId := fmt.Sprintf("idf_%s", iscsiId) dskDevId := fmt.Sprintf("idd_%s", iscsiId) dskBusId := fmt.Sprintf("idb_%s", iscsiId) slot += 1 cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "pcie-root-port,id=%s,slot=%d", dskBusId, slot, )) cmd = append(cmd, "-blockdev") cmd = append(cmd, fmt.Sprintf( "driver=iscsi,node-name=%s,transport=tcp,"+ "url=%s,cache.direct=on", dskFileId, device.Uri, )) cmd = append(cmd, "-blockdev") cmd = append(cmd, fmt.Sprintf( "driver=raw,node-name=%s,file=%s,"+ "cache.direct=on,cache.no-flush=off", dskId, dskFileId, )) cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-blk-pci,drive=%s,num-queues=%d,id=%s,"+ "bus=%s,write-cache=on,packed=on", dskId, q.GetDiskQueues(), dskDevId, dskBusId, )) } } for _, mount := range q.Mounts { vfsId := fmt.Sprintf("vfs_%s", mount.Id) vfsDevId := fmt.Sprintf("vfsd_%s", mount.Id) vfsBusId := fmt.Sprintf("vfsb_%s", mount.Id) slot += 1 cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "pcie-root-port,id=%s,slot=%d", vfsBusId, slot, )) cmd = append(cmd, "-chardev") cmd = append(cmd, fmt.Sprintf( "socket,id=%s,path=%s", vfsId, mount.Sock, )) cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "vhost-user-fs-pci,chardev=%s,tag=\"%s\",id=%s,bus=%s", vfsId, mount.Name, vfsDevId, vfsBusId, )) } for i, network := range q.Networks { cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf( "virtio-net-pci,netdev=net%d,mac=%s,mq=on,"+ "packed=on,rss=on,vectors=%d", i, network.MacAddress, q.GetNetworkVectors(), )) cmd = append(cmd, "-netdev") cmd = append(cmd, fmt.Sprintf( "tap,id=net%d,ifname=%s,script=no,vhost=on,queues=%d", i, network.Iface, q.GetNetworkQueues(), )) } cmd = append(cmd, "-drive") cmd = append(cmd, fmt.Sprintf( "file=%s,media=cdrom,index=0", paths.GetInitPath(q.Id), )) if len(q.Isos) > 0 { for i, iso := range q.Isos { cmd = append(cmd, "-drive") cmd = append(cmd, fmt.Sprintf( "file=%s,media=cdrom,index=%d", path.Join( localIsosPath, path.Base(utils.FilterRelPath(iso.Name)), ), i+1, )) } } cmd = append(cmd, "-monitor") cmd = append(cmd, fmt.Sprintf( "unix:%s,server=on,wait=off", paths.GetSockPath(q.Id), )) cmd = append(cmd, "-qmp") cmd = append(cmd, fmt.Sprintf( "unix:%s,server=on,wait=off", paths.GetQmpSockPath(q.Id), )) cmd = append(cmd, "-pidfile") cmd = append(cmd, paths.GetPidPath(q.Id)) if q.Tpm { cmd = append(cmd, "-chardev") cmd = append(cmd, fmt.Sprintf( "socket,id=tpmsock0,path=%s", paths.GetTpmSockPath(q.Id), )) cmd = append(cmd, "-tpmdev") cmd = append(cmd, "emulator,id=tpmdev0,chardev=tpmsock0") cmd = append(cmd, "-device") cmd = append(cmd, "tpm-tis,tpmdev=tpmdev0") } guestPath := paths.GetGuestPath(q.Id) cmd = append(cmd, "-chardev") cmd = append(cmd, fmt.Sprintf( "socket,path=%s,server=on,wait=off,id=guest", guestPath, )) cmd = append(cmd, "-device") cmd = append(cmd, "virtio-serial") cmd = append(cmd, "-device") cmd = append(cmd, "virtserialport,chardev=guest,name=org.qemu.guest_agent.0") if !settings.Hypervisor.NoSandbox { cmd = append(cmd, "-sandbox") if q.Gui { cmd = append(cmd, "on,obsolete=deny,elevateprivileges=allow,"+ "spawn=allow,resourcecontrol=deny") } else { cmd = append(cmd, "on,obsolete=deny,elevateprivileges=allow,"+ "spawn=deny,resourcecontrol=deny") } } if q.Gui && !settings.Hypervisor.NoVirtioHid { cmd = append(cmd, "-device") cmd = append(cmd, "virtio-tablet-pci") cmd = append(cmd, "-device") cmd = append(cmd, "virtio-keyboard-pci") } if node.Self.UsbPassthrough || q.Vnc || q.Spice || q.Gui { slot += 1 cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf("pcie-root-port,id=usbbus,slot=%d", slot)) cmd = append(cmd, "-device") cmd = append(cmd, fmt.Sprintf("qemu-xhci,bus=usbbus,p2=%d,p3=%d", settings.Hypervisor.UsbHsPorts, settings.Hypervisor.UsbSsPorts, )) if (q.Vnc || q.Spice) || (q.Gui && settings.Hypervisor.NoVirtioHid) { cmd = append(cmd, "-device") cmd = append(cmd, "usb-tablet") cmd = append(cmd, "-device") cmd = append(cmd, "usb-kbd") } for _, device := range q.UsbDevices { cmd = append(cmd, "-device", fmt.Sprintf( "usb-host,hostdevice=%s,id=%s", device.BusPath, device.Id, ), ) } } compositorEnv := "" if q.Gui { compositorEnv, err = compositor.GetEnv( q.GuiUser, nodeVgaRenderPath, vgaPrime) if err != nil { return } } protectTmp := "" if q.ProtectTmp { protectTmp = "true" } else { protectTmp = "false" } protectHome := "" if q.ProtectHome { protectHome = "true" } else { protectHome = "read-only" } if q.Namespace == "" { output = fmt.Sprintf( systemdTemplateExternalNet, q.Data, compositorEnv, paths.GetCacheDir(q.Id), strings.Join(cmd, " "), protectTmp, protectHome, ) } else { output = fmt.Sprintf( systemdTemplate, q.Data, compositorEnv, paths.GetCacheDir(q.Id), strings.Join(cmd, " "), protectTmp, protectHome, q.Namespace, ) } return } ================================================ FILE: qemu/routes.go ================================================ package qemu import ( "fmt" "net" "strconv" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" ) func GetRoutes(instId bson.ObjectID) (icmpRedirects bool, routes []vpc.Route, routes6 []vpc.Route, err error) { namespace := vm.GetNamespace(instId, 0) icmpRedirects = true output, _ := utils.ExecCombinedOutputLogged( nil, "ip", "netns", "exec", namespace, "sysctl", "net.ipv4.conf.br0.send_redirects", ) if output != "" { parts := strings.Split(strings.TrimSpace(output), "=") if len(parts) == 2 { valueStr := strings.TrimSpace(parts[1]) value, _ := strconv.Atoi(valueStr) icmpRedirects = value == 1 } } output, _ = utils.ExecCombinedOutputLogged( []string{ "not configured in this system", }, "ip", "netns", "exec", namespace, "route", "-n", ) if output == "" { return } routes = []vpc.Route{} routes6 = []vpc.Route{} lines := strings.Split(output, "\n") if len(lines) > 2 { for _, line := range lines[2:] { if line == "" { continue } fields := strings.Fields(line) if len(fields) < 8 { continue } if fields[4] != "97" { continue } if fields[0] == "0.0.0.0" || fields[1] == "0.0.0.0" { continue } mask := utils.ParseIpMask(fields[2]) if mask == nil { continue } cidr, _ := mask.Size() route := vpc.Route{ Destination: fmt.Sprintf("%s/%d", fields[0], cidr), Target: fields[1], } routes = append(routes, route) } } output, _ = utils.ExecCombinedOutputLogged( []string{ "not configured in this system", }, "ip", "netns", "exec", namespace, "route", "-6", "-n", ) if output == "" { return } lines = strings.Split(output, "\n") if len(lines) > 2 { for _, line := range lines[2:] { if line == "" { continue } fields := strings.Fields(line) if len(fields) < 7 { continue } if fields[3] != "97" { continue } _, destination, e := net.ParseCIDR(fields[0]) if e != nil || destination == nil { continue } target := net.ParseIP(fields[1]) if target == nil { continue } route := vpc.Route{ Destination: destination.String(), Target: target.String(), } routes6 = append(routes6, route) } } return } ================================================ FILE: qemu/sort.go ================================================ package qemu type Disks []*Disk func (d Disks) Len() int { return len(d) } func (d Disks) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d Disks) Less(i, j int) bool { return d[i].Index < d[j].Index } ================================================ FILE: qemu/usb.go ================================================ package qemu import ( "time" "github.com/pritunl/pritunl-cloud/qms" "github.com/pritunl/pritunl-cloud/store" "github.com/pritunl/pritunl-cloud/vm" ) func UpdateVmUsb(virt *vm.VirtualMachine) (err error) { for i := 0; i < 10; i++ { if virt.State == vm.Running { usbs, e := qms.GetUsbDevices(virt.Id) if e != nil { if i < 9 { time.Sleep(300 * time.Millisecond) _ = UpdateState(virt) continue } err = e return } virt.UsbDevices = usbs store.SetUsbs(virt.Id, usbs) } break } return } ================================================ FILE: qemu/utils.go ================================================ package qemu import ( "encoding/json" "sort" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) func NewQemu(virt *vm.VirtualMachine) (qm *Qemu, err error) { data, err := json.Marshal(virt) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "qemu: Failed to marshal virt"), } return } ovmfCodePath := "" if virt.Uefi { ovmfCodePath, err = paths.FindOvmfCodePath(virt.SecureBoot) if err != nil { return } } guiUser := node.Self.GuiUser guiMode := node.Self.GuiMode if guiMode == "" { guiMode = node.Sdl } namespace := "" if !virt.HasExternalNetwork() { namespace = vm.GetNamespace(virt.Id, 0) } qm = &Qemu{ Id: virt.Id, Data: string(data), Kvm: node.Self.Hypervisor == node.Kvm, Machine: "q35", Cpu: "host", Cores: virt.Processors, Threads: 1, Dies: 1, Sockets: 1, Boot: "c", Uefi: virt.Uefi, SecureBoot: virt.SecureBoot, Tpm: virt.Tpm, OvmfCodePath: ovmfCodePath, OvmfVarsPath: paths.GetOvmfVarsPath(virt.Id), Memory: virt.Memory, Hugepages: virt.Hugepages, Vnc: virt.Vnc && virt.VncDisplay != 0, VncDisplay: virt.VncDisplay, Spice: virt.Spice && virt.SpicePort != 0, SpicePort: virt.SpicePort, Gui: virt.Gui && node.Self.Gui && guiUser != "", GuiUser: guiUser, GuiMode: guiMode, ProtectHome: virt.ProtectHome(), ProtectTmp: virt.ProtectTmp(), Namespace: namespace, Disks: []*Disk{}, Networks: []*Network{}, Isos: []*Iso{}, UsbDevices: []*UsbDevice{}, PciDevices: []*PciDevice{}, DriveDevices: []*DriveDevice{}, IscsiDevices: []*IscsiDevice{}, Mounts: []*Mount{}, } for _, disk := range virt.Disks { qm.Disks = append(qm.Disks, &Disk{ Id: disk.Id.Hex(), Index: disk.Index, File: disk.Path, Format: "qcow2", }) } sort.Sort(qm.Disks) for i, net := range virt.NetworkAdapters { qm.Networks = append(qm.Networks, &Network{ MacAddress: net.MacAddress, Iface: vm.GetIface(virt.Id, i), }) } for _, is := range virt.Isos { qm.Isos = append(qm.Isos, &Iso{ Name: is.Name, }) } for _, device := range virt.UsbDevices { usbDevice, _ := device.GetDevice() if usbDevice == nil { continue } qm.UsbDevices = append(qm.UsbDevices, &UsbDevice{ Id: usbDevice.GetQemuId(), Vendor: usbDevice.Vendor, Product: usbDevice.Product, Bus: usbDevice.Bus, Address: usbDevice.Address, BusPath: usbDevice.BusPath, }) } for _, device := range virt.PciDevices { dev, e := pci.GetVfio(device.Slot) if e != nil { err = e return } if dev == nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "device_slot": device.Slot, }).Error("qemu: Failed to find vfio device") continue } name := strings.ToLower(dev.Name) qm.PciDevices = append(qm.PciDevices, &PciDevice{ Slot: device.Slot, Gpu: strings.Contains(name, "vga compatible") || strings.Contains(name, "vga controller") || strings.Contains(name, "graphics controller") || strings.Contains(name, "display controller"), }) } for _, device := range virt.DriveDevices { qm.DriveDevices = append(qm.DriveDevices, &DriveDevice{ Id: device.Id, Type: device.Type, VgName: device.VgName, LvName: device.LvName, }) } for _, device := range virt.IscsiDevices { qm.IscsiDevices = append(qm.IscsiDevices, &IscsiDevice{ Uri: device.Uri, }) } for _, mount := range virt.Mounts { shareId := paths.GetShareId(virt.Id, mount.Name) sockPath := paths.GetShareSockPath(virt.Id, shareId) qm.Mounts = append(qm.Mounts, &Mount{ Id: shareId, Name: utils.FilterNameCmd(mount.Name), Sock: sockPath, }) } return } ================================================ FILE: qga/qga.go ================================================ package qga import ( "bytes" "encoding/json" "net" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) type Command struct { Execute string `json:"execute"` } type Address struct { Type string `json:"ip-address-type"` Address string `json:"ip-address"` Prefix int `json:"prefix"` } type Interface struct { Name string `json:"name"` MacAddress string `json:"hardware-address"` Addresses []*Address `json:"ip-addresses"` } type Interfaces struct { Interfaces []*Interface `json:"return"` } func (i *Interfaces) GetAddr(macAddr string) (guestAddr, guestAddr6 string) { macAddr = strings.ToLower(macAddr) if i.Interfaces != nil { for _, iface := range i.Interfaces { if strings.ToLower(iface.MacAddress) != macAddr { continue } if iface.Addresses != nil { for _, addr := range iface.Addresses { if addr.Type == "ipv4" && guestAddr == "" { guestAddr = addr.Address } else if addr.Type == "ipv6" && guestAddr6 == "" { ipAddr := strings.ToLower(addr.Address) if !strings.HasPrefix(ipAddr, "fe") { guestAddr6 = strings.ToLower(addr.Address) } } } } break } } return } func GetInterfaces(sockPath string) (ifaces *Interfaces, err error) { conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ConnectionError{ errors.Wrap(err, "qga: Failed to connect to guest agent"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { return } cmd := &Command{ Execute: "guest-network-get-interfaces", } cmdByte, err := json.Marshal(cmd) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "qga: Failed to parse guest agent command"), } return } _, err = conn.Write(cmdByte) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "qga: Failed to write to guest agent"), } return } buffer := make([]byte, 5000000) n, err := conn.Read(buffer) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "qga: Failed to read from guest agent"), } return } buffer = buffer[:n] respByt := bytes.Trim(buffer, "\x00") respByt = bytes.TrimSpace(respByt) ifaces = &Interfaces{} err = json.Unmarshal(respByt, ifaces) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "qga: Failed to parse guest agent response"), } return } return } ================================================ FILE: qmp/backup.go ================================================ package qmp import ( "path" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type driveBackupArgs struct { Device string `json:"device"` Sync string `json:"sync"` Target string `json:"target"` Format string `json:"format"` } type blockDeviceImage struct { Filename string `json:"filename"` } type blockDeviceInserted struct { Image blockDeviceImage `json:"image"` } type blockDevice struct { Device string `json:"device"` Inserted blockDeviceInserted `json:"inserted"` } type blockDeviceReturn struct { Return []*blockDevice `json:"return"` Error *CommandError `json:"error"` } func driveGetDevice(vmId bson.ObjectID, dsk *disk.Disk) ( name string, err error) { cmd := &Command{ Execute: "query-block", } returnData := &blockDeviceReturn{} err = RunCommand(vmId, cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } if returnData.Return == nil { err = &errortypes.ParseError{ errors.Newf("qmp: Return nil"), } return } for _, blockDev := range returnData.Return { idStr := strings.Split(path.Base( blockDev.Inserted.Image.Filename), ".")[0] diskId, err := bson.ObjectIDFromHex(idStr) if err != nil { continue } if diskId == dsk.Id { name = blockDev.Device break } } return } func driveBackup(vmId bson.ObjectID, dsk *disk.Disk, destPth string) (deviceName string, err error) { deviceName, err = driveGetDevice(vmId, dsk) if err != nil { return } if deviceName == "" { err = &DiskNotFound{ errors.Newf("qmp: Disk not found %s", dsk.Id.Hex()), } return } cmd := &Command{ Execute: "drive-backup", Arguments: &driveBackupArgs{ Device: deviceName, Sync: "full", Target: destPth, Format: "qcow2", }, } returnData := &CommandReturn{} err = RunCommand(vmId, cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } time.Sleep(1 * time.Second) return } func driveBackupCheck(vmId bson.ObjectID, deviceName string) ( complete bool, err error) { cmd := &Command{ Execute: "query-jobs", } returnData := &JobStatusReturn{} err = RunCommand(vmId, cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } if returnData.Return == nil { err = &errortypes.ParseError{ errors.Newf("qmp: Return nil"), } return } for _, status := range returnData.Return { if status.Type == "backup" && status.Id == deviceName && status.Status != "concluded" { return } } complete = true return } func BackupDisk(vmId bson.ObjectID, dsk *disk.Disk, destPth string) (err error) { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "disk_id": dsk.Id.Hex(), }).Info("qmp: Backing up disk") deviceName, err := driveBackup(vmId, dsk, destPth) if err != nil { return } for { complete, e := driveBackupCheck(vmId, deviceName) if e != nil { err = e return } if complete { break } time.Sleep(3 * time.Second) } return } ================================================ FILE: qmp/disk.go ================================================ package qmp import ( "fmt" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/features" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) type blockDevFileArgs struct { Driver string `json:"driver"` NodeName string `json:"node-name"` Aio string `json:"aio"` Discard string `json:"discard"` Filename string `json:"filename"` Cache blockDevCache `json:"cache"` } type blockDevArgs struct { Driver string `json:"driver"` NodeName string `json:"node-name"` File string `json:"file"` Cache blockDevCache `json:"cache"` } type blockDevCache struct { NoFlush bool `json:"no-flush"` Direct bool `json:"direct"` } type deviceAddArgs struct { Id string `json:"id"` Driver string `json:"driver"` Drive string `json:"drive"` Bus string `json:"bus"` } type blockDevEventData struct { Device string `json:"device"` Path string `json:"path"` } type blockDevEvent struct { Event string `json:"event"` Data blockDevEventData `json:"data"` } func AddDisk(vmId bson.ObjectID, dsk *vm.Disk) (err error) { dskId := fmt.Sprintf("fd_%s", dsk.Id.Hex()) dskFileId := fmt.Sprintf("fdf_%s", dsk.Id.Hex()) dskDevId := fmt.Sprintf("fdd_%s", dsk.Id.Hex()) logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "disk_id": dsk.Id.Hex(), "disk_index": dsk.Index, }).Info("qmp: Connecting virtual disk") diskAio := settings.Hypervisor.DiskAio if diskAio == "" { supported, e := features.GetUringSupport() if e != nil { err = e return } if supported { diskAio = "io_uring" } else { diskAio = "threads" } } conn := NewConnection(vmId, true) defer conn.Close() _, err = conn.Connect() if err != nil { return } cmd := &Command{ Execute: "blockdev-add", Arguments: &blockDevFileArgs{ Driver: "file", NodeName: dskFileId, Aio: diskAio, Discard: "unmap", Filename: dsk.Path, Cache: blockDevCache{ NoFlush: false, Direct: true, }, }, } returnData := &CommandReturn{} err = conn.Send(cmd, returnData) if err != nil { return } if returnData.Error != nil && !strings.Contains( strings.ToLower(returnData.Error.Desc), "duplicate", ) { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } cmd = &Command{ Execute: "blockdev-add", Arguments: &blockDevArgs{ Driver: "qcow2", NodeName: dskId, File: dskFileId, Cache: blockDevCache{ NoFlush: false, Direct: true, }, }, } returnData = &CommandReturn{} err = conn.Send(cmd, returnData) if err != nil { return } if returnData.Error != nil && !strings.Contains( strings.ToLower(returnData.Error.Desc), "duplicate", ) { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } cmd = &Command{ Execute: "device_add", Arguments: &deviceAddArgs{ Id: dskDevId, Driver: "virtio-blk-pci", Drive: dskId, Bus: fmt.Sprintf("diskbus%d", dsk.Index), }, } returnData = &CommandReturn{} err = conn.Send(cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } time.Sleep(1 * time.Second) return } func RemoveDisk(vmId bson.ObjectID, dsk *vm.Disk) (err error) { dskId := fmt.Sprintf("fd_%s", dsk.Id.Hex()) dskFileId := fmt.Sprintf("fdf_%s", dsk.Id.Hex()) dskDevId := fmt.Sprintf("fdd_%s", dsk.Id.Hex()) logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "disk_id": dsk.Id.Hex(), "disk_index": dsk.Index, }).Info("qmp: Disconnecting virtual disk") diskAio := settings.Hypervisor.DiskAio if diskAio == "" { supported, e := features.GetUringSupport() if e != nil { err = e return } if supported { diskAio = "io_uring" } else { diskAio = "threads" } } conn := NewConnection(vmId, true) defer conn.Close() conn.SetDeadline(30 * time.Second) _, err = conn.Connect() if err != nil { return } cmd := &Command{ Execute: "device_del", Arguments: &CommandId{ Id: dskDevId, }, } returnData := &CommandReturn{} err = conn.Send(cmd, returnData) if err != nil { return } skipEvent := false if returnData.Error != nil && (strings.Contains( strings.ToLower(returnData.Error.Desc), "process of unplug") || strings.Contains( strings.ToLower(returnData.Error.Desc), "not found") || strings.Contains( strings.ToLower(returnData.Error.Desc), "failed to find")) { skipEvent = true } else if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } if !skipEvent { event := &blockDevEvent{} err = conn.Event(event, func() (resp interface{}, err error) { if event.Event == "DEVICE_DELETED" && event.Data.Device == dskDevId { return } event = &blockDevEvent{} resp = event return }) if err != nil { return } } cmd = &Command{ Execute: "blockdev-del", Arguments: &CommandNode{ NodeName: dskId, }, } returnData = &CommandReturn{} err = conn.Send(cmd, returnData) if err != nil { return } if returnData.Error != nil && !strings.Contains( strings.ToLower(returnData.Error.Desc), "process of unplug") && !strings.Contains( strings.ToLower(returnData.Error.Desc), "not found") && !strings.Contains( strings.ToLower(returnData.Error.Desc), "failed to find") { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } cmd = &Command{ Execute: "blockdev-del", Arguments: &CommandNode{ NodeName: dskFileId, }, } returnData = &CommandReturn{} err = conn.Send(cmd, returnData) if err != nil { return } if returnData.Error != nil && !strings.Contains( strings.ToLower(returnData.Error.Desc), "process of unplug") && !strings.Contains( strings.ToLower(returnData.Error.Desc), "not found") && !strings.Contains( strings.ToLower(returnData.Error.Desc), "failed to find") { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } return } type blockQueryReturn struct { Return []blockQueryDevice `json:"return"` Error *CommandError `json:"error"` } type blockQueryDevice struct { Device string `json:"device"` Locked bool `json:"locked"` Removable bool `json:"removable"` Inserted blockQueryInserted `json:"inserted"` } type blockQueryInserted struct { NodeName string `json:"node-name"` Drv string `json:"drv"` File string `json:"file"` Cache blockQueryCache `json:"cache"` Image blockQueryImage `json:"image"` } type blockQueryCache struct { NoFlush bool `json:"no-flush"` Direct bool `json:"direct"` Writeback bool `json:"writeback"` } type blockQueryImage struct { VirtualSize int64 `json:"virtual-size"` Filename string `json:"filename"` Format string `json:"format"` ActualSize int64 `json:"actual-size"` } type pciQueryReturn struct { Return []pciQueryBus `json:"return"` Error *CommandError `json:"error"` } type pciQueryBus struct { Bus int `json:"bus"` Slot int `json:"slot"` QdevId string `json:"qdev_id"` Devices []pciQueryBus `json:"devices,omitempty"` PciBridge pciQueryBridge `json:"pci_bridge,omitempty"` } type pciQueryBridge struct { Devices []pciQueryDevice `json:"devices,omitempty"` } type pciQueryDevice struct { Bus int `json:"bus"` Slot int `json:"slot"` QdevId string `json:"qdev_id"` } func GetDisks(vmId bson.ObjectID) (info *QemuInfo, disks []*vm.Disk, err error) { conn := NewConnection(vmId, false) defer conn.Close() info, err = conn.Connect() if err != nil { return } cmd := &Command{ Execute: "query-block", } returnData := &blockQueryReturn{} err = conn.Send(cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } disksMap := map[bson.ObjectID]*vm.Disk{} index := 0 for _, disk := range returnData.Return { var idSpl []string if strings.HasPrefix(disk.Device, "disk_") { idSpl = strings.Split(disk.Device, "_") } else if strings.HasPrefix(disk.Inserted.NodeName, "fd_") { idSpl = strings.Split(disk.Inserted.NodeName, "_") } else { continue } if len(idSpl) < 2 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "qmp_names": idSpl, "qmp_device": disk.Device, "qmp_node_name": disk.Inserted.NodeName, "qmp_file": disk.Inserted.File, "qmp_filename": disk.Inserted.Image.Filename, }).Error("qmp: Disk id invalid") continue } dskId, ok := utils.ParseObjectId(idSpl[1]) if !ok { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "qmp_names": idSpl, "qmp_device": disk.Device, "qmp_node_name": disk.Inserted.NodeName, "qmp_file": disk.Inserted.File, "qmp_filename": disk.Inserted.Image.Filename, }).Error("qmp: Disk id parse failed") continue } filename := disk.Inserted.Image.Filename if filename == "" { filename = disk.Inserted.File if filename == "" { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "qmp_names": idSpl, "qmp_device": disk.Device, "qmp_node_name": disk.Inserted.NodeName, "qmp_file": disk.Inserted.File, "qmp_filename": disk.Inserted.Image.Filename, }).Error("qmp: Disk filename invalid") continue } } dsk := &vm.Disk{ Id: dskId, Index: index, Path: filename, } disks = append(disks, dsk) disksMap[dsk.Id] = dsk index += 1 } cmd = &Command{ Execute: "query-pci", } pciReturnData := &pciQueryReturn{} err = conn.Send(cmd, pciReturnData) if err != nil { return } if pciReturnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", pciReturnData.Error.Desc), } return } for _, rootBus := range pciReturnData.Return { if rootBus.Devices == nil { continue } for _, subBus := range rootBus.Devices { if !strings.HasPrefix(subBus.QdevId, "diskbus") || subBus.PciBridge.Devices == nil { continue } for _, device := range subBus.PciBridge.Devices { if !strings.HasPrefix(device.QdevId, "fdd_") { continue } dskIndex, e := strconv.Atoi(subBus.QdevId[7:]) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "qmp_diskbus": subBus.QdevId, "qmp_device": device.QdevId, }).Error("qmp: Disk bus parse failed") continue } dskId, ok := utils.ParseObjectId(device.QdevId[4:]) if !ok { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "qmp_diskbus": subBus.QdevId, "qmp_device": device.QdevId, }).Error("qmp: Disk bus id parse failed") continue } dsk := disksMap[dskId] if dsk == nil { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "disk_id": dskId.Hex(), "qmp_diskbus": subBus.QdevId, "qmp_device": device.QdevId, }).Error("qmp: Unknown disk found") continue } dsk.Index = dskIndex } } } return } ================================================ FILE: qmp/errors.go ================================================ package qmp import "github.com/dropbox/godropbox/errors" type DiskNotFound struct { errors.DropboxError } ================================================ FILE: qmp/password.go ================================================ package qmp import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) const ( Spice = "spice" Vnc = "vnc" ) type setPasswordArgs struct { Protocol string `json:"protocol"` Password string `json:"password"` } func SetPassword(vmId bson.ObjectID, proto, passwd string) (err error) { cmd := &Command{ Execute: "set_password", Arguments: &setPasswordArgs{ Protocol: proto, Password: passwd, }, } returnData := &CommandReturn{} err = RunCommand(vmId, cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } time.Sleep(50 * time.Millisecond) return } ================================================ FILE: qmp/power.go ================================================ package qmp import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) func Shutdown(vmId bson.ObjectID) (err error) { cmd := &Command{ Execute: "system_powerdown", } returnData := &CommandReturn{} err = RunCommand(vmId, cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } time.Sleep(1000 * time.Millisecond) return } ================================================ FILE: qmp/qmp.go ================================================ package qmp import ( "bytes" "encoding/json" "fmt" "net" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/utils" ) type Command struct { Execute string `json:"execute"` Arguments interface{} `json:"arguments,omitempty"` } type CommandId struct { Id string `json:"id"` } type CommandNode struct { NodeName string `json:"node-name"` } type JobStatus struct { Id string `json:"id"` Type string `json:"type"` Status string `json:"status"` } type JobStatusReturn struct { Return []*JobStatus `json:"return"` Error *CommandError `json:"error"` } type QmpVersionData struct { Major int `json:"major"` Minor int `json:"minor"` Micro int `json:"micro"` } type QmpVersion struct { Qemu QmpVersionData `json:"qemu"` } type QmpData struct { Version QmpVersion `json:"version"` } type QmpCapabilities struct { QMP QmpData `json:"QMP"` } type QemuInfo struct { VersionMajor int VersionMinor int VersionMicro int } type CommandError struct { Class string `json:"class"` Desc string `json:"desc"` } type CommandReturn struct { Return interface{} `json:"return"` Error *CommandError `json:"error"` } type EventCallback func() (resp interface{}, err error) var ( socketsLock = utils.NewMultiTimeoutLock(1 * time.Minute) ) type Connection struct { vmId bson.ObjectID sock net.Conn lockId bson.ObjectID deadline time.Duration logging bool command interface{} response interface{} } func (c *Connection) connect() (info *QemuInfo, err error) { // TODO Backward compatibility sockPath := paths.GetQmpSockPath(c.vmId) sockPathOld := paths.GetQmpSockPathOld(c.vmId) exists, err := utils.Exists(sockPath) if err != nil { return } if !exists { sockPath = sockPathOld } c.lockId = socketsLock.Lock(c.vmId.Hex()) c.sock, err = net.DialTimeout( "unix", sockPath, 10*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qmp: Failed to open socket"), } return } deadline := c.deadline if deadline == 0 { deadline = 6 * time.Second } err = c.sock.SetDeadline(time.Now().Add(deadline)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qmp: Failed set deadline"), } return } var infoByt []byte for { buffer := make([]byte, 5000000) n, e := c.sock.Read(buffer) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qmp: Failed to read socket"), } return } buffer = buffer[:n] lines := bytes.Split(buffer, []byte("\n")) for _, line := range lines { if !constants.Production && c.logging { fmt.Println(string(line)) } if bytes.Contains(line, []byte(`"QMP"`)) { infoByt = line break } } if infoByt != nil { break } } if infoByt == nil { err = &errortypes.ReadError{ errors.New("qmp: No info message from socket"), } return } connInfo := &QmpCapabilities{} err = json.Unmarshal(infoByt, connInfo) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "qmp: Failed to unmarshal info '%s'", string(infoByt), ), } return } info = &QemuInfo{ VersionMajor: connInfo.QMP.Version.Qemu.Major, VersionMinor: connInfo.QMP.Version.Qemu.Minor, VersionMicro: connInfo.QMP.Version.Qemu.Micro, } return } func (c *Connection) Close() { sock := c.sock if sock != nil { _ = sock.Close() } socketsLock.Unlock(c.vmId.Hex(), c.lockId) } func (c *Connection) SetDeadline(deadline time.Duration) { c.deadline = deadline } func (c *Connection) Send(command interface{}, resp interface{}) ( err error) { cmdData, err := json.Marshal(command) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "qmp: Failed to marshal command"), } return } if !constants.Production && c.logging { fmt.Println(string(cmdData)) } cmdData = append(cmdData, '\n') _, err = c.sock.Write(cmdData) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qmp: Failed to write socket"), } return } var returnData []byte returnWait := make(chan bool, 2) go func() { defer func() { returnWait <- true }() for { buffer := make([]byte, 5000000) n, e := c.sock.Read(buffer) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qmp: Failed to read socket"), } return } buffer = buffer[:n] lines := bytes.Split(buffer, []byte("\n")) for _, line := range lines { if !constants.Production && c.logging { fmt.Println(string(line)) } if bytes.Contains(line, []byte(`"return"`)) || bytes.Contains(line, []byte(`"error"`)) { returnData = line returnWait <- true return } } } }() <-returnWait if err != nil { return } if returnData == nil { err = &errortypes.ReadError{ errors.New("qmp: No data from socket"), } return } err = json.Unmarshal(returnData, resp) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "qmp: Failed to unmarshal return '%s'", string(returnData), ), } return } return } func (c *Connection) Event(resp interface{}, callback EventCallback) ( err error) { for { buffer := make([]byte, 5000000) n, e := c.sock.Read(buffer) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qmp: Failed to read socket"), } return } buffer = buffer[:n] lines := bytes.Split(buffer, []byte("\n")) for _, line := range lines { if !constants.Production && c.logging { fmt.Println(string(line)) } if bytes.Contains(line, []byte(`"event"`)) { err = json.Unmarshal(line, resp) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "qmp: Failed to unmarshal return '%s'", string(line), ), } return } resp, err = callback() if err != nil || resp == nil { return } } } } return } func (c *Connection) Connect() (info *QemuInfo, err error) { info, err = c.connect() if err != nil { return } initCmd := &Command{ Execute: "qmp_capabilities", } initResp := &CommandReturn{} err = c.Send(initCmd, initResp) if err != nil { return } if initResp.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error '%s'", initResp.Error.Desc), } return } return } func NewConnection(vmId bson.ObjectID, logging bool) (conn *Connection) { conn = &Connection{ vmId: vmId, logging: logging, } return } func RunCommand(vmId bson.ObjectID, cmd interface{}, resp interface{}) (err error) { conn := NewConnection(vmId, true) defer conn.Close() _, err = conn.Connect() if err != nil { return } err = conn.Send(cmd, resp) if err != nil { return } return } ================================================ FILE: qmp/vnc.go ================================================ package qmp import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) type vncPasswordArgs struct { Password string `json:"password"` } func VncPassword(vmId bson.ObjectID, passwd string) (err error) { cmd := &Command{ Execute: "change-vnc-password", Arguments: &vncPasswordArgs{ Password: passwd, }, } returnData := &CommandReturn{} err = RunCommand(vmId, cmd, returnData) if err != nil { return } if returnData.Error != nil { err = &errortypes.ApiError{ errors.Newf("qmp: Return error %s", returnData.Error.Desc), } return } time.Sleep(1 * time.Second) return } ================================================ FILE: qms/disk.go ================================================ package qms import ( "bytes" "fmt" "net" "path" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) func GetDisks(vmId bson.ObjectID) (disks []*vm.Disk, err error) { disks = []*vm.Disk{} sockPath, err := GetSockPath(vmId) if err != nil { return } lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(3 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } buffer := []byte{} for { buf := make([]byte, 5000000) n, e := conn.Read(buf) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to read socket"), } return } buffer = append(buffer, buf[:n]...) if bytes.Contains(bytes.TrimSpace(buffer), []byte("(qemu)")) { break } } _, err = conn.Write([]byte("info block\n")) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } buffer = []byte{} for { buf := make([]byte, 5000000) n, e := conn.Read(buf) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to read socket"), } return } buffer = append(buffer, buf[:n]...) if bytes.Contains(bytes.TrimSpace(buffer), []byte("(qemu)")) { break } } index := 0 for _, line := range strings.Split(string(buffer), "\n") { if len(line) < 10 { continue } // TODO Backwards compatibility if strings.HasPrefix(line, "virtio") { line = strings.Replace(line, "\r", "", -1) if !strings.HasPrefix(line, "virtio") || len(line) < 10 { continue } line = strings.Replace(line, "\r", "", -1) lineSpl := strings.SplitN(line[6:], ":", 2) if len(lineSpl) != 2 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu disk path") continue } indexStr := strings.Fields(strings.TrimSpace(lineSpl[0]))[0] indx, e := strconv.Atoi(indexStr) if e != nil { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu disk path index") continue } diskPath := strings.Fields(strings.TrimSpace(lineSpl[1]))[0] idStr := strings.Split(path.Base(diskPath), ".")[0] diskId, err := bson.ObjectIDFromHex(idStr) if err != nil { continue } dsk := &vm.Disk{ Id: diskId, Index: indx, Path: diskPath, } disks = append(disks, dsk) continue } if !strings.HasPrefix(line, "disk_") && !strings.HasPrefix(line, "fd_") { continue } line = strings.Replace(line, "\r", "", -1) lineSpl := strings.SplitN(line, ":", 2) if len(lineSpl) != 2 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu disk id") continue } idIndexStr := strings.Fields(strings.TrimSpace(lineSpl[0]))[0] if len(idIndexStr) < 6 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu disk id length") continue } idIndexStrSpl := strings.Split(idIndexStr, "_") if len(idIndexStrSpl) < 2 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu disk id invalid") continue } dskId, ok := utils.ParseObjectId(idIndexStrSpl[1]) if !ok { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu disk id parse") continue } diskPath := strings.Fields(strings.TrimSpace(lineSpl[1]))[0] dsk := &vm.Disk{ Id: dskId, Index: index, Path: diskPath, } disks = append(disks, dsk) index += 1 } return } func AddDisk(vmId bson.ObjectID, dsk *vm.Disk, virt *vm.VirtualMachine) (err error) { dskId := fmt.Sprintf("disk_%s", dsk.Id.Hex()) dskDevId := fmt.Sprintf("diskdev_%s", dsk.Id.Hex()) sockPath, err := GetSockPath(vmId) if err != nil { return } logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "disk_path": dsk.Path, }).Info("qemu: Connecting virtual machine disk") lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } drive := fmt.Sprintf( "file=%s,media=disk,format=qcow2,cache=none,"+ "discard=unmap,if=none,id=%s", dsk.Path, dskId, ) _, err = conn.Write([]byte(fmt.Sprintf( "drive_add 0 %s\n", drive, ))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } queues := virt.Processors / 2 if queues > settings.Hypervisor.DiskQueuesMax { queues = settings.Hypervisor.DiskQueuesMax } else if queues < settings.Hypervisor.DiskQueuesMin { queues = settings.Hypervisor.DiskQueuesMin } device := fmt.Sprintf( "virtio-blk-pci,drive=%s,num-queues=%d,id=%s,bus=diskbus%d", dskId, queues, dskDevId, dsk.Index, ) _, err = conn.Write([]byte(fmt.Sprintf( "device_add %s\n", device, ))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } time.Sleep(1 * time.Second) return } func RemoveDisk(vmId bson.ObjectID, dsk *vm.Disk) (err error) { sockPath, err := GetSockPath(vmId) if err != nil { return } logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "disk_path": dsk.Path, }).Info("qemu: Disconnecting virtual machine disk") lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } _, err = conn.Write([]byte( fmt.Sprintf("device_del diskdev_%s\n", dsk.Id.Hex()))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } time.Sleep(50 * time.Millisecond) _, err = conn.Write([]byte( fmt.Sprintf("drive_del disk_%s\n", dsk.Id.Hex()))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } time.Sleep(1 * time.Second) return } ================================================ FILE: qms/power.go ================================================ package qms import ( "net" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) func Shutdown(vmId bson.ObjectID) (err error) { sockPath, err := GetSockPath(vmId) if err != nil { return } lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } _, err = conn.Write([]byte("system_powerdown\n")) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } time.Sleep(500 * time.Millisecond) return } ================================================ FILE: qms/qms.go ================================================ package qms import ( "time" "github.com/pritunl/pritunl-cloud/utils" ) var ( socketsLock = utils.NewMultiTimeoutLock(1 * time.Minute) ) ================================================ FILE: qms/usb.go ================================================ package qms import ( "bytes" "fmt" "net" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) func GetUsbDevices(vmId bson.ObjectID) ( devices []*vm.UsbDevice, err error) { sockPath, err := GetSockPath(vmId) if err != nil { return } lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(3 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } buffer := []byte{} for { buf := make([]byte, 5000000) n, e := conn.Read(buf) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to read socket"), } return } buffer = append(buffer, buf[:n]...) if bytes.Contains(bytes.TrimSpace(buffer), []byte("(qemu)")) { break } } _, err = conn.Write([]byte("info usb\n")) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } buffer = []byte{} for { buf := make([]byte, 5000000) n, e := conn.Read(buf) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "qemu: Failed to read socket"), } return } buffer = append(buffer, buf[:n]...) if bytes.Contains(bytes.TrimSpace(buffer), []byte("(qemu)")) { break } } for _, line := range strings.Split(string(buffer), "\n") { line = strings.TrimSpace(line) if !strings.HasPrefix(line, "Device") || len(line) < 10 { continue } line = strings.Replace(line, "\r", "", -1) if !strings.Contains(line, "ID:") { continue } lineSpl := strings.Split(line, "ID:") if len(lineSpl) != 2 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu usb info") continue } deviceId := strings.Fields(lineSpl[1])[0] if strings.HasPrefix(deviceId, "usb_") { lineSpl = strings.Split(deviceId, "_") if len(lineSpl) != 5 && len(lineSpl) != 6 { logrus.WithFields(logrus.Fields{ "instance_id": vmId.Hex(), "line": line, }).Error("qemu: Unexpected qemu usb id") continue } device := &vm.UsbDevice{ Id: deviceId, Bus: lineSpl[1], Address: lineSpl[2], Vendor: lineSpl[3], Product: lineSpl[4], } devices = append(devices, device) } } return } func AddUsb(virt *vm.VirtualMachine, device *vm.UsbDevice) (err error) { sockPath, err := GetSockPath(virt.Id) if err != nil { return } usbDevice, err := device.GetDevice() if err != nil { return } if usbDevice == nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "usb_vendor": device.Vendor, "usb_product": device.Product, "usb_bus": device.Bus, "usb_address": device.Address, }).Warn("qemu: Failed to find usb device for attachment") return } if usbDevice.Bus == "" || usbDevice.Address == "" || usbDevice.Vendor == "" || usbDevice.Product == "" { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "usb_name": usbDevice.Name, "usb_vendor": usbDevice.Vendor, "usb_product": usbDevice.Product, "usb_bus": usbDevice.Bus, "usb_address": usbDevice.Address, "usb_path": usbDevice.BusPath, }).Warn("qemu: Failed to load usb device info for attachment") return } logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "usb_name": usbDevice.Name, "usb_vendor": usbDevice.Vendor, "usb_product": usbDevice.Product, "usb_bus": usbDevice.Bus, "usb_address": usbDevice.Address, "usb_path": usbDevice.BusPath, }).Info("qemu: Connecting virtual machine usb") err = permission.Chown(virt, usbDevice.BusPath) if err != nil { return } lockId := socketsLock.Lock(virt.Id.Hex()) defer socketsLock.Unlock(virt.Id.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } deviceLine := fmt.Sprintf( "usb-host,hostdevice=%s,id=%s", usbDevice.BusPath, usbDevice.GetQemuId(), ) _, err = conn.Write([]byte( fmt.Sprintf("device_add %s\n", deviceLine))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } time.Sleep(1 * time.Second) return } func RemoveUsb(virt *vm.VirtualMachine, device *vm.UsbDevice) (err error) { sockPath, err := GetSockPath(virt.Id) if err != nil { return } usbDevice, err := device.GetDevice() if err != nil { return } if usbDevice != nil { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "usb_id": device.Id, "usb_name": usbDevice.Name, "usb_vendor": usbDevice.Vendor, "usb_product": usbDevice.Product, "usb_bus": usbDevice.Bus, "usb_address": usbDevice.Address, "usb_path": usbDevice.BusPath, }).Info("qemu: Disconnecting active usb device") } else { logrus.WithFields(logrus.Fields{ "instance_id": virt.Id.Hex(), "usb_id": device.Id, "usb_vendor": device.Vendor, "usb_product": device.Product, "usb_bus": device.Bus, "usb_address": device.Address, }).Info("qemu: Disconnecting inactive usb device") } lockId := socketsLock.Lock(virt.Id.Hex()) defer socketsLock.Unlock(virt.Id.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } if device.Id != "" { _, err = conn.Write([]byte( fmt.Sprintf("device_del %s\n", device.Id))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } } time.Sleep(1 * time.Second) if usbDevice != nil && usbDevice.BusPath != "" { err = permission.Restore(usbDevice.BusPath) if err != nil { return } } return } ================================================ FILE: qms/utils.go ================================================ package qms import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/utils" ) // TODO Backward compatibility func GetSockPath(virtId bson.ObjectID) (pth string, err error) { sockPath := paths.GetSockPath(virtId) sockPathOld := paths.GetSockPathOld(virtId) exists, err := utils.Exists(sockPath) if err != nil { return } if exists { pth = sockPath } else { pth = sockPathOld } return } ================================================ FILE: qms/vnc.go ================================================ package qms import ( "fmt" "net" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) func VncPassword(vmId bson.ObjectID, passwd string) (err error) { sockPath, err := GetSockPath(vmId) if err != nil { return } lockId := socketsLock.Lock(vmId.Hex()) defer socketsLock.Unlock(vmId.Hex(), lockId) conn, err := net.DialTimeout( "unix", sockPath, 3*time.Second, ) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to open socket"), } return } defer conn.Close() err = conn.SetDeadline(time.Now().Add(5 * time.Second)) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed set deadline"), } return } _, err = conn.Write( []byte(fmt.Sprintf("change vnc password\n%s\n", passwd))) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "qemu: Failed to write socket"), } return } time.Sleep(800 * time.Millisecond) return } ================================================ FILE: redirect/acme.go ================================================ package main import ( "sync" "time" ) type Challenge struct { Token string `json:"token"` Response string `json:"response"` } var ( challenges = map[string]*Challenge{} challengesLock = sync.Mutex{} clearTimer *time.Timer timerLock = sync.Mutex{} ) func AddChallenge(chal *Challenge) { timerLock.Lock() if clearTimer != nil { clearTimer.Stop() clearTimer = nil } clearTimer = time.AfterFunc(60*time.Second, func() { challengesLock.Lock() challenges = map[string]*Challenge{} challengesLock.Unlock() timerLock.Lock() clearTimer = nil timerLock.Unlock() }) timerLock.Unlock() challengesLock.Lock() challenges[chal.Token] = chal challengesLock.Unlock() } func GetChallenge(token string) (chal *Challenge) { challengesLock.Lock() chal = challenges[token] challengesLock.Unlock() return } ================================================ FILE: redirect/crypto/crypto.go ================================================ package crypto import ( "crypto/hmac" "crypto/rand" "crypto/sha512" "crypto/subtle" "encoding/base64" "encoding/json" "io" "github.com/pritunl/tools/errors" "github.com/pritunl/tools/errortypes" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/nacl/sign" ) type Message struct { Nonce string Message string Signature string } type AsymNaclHmacKey struct { Key string Secret string PublicKey string PrivateKey string } type AsymNaclHmac struct { key *[32]byte secret *[32]byte publicKey *[32]byte privateKey *[64]byte nonceHandler func(nonce []byte) error } func (a *AsymNaclHmac) RegisterNonce(handler func(nonce []byte) error) { a.nonceHandler = handler } func (a *AsymNaclHmac) Seal(input any) (msg *Message, err error) { if a.key == nil || a.secret == nil || a.privateKey == nil { err = &errortypes.AuthenticationError{ errors.New("crypto: Private key and secret not loaded"), } return } nonce := new([24]byte) _, err = io.ReadFull(rand.Reader, nonce[:]) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to generate nonce"), } return } nonceStr := base64.StdEncoding.EncodeToString(nonce[:]) data, err := json.Marshal(input) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to marshal json data"), } return } encByt := secretbox.Seal(nil, data, nonce, a.key) sigEncByt := sign.Sign(nil, encByt, a.privateKey) sigEncStr := base64.StdEncoding.EncodeToString(sigEncByt) hashFunc := hmac.New(sha512.New, a.secret[:]) hashFunc.Write([]byte(sigEncStr)) rawSignature := hashFunc.Sum(nil) sigStr := base64.StdEncoding.EncodeToString(rawSignature) msg = &Message{ Nonce: nonceStr, Message: sigEncStr, Signature: sigStr, } return } func (a *AsymNaclHmac) SealJson(input any) (output string, err error) { msg, err := a.Seal(input) if err != nil { return } outputByt, err := json.Marshal(msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to marshal message"), } return } output = string(outputByt) return } func (a *AsymNaclHmac) Unseal(msg *Message, output any) (err error) { if a.key == nil || a.secret == nil || a.publicKey == nil { err = &errortypes.AuthenticationError{ errors.New("crypto: Private key and secret not loaded"), } return } hashFunc := hmac.New(sha512.New, a.secret[:]) hashFunc.Write([]byte(msg.Message)) rawSignature := hashFunc.Sum(nil) sigStr := base64.StdEncoding.EncodeToString(rawSignature) if subtle.ConstantTimeCompare([]byte(sigStr), []byte(msg.Signature)) != 1 { err = &errortypes.AuthenticationError{ errors.New("crypto: Invalid message signature"), } return } nonceByt, err := base64.StdEncoding.DecodeString(msg.Nonce) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode nonce"), } return } if len(nonceByt) != 24 { err = &errortypes.ParseError{ errors.New("crypto: Invalid nonce length"), } return } if a.nonceHandler != nil { err = a.nonceHandler(nonceByt) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Nonce validate failed"), } return } } nonce := new([24]byte) copy(nonce[:], nonceByt) sigEncByt, err := base64.StdEncoding.DecodeString(msg.Message) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode message"), } return } encByt, valid := sign.Open(nil, sigEncByt, a.publicKey) if !valid { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to verify message signature"), } return } decByt, ok := secretbox.Open(nil, encByt, nonce, a.key) if !ok { err = &errortypes.AuthenticationError{ errors.New("crypto: Failed to decrypt message"), } return } err = json.Unmarshal(decByt, output) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to unmarshal data"), } return } return } func (a *AsymNaclHmac) UnsealJson(input string, output any) (err error) { msg := &Message{} err = json.Unmarshal([]byte(input), msg) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to unmarshal message"), } return } err = a.Unseal(msg, output) if err != nil { return } return } func (a *AsymNaclHmac) Export() AsymNaclHmacKey { return AsymNaclHmacKey{ Key: base64.StdEncoding.EncodeToString(a.key[:]), Secret: base64.StdEncoding.EncodeToString(a.secret[:]), PublicKey: base64.StdEncoding.EncodeToString(a.publicKey[:]), PrivateKey: base64.StdEncoding.EncodeToString(a.privateKey[:]), } } func (a *AsymNaclHmac) Import(key AsymNaclHmacKey) (err error) { keyByt, err := base64.StdEncoding.DecodeString(key.Key) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode key"), } return } if len(keyByt) != 32 { err = &errortypes.ParseError{ errors.New("crypto: Invalid key length"), } return } secrByt, err := base64.StdEncoding.DecodeString(key.Secret) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode secret key"), } return } if len(secrByt) != 32 { err = &errortypes.ParseError{ errors.New("crypto: Invalid secret key length"), } return } pubKeyByt, err := base64.StdEncoding.DecodeString( key.PublicKey) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to decode public key"), } return } if len(pubKeyByt) != 32 { err = &errortypes.ParseError{ errors.New("crypto: Invalid public key length"), } return } if key.PrivateKey != "" { privKeyByt, e := base64.StdEncoding.DecodeString( key.PrivateKey) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "crypto: Failed to decode private key"), } return } if len(privKeyByt) != 64 { err = &errortypes.ParseError{ errors.New("crypto: Invalid private key length"), } return } if a.privateKey == nil { a.privateKey = new([64]byte) } copy(a.privateKey[:], privKeyByt) } if a.key == nil { a.key = new([32]byte) } if a.secret == nil { a.secret = new([32]byte) } if a.publicKey == nil { a.publicKey = new([32]byte) } copy(a.key[:], keyByt) copy(a.secret[:], secrByt) copy(a.publicKey[:], pubKeyByt) return } func (a *AsymNaclHmac) Generate() (err error) { key := new([32]byte) _, err = io.ReadFull(rand.Reader, key[:]) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to generate key"), } return } secKey := new([32]byte) _, err = io.ReadFull(rand.Reader, secKey[:]) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "crypto: Failed to generate secret key"), } return } signPubKey, signPrivKey, err := sign.GenerateKey(rand.Reader) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "crypto: Failed to generate signing key"), } return } a.key = key a.secret = secKey a.publicKey = signPubKey a.privateKey = signPrivKey return } ================================================ FILE: redirect/go.mod ================================================ module github.com/pritunl/pritunl-cloud/redirect go 1.24.0 toolchain go1.24.6 require ( github.com/pritunl/tools v1.2.6 golang.org/x/crypto v0.45.0 ) require golang.org/x/sys v0.38.0 // indirect ================================================ FILE: redirect/go.sum ================================================ github.com/pritunl/tools v1.2.6 h1:rxqJjmLEHc/SLf8wjWpZX0J+2JoRO0ShbMZ/R19efY8= github.com/pritunl/tools v1.2.6/go.mod h1:BiNzTb2ZCesQ5k/Mx0mhOwGXNJNdZk+4jqg39GjRXKU= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= ================================================ FILE: redirect/main.go ================================================ package main import ( "crypto/tls" "fmt" "io" "net" "net/http" "os" "strconv" "strings" "time" "github.com/pritunl/pritunl-cloud/redirect/crypto" "github.com/pritunl/tools/errors" "github.com/pritunl/tools/errortypes" "github.com/pritunl/tools/logger" ) func main() { publicKey := os.Getenv("PUBLIC_KEY") key := os.Getenv("KEY") secret := os.Getenv("SECRET") os.Unsetenv("PUBLIC_KEY") os.Unsetenv("KEY") os.Unsetenv("SECRET") logger.Init() logger.AddHandler(func(record *logger.Record) { fmt.Print(record.String()) }) err := runServer(publicKey, key, secret) if err != nil { logger.WithFields(logger.Fields{ "error": err, }).Error("redirect: Redirect server error") os.Exit(1) } } func runServer(publicKey, key, secret string) (err error) { webPort, err := strconv.Atoi(os.Getenv("WEB_PORT")) if err != nil { err = &errortypes.ParseError{ errors.Wrapf(err, "redirect: Failed to parse web port"), } return } naclKey := crypto.AsymNaclHmacKey{ PublicKey: publicKey, Key: key, Secret: secret, } box := &crypto.AsymNaclHmac{} err = box.Import(naclKey) if err != nil { return } logger.WithFields(logger.Fields{ "port": 80, "web_port": webPort, }).Info("redirect: Starting HTTP redirect server") go sandboxTest() file := os.NewFile(uintptr(3), "systemd-socket") listener, err := net.FileListener(file) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "redirect: Failed to get socket listener"), } return } server := &http.Server{ Addr: ":80", ReadTimeout: 1 * time.Minute, WriteTimeout: 1 * time.Minute, Handler: http.HandlerFunc(func( w http.ResponseWriter, req *http.Request) { if req.Method == "GET" && strings.HasPrefix(req.URL.Path, "/.well-known/acme-challenge/") { pathSplit := strings.Split(req.URL.Path, "/") token := pathSplit[len(pathSplit)-1] chal := GetChallenge(token) if chal == nil { w.WriteHeader(404) fmt.Fprint(w, "404 page not found") return } w.WriteHeader(200) fmt.Fprint(w, chal.Response) return } else if req.Method == "POST" && req.URL.Path == "/token" { bodyBytes := make([]byte, 8096) n, err := io.LimitReader(req.Body, 8096).Read(bodyBytes) if err != nil && err != io.EOF { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "Internal server error") return } bodyBytes = bodyBytes[:n] chal := &Challenge{} err = box.UnsealJson(string(bodyBytes), chal) if err != nil && err != io.EOF { w.WriteHeader(http.StatusUnauthorized) fmt.Fprint(w, "Failed to authorize") return } AddChallenge(chal) w.WriteHeader(200) fmt.Fprint(w, "success") return } req.URL.Scheme = "https" req.URL.Host = StripPort(req.Host) if webPort != 443 { req.URL.Host += fmt.Sprintf(":%d", webPort) } http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently) }), } err = server.Serve(listener) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "redirect: Failed to bind web server"), } return } return } func sandboxTest() { time.Sleep(3 * time.Second) client := &http.Client{ Timeout: 3 * time.Second, Transport: &http.Transport{ TLSHandshakeTimeout: 3 * time.Second, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, }, } req, err := http.NewRequest( "GET", "https://127.0.0.1", nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Sandbox test request failed"), } return } resp, err := client.Do(req) if err == nil { logger.WithFields(logger.Fields{ "status_code": resp.StatusCode, }).Error("redirect: Sandbox escape test failed") } else { logger.Info("redirect: Sandbox escape test successful") } } ================================================ FILE: redirect/utils.go ================================================ package main import ( "strings" ) func StripPort(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { return hostport } n := strings.Count(hostport, ":") if n > 1 { if i := strings.IndexByte(hostport, ']'); i != -1 { return strings.TrimPrefix(hostport[:i], "[") } return hostport } return hostport[:colon] } ================================================ FILE: relations/definitions/block.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) var Block = relations.Query{ Label: "Block", Collection: "blocks", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "subnets", Label: "Subnets", }}, Relations: []relations.Relation{{ Key: "blocks_ip", Label: "Block IP", From: "blocks_ip", LocalField: "_id", ForeignField: "block", BlockDelete: true, Sort: map[string]int{ "ip": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "ip", Label: "IP", Format: func(vals ...any) any { return utils.Int2IpAddress(vals[0].(int64)).String() }, }, { Key: "instance", }}, Relations: []relations.Relation{{ Key: "instances", Label: "Instance", From: "instances", LocalField: "instance", ForeignField: "_id", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }}, }}, } func init() { relations.Register("block", Block) } ================================================ FILE: relations/definitions/certificate.go ================================================ package definitions import ( "github.com/pritunl/pritunl-cloud/relations" ) var Certificate = relations.Query{ Label: "Certificate", Collection: "certificates", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "nodes", Label: "Node", From: "nodes", LocalField: "_id", ForeignField: "certificates", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "types", Label: "Modes", }, { Key: "admin_domain", Label: "Admin Domain", }, { Key: "user_domain", Label: "User Domain", }, { Key: "webauthn_domain", Label: "WebAuthn Domain", }, { Key: "network_mode", Label: "Network Mode IPv4", }, { Key: "network_mode6", Label: "Network Mode IPv6", }}, }, { Key: "balancers", Label: "Load Balancer", From: "balancers", LocalField: "_id", ForeignField: "certificates", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "state", Label: "State", }}, }}, } func init() { relations.Register("certificate", Certificate) } ================================================ FILE: relations/definitions/datacenter.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Datacenter = relations.Query{ Label: "Datacenter", Collection: "datacenters", Project: []relations.Project{{ Key: "name", Label: "Name", }}, Relations: []relations.Relation{{ Key: "zones", Label: "Zone", From: "zones", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "nodes", Label: "Node", From: "nodes", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "types", Label: "Modes", }, { Key: "admin_domain", Label: "Admin Domain", }, { Key: "user_domain", Label: "User Domain", }, { Key: "webauthn_domain", Label: "WebAuthn Domain", }, { Key: "network_mode", Label: "Network Mode IPv4", }, { Key: "network_mode6", Label: "Network Mode IPv6", }}, }, { Key: "vpcs", Label: "VPC", From: "vpcs", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "vpc_id", Label: "VPC ID", }, { Key: "network", Label: "Network IPv4", }}, }, { Key: "balancers", Label: "Load Balancer", From: "balancers", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "state", Label: "State", }}, }, { Key: "deployments", Label: "Deployment", From: "deployments", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "kind", Label: "Kind", }, { Key: "state", Label: "State", }, { Key: "status", Label: "Status", }, { Key: "timestamp", Label: "Age", Format: func(vals ...any) any { val := vals[0] if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }}, }, { Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "datacenter", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }, { Key: "disks", Label: "Disk", From: "disks", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "index": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "index", Label: "Index", }, { Key: "size", Label: "Size", }}, }, { Key: "nodeports", Label: "Nodeport", From: "nodeports", LocalField: "_id", ForeignField: "datacenter", BlockDelete: true, Sort: map[string]int{ "port": 1, }, Project: []relations.Project{{ Key: "port", Label: "Port", }, { Key: "protocol", Label: "Protocol", }}, }}, } func init() { relations.Register("datacenter", Datacenter) } ================================================ FILE: relations/definitions/definitions.go ================================================ package definitions func Init() { } ================================================ FILE: relations/definitions/firewall.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Firewall = relations.Query{ Label: "Firewall", Collection: "firewalls", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "instances", Label: "Instance", From: "instances", LocalField: "roles", ForeignField: "roles", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }}, } func init() { relations.Register("firewall", Firewall) } ================================================ FILE: relations/definitions/instance.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Instance = relations.Query{ Label: "Instance", Collection: "instances", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }, { Key: "node", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, Relations: []relations.Relation{{ Key: "nodes", Label: "Node", From: "nodes", LocalField: "node", ForeignField: "_id", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "types", Label: "Modes", }, { Key: "network_mode", Label: "Network Mode IPv4", }, { Key: "network_mode6", Label: "Network Mode IPv6", }}, }, { Key: "disks", Label: "Disk", From: "disks", LocalField: "_id", ForeignField: "instance", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "type", Label: "Type", }, { Key: "state", Label: "State", }, { Key: "size", Label: "Size", }}, }, // { // TODO Match organization // Key: "firewalls", // Label: "Firewall", // From: "firewalls", // LocalField: "roles", // ForeignField: "roles", // Sort: map[string]int{ // "name": 1, // }, // Project: []relations.Project{{ // Key: "name", // Label: "Name", // }, { // Key: "roles", // Label: "Roles", // }, { // Key: "ingress", // Label: "Ingress", // Format: func(vals ...any) any { // rules := vals[0].(bson.A) // rulesStr := []string{} // for _, ruleInf := range rules { // rule := ruleInf.(primitive.M) // ruleStr := "" // protocol := rule["protocol"].(string) // port := rule["port"].(string) // sourceIps := rule["source_ips"].(bson.A) // switch protocol { // case firewall.All, firewall.Icmp: // ruleStr = protocol // default: // ruleStr = port + "/" + protocol // } // ruleStr += " (" // sourceIpsLen := len(sourceIps) // for i, sourceIp := range sourceIps { // ruleStr += sourceIp.(string) // if i+1 < sourceIpsLen { // ruleStr += ", " // } // } // ruleStr += ")" // rulesStr = append(rulesStr, ruleStr) // } // return rulesStr // }, // }}, // } }, } func init() { relations.Register("instance", Instance) } ================================================ FILE: relations/definitions/node.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Node = relations.Query{ Label: "Node", Collection: "nodes", Project: []relations.Project{{ Key: "name", Label: "Name", }}, Relations: []relations.Relation{{ Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "node", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }}, }, { Key: "disks", Label: "Disk", From: "disks", LocalField: "_id", ForeignField: "node", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "type", Label: "Type", }, { Key: "state", Label: "State", }, { Key: "size", Label: "Size", }}, }, // { // TODO Match organization // Key: "firewalls", // Label: "Firewall", // From: "firewalls", // LocalField: "roles", // ForeignField: "roles", // Sort: map[string]int{ // "name": 1, // }, // Project: []relations.Project{{ // Key: "name", // Label: "Name", // }, { // Key: "roles", // Label: "Roles", // }, { // Key: "ingress", // Label: "Ingress", // Format: func(vals ...any) any { // rules := vals[0].(bson.A) // rulesStr := []string{} // for _, ruleInf := range rules { // rule := ruleInf.(primitive.M) // ruleStr := "" // protocol := rule["protocol"].(string) // port := rule["port"].(string) // sourceIps := rule["source_ips"].(bson.A) // switch protocol { // case firewall.All, firewall.Icmp: // ruleStr = protocol // default: // ruleStr = port + "/" + protocol // } // ruleStr += " (" // sourceIpsLen := len(sourceIps) // for i, sourceIp := range sourceIps { // ruleStr += sourceIp.(string) // if i+1 < sourceIpsLen { // ruleStr += ", " // } // } // ruleStr += ")" // rulesStr = append(rulesStr, ruleStr) // } // return rulesStr // }, // }}, // } }, } func init() { relations.Register("node", Node) } ================================================ FILE: relations/definitions/organization.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Organization = relations.Query{ Label: "Organization", Collection: "organizations", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "certificates", Label: "Certificate", From: "certificates", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "secrets", Label: "Secret", From: "secrets", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "vpc", Label: "VPCs", From: "vpc", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "domains", Label: "Domain", From: "domains", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "balancers", Label: "Load Balancer", From: "balancers", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "images", Label: "Image", From: "images", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "plans", Label: "Plan", From: "plans", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "disks", Label: "Disk", From: "disks", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "size", Label: "Size", }}, }, { Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }, { Key: "pods", Label: "Pod", From: "pods", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "deployments", Label: "Deployment", From: "deployments", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "firewalls", Label: "Firewall", From: "firewalls", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "authorities", Label: "Authority", From: "authorities", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }, { Key: "alerts", Label: "Alert", From: "alerts", LocalField: "_id", ForeignField: "organization", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }}, }}, } func init() { relations.Register("organization", Organization) } ================================================ FILE: relations/definitions/pod.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Pod = relations.Query{ Label: "Pod", Collection: "pods", Project: []relations.Project{{ Key: "name", Label: "Name", }}, Relations: []relations.Relation{{ Key: "units", Label: "Unit", From: "units", LocalField: "_id", ForeignField: "pod", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "kind", Label: "Kind", }, { Key: "count", Label: "Count", }}, Relations: []relations.Relation{{ Key: "deployments", Label: "Deployment", From: "deployments", LocalField: "_id", ForeignField: "unit", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "kind", Label: "Kind", }, { Key: "state", Label: "State", }, { Key: "status", Label: "Status", }, { Key: "timestamp", Label: "Age", Format: func(vals ...any) any { val := vals[0] if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }}, Relations: []relations.Relation{{ Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "deployment", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }, { Key: "disks", Label: "Disk", From: "disks", LocalField: "_id", ForeignField: "deployment", Sort: map[string]int{ "index": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "index", Label: "Index", }, { Key: "size", Label: "Size", }}, }}, }}, }}, } func init() { relations.Register("pod", Pod) } ================================================ FILE: relations/definitions/policy.go ================================================ package definitions import ( "github.com/pritunl/pritunl-cloud/relations" ) var Policy = relations.Query{ Label: "Policy", Collection: "policies", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "users", Label: "User", From: "users", LocalField: "roles", ForeignField: "roles", Sort: map[string]int{ "username": 1, }, Project: []relations.Project{{ Key: "username", Label: "Username", }, { Key: "type", Label: "Type", }}, }}, } func init() { relations.Register("policy", Policy) } ================================================ FILE: relations/definitions/secret.go ================================================ package definitions import ( "github.com/pritunl/pritunl-cloud/relations" ) var Secret = relations.Query{ Label: "Secret", Collection: "secrets", Project: []relations.Project{{ Key: "name", Label: "Name", }}, Relations: []relations.Relation{{ Key: "domains", Label: "Domain", From: "domains", LocalField: "_id", ForeignField: "secret", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "root_domain", Label: "Root Domain", }}, }, { Key: "certificates", Label: "Certificate", From: "certificates", LocalField: "_id", ForeignField: "acme_secret", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "type", Label: "Type", }, { Key: "acme_domains", Label: "Lets Encrypt Domains", }}, }}, } func init() { relations.Register("secret", Secret) } ================================================ FILE: relations/definitions/shape.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Shape = relations.Query{ Label: "Shape", Collection: "shapes", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "nodes", Label: "Node", From: "nodes", LocalField: "roles", ForeignField: "roles", Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "types", Label: "Modes", }, { Key: "admin_domain", Label: "Admin Domain", }, { Key: "user_domain", Label: "User Domain", }, { Key: "webauthn_domain", Label: "WebAuthn Domain", }, { Key: "network_mode", Label: "Network Mode IPv4", }, { Key: "network_mode6", Label: "Network Mode IPv6", }}, }, { Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "shape", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }}, } func init() { relations.Register("shape", Shape) } ================================================ FILE: relations/definitions/vpc.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Vpc = relations.Query{ Label: "VPC", Collection: "vpcs", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "vpc", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }}, } func init() { relations.Register("vpc", Vpc) } ================================================ FILE: relations/definitions/zone.go ================================================ package definitions import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/vm" ) var Zone = relations.Query{ Label: "Zone", Collection: "zones", Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "roles", Label: "Roles", }}, Relations: []relations.Relation{{ Key: "nodes", Label: "Node", From: "nodes", LocalField: "_id", ForeignField: "zone", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "types", Label: "Modes", }, { Key: "admin_domain", Label: "Admin Domain", }, { Key: "user_domain", Label: "User Domain", }, { Key: "webauthn_domain", Label: "WebAuthn Domain", }, { Key: "network_mode", Label: "Network Mode IPv4", }, { Key: "network_mode6", Label: "Network Mode IPv6", }}, }, { Key: "deployments", Label: "Deployment", From: "deployments", LocalField: "_id", ForeignField: "zone", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "kind", Label: "Kind", }, { Key: "state", Label: "State", }, { Key: "status", Label: "Status", }, { Key: "timestamp", Label: "Age", Format: func(vals ...any) any { val := vals[0] if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }}, }, { Key: "instances", Label: "Instance", From: "instances", LocalField: "_id", ForeignField: "zone", BlockDelete: true, Sort: map[string]int{ "name": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Keys: []string{ "action", "state", }, Label: "Status", Format: func(vals ...any) any { action, _ := vals[0].(string) state, _ := vals[1].(string) switch action { case instance.Start: switch state { case vm.Starting: return "Starting" case vm.Running: return "Running" case vm.Stopped: return "Starting" case vm.Failed: return "Starting" case vm.Updating: return "Updating" case vm.Provisioning: return "Provisioning" case "": return "Provisioning" } case instance.Cleanup: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopping" case vm.Failed: return "Stopping" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopping" case "": return "Stopping" } case instance.Stop: switch state { case vm.Starting: return "Stopping" case vm.Running: return "Stopping" case vm.Stopped: return "Stopped" case vm.Failed: return "Failed" case vm.Updating: return "Updating" case vm.Provisioning: return "Stopped" case "": return "Stopped" } case instance.Restart: return "Restarting" case instance.Destroy: return "Destroying" } return state }, }, { Keys: []string{ "timestamp", "action", "state", }, Label: "Uptime", Format: func(vals ...any) any { val := vals[0] action, _ := vals[1].(string) state, _ := vals[2].(string) isActive := action == instance.Start || state == vm.Running || state == vm.Starting || state == vm.Provisioning if !isActive { return "-" } if mongoTime, ok := val.(bson.DateTime); ok { valTime := mongoTime.Time() return systemd.FormatUptimeShort(valTime) } if goTime, ok := val.(time.Time); ok { return systemd.FormatUptimeShort(goTime) } return "-" }, }, { Key: "processors", Label: "Processors", }, { Key: "memory", Label: "Memory", }, { Key: "private_ips", Label: "Private IPv4", }, { Key: "public_ips", Label: "Public IPv4", }}, }, { Key: "disks", Label: "Disk", From: "disks", LocalField: "_id", ForeignField: "zone", BlockDelete: true, Sort: map[string]int{ "index": 1, }, Project: []relations.Project{{ Key: "name", Label: "Name", }, { Key: "index", Label: "Index", }, { Key: "size", Label: "Size", }}, }}, } func init() { relations.Register("zone", Zone) } ================================================ FILE: relations/registry.go ================================================ package relations import ( "fmt" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/imds/server/errortypes" ) var registry = map[string]Query{} func Register(kind string, definition Query) { registry[kind] = definition } func Aggregate(db *database.Database, kind string, id bson.ObjectID) ( resp *Response, err error) { definition, ok := registry[kind] if !ok { return } definition.Id = id resp, err = definition.Aggregate(db) if err != nil { return } return } func AggregateOrg(db *database.Database, kind string, orgId, id bson.ObjectID) (resp *Response, err error) { definition, ok := registry[kind] if !ok { return } definition.Id = id definition.Organization = orgId resp, err = definition.Aggregate(db) if err != nil { return } return } func blockDelete(resources []Resource) string { for _, resource := range resources { if resource.BlockDelete { return resource.Type } for _, related := range resource.Relations { label := blockDelete(related.Resources) if label != "" { return label } } } return "" } func CanDelete(db *database.Database, kind string, id bson.ObjectID) ( errData *errortypes.ErrorData, err error) { definition, ok := registry[kind] if !ok { return } definition.Id = id resp, err := definition.Aggregate(db) if err != nil { return } if resp.DeleteProtection { errData = &errortypes.ErrorData{ Error: "delete_protected_resource", Message: "Cannot delete resource with delete protection enabled", } return } labels := []string{} for _, related := range resp.Relations { label := blockDelete(related.Resources) if label != "" { labels = append(labels, label) } } if len(labels) > 0 { errData = &errortypes.ErrorData{ Error: "related_resources_exist", Message: fmt.Sprintf( "Related [%s] resources must be deleted first. "+ "Check resource overview", strings.Join(labels, ", "), ), } return } return } func CanDeleteOrg(db *database.Database, kind string, orgId, id bson.ObjectID) (errData *errortypes.ErrorData, err error) { definition, ok := registry[kind] if !ok { return } definition.Id = id definition.Organization = orgId resp, err := definition.Aggregate(db) if err != nil { return } if resp.DeleteProtection { errData = &errortypes.ErrorData{ Error: "delete_protected_resource", Message: "Cannot delete resource with delete protection enabled", } return } labels := []string{} for _, related := range resp.Relations { label := blockDelete(related.Resources) if label != "" { labels = append(labels, label) } } if len(labels) > 0 { errData = &errortypes.ErrorData{ Error: "related_resources_exist", Message: fmt.Sprintf( "Related [%s] resources must be deleted first. "+ "Check resource overview", strings.Join(labels, ", "), ), } return } return } func CanDeleteAll(db *database.Database, kind string, ids []bson.ObjectID) (errData *errortypes.ErrorData, err error) { for _, id := range ids { errData, err = CanDelete(db, kind, id) if err != nil { return } if errData != nil { return } } return } func CanDeleteOrgAll(db *database.Database, kind string, orgId bson.ObjectID, ids []bson.ObjectID) ( errData *errortypes.ErrorData, err error) { for _, id := range ids { errData, err = CanDeleteOrg(db, kind, orgId, id) if err != nil { return } if errData != nil { return } } return } ================================================ FILE: relations/relations.go ================================================ package relations import ( "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) type Query struct { Id any Organization bson.ObjectID Label string Collection string Project []Project Relations []Relation } type Relation struct { Key string Label string From string LocalField string ForeignField string Sort map[string]int Project []Project Relations []Relation BlockDelete bool } type Project struct { Key string Keys []string Label string Format func(values ...any) any } func (r *Query) addRelation(pipeline []bson.M, relation Relation) []bson.M { lookup := bson.M{ "from": relation.From, "localField": relation.LocalField, "foreignField": relation.ForeignField, "as": relation.From, } if len(relation.Project) > 0 || len(relation.Sort) > 0 || len(relation.Relations) > 0 { nestedPipeline := []bson.M{} if len(relation.Project) > 0 { projection := bson.M{ "_id": 1, } for _, proj := range relation.Project { if len(proj.Keys) > 0 { for _, key := range proj.Keys { projection[key] = 1 } } else { projection[proj.Key] = 1 } } nestedPipeline = append(nestedPipeline, bson.M{ "$project": projection, }) } if len(relation.Sort) > 0 { nestedPipeline = append(nestedPipeline, bson.M{ "$sort": relation.Sort, }) } for _, nestedRelation := range relation.Relations { nestedPipeline = r.addRelation(nestedPipeline, nestedRelation) } lookup["pipeline"] = nestedPipeline } return append(pipeline, bson.M{ "$lookup": lookup, }) } func (r *Query) convertToResponse(doc bson.M) *Response { response := &Response{ Id: doc["_id"], Label: r.Label, Fields: []Field{}, Relations: []Related{}, } deleteProtection, _ := doc["delete_protection"].(bool) if deleteProtection { response.DeleteProtection = true } for _, proj := range r.Project { if proj.Label == "" { continue } if len(proj.Keys) > 0 { value, ok := doc[proj.Keys[0]] if ok { if proj.Format != nil { values := []any{} for _, key := range proj.Keys { val, ok := doc[key] if !ok { values = append(values, nil) } else { values = append(values, val) } } value = proj.Format(values...) } response.Fields = append(response.Fields, Field{ Key: proj.Key, Label: proj.Label, Value: value, }) } } else { value, ok := doc[proj.Key] if ok { if proj.Format != nil { value = proj.Format(value) } response.Fields = append(response.Fields, Field{ Key: proj.Key, Label: proj.Label, Value: value, }) } } } for _, relation := range r.Relations { docs, ok := doc[relation.From].(bson.A) if ok { response.Relations = append( response.Relations, r.convertToRelated(relation, docs), ) } } return response } func (r *Query) convertToRelated(relation Relation, docs bson.A) Related { related := Related{ Label: relation.Label, Resources: []Resource{}, } for _, docInf := range docs { var doc bson.M if bsonDoc, ok := docInf.(bson.D); ok { doc = make(bson.M) for _, elem := range bsonDoc { doc[elem.Key] = elem.Value } } else if mapDoc, ok := docInf.(bson.M); ok { doc = mapDoc } else { continue } resource := Resource{ Id: doc["_id"], Type: relation.Label, Fields: []Field{}, Relations: []Related{}, BlockDelete: relation.BlockDelete, } for _, proj := range relation.Project { if proj.Label == "" { continue } if len(proj.Keys) > 0 { value, ok := doc[proj.Keys[0]] if ok { if proj.Format != nil { values := []any{} for _, key := range proj.Keys { val, ok := doc[key] if !ok { values = append(values, nil) } else { values = append(values, val) } } value = proj.Format(values...) } resource.Fields = append(resource.Fields, Field{ Key: proj.Key, Label: proj.Label, Value: value, }) } } else { value, ok := doc[proj.Key] if ok { if proj.Format != nil { value = proj.Format(value) } resource.Fields = append(resource.Fields, Field{ Key: proj.Key, Label: proj.Label, Value: value, }) } } } for _, relation := range relation.Relations { docs, ok := doc[relation.From].(bson.A) if ok { resource.Relations = append( resource.Relations, r.convertToRelated(relation, docs), ) } } related.Resources = append(related.Resources, resource) } return related } func (r *Query) Aggregate(db *database.Database) ( resp *Response, err error) { coll := db.GetCollection(r.Collection) query := bson.M{ "_id": r.Id, } if !r.Organization.IsZero() { query["organization"] = r.Organization } pipeline := []bson.M{ { "$match": query, }, } if len(r.Project) > 0 { projection := bson.M{"_id": 1, "delete_protection": 1} for _, proj := range r.Project { if len(proj.Keys) > 0 { for _, key := range proj.Keys { projection[key] = 1 } } else { projection[proj.Key] = 1 } } pipeline = append(pipeline, bson.M{"$project": projection}) } for _, relation := range r.Relations { pipeline = r.addRelation(pipeline, relation) } cursor, err := coll.Aggregate(db, pipeline) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) var results []bson.M err = cursor.All(db, &results) if err != nil { err = database.ParseError(err) return } if len(results) == 0 { err = &database.NotFoundError{ errors.New("relations: Resource not found"), } return } resp = r.convertToResponse(results[0]) return } ================================================ FILE: relations/response.go ================================================ package relations import ( "fmt" "reflect" "strings" ) type Response struct { Id any Label string Fields []Field Relations []Related DeleteProtection bool } type Related struct { Label string Resources []Resource } type Resource struct { Id any Type string Fields []Field Relations []Related BlockDelete bool } type Field struct { Key string Label string Value any } func (r *Response) Yaml() string { var output strings.Builder output.WriteString(fmt.Sprintf("ID: %v\n", r.Id)) output.WriteString(fmt.Sprintf("Label: %s\n", r.Label)) if len(r.Fields) > 0 { output.WriteString("Fields:\n") for _, field := range r.Fields { output.WriteString(fmt.Sprintf( " %s: %s\n", field.Label, field.yaml(), )) } } if len(r.Relations) > 0 { output.WriteString("Relations:\n") for _, rel := range r.Relations { for _, resource := range rel.Resources { output.WriteString(resource.yaml(0)) } } } return strings.TrimRight(output.String(), "\n") } func (r Resource) yaml(indent int) string { var output strings.Builder indentStr := strings.Repeat(" ", indent) output.WriteString(fmt.Sprintf("%s- ID: %v\n", indentStr, r.Id)) output.WriteString(fmt.Sprintf("%s Type: %s\n", indentStr, r.Type)) if len(r.Fields) > 0 { output.WriteString(fmt.Sprintf("%s Fields:\n", indentStr)) for _, field := range r.Fields { output.WriteString(fmt.Sprintf( "%s %s: %s\n", indentStr, field.Label, field.yaml(), )) } } if len(r.Relations) > 0 { output.WriteString(fmt.Sprintf("%s Relations:\n", indentStr)) for _, rel := range r.Relations { for _, resource := range rel.Resources { output.WriteString(resource.yaml(indent + 2)) } } } return output.String() } func (f Field) yaml() string { if f.Value == nil { return "null" } v := reflect.ValueOf(f.Value) if v.Kind() == reflect.Slice || v.Kind() == reflect.Array { var items []string for i := 0; i < v.Len(); i++ { item := v.Index(i).Interface() items = append(items, fmt.Sprintf("%v", item)) } return "[" + strings.Join(items, ", ") + "]" } if v.Kind() == reflect.String { s := f.Value.(string) if strings.ContainsAny(s, ":#{}[]&*!|>'\"\n") { return "\"" + strings.ReplaceAll(s, "\"", "\\\"") + "\"" } return s } return fmt.Sprintf("%v", f.Value) } ================================================ FILE: relations/utils.go ================================================ package relations import ( "encoding/json" "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) func PrintPipeline(pipeline []bson.M) { println("**************************************************") for _, stage := range pipeline { jsonData, err := json.MarshalIndent(stage, "", " ") if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "relations: Failed to marshal json"), } fmt.Println(err.Error()) continue } fmt.Printf("%s\n", string(jsonData)) } println("**************************************************") } func PrintResults(results []bson.M) { jsonData, err := json.MarshalIndent(results, "", " ") if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "relations: Failed to marshal json"), } fmt.Println(err.Error()) return } println("**************************************************") fmt.Printf("%s\n", string(jsonData)) println("**************************************************") } ================================================ FILE: render/constants.go ================================================ package render const ( RendersDir = "/dev/dri/by-path" ) ================================================ FILE: render/render.go ================================================ package render import ( "io/ioutil" "path" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) var ( renders = []string{} lastRendersSync time.Time ) func GetRenders() (rendrs []string, err error) { if time.Since(lastRendersSync) < 300*time.Second { rendrs = renders return } rendersNew := []string{} exists, err := utils.ExistsDir(RendersDir) if err != nil { return } if !exists { return } renderFiles, err := ioutil.ReadDir(RendersDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "backup: Failed to read renders directory"), } return } for _, item := range renderFiles { name := item.Name() if !strings.Contains(name, "render") { continue } rendersNew = append(rendersNew, item.Name()) } renders = rendersNew lastRendersSync = time.Now() rendrs = rendersNew return } func GetRender(render string) (pth string, err error) { rendrs, err := GetRenders() if err != nil { return } for _, rendr := range rendrs { if rendr == render { pth = path.Join(RendersDir, rendr) return } } err = &errortypes.ReadError{ errors.Newf("render: Failed to find render '%s'", render), } return } ================================================ FILE: requires/errors.go ================================================ package requires import ( "github.com/dropbox/godropbox/errors" ) type InitError struct { errors.DropboxError } ================================================ FILE: requires/requires.go ================================================ // Init system with before and after constraints. package requires import ( "container/list" "fmt" "os" "strings" "github.com/dropbox/godropbox/container/set" ) var ( modules = list.New() ) type Module struct { name string before set.Set after set.Set Handler func() (err error) } func (m *Module) Before(name string) { m.before.Add(name) } func (m *Module) After(name string) { m.after.Add(name) } func New(name string) (module *Module) { module = &Module{ name: name, before: set.NewSet(), after: set.NewSet(), } modules.PushBack(module) return } func Init(ignore []string) { loaded := false ignoreSet := set.NewSet() if ignore != nil { for _, name := range ignore { ignoreSet.Add(name) } } Loop: for count := 0; count < 100; count += 1 { i := modules.Front() for i != nil { module := i.Value.(*Module) j := i.Prev() for j != nil { if module.before.Contains(j.Value.(*Module).name) { modules.MoveBefore(i, j) continue Loop } j = j.Prev() } j = i.Next() for j != nil { if module.after.Contains(j.Value.(*Module).name) { modules.MoveAfter(i, j) continue Loop } j = j.Next() } i = i.Next() } loaded = true break Loop } if !loaded { fmt.Fprint(os.Stderr, "Requires failed to satisfy constraints\n") i := modules.Front() for i != nil { module := i.Value.(*Module) var line strings.Builder line.WriteString(module.name) for val := range module.before.Iter() { line.WriteString(fmt.Sprintf(" before: %s", val.(string))) } for val := range module.after.Iter() { line.WriteString(fmt.Sprintf(" after: %s", val.(string))) } fmt.Fprint(os.Stderr, line.String()+"\n") i = i.Next() } i = modules.Front() Loop2: for i != nil { module := i.Value.(*Module) j := i.Prev() for j != nil { val := j.Value.(*Module).name if module.before.Contains(val) { fmt.Fprintf(os.Stderr, "'%s' not before '%s'\n", module.name, val) break Loop2 } j = j.Prev() } j = i.Next() for j != nil { val := j.Value.(*Module).name if module.after.Contains(val) { fmt.Fprintf(os.Stderr, "'%s' not after '%s'\n", module.name, val) break Loop2 } j = j.Next() } i = i.Next() } os.Exit(1) } i := modules.Front() for i != nil { if !ignoreSet.Contains(i.Value.(*Module).name) { err := i.Value.(*Module).Handler() if err != nil { panic(err) } } i = i.Next() } } ================================================ FILE: rokey/cache.go ================================================ package rokey import ( "fmt" "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" ) var ( cache = map[bson.ObjectID]*Rokey{} cacheLock = sync.RWMutex{} cacheTime = map[string]*Rokey{} cacheTimeLock = sync.RWMutex{} ) func GetCache(typ string, timeblock time.Time) *Rokey { cacheTimeLock.RLock() rkey := cacheTime[fmt.Sprintf("%s-%d", typ, timeblock.Unix())] cacheTimeLock.RUnlock() if rkey != nil && rkey.Type == typ { return rkey } return nil } func GetCacheId(typ string, rkeyId bson.ObjectID) *Rokey { cacheLock.RLock() rkey := cache[rkeyId] cacheLock.RUnlock() if rkey != nil && rkey.Type == typ { return rkey } return nil } func PutCache(rkey *Rokey) { cacheLock.Lock() cache[rkey.Id] = rkey cacheLock.Unlock() cacheTimeLock.Lock() cacheTime[fmt.Sprintf("%s-%d", rkey.Type, rkey.Timeblock.Unix())] = rkey cacheTimeLock.Unlock() } func CleanCache() { cacheLock.Lock() for key, rkey := range cache { if time.Since(rkey.Timestamp) >= 721*time.Hour { delete(cache, key) } } cacheLock.Unlock() cacheTimeLock.Lock() for key, rkey := range cacheTime { if time.Since(rkey.Timestamp) >= 721*time.Hour { delete(cacheTime, key) } } cacheTimeLock.Unlock() } func init() { go func() { time.Sleep(1 * time.Hour) CleanCache() }() } ================================================ FILE: rokey/rokey.go ================================================ package rokey import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" ) type Rokey struct { Id bson.ObjectID `bson:"_id,omitempty" json:"_id"` Type string `bson:"type" json:"type"` Timeblock time.Time `bson:"timeblock" json:"timeblock"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Secret string `bson:"secret" json:"-"` } ================================================ FILE: rokey/utils.go ================================================ package rokey import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, typ string) (rkey *Rokey, err error) { timestamp := time.Now() timeblock := time.Date( timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), 0, 0, 0, timestamp.Location(), ) rkey = GetCache(typ, timeblock) if rkey != nil { return } secret, err := utils.RandStr(64) if err != nil { return } coll := db.Rokeys() rkey = &Rokey{ Type: typ, Timeblock: timeblock, Timestamp: timestamp, Secret: secret, } err = coll.FindOneAndUpdate( db, &bson.M{ "type": typ, "timeblock": timeblock, }, &bson.M{ "$setOnInsert": rkey, }, options.FindOneAndUpdate(). SetUpsert(true). SetReturnDocument(options.After), ).Decode(rkey) if err != nil { err = database.ParseError(err) return } PutCache(rkey) return } func GetId(db *database.Database, typ string, rkeyId bson.ObjectID) (rkey *Rokey, err error) { rkey = GetCacheId(typ, rkeyId) if rkey != nil { return } coll := db.Rokeys() rkey = &Rokey{} err = coll.FindOneId(rkeyId, rkey) if err != nil { if _, ok := err.(*database.NotFoundError); ok { rkey = nil err = nil } else { return } } if rkey != nil { PutCache(rkey) if rkey.Type != typ { rkey = nil } } return } ================================================ FILE: router/certificates.go ================================================ package router import ( "crypto/tls" "crypto/x509" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/sirupsen/logrus" ) type Certificates struct { selfCert *tls.Certificate domainMap map[string]*tls.Certificate wildcardMap map[string]*tls.Certificate } func (c *Certificates) Init() (err error) { if c.domainMap == nil { c.domainMap = map[string]*tls.Certificate{} } if c.wildcardMap == nil { c.wildcardMap = map[string]*tls.Certificate{} } if c.selfCert == nil { err = c.loadSelfCert() if err != nil { return } } return } func (c *Certificates) loadSelfCert() (err error) { certPem, keyPem, err := node.SelfCert() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Web server self certificate error") return } keypair, err := tls.X509KeyPair(certPem, keyPem) if err != nil { err = &errortypes.ReadError{ errors.Wrap( err, "router: Failed to load self certificate", ), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Web server self certificate error") return } c.selfCert = &keypair return } func (c *Certificates) GetCertificate(info *tls.ClientHelloInfo) ( cert *tls.Certificate, err error) { name := strings.ToLower(info.ServerName) for len(name) > 0 && name[len(name)-1] == '.' { name = name[:len(name)-1] } cert = c.domainMap[name] if cert == nil { index := strings.Index(name, ".") if index > 0 { cert = c.wildcardMap[name[index+1:]] } } if cert == nil { cert = c.selfCert } return } func (c *Certificates) Update(db *database.Database, balncs []*balancer.Balancer) (err error) { loaded := set.NewSet() certificates := []*certificate.Certificate{} nodeCerts := node.Self.Certificates if nodeCerts != nil { for _, certId := range nodeCerts { if loaded.Contains(certId) { continue } loaded.Add(certId) cert, e := certificate.Get(db, certId) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { cert = nil err = nil } else { return } } if cert != nil { certificates = append(certificates, cert) } } } for _, balnc := range balncs { for _, certId := range balnc.Certificates { cert, e := certificate.Get(db, certId) if e != nil { err = e if _, ok := err.(*database.NotFoundError); ok { cert = nil err = nil } else { return } } if cert != nil { if cert.Organization != balnc.Organization || loaded.Contains(certId) { continue } loaded.Add(certId) certificates = append(certificates, cert) } } } domainMap := map[string]*tls.Certificate{} wildcardMap := map[string]*tls.Certificate{} for _, cert := range certificates { if cert.Certificate == "" || cert.Key == "" { continue } keypair, e := tls.X509KeyPair( []byte(cert.Certificate), []byte(cert.Key), ) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "router: Failed to load certificate"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Web server certificate error") err = nil continue } tlsCert := &keypair x509Cert := tlsCert.Leaf if x509Cert == nil { var e error x509Cert, e = x509.ParseCertificate(tlsCert.Certificate[0]) if e != nil { continue } } if len(x509Cert.Subject.CommonName) > 0 { if strings.HasPrefix(x509Cert.Subject.CommonName, "*.") { base := strings.Replace( x509Cert.Subject.CommonName, "*.", "", 1, ) wildcardMap[base] = tlsCert } else { domainMap[x509Cert.Subject.CommonName] = tlsCert } } for _, san := range x509Cert.DNSNames { if strings.HasPrefix(san, "*.") { base := strings.Replace(san, "*.", "", 1) wildcardMap[base] = tlsCert } else { domainMap[san] = tlsCert } } } c.domainMap = domainMap c.wildcardMap = wildcardMap return } ================================================ FILE: router/constants.go ================================================ package router import ( "text/template" ) const redirectConfTempl = `# pritunl-zero redirect server environment WEB_PORT={{.WebPort}} PUBLIC_KEY={{.PublicKey}} KEY={{.Key}} SECRET={{.Secret}} ` var ( redirectConf = template.Must( template.New("redirect").Parse(redirectConfTempl)) ) type redirectConfData struct { WebPort int PublicKey string Key string Secret string } ================================================ FILE: router/router.go ================================================ package router import ( "bytes" "context" "crypto/md5" "crypto/tls" "fmt" "io" "math/rand" "net/http" "path" "strconv" "strings" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/acme" "github.com/pritunl/pritunl-cloud/ahandlers" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/crypto" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/proxy" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/uhandlers" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) var ( client = &http.Client{ Timeout: 10 * time.Second, } lastAlertLog = time.Time{} ) type Router struct { nodeHash []byte singleType bool adminType bool userType bool balancerType bool http2 bool port int noRedirectServer bool redirectSystemd bool forceRedirectSystemd bool protocol string adminDomain string userDomain string stateLock sync.Mutex balancers []*balancer.Balancer certificates *Certificates box *crypto.AsymNaclHmac aRouter *gin.Engine uRouter *gin.Engine waiter *sync.WaitGroup lock sync.Mutex redirectServer *http.Server redirectContext context.Context redirectCancel context.CancelFunc webServer *http.Server proxy *proxy.Proxy stop bool } func (r *Router) ServeHTTP(w http.ResponseWriter, re *http.Request) { if node.Self.ForwardedProtoHeader != "" && strings.ToLower(re.Header.Get( node.Self.ForwardedProtoHeader)) == "http" { re.URL.Host = utils.StripPort(re.Host) re.URL.Scheme = "https" http.Redirect(w, re, re.URL.String(), http.StatusMovedPermanently) return } if r.singleType { if r.adminType { r.aRouter.ServeHTTP(w, re) } else if r.userType { r.uRouter.ServeHTTP(w, re) } else if r.balancerType { r.proxy.ServeHTTP(utils.StripPort(re.Host), w, re) } else { utils.WriteStatus(w, 520) } return } else { hst := utils.StripPort(re.Host) if r.adminType && hst == r.adminDomain { r.aRouter.ServeHTTP(w, re) return } else if r.userType && hst == r.userDomain { r.uRouter.ServeHTTP(w, re) return } else if r.balancerType { r.proxy.ServeHTTP(hst, w, re) return } } if re.URL.Path == "/check" { utils.WriteText(w, 200, "ok") return } utils.WriteStatus(w, 404) } func (r *Router) initRedirect() (err error) { if r.redirectSystemd { libPath := settings.Hypervisor.LibPath err = utils.ExistsMkdir(libPath, 0755) if err != nil { return } redirectPth := path.Join(libPath, "redirect.conf") r.box = &crypto.AsymNaclHmac{} err = r.box.Generate() if err != nil { return } key := r.box.Export() redirectOutput := &bytes.Buffer{} redirectData := &redirectConfData{ WebPort: r.port, PublicKey: key.PublicKey, Key: key.Key, Secret: key.Secret, } err = redirectConf.Execute(redirectOutput, redirectData) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "router: Failed to exec redirect template"), } return } err = utils.CreateWrite( redirectPth, redirectOutput.String(), 0600, ) if err != nil { return } } r.redirectServer = &http.Server{ Addr: ":80", ReadTimeout: 1 * time.Minute, WriteTimeout: 1 * time.Minute, IdleTimeout: 1 * time.Minute, MaxHeaderBytes: 8192, Handler: http.HandlerFunc(func( w http.ResponseWriter, req *http.Request) { if strings.HasPrefix(req.URL.Path, acme.AcmePath) { token := acme.ParsePath(req.URL.Path) token = utils.FilterStr(token, 96) if token != "" { chal, err := acme.GetChallenge(token) if err != nil { utils.WriteStatus(w, 400) } else { logrus.WithFields(logrus.Fields{ "token": token, }).Info("router: Acme challenge requested") utils.WriteText(w, 200, chal.Resource) } return } } else if req.URL.Path == "/check" { utils.WriteText(w, 200, "ok") return } newHost := utils.StripPort(req.Host) if r.port != 443 { newHost += fmt.Sprintf(":%d", r.port) } req.URL.Host = newHost req.URL.Scheme = "https" http.Redirect(w, req, req.URL.String(), http.StatusMovedPermanently) }), } return } func (r *Router) redirectChallengeListen(ctx context.Context) { db := database.GetDatabase() defer db.Close() lst, e := event.SubscribeListener(db, []string{"acme"}) if e != nil { select { case <-ctx.Done(): return default: } logrus.WithFields(logrus.Fields{ "error": e, }).Error("acme: Event watch error") return } sub := lst.Listen() defer lst.Close() for { select { case <-ctx.Done(): return case msg, ok := <-sub: if !ok { break } go func() { err := r.sendChallenge(msg.Data) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Failed to send challenge " + "to redirect server") } }() } } } func (r *Router) stopRedirectSystemd() { _, _ = commander.Exec(&commander.Opt{ Name: "systemctl", Args: []string{ "stop", "pritunl-cloud-redirect.service", }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) _, _ = commander.Exec(&commander.Opt{ Name: "systemctl", Args: []string{ "stop", "pritunl-cloud-redirect.socket", }, Timeout: 10 * time.Second, PipeOut: true, PipeErr: true, }) } func (r *Router) startRedirectSystemd() (err error) { r.stopRedirectSystemd() resp, err := commander.Exec(&commander.Opt{ Name: "systemctl", Args: []string{ "start", "pritunl-cloud-redirect.service", }, Timeout: 30 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { logrus.WithFields(resp.Map()).Error( "router: Failed to start systemd redirect server") return } for i := 0; i < 32; i++ { time.Sleep(250 * time.Millisecond) resp, err = commander.Exec(&commander.Opt{ Name: "systemctl", Args: []string{ "is-active", "pritunl-cloud-redirect.service", }, Timeout: 5 * time.Second, PipeOut: true, PipeErr: true, }) if err == nil { return } } r.stopRedirectSystemd() err = &errortypes.ExecError{ errors.New("router: Timeout on systemd redirect server"), } return } func (r *Router) startRedirect() { defer r.waiter.Done() if r.port == 80 || r.noRedirectServer { return } if r.redirectSystemd { defer r.stopRedirectSystemd() err := r.startRedirectSystemd() if err != nil { if r.forceRedirectSystemd { logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Failed to start systemd redirect server") return } else { logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Falling back to main process redirect server") } } else { logrus.WithFields(logrus.Fields{ "production": constants.Production, "protocol": "http", "port": 80, }).Info("router: Started systemd redirect server") ctx, cancel := context.WithCancel(context.Background()) r.redirectContext = ctx r.redirectCancel = cancel for { r.redirectChallengeListen(ctx) select { case <-ctx.Done(): return default: } } } } r.stopRedirectSystemd() logrus.WithFields(logrus.Fields{ "production": constants.Production, "protocol": "http", "port": 80, }).Error("router: Starting fallback main process redirect server") err := r.redirectServer.ListenAndServe() if err != nil { if err == http.ErrServerClosed { err = nil } else { err = &errortypes.UnknownError{ errors.Wrap(err, "router: Server listen failed"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Redirect server error") } } } func (r *Router) sendChallenge(chal any) (err error) { encData, err := r.box.SealJson(chal) if err != nil { return } req, err := http.NewRequest( "POST", "http://127.0.0.1:80/token", bytes.NewReader([]byte(encData)), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Redirect token request failed"), } return } resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "acme: Redirect token request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { logrus.WithFields(logrus.Fields{ "status_code": resp.StatusCode, }).Error("acme: Redirect request bad status") return } return } func (r *Router) initWeb() (err error) { r.adminType = node.Self.IsAdmin() r.userType = node.Self.IsUser() r.balancerType = node.Self.IsBalancer() r.adminDomain = node.Self.AdminDomain r.userDomain = node.Self.UserDomain r.http2 = node.Self.Http2 r.noRedirectServer = node.Self.NoRedirectServer r.redirectSystemd = utils.IsSystemd() || settings.Router.ForceRedirectSystemd r.forceRedirectSystemd = settings.Router.ForceRedirectSystemd if r.adminType && !r.userType && !r.balancerType { r.singleType = true } else if r.userType && !r.balancerType && !r.adminType { r.singleType = true } else if r.balancerType && !r.adminType && !r.userType { r.singleType = true } else { r.singleType = false } r.port = node.Self.Port if r.port == 0 { r.port = 443 } r.protocol = node.Self.Protocol if r.protocol == "" { r.protocol = "https" } if r.adminType { r.aRouter = gin.New() if constants.DebugWeb { r.aRouter.Use(gin.Logger()) } ahandlers.Register(r.aRouter) } if r.userType { r.uRouter = gin.New() if constants.DebugWeb { r.uRouter.Use(gin.Logger()) } uhandlers.Register(r.uRouter) } readTimeout := time.Duration(settings.Router.ReadTimeout) * time.Second readHeaderTimeout := time.Duration( settings.Router.ReadHeaderTimeout) * time.Second writeTimeout := time.Duration(settings.Router.WriteTimeout) * time.Second idleTimeout := time.Duration(settings.Router.IdleTimeout) * time.Second r.webServer = &http.Server{ Addr: fmt.Sprintf(":%d", r.port), Handler: r, ReadTimeout: readTimeout, ReadHeaderTimeout: readHeaderTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, MaxHeaderBytes: settings.Router.MaxHeaderBytes, } if !r.http2 { r.webServer.TLSNextProto = make(map[string]func( *http.Server, *tls.Conn, http.Handler)) } if r.http2 && r.protocol == "http" { h2s := &http2.Server{ IdleTimeout: idleTimeout, ReadIdleTimeout: readTimeout, } r.webServer.Handler = h2c.NewHandler(r, h2s) } return } func (r *Router) startWeb() { defer r.waiter.Done() logrus.WithFields(logrus.Fields{ "production": constants.Production, "protocol": r.protocol, "port": r.port, "http2": r.http2, "read_timeout": settings.Router.ReadTimeout, "write_timeout": settings.Router.WriteTimeout, "idle_timeout": settings.Router.IdleTimeout, "read_header_timeout": settings.Router.ReadHeaderTimeout, }).Info("router: Starting web server") if r.protocol == "http" { err := r.webServer.ListenAndServe() if err != nil { if err == http.ErrServerClosed { err = nil } else { err = &errortypes.UnknownError{ errors.Wrap(err, "router: Server listen failed"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Web server error") return } } } else { tlsConfig := &tls.Config{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, CipherSuites: []uint16{ tls.TLS_AES_128_GCM_SHA256, // 0x1301 tls.TLS_AES_256_GCM_SHA384, // 0x1302 tls.TLS_CHACHA20_POLY1305_SHA256, // 0x1303 tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, // 0xc02b tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // 0xc02f tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, // 0xc02c tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, // 0xc030 tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, // 0xcca9 tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, // 0xcca8 }, GetCertificate: r.certificates.GetCertificate, } if r.http2 { tlsConfig.NextProtos = []string{"h2"} } listener, err := tls.Listen("tcp", r.webServer.Addr, tlsConfig) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "router: TLS listen failed"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Web server TLS error") return } err = r.webServer.Serve(listener) if err != nil { if err == http.ErrServerClosed { err = nil } else { err = &errortypes.UnknownError{ errors.Wrap(err, "router: Server listen failed"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Web server error") return } } } return } func (r *Router) initServers() (err error) { r.lock.Lock() defer r.lock.Unlock() err = r.certificates.Init() if err != nil { return } err = r.updateState() if err != nil { return } err = r.initWeb() if err != nil { return } err = r.initRedirect() if err != nil { return } return } func (r *Router) startServers() { r.lock.Lock() defer r.lock.Unlock() if r.webServer == nil { return } if !r.redirectSystemd && r.redirectServer == nil { return } r.waiter.Add(2) go r.startRedirect() go r.startWeb() time.Sleep(250 * time.Millisecond) return } func (r *Router) Restart() { r.lock.Lock() defer r.lock.Unlock() if r.redirectServer != nil { redirectCtx, redirectCancel := context.WithTimeout( context.Background(), 1*time.Second, ) defer redirectCancel() r.redirectServer.Shutdown(redirectCtx) } if r.webServer != nil { webCtx, webCancel := context.WithTimeout( context.Background(), 1*time.Second, ) defer webCancel() r.webServer.Shutdown(webCtx) } func() { defer func() { recover() }() if r.redirectServer != nil { r.redirectServer.Close() } if r.webServer != nil { r.webServer.Close() } if r.redirectCancel != nil { r.redirectCancel() } }() event.WebSocketsStop() r.redirectServer = nil r.webServer = nil time.Sleep(250 * time.Millisecond) } func (r *Router) Shutdown() { r.stop = true r.Restart() time.Sleep(1 * time.Second) r.Restart() time.Sleep(1 * time.Second) r.Restart() } func (r *Router) hashNode() []byte { hash := md5.New() for _, typ := range node.Self.Types { io.WriteString(hash, typ) } io.WriteString(hash, node.Self.AdminDomain) io.WriteString(hash, node.Self.UserDomain) io.WriteString(hash, strconv.Itoa(node.Self.Port)) io.WriteString(hash, fmt.Sprintf("%t", node.Self.NoRedirectServer)) io.WriteString(hash, fmt.Sprintf("%t", node.Self.Http2)) io.WriteString(hash, node.Self.Protocol) io.WriteString(hash, strconv.Itoa(settings.Router.ReadTimeout)) io.WriteString(hash, strconv.Itoa(settings.Router.ReadHeaderTimeout)) io.WriteString(hash, strconv.Itoa(settings.Router.WriteTimeout)) io.WriteString(hash, strconv.Itoa(settings.Router.IdleTimeout)) io.WriteString(hash, strconv.Itoa(settings.Router.MaxHeaderBytes)) io.WriteString(hash, strconv.FormatBool( utils.IsSystemd() || settings.Router.ForceRedirectSystemd)) io.WriteString(hash, strconv.FormatBool( settings.Router.ForceRedirectSystemd)) return hash.Sum(nil) } func (r *Router) watchNode() { for { time.Sleep(1 * time.Second) if settings.Local.DisableWeb { r.Restart() continue } hash := r.hashNode() if bytes.Compare(r.nodeHash, hash) != 0 { r.nodeHash = hash time.Sleep(time.Duration(rand.Intn(3)) * time.Second) r.Restart() time.Sleep(2 * time.Second) } } } func (r *Router) refreshResolver() { db := database.GetDatabase() defer db.Close() err := proxy.ResolverRefresh(db) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("proxy: Failed to load proxy state") } } func (r *Router) watchResolver() { for { time.Sleep(time.Duration( settings.Router.ProxyResolverRefresh) * time.Second) if node.Self.IsBalancer() { r.refreshResolver() } } } func (r *Router) updateState() (err error) { db := database.GetDatabase() defer db.Close() if node.Self.IsBalancer() { dcId, e := node.Self.GetDatacenter(db) if e != nil { err = e return } balncs, e := balancer.GetAll(db, &bson.M{ "datacenter": dcId, }) if e != nil { r.balancers = []*balancer.Balancer{} return } r.balancers = balncs } else { r.balancers = []*balancer.Balancer{} } r.stateLock.Lock() defer r.stateLock.Unlock() err = r.certificates.Update(db, r.balancers) if err != nil { return } err = r.proxy.Update(db, r.balancers) if err != nil { return } return } func (r *Router) watchState() { for { time.Sleep(4 * time.Second) err := r.updateState() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("proxy: Failed to load proxy state") } } } func (r *Router) Run() (err error) { r.nodeHash = r.hashNode() go r.watchNode() go r.watchState() go r.watchResolver() for { if settings.Local.DisableWeb { if time.Since(lastAlertLog) > 3*time.Minute { lastAlertLog = time.Now() logrus.WithFields(logrus.Fields{ "message": settings.Local.DisableMsg, }).Error("router: Web server disabled from vulnerability alert") } time.Sleep(1 * time.Second) continue } if !node.Self.IsAdmin() && !node.Self.IsUser() && !node.Self.IsBalancer() { time.Sleep(500 * time.Millisecond) continue } r.refreshResolver() err = r.initServers() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("router: Failed to init web servers") time.Sleep(1 * time.Second) continue } r.waiter = &sync.WaitGroup{} r.startServers() r.waiter.Wait() if r.stop { break } } return } func (r *Router) Init() { if constants.DebugWeb { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } r.certificates = &Certificates{} r.proxy = &proxy.Proxy{} r.proxy.Init() } ================================================ FILE: scheduler/constants.go ================================================ package scheduler const ( UnitKind = "unit" InstanceUnitKind = "unit-instance" OffsetCount = 3 OffsetInit = 15 OffsetInc = 10 ) ================================================ FILE: scheduler/scheduler.go ================================================ package scheduler import ( "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" ) type Scheduler struct { Id bson.ObjectID `bson:"_id" json:"id"` Organization bson.ObjectID `bson:"organization" json:"organization"` Pod bson.ObjectID `bson:"pod" json:"pod"` Kind string `bson:"kind" json:"kind"` Created time.Time `bson:"created" json:"created"` Modified time.Time `bson:"modified" json:"modified"` Count int `bson:"count" json:"count"` Spec bson.ObjectID `bson:"spec" json:"spec"` OverrideCount int `bson:"override_count" json:"override_count"` Consumed int `bson:"consumed" json:"consumed"` Tickets TicketsStore `bson:"tickets" json:"tickets"` Failures map[bson.ObjectID]int `bson:"failures" json:"failures"` } type Ticket struct { Node bson.ObjectID `bson:"n" json:"n"` Offset int `bson:"t" json:"t"` } type TicketsStore map[bson.ObjectID][]*Ticket func (s *Scheduler) Refresh(db *database.Database) (exists bool, err error) { coll := db.Schedulers() schd := &Scheduler{} err = coll.FindOne(db, bson.M{ "_id": s.Id, }, database.FindOneProject( "count", "consumed", "tickets", "failures", )).Decode(schd) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } return } exists = true s.Count = schd.Count s.Consumed = schd.Consumed s.Tickets = schd.Tickets s.Failures = schd.Failures return } func (s *Scheduler) ClearTickets(db *database.Database) (err error) { coll := db.Schedulers() schd := &Scheduler{} err = coll.FindOneAndUpdate(db, bson.M{ "_id": s.Id, }, bson.M{ "$unset": bson.M{ "tickets." + node.Self.Id.Hex(): "", }, }, options.FindOneAndUpdate().SetReturnDocument( options.After)).Decode(schd) if err != nil { err = database.ParseError(err) return } s.Count = schd.Count s.Consumed = schd.Consumed s.Tickets = schd.Tickets s.Failures = schd.Failures return } func (s *Scheduler) Failure(db *database.Database) (limit bool, err error) { coll := db.Schedulers() schd := &Scheduler{} if s.Failures == nil { s.Failures = map[bson.ObjectID]int{} } s.Failures[node.Self.Id] += 1 update := bson.M{ "$inc": bson.M{ "failures." + node.Self.Id.Hex(): 1, }, } if s.Failures[node.Self.Id] >= settings.Hypervisor.MaxDeploymentFailures { limit = true update["$unset"] = bson.M{ "tickets." + node.Self.Id.Hex(): "", } } err = coll.FindOneAndUpdate(db, bson.M{ "_id": s.Id, }, update, options.FindOneAndUpdate().SetReturnDocument( options.After)).Decode(schd) if err != nil { err = database.ParseError(err) return } s.Count = schd.Count s.Consumed = schd.Consumed s.Tickets = schd.Tickets s.Failures = schd.Failures return } func (s *Scheduler) Ready() bool { if s.Failures == nil { return true } return s.Failures[node.Self.Id] < settings.Hypervisor.MaxDeploymentFailures } func (s *Scheduler) Consume(db *database.Database) (err error) { coll := db.Schedulers() schd := &Scheduler{} err = coll.FindOneAndUpdate(db, bson.M{ "_id": s.Id, "$expr": bson.M{ "$lt": []interface{}{"$consumed", "$count"}, }, }, bson.M{ "$set": bson.M{ "modified": time.Now(), }, "$inc": bson.M{ "consumed": 1, }, }, options.FindOneAndUpdate().SetReturnDocument( options.After)).Decode(schd) if err != nil { err = database.ParseError(err) return } s.Count = schd.Count s.Consumed = schd.Consumed s.Failures = schd.Failures return } func (s *Scheduler) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { return } func (s *Scheduler) Commit(db *database.Database) (err error) { coll := db.Schedulers() err = coll.Commit(s.Id, s) if err != nil { return } return } func (s *Scheduler) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Schedulers() err = coll.CommitFields(s.Id, s, fields) if err != nil { return } return } func (s *Scheduler) Insert(db *database.Database) (err error) { coll := db.Schedulers() _, err = coll.InsertOne(db, s) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: scheduler/unit.go ================================================ package scheduler import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/sirupsen/logrus" ) type InstanceUnit struct { unit *unit.Unit spec *spec.Spec count int nodes spec.Nodes } func (u *InstanceUnit) Schedule(db *database.Database, count int) (err error) { if u.unit.Kind != deployment.Instance && u.unit.Kind != deployment.Image { err = &errortypes.ParseError{ errors.New("scheduler: Invalid unit kind"), } return } if u.spec.Instance == nil { err = &errortypes.ParseError{ errors.New("scheduler: Missing instance data"), } return } if u.spec.Instance.Shape.IsZero() && u.spec.Instance.Node.IsZero() { err = &errortypes.ParseError{ errors.New("scheduler: Missing shape or node"), } return } overrideCount := 0 if count == 0 { u.count = u.unit.Count - len(u.unit.Deployments) } else { u.count = count overrideCount = len(u.unit.Deployments) + count } schd := &Scheduler{ Id: u.unit.Id, Organization: u.unit.Organization, Pod: u.unit.Pod, Kind: InstanceUnitKind, Spec: u.spec.Id, Count: u.count, OverrideCount: overrideCount, Failures: map[bson.ObjectID]int{}, } if !u.spec.Instance.Node.IsZero() { nde, e := node.Get(db, u.spec.Instance.Node) if e != nil { err = e return } u.nodes = []*node.Node{nde} } else { ndes, offlineCount, noMountCount, e := u.spec.GetAllNodes(db) if e != nil { err = e return } u.nodes = ndes if len(u.nodes) == 0 { logrus.WithFields(logrus.Fields{ "unit": u.unit.Id.Hex(), "shape": u.spec.Instance.Shape.Hex(), "offline_count": offlineCount, "missing_mount_count": noMountCount, }).Error("scheduler: Failed to find nodes to schedule") return } } if u.count == 0 { err = &errortypes.ParseError{ errors.New("scheduler: Cannot schedule zero count unit"), } return } primaryNodes, backupNodes := u.processNodes(u.nodes) var tickets TicketsStore if u.count < len(primaryNodes) { tickets, err = u.scheduleSimple(db, primaryNodes, backupNodes) if err != nil { return } } else { tickets, err = u.scheduleComplex(db, primaryNodes, backupNodes) if err != nil { return } } schd.Tickets = tickets schd.Created = time.Now() schd.Modified = time.Now() logrus.WithFields(logrus.Fields{ "unit": u.unit.Id.Hex(), "count": u.count, "primary_nodes": len(primaryNodes), "backup_nodes": len(backupNodes), "tickets": len(tickets), }).Info("scheduler: Scheduling unit") err = schd.Insert(db) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } return } return } func (u *InstanceUnit) processNodes(nodes spec.Nodes) ( primaryNodes, backupNodes spec.Nodes) { nodes.Sort() for _, nde := range nodes { if nde.SizeResource(u.spec.Instance.Memory, u.spec.Instance.Processors) { primaryNodes = append(primaryNodes, nde) } else { backupNodes = append(backupNodes, nde) } } return } func (u *InstanceUnit) scheduleSimple(db *database.Database, primaryNodes, backupNodes spec.Nodes) (tickets TicketsStore, err error) { tickets = TicketsStore{} count := u.count offset := 0 for _, nde := range primaryNodes { if count <= 0 { count = u.count if offset == 0 { offset += OffsetInit } else { offset += OffsetInc } } tickets[nde.Id] = append(tickets[nde.Id], &Ticket{ Node: nde.Id, Offset: offset, }) count -= 1 } for _, nde := range backupNodes { if count <= 0 { count = u.count if offset == 0 { offset += OffsetInit } else { offset += OffsetInc } } tickets[nde.Id] = append(tickets[nde.Id], &Ticket{ Node: nde.Id, Offset: offset, }) count -= 1 } return } func (u *InstanceUnit) scheduleComplex(db *database.Database, primaryNodes, backupNodes spec.Nodes) (tickets TicketsStore, err error) { tickets = TicketsStore{} count := u.count offset := 0 overscheduled := 0 if primaryNodes.Len() != 0 { for _, nde := range primaryNodes { tickets[nde.Id] = append(tickets[nde.Id], &Ticket{ Node: nde.Id, Offset: offset, }) count -= 1 nde.CpuUnitsRes += u.spec.Instance.Processors nde.MemoryUnitsRes += u.spec.Instance.MemoryUnits() if count <= 0 { break } } } else { for _, nde := range backupNodes { tickets[nde.Id] = append(tickets[nde.Id], &Ticket{ Node: nde.Id, Offset: offset, }) count -= 1 overscheduled += 1 nde.CpuUnitsRes += u.spec.Instance.Processors nde.MemoryUnitsRes += u.spec.Instance.MemoryUnits() if count <= 0 { break } } } for i := 0; i < OffsetCount; i++ { attempts := 0 for attempts = 0; attempts < 100; attempts++ { if count <= 0 { break } for { primaryNodes, _ = u.processNodes(u.nodes) if primaryNodes.Len() == 0 { break } for _, nde := range primaryNodes { tickets[nde.Id] = append(tickets[nde.Id], &Ticket{ Node: nde.Id, Offset: offset, }) count -= 1 nde.CpuUnitsRes += u.spec.Instance.Processors nde.MemoryUnitsRes += u.spec.Instance.MemoryUnits() break } if count <= 0 { break } } if count <= 0 { break } for { _, backupNodes = u.processNodes(u.nodes) if backupNodes.Len() == 0 { break } for _, nde := range backupNodes { tickets[nde.Id] = append(tickets[nde.Id], &Ticket{ Node: nde.Id, Offset: offset, }) count -= 1 if i == 0 { overscheduled += 1 } nde.CpuUnitsRes += u.spec.Instance.Processors nde.MemoryUnitsRes += u.spec.Instance.MemoryUnits() break } if count <= 0 { break } } } if count != 0 { err = &errortypes.ParseError{ errors.Newf("schedule: Count %d remaining after %d "+ "complex schedule attempts", count, attempts), } return } count = u.count if offset == 0 { offset += OffsetInit } else { offset += OffsetInc } } if overscheduled > 0 { logrus.WithFields(logrus.Fields{ "unit": u.unit.Id.Hex(), "kind": u.unit.Kind, "shape": u.spec.Instance.Shape.Hex(), "overscheduled": overscheduled, }).Info("scheduler: Overscheduled unit") } return } func NewInstanceUnit(unt *unit.Unit, spc *spec.Spec) ( instUnit *InstanceUnit) { instUnit = &InstanceUnit{ unit: unt, spec: spc, } return } ================================================ FILE: scheduler/utils.go ================================================ package scheduler import ( "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" ) func Exists(db *database.Database, schdId bson.ObjectID) ( exists bool, err error) { coll := db.Schedulers() schd := &Scheduler{} err = coll.FindOneId(schdId, schd) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } return } exists = true return } func Get(db *database.Database, schdId bson.ObjectID) ( schd *Scheduler, err error) { coll := db.Schedulers() schd = &Scheduler{} err = coll.FindOneId(schdId, schd) if err != nil { return } return } func GetAll(db *database.Database) (schds []*Scheduler, err error) { coll := db.Schedulers() schds = []*Scheduler{} cursor, err := coll.Find(db, bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { schd := &Scheduler{} err = cursor.Decode(schd) if err != nil { err = database.ParseError(err) return } schds = append(schds, schd) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllActive(db *database.Database) (schds []*Scheduler, err error) { coll := db.Schedulers() schds = []*Scheduler{} cursor, err := coll.Find(db, bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { schd := &Scheduler{} err = cursor.Decode(schd) if err != nil { err = database.ParseError(err) return } if schd.Consumed < schd.Count { schds = append(schds, schd) } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, schdId bson.ObjectID) ( deleted bool, err error) { coll := db.Schedulers() resp, err := coll.DeleteOne(db, &bson.M{ "_id": schdId, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if resp.DeletedCount > 0 { deleted = true } return } func Schedule(db *database.Database, unt *unit.Unit) (err error) { exists, e := Exists(db, unt.Id) if e != nil { err = e return } if exists { return } spc, err := spec.Get(db, unt.DeploySpec) if err != nil { return } errData, err := spc.Refresh(db) if err != nil { return } if errData != nil { err = errData.GetError() return } switch unt.Kind { case deployment.Instance, deployment.Image: schd := NewInstanceUnit(unt, spc) err = schd.Schedule(db, 0) if err != nil { return } } return } func ManualSchedule(db *database.Database, unt *unit.Unit, specId bson.ObjectID, count int) ( errData *errortypes.ErrorData, err error) { exists, e := Exists(db, unt.Id) if e != nil { err = e return } if exists { errData = &errortypes.ErrorData{ Error: "scheduler_active", Message: "Cannot schedule deployments while scheduler is active", } return } if specId.IsZero() { specId = unt.DeploySpec } spc, err := spec.Get(db, specId) if err != nil { return } errData, err = spc.Refresh(db) if err != nil { return } if errData != nil { return } if spc.Unit != unt.Id { errData = &errortypes.ErrorData{ Error: "unit_deploy_spec_invalid", Message: "Invalid unit deployment commit", } return } switch unt.Kind { case deployment.Instance, deployment.Image: if unt.Kind == deployment.Image { count = 1 } schd := NewInstanceUnit(unt, spc) err = schd.Schedule(db, count) if err != nil { return } default: err = &errortypes.ParseError{ errors.Newf("scheduler: Unknown unit kind %s", unt.Kind), } return } return } ================================================ FILE: secondary/constants.go ================================================ package secondary import "github.com/pritunl/mongo-go-driver/v2/bson" const ( Duo = "duo" OneLogin = "one_login" Okta = "okta" Push = "push" Phone = "phone" Passcode = "passcode" Sms = "sms" Admin = "admin" AdminDevice = "admin_device" AdminDeviceRegister = "admin_device_register" User = "user" UserDevice = "user_device" UserDeviceRegister = "user_device_register" UserManage = "user_manage" UserManageDevice = "user_manage_device" UserManageDeviceRegister = "user_manage_device_register" ) var ( DeviceProvider, _ = bson.ObjectIDFromHex("100000000000000000000000") ) ================================================ FILE: secondary/duo.go ================================================ package secondary import ( "encoding/json" "net/http" "net/url" "github.com/sirupsen/logrus" "github.com/dropbox/godropbox/errors" duoapi "github.com/duosecurity/duo_api_golang" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" ) type duoApiResp struct { Result string `json:"result"` Status string `json:"status"` StatusMsg string `json:"status_msg"` } type duoApi struct { Stat string `json:"stat"` Code int `json:"code"` Message string `json:"message"` Response duoApiResp `json:"response"` } func duo(db *database.Database, provider *settings.SecondaryProvider, r *http.Request, usr *user.User, factor, passcode string) ( result bool, err error) { if factor == Passcode && passcode == "" { err = &errortypes.AuthenticationError{ errors.New("secondary: Duo passcode empty"), } return } api := duoapi.NewDuoApi( provider.DuoKey, provider.DuoSecret, provider.DuoHostname, "pritunl-cloud", ) query := url.Values{} query.Set("username", usr.Username) query.Set("ipaddr", node.Self.GetRemoteAddr(r)) switch factor { case Push: query.Set("factor", "push") query.Set("device", "auto") break case Phone: query.Set("factor", "phone") query.Set("device", "auto") break case Passcode: query.Set("factor", "passcode") query.Set("passcode", passcode) break case Sms: query.Set("factor", "sms") query.Set("device", "auto") break } resp, data, err := api.SignedCall( "POST", "/auth/v2/auth", query, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Duo auth request failed"), } return } if data == nil { err = &errortypes.RequestError{ errors.Newf( "secondary: Duo auth request failed %d", resp.StatusCode, ), } return } duoData := &duoApi{} err = json.Unmarshal(data, duoData) if err != nil { err = &errortypes.ParseError{ errors.Wrapf( err, "secondary: Failed to parse Duo response %d", resp.StatusCode, ), } return } if resp.StatusCode != 200 { logrus.WithFields(logrus.Fields{ "username": usr.Username, "status_code": resp.StatusCode, "duo_factor": factor, "duo_stat": duoData.Stat, "duo_code": duoData.Code, "duo_msg": duoData.Message, "duo_result": duoData.Response.Result, "duo_status": duoData.Response.Status, "duo_status_msg": duoData.Response.StatusMsg, }).Error("secondary: Duo auth request failed") err = &errortypes.RequestError{ errors.New("secondary: Duo auth request failed"), } } switch duoData.Response.Result { case "allow": err = audit.New( db, r, usr.Id, audit.DuoApprove, audit.Fields{ "duo_factor": factor, }, ) if err != nil { return } result = true break case "deny": if factor != Sms { err = audit.New( db, r, usr.Id, audit.DuoDeny, audit.Fields{ "duo_factor": factor, "duo_status": duoData.Response.Status, "duo_status_msg": duoData.Response.StatusMsg, }, ) if err != nil { return } } break default: logrus.WithFields(logrus.Fields{ "username": usr.Username, "status_code": resp.StatusCode, "duo_factor": factor, "duo_stat": duoData.Stat, "duo_code": duoData.Code, "duo_msg": duoData.Message, "duo_result": duoData.Response.Result, "duo_status": duoData.Response.Status, "duo_status_msg": duoData.Response.StatusMsg, }).Error("secondary: Duo auth request unknown") err = &errortypes.RequestError{ errors.New("secondary: Duo auth request unknown"), } return } return } ================================================ FILE: secondary/errors.go ================================================ package secondary import ( "github.com/dropbox/godropbox/errors" ) type IncompleteError struct { errors.DropboxError } ================================================ FILE: secondary/okta.go ================================================ package secondary import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" ) var ( oktaClient = &http.Client{ Timeout: 20 * time.Second, } ) type oktaProfile struct { Email string `json:"email"` Login string `json:"login"` } type oktaUser struct { Id string `json:"id"` Status string `json:"status"` Profile oktaProfile `json:"profile"` } type oktaFactor struct { Id string `json:"id"` FactorType string `json:"factorType"` Provider string `json:"provider"` Status string `json:"status"` } type oktaVerifyParams struct { Passcode string `json:"passCode,omitempty"` } type oktaLink struct { Href string `json:"href"` } type oktaLinks struct { Poll oktaLink `json:"poll"` } type oktaVerify struct { FactorResult string `json:"factorResult"` Links oktaLinks `json:"_links"` } func okta(db *database.Database, provider *settings.SecondaryProvider, r *http.Request, usr *user.User, factor, passcode string) ( result bool, err error) { if factor != Push && factor != Passcode { err = &errortypes.UnknownError{ errors.New("secondary: Okta invalid factor"), } return } if factor == Passcode && passcode == "" { err = &errortypes.AuthenticationError{ errors.New("secondary: Okta passcode empty"), } return } apiUrl := fmt.Sprintf( "https://%s", provider.OktaDomain, ) apiHeader := fmt.Sprintf( "SSWS %s", provider.OktaToken, ) reqUrl, _ := url.Parse(apiUrl + "/api/v1/users/" + usr.Username) req, err := http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta users request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err := oktaClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta users request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: Okta request error") if err != nil { return } oktaUsr := &oktaUser{} err = json.NewDecoder(resp.Body).Decode(oktaUsr) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: Okta users parse failed"), } return } shortUsername := "" if oktaUsr.Id == "" && strings.Contains(usr.Username, "@") { shortUsername = strings.SplitN(usr.Username, "@", 2)[0] reqUrl, _ = url.Parse(apiUrl + "/api/v1/users/" + shortUsername) req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta users request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err = oktaClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta users request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: Okta request error") if err != nil { return } oktaUsr = &oktaUser{} err = json.NewDecoder(resp.Body).Decode(oktaUsr) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: Okta users parse failed"), } return } } if oktaUsr.Id == "" { err = &errortypes.NotFoundError{ errors.New("secondary: Okta users not found"), } return } if usr.Username != oktaUsr.Profile.Login && usr.Username != oktaUsr.Profile.Email && (shortUsername != "" && shortUsername != oktaUsr.Profile.Login) { err = &errortypes.AuthenticationError{ errors.New("secondary: Okta username mismatch"), } return } if strings.ToLower(oktaUsr.Status) != "active" { err = &errortypes.AuthenticationError{ errors.New("secondary: Okta user is not active"), } return } userId := oktaUsr.Id reqUrl, _ = url.Parse(apiUrl + fmt.Sprintf( "/api/v1/users/%s/factors", userId)) req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta factors request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err = oktaClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta factors request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: Okta request error") if err != nil { return } factors := []*oktaFactor{} err = json.NewDecoder(resp.Body).Decode(&factors) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: Okta factors parse failed"), } return } if len(factors) == 0 { err = &errortypes.NotFoundError{ errors.New("secondary: Okta user has no factors"), } return } factorId := "" for _, fctr := range factors { if fctr.Id == "" { continue } if strings.ToLower(fctr.Status) != "active" || strings.ToLower(fctr.Provider) != "okta" { continue } switch factor { case Push: if strings.ToLower(fctr.FactorType) != "push" { continue } break case Passcode: if strings.ToLower(fctr.FactorType) != "token:software:totp" { continue } break default: continue } factorId = fctr.Id } verifyParams := &oktaVerifyParams{ Passcode: passcode, } verifyBody, err := json.Marshal(verifyParams) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "secondary: Okta failed to parse verify params"), } return } reqUrl, _ = url.Parse(apiUrl + fmt.Sprintf( "/api/v1/users/%s/factors/%s/verify", userId, factorId)) req, err = http.NewRequest( "POST", reqUrl.String(), bytes.NewBuffer(verifyBody), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta verify request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", apiHeader) req.Header.Set("User-Agent", r.UserAgent()) req.Header.Set("X-Forwarded-For", node.Self.GetRemoteAddr(r)) resp, err = oktaClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta verify request failed"), } return } defer resp.Body.Close() err = utils.CheckRequestN( resp, "secondary: Okta request error", []int{200, 201}, ) if err != nil { return } verify := &oktaVerify{} err = json.NewDecoder(resp.Body).Decode(verify) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: Okta verify parse failed"), } return } if strings.ToLower(verify.FactorResult) == "waiting" && verify.Links.Poll.Href != "" { start := time.Now() for { if time.Now().Sub(start) > 45*time.Second { err = audit.New( db, r, usr.Id, audit.OktaDeny, audit.Fields{ "okta_factor": factor, "okta_error": "timeout", }, ) if err != nil { return } result = false return } reqUrl, _ = url.Parse(verify.Links.Poll.Href) req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta verify request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) req.Header.Set("User-Agent", r.UserAgent()) req.Header.Set("X-Forwarded-For", node.Self.GetRemoteAddr(r)) resp, err = oktaClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: Okta verify request failed"), } return } defer resp.Body.Close() err = utils.CheckRequestN( resp, "secondary: Okta request error", []int{200, 201}, ) if err != nil { return } verify = &oktaVerify{} err = json.NewDecoder(resp.Body).Decode(verify) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: Okta verify parse failed"), } return } if strings.ToLower(verify.FactorResult) == "waiting" && verify.Links.Poll.Href != "" { continue } break } } if strings.ToLower(verify.FactorResult) == "success" { err = audit.New( db, r, usr.Id, audit.OktaApprove, audit.Fields{ "okta_factor": factor, }, ) if err != nil { return } result = true } else { err = audit.New( db, r, usr.Id, audit.OktaDeny, audit.Fields{ "okta_factor": factor, }, ) if err != nil { return } result = false } return } ================================================ FILE: secondary/onelogin.go ================================================ package secondary import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" ) var ( oneloginClient = &http.Client{ Timeout: 20 * time.Second, } ) type oneloginAuthParams struct { GrantType string `json:"grant_type"` } type oneloginAuth struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` TokenType string `json:"token_type"` } type oneloginUsersData struct { Id int `json:"id"` Username string `json:"username"` Email string `json:"email"` Status int `json:"status"` } type oneloginUsers struct { Data []oneloginUsersData `json:"data"` } type oneloginOtpDevicesDataDevices struct { Id int `json:"id"` TypeDisplayName string `json:"type_display_name"` UserDisplayName string `json:"user_display_name"` AuthFactorName string `json:"auth_factor_name"` Active bool `json:"boolean"` Default bool `json:"default"` NeedsTrigger bool `json:"needs_trigger"` } type oneloginOtpDevicesData struct { OtpDevices []oneloginOtpDevicesDataDevices `json:"otp_devices"` } type oneloginOtpDevices struct { Data oneloginOtpDevicesData `json:"data"` } type oneloginActivateParams struct { IpAddr string `json:"ipaddr"` } type oneloginActivateData struct { Id int `json:"id"` DeviceId int `json:"device_id"` StateToken string `json:"state_token"` } type oneloginActivate struct { Data []oneloginActivateData `json:"data"` } type oneloginVerifyParams struct { OtpToken string `json:"otp_token,omitempty"` StateToken string `json:"state_token,omitempty"` } type oneloginVerifyStatus struct { Type string `json:"type"` Code int `json:"code"` Message string `json:"message"` Error bool `json:"error"` } type oneloginVerify struct { Status oneloginVerifyStatus `json:"status"` } func onelogin(db *database.Database, provider *settings.SecondaryProvider, r *http.Request, usr *user.User, factor, passcode string) ( result bool, err error) { if factor != Push && factor != Passcode { err = &errortypes.UnknownError{ errors.New("secondary: OneLogin invalid factor"), } return } if factor == Passcode && passcode == "" { err = &errortypes.AuthenticationError{ errors.New("secondary: OneLogin passcode empty"), } return } apiUrl := fmt.Sprintf( "https://api.%s.onelogin.com", provider.OneLoginRegion, ) authParams := &oneloginAuthParams{ GrantType: "client_credentials", } authBody, err := json.Marshal(authParams) if err != nil { err = &errortypes.ParseError{ errors.New("secondary: OneLogin failed to parse auth params"), } return } reqUrl, _ := url.Parse(apiUrl + "/auth/oauth2/v2/token") req, err := http.NewRequest( "POST", reqUrl.String(), bytes.NewBuffer(authBody), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin auth request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set( "Authorization", fmt.Sprintf( "client_id:%s, client_secret:%s", provider.OneLoginId, provider.OneLoginSecret, ), ) resp, err := oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin auth request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: OneLogin request error") if err != nil { return } auth := &oneloginAuth{} err = json.NewDecoder(resp.Body).Decode(auth) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin auth parse failed"), } return } apiHeader := fmt.Sprintf( "bearer:%s", auth.AccessToken, ) reqVals := url.Values{} reqVals.Set("username", usr.Username) reqVals.Set("fields", "id,username,email,status") reqUrl, _ = url.Parse(apiUrl + "/api/1/users") reqUrl.RawQuery = reqVals.Encode() req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin users request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err = oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin users request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: OneLogin request error") if err != nil { return } users := &oneloginUsers{} err = json.NewDecoder(resp.Body).Decode(users) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin users parse failed"), } return } if users.Data == nil || len(users.Data) == 0 { reqVals := url.Values{} reqVals.Set("email", usr.Username) reqVals.Set("fields", "id,username,email,status") reqUrl, _ = url.Parse(apiUrl + "/api/1/users") reqUrl.RawQuery = reqVals.Encode() req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "secondary: OneLogin users request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err = oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "secondary: OneLogin users request failed"), } return } defer resp.Body.Close() users = &oneloginUsers{} err = json.NewDecoder(resp.Body).Decode(users) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin users parse failed"), } return } } shortUsername := "" if (users.Data == nil || len(users.Data) == 0) && strings.Contains(usr.Username, "@") { shortUsername = strings.SplitN(usr.Username, "@", 2)[0] reqVals := url.Values{} reqVals.Set("username", shortUsername) reqVals.Set("fields", "id,username,email,status") reqUrl, _ = url.Parse(apiUrl + "/api/1/users") reqUrl.RawQuery = reqVals.Encode() req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "secondary: OneLogin users request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err = oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "secondary: OneLogin users request failed"), } return } defer resp.Body.Close() users = &oneloginUsers{} err = json.NewDecoder(resp.Body).Decode(users) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin users parse failed"), } return } } if users.Data == nil || len(users.Data) == 0 { err = &errortypes.NotFoundError{ errors.New("secondary: OneLogin user not found"), } return } if users.Data[0].Id == 0 { err = &errortypes.NotFoundError{ errors.New("secondary: OneLogin unknown user ID"), } return } if usr.Username != users.Data[0].Username && usr.Username != users.Data[0].Email && (shortUsername != "" && shortUsername != users.Data[0].Username) { err = &errortypes.AuthenticationError{ errors.New("secondary: OneLogin username mismatch"), } return } if users.Data[0].Status != 1 { err = &errortypes.AuthenticationError{ errors.New("secondary: OneLogin user is not active"), } return } userId := users.Data[0].Id reqUrl, _ = url.Parse(apiUrl + fmt.Sprintf( "/api/1/users/%d/otp_devices", userId, )) req, err = http.NewRequest( "GET", reqUrl.String(), nil, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin devices request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Authorization", apiHeader) resp, err = oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin devices request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: OneLogin request error") if err != nil { return } devices := &oneloginOtpDevices{} err = json.NewDecoder(resp.Body).Decode(devices) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin users parse failed"), } return } if devices.Data.OtpDevices == nil || len(devices.Data.OtpDevices) == 0 { err = &errortypes.NotFoundError{ errors.New("secondary: OneLogin user has no devices"), } return } deviceId := 0 needsTrigger := false for _, device := range devices.Data.OtpDevices { if device.AuthFactorName != "OneLogin Protect" { continue } if device.Default { deviceId = device.Id needsTrigger = device.NeedsTrigger break } else if deviceId == 0 { deviceId = device.Id needsTrigger = device.NeedsTrigger } } if deviceId == 0 { err = &errortypes.NotFoundError{ errors.New("secondary: OneLogin user device type not found"), } return } stateToken := "" if needsTrigger || factor == Push { reqUrl, _ = url.Parse(apiUrl + fmt.Sprintf( "/api/1/users/%d/otp_devices/%d/trigger", userId, deviceId, )) var activateBuffer *bytes.Buffer if factor == Push { activateParams := &oneloginActivateParams{ IpAddr: node.Self.GetRemoteAddr(r), } activateBody, e := json.Marshal(activateParams) if e != nil { err = &errortypes.ParseError{ errors.Wrap( e, "secondary: OneLogin failed to parse activate params", ), } return } activateBuffer = bytes.NewBuffer(activateBody) } req, err = http.NewRequest( "POST", reqUrl.String(), activateBuffer, ) if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "secondary: OneLogin activate request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", apiHeader) req.Header.Set("User-Agent", r.UserAgent()) req.Header.Set("X-Forwarded-For", node.Self.GetRemoteAddr(r)) resp, err = oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap( err, "secondary: OneLogin activate request failed"), } return } defer resp.Body.Close() err = utils.CheckRequest(resp, "secondary: OneLogin request error") if err != nil { return } activate := &oneloginActivate{} err = json.NewDecoder(resp.Body).Decode(activate) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin activate parse failed"), } return } if activate.Data == nil || len(activate.Data) == 0 { err = &errortypes.UnknownError{ errors.New("secondary: OneLogin activate empty data"), } return } if activate.Data[0].Id != userId { err = &errortypes.AuthenticationError{ errors.New("secondary: OneLogin activate user id mismatch"), } return } if activate.Data[0].DeviceId != deviceId { err = &errortypes.AuthenticationError{ errors.New("secondary: OneLogin activate device id mismatch"), } return } if activate.Data[0].StateToken == "" { err = &errortypes.AuthenticationError{ errors.New("secondary: OneLogin activate state token empty"), } return } stateToken = activate.Data[0].StateToken } verifyParams := &oneloginVerifyParams{ OtpToken: passcode, StateToken: stateToken, } verifyBody, err := json.Marshal(verifyParams) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "secondary: OneLogin failed to parse verify params"), } return } start := time.Now() for { if time.Now().Sub(start) > 45*time.Second { err = audit.New( db, r, usr.Id, audit.OneLoginDeny, audit.Fields{ "one_login_factor": factor, "one_login_error": "timeout", }, ) if err != nil { return } result = false return } reqUrl, _ = url.Parse(apiUrl + fmt.Sprintf( "/api/1/users/%d/otp_devices/%d/verify", userId, deviceId, )) req, err = http.NewRequest( "POST", reqUrl.String(), bytes.NewBuffer(verifyBody), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin verify request failed"), } return } req.Header.Set("Accept", "application/json") req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", apiHeader) req.Header.Set("User-Agent", r.UserAgent()) req.Header.Set("X-Forwarded-For", node.Self.GetRemoteAddr(r)) resp, err = oneloginClient.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secondary: OneLogin verify request failed"), } return } defer resp.Body.Close() err = utils.CheckRequestN( resp, "secondary: OneLogin verify request failed", []int{200, 401}, ) if err != nil { return } verify := &oneloginVerify{} err = json.NewDecoder(resp.Body).Decode(verify) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secondary: OneLogin verify parse failed"), } return } if resp.StatusCode == 401 { if strings.Contains( verify.Status.Message, "Authentication pending") { time.Sleep(500 * time.Millisecond) continue } err = audit.New( db, r, usr.Id, audit.OneLoginDeny, audit.Fields{ "one_login_factor": factor, }, ) if err != nil { return } result = false return } if verify.Status.Type != "success" || verify.Status.Code != 200 || verify.Status.Error { err = &errortypes.UnknownError{ errors.New("secondary: OneLogin verify request bad data"), } return } err = audit.New( db, r, usr.Id, audit.OneLoginApprove, audit.Fields{ "one_login_factor": factor, }, ) if err != nil { return } result = true return } return } ================================================ FILE: secondary/secondary.go ================================================ package secondary import ( "bytes" "fmt" "io" "net/http" "net/url" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/webauthn" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type SecondaryData struct { Token string `json:"token"` Label string `json:"label"` Push bool `json:"push"` Phone bool `json:"phone"` Passcode bool `json:"passcode"` Sms bool `json:"sms"` Device bool `json:"device"` DeviceRegister bool `json:"device_register"` } type Secondary struct { usr *user.User `bson:"-"` provider *settings.SecondaryProvider `bson:"-"` Id string `bson:"_id"` ProviderId bson.ObjectID `bson:"provider_id,omitempty"` UserId bson.ObjectID `bson:"user_id"` Type string `bson:"type"` Timestamp time.Time `bson:"timestamp"` PushSent bool `bson:"push_sent"` PhoneSent bool `bson:"phone_sent"` SmsSent bool `bson:"sms_sent"` Disabled bool `bson:"disabled"` WanSession *webauthn.SessionData `bson:"wan_session"` } // TODO Disable secondary after login func (s *Secondary) Push(db *database.Database, r *http.Request) ( errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication has already been completed", } return } if s.PushSent { err = &errortypes.AuthenticationError{ errors.New("secondary: Push already sent"), } return } s.PushSent = true err = s.CommitFields(db, set.NewSet("push_sent")) if err != nil { return } provider, err := s.GetProvider() if err != nil { return } if !provider.PushFactor { err = &errortypes.AuthenticationError{ errors.New("secondary: Push factor not available"), } return } usr, err := s.GetUser(db) if err != nil { return } result := false switch provider.Type { case Duo: result, err = duo(db, provider, r, usr, Push, "") if err != nil { return } break case OneLogin: result, err = onelogin(db, provider, r, usr, Push, "") if err != nil { return } break case Okta: result, err = okta(db, provider, r, usr, Push, "") if err != nil { return } break default: err = &errortypes.UnknownError{ errors.New("secondary: Unknown secondary provider type"), } return } if !result { errData = &errortypes.ErrorData{ Error: "secondary_denied", Message: "Secondary authentication was denied", } return } return } func (s *Secondary) Phone(db *database.Database, r *http.Request) ( errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication has already been completed", } return } if s.PhoneSent { err = &errortypes.AuthenticationError{ errors.New("secondary: Phone already sent"), } return } s.PhoneSent = true err = s.CommitFields(db, set.NewSet("phone_sent")) if err != nil { return } provider, err := s.GetProvider() if err != nil { return } if !provider.PhoneFactor { err = &errortypes.AuthenticationError{ errors.New("secondary: Phone factor not available"), } return } usr, err := s.GetUser(db) if err != nil { return } result := false switch provider.Type { case Duo: result, err = duo(db, provider, r, usr, Phone, "") if err != nil { return } break default: err = &errortypes.UnknownError{ errors.New("secondary: Unknown secondary provider type"), } return } if !result { errData = &errortypes.ErrorData{ Error: "secondary_denied", Message: "Secondary authentication was denied", } return } return } func (s *Secondary) Passcode(db *database.Database, r *http.Request, passcode string) (errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication has already been completed", } return } provider, err := s.GetProvider() if err != nil { return } if !provider.PasscodeFactor { err = &errortypes.AuthenticationError{ errors.New("secondary: Passcode factor not available"), } return } usr, err := s.GetUser(db) if err != nil { return } result := false switch provider.Type { case Duo: result, err = duo(db, provider, r, usr, Passcode, passcode) if err != nil { return } break case OneLogin: result, err = onelogin(db, provider, r, usr, Passcode, passcode) if err != nil { return } break case Okta: result, err = okta(db, provider, r, usr, Passcode, passcode) if err != nil { return } break default: err = &errortypes.UnknownError{ errors.New("secondary: Unknown secondary provider type"), } return } if !result { errData = &errortypes.ErrorData{ Error: "secondary_denied", Message: "Secondary authentication was denied", } return } return } func (s *Secondary) Sms(db *database.Database, r *http.Request) ( errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication has already been completed", } return } if s.SmsSent { err = &errortypes.AuthenticationError{ errors.New("secondary: Sms already sent"), } return } provider, err := s.GetProvider() if err != nil { return } if !provider.SmsFactor { err = &errortypes.AuthenticationError{ errors.New("secondary: Sms factor not available"), } return } usr, err := s.GetUser(db) if err != nil { return } switch provider.Type { case Duo: _, err = duo(db, provider, r, usr, Sms, "") if err != nil { return } break default: err = &errortypes.UnknownError{ errors.New("secondary: Unknown secondary provider type"), } return } s.SmsSent = true err = s.CommitFields(db, set.NewSet("sms_sent")) if err != nil { return } err = &IncompleteError{ errors.New("secondary: Secondary auth is incomplete"), } return } func (s *Secondary) DeviceRegisterRequest(db *database.Database, origin string) (jsonResp interface{}, errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary registration has already been completed", } return } if s.ProviderId != DeviceProvider { err = &errortypes.AuthenticationError{ errors.New("secondary: Device register not available"), } return } if s.WanSession != nil { err = &errortypes.AuthenticationError{ errors.New("secondary: Device registration already requested"), } return } usr, err := s.GetUser(db) if err != nil { return } web, err := node.Self.GetWebauthn(origin, true) if err != nil { return } options, sessionData, err := web.BeginRegistration(usr) if err != nil { err = utils.ParseWebauthnError(err) return } s.WanSession = sessionData err = s.CommitFields(db, set.NewSet("wan_session")) if err != nil { return } jsonResp = options return } func (s *Secondary) DeviceRegisterResponse(db *database.Database, origin string, body io.Reader, name string) ( devc *device.Device, errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary registration has already been completed", } return } if s.ProviderId != DeviceProvider { err = &errortypes.AuthenticationError{ errors.New("secondary: Device register not available"), } return } if s.WanSession == nil { err = &errortypes.AuthenticationError{ errors.New("secondary: Device registration not requested"), } return } usr, err := s.GetUser(db) if err != nil { return } data, err := protocol.ParseCredentialCreationResponseBody(body) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Webauthn parse error"), } return } web, err := node.Self.GetWebauthn(origin, true) if err != nil { return } credential, err := web.CreateCredential(usr, *s.WanSession, data) if err != nil { err = utils.ParseWebauthnError(err) return } devc = device.New(usr.Id, device.WebAuthn, device.Secondary) devc.User = usr.Id devc.Name = name devc.WanRpId = web.Config.RPID devc.MarshalWebauthn(credential) errData, err = devc.Validate(db) if err != nil || errData != nil { return } err = devc.Insert(db) if err != nil { return } return } func (s *Secondary) DeviceRequest(db *database.Database, origin string) ( jsonResp interface{}, errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication has already been completed", } return } if s.ProviderId != DeviceProvider { err = &errortypes.AuthenticationError{ errors.New("secondary: Device sign not available"), } return } if s.WanSession != nil { err = &errortypes.AuthenticationError{ errors.New("secondary: Device sign already requested"), } return } usr, err := s.GetUser(db) if err != nil { return } web, err := node.Self.GetWebauthn(origin, false) if err != nil { return } _, hasU2f, err := usr.LoadWebAuthnDevices(db) if err != nil { return } loginOpts := []webauthn.LoginOption{ webauthn.WithUserVerification(protocol.VerificationPreferred), } if hasU2f { loginOpts = append( loginOpts, webauthn.WithAssertionExtensions( protocol.AuthenticationExtensions{ "appid": settings.Local.AppId, }, ), ) } options, sessionData, err := web.BeginLogin(usr, loginOpts...) if err != nil { err = utils.ParseWebauthnError(err) return } s.WanSession = sessionData err = s.CommitFields(db, set.NewSet("wan_session")) if err != nil { return } jsonResp = options return } func (s *Secondary) DeviceRespond(db *database.Database, origin string, body io.Reader) (errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication has already been completed", } return } if s.ProviderId != DeviceProvider { err = &errortypes.AuthenticationError{ errors.New("secondary: Device sign not available"), } return } if s.WanSession == nil { err = &errortypes.AuthenticationError{ errors.New("secondary: Device sign not requested"), } return } usr, err := s.GetUser(db) if err != nil { return } data, err := protocol.ParseCredentialRequestResponseBody(body) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Webauthn parse error"), } return } web, err := node.Self.GetWebauthn(origin, false) if err != nil { return } devices, _, err := usr.LoadWebAuthnDevices(db) if err != nil { return } credential, err := web.ValidateLogin( usr, *s.WanSession, data) if err != nil { err = utils.ParseWebauthnError(err) logrus.WithFields(logrus.Fields{ "user_id": s.UserId.Hex(), "error": err, }).Error("secondary: Secondary authentication was denied") errData = &errortypes.ErrorData{ Error: "secondary_denied", Message: "Secondary authentication was denied", } return } for _, devc := range devices { if devc.Type == device.U2f { if !bytes.Equal(devc.U2fKeyHandle, credential.ID) { continue } } else if devc.Type == device.WebAuthn { if !bytes.Equal(devc.WanId, credential.ID) || !bytes.Equal(devc.WanPublicKey, credential.PublicKey) { continue } } else { continue } devc.LastActive = time.Now() devc.MarshalWebauthn(credential) err = devc.CommitFields(db, set.NewSet( "last_active", "u2f_counter", "wan_authenticator")) if err != nil { return } return } errData = &errortypes.ErrorData{ Error: "secondary_denied", Message: "Secondary authentication was denied", } return } func (s *Secondary) GetData() (data *SecondaryData, err error) { if s.ProviderId == DeviceProvider { label := "" register := false if strings.Contains(s.Type, "register") { label = "Register Device" register = true } else { label = "Device Authentication" register = false } data = &SecondaryData{ Token: s.Id, Label: label, Push: false, Phone: false, Passcode: false, Sms: false, Device: !register, DeviceRegister: register, } return } provider, err := s.GetProvider() if err != nil { return } data = &SecondaryData{ Token: s.Id, Label: provider.Label, Push: provider.PushFactor, Phone: provider.PhoneFactor, Passcode: provider.PasscodeFactor || provider.SmsFactor, Sms: provider.SmsFactor, } return } func (s *Secondary) GetQuery() (query string, err error) { if s.ProviderId == DeviceProvider { label := "" factor := "" if strings.Contains(s.Type, "register") { label = "Register Device" factor = "device_register" } else { label = "Device Authentication" factor = "device" } query = fmt.Sprintf( "secondary=%s&label=%s&factors=%s", s.Id, url.PathEscape(label), factor, ) return } provider, err := s.GetProvider() if err != nil { return } factors := []string{} if provider.PushFactor { factors = append(factors, "push") } if provider.PhoneFactor { factors = append(factors, "phone") } if provider.PasscodeFactor || provider.SmsFactor { factors = append(factors, "passcode") } if provider.SmsFactor { factors = append(factors, "sms") } query = fmt.Sprintf( "secondary=%s&label=%s&factors=%s", s.Id, url.PathEscape(provider.Label), strings.Join(factors, ","), ) return } func (s *Secondary) Complete(db *database.Database) ( errData *errortypes.ErrorData, err error) { if s.Disabled { errData = &errortypes.ErrorData{ Error: "secondary_disabled", Message: "Secondary authentication is already completed", } return } s.Disabled = true coll := db.SecondaryTokens() resp, err := coll.UpdateOne(db, &bson.M{ "_id": s.Id, "disabled": false, }, &bson.M{ "$set": &bson.M{ "disabled": true, }, }) if err != nil { err = database.ParseError(err) return } if resp.ModifiedCount == 0 { errData = &errortypes.ErrorData{ Error: "secondary_update_disabled", Message: "Secondary authentication update is already completed", } return } return } func (s *Secondary) Handle(db *database.Database, r *http.Request, factor, passcode string) (errData *errortypes.ErrorData, err error) { switch factor { case Push: errData, err = s.Push(db, r) break case Phone: errData, err = s.Phone(db, r) break case Passcode: errData, err = s.Passcode(db, r, passcode) break case Sms: errData, err = s.Sms(db, r) break default: err = &errortypes.UnknownError{ errors.New("secondary: Unknown secondary factor"), } } if err == nil && errData == nil && factor != Sms { errData, err = s.Complete(db) if err != nil || errData != nil { return } } return } func (s *Secondary) GetUser(db *database.Database) ( usr *user.User, err error) { if s.usr != nil { usr = s.usr return } usr, err = user.Get(db, s.UserId) if err != nil { return } s.usr = usr return } func (s *Secondary) GetProvider() (provider *settings.SecondaryProvider, err error) { provider = settings.Auth.GetSecondaryProvider(s.ProviderId) if provider == nil { err = &errortypes.NotFoundError{ errors.New("secondary: Secondary provider not found"), } return } return } func (s *Secondary) Commit(db *database.Database) (err error) { coll := db.SecondaryTokens() err = coll.Commit(s.Id, s) if err != nil { return } return } func (s *Secondary) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.SecondaryTokens() err = coll.CommitFields(s.Id, s, fields) if err != nil { return } return } func (s *Secondary) Insert(db *database.Database) (err error) { coll := db.SecondaryTokens() _, err = coll.InsertOne(db, s) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: secondary/utils.go ================================================ package secondary import ( "math/rand" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) func New(db *database.Database, userId bson.ObjectID, typ string, proivderId bson.ObjectID) (secd *Secondary, err error) { token, err := utils.RandStr(64) if err != nil { return } secd = &Secondary{ Id: token, UserId: userId, Type: typ, ProviderId: proivderId, Timestamp: time.Now(), } err = secd.Insert(db) if err != nil { return } return } func Get(db *database.Database, token string, typ string) ( secd *Secondary, err error) { coll := db.SecondaryTokens() secd = &Secondary{} timestamp := time.Now().Add( -time.Duration(settings.Auth.SecondaryExpire) * time.Second) time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) err = coll.FindOne(db, &bson.M{ "_id": token, "type": typ, "timestamp": &bson.M{ "$gte": timestamp, }, }).Decode(secd) if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, token string) (err error) { coll := db.SecondaryTokens() _, err = coll.DeleteMany(db, &bson.M{ "_id": token, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: secret/constants.go ================================================ package secret import "github.com/pritunl/mongo-go-driver/v2/bson" const ( AWS = "aws" Cloudflare = "cloudflare" OracleCloud = "oracle_cloud" GoogleCloud = "google_cloud" Json = "json" ) var ( Global = bson.NilObjectID ) ================================================ FILE: secret/oracle.go ================================================ package secret import ( "crypto/rsa" "fmt" "github.com/dropbox/godropbox/errors" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/core" "github.com/oracle/oci-go-sdk/v65/dns" "github.com/pritunl/pritunl-cloud/errortypes" ) type OracleProvider struct { privateKey *rsa.PrivateKey tenancy string user string fingerprint string region string compartment string dnsClient *dns.DnsClient computeClient *core.ComputeClient } func (p *OracleProvider) AuthType() (common.AuthConfig, error) { return common.AuthConfig{ AuthType: common.UserPrincipal, IsFromConfigFile: false, OboToken: nil, }, nil } func (p *OracleProvider) PrivateRSAKey() (*rsa.PrivateKey, error) { return p.privateKey, nil } func (p *OracleProvider) KeyID() (string, error) { return fmt.Sprintf("%s/%s/%s", p.tenancy, p.user, p.fingerprint), nil } func (p *OracleProvider) TenancyOCID() (string, error) { return p.tenancy, nil } func (p *OracleProvider) UserOCID() (string, error) { return p.user, nil } func (p *OracleProvider) KeyFingerprint() (string, error) { return p.fingerprint, nil } func (p *OracleProvider) Region() (string, error) { return p.region, nil } func (p *OracleProvider) CompartmentOCID() (string, error) { return p.compartment, nil } func (p *OracleProvider) GetDnsClient() ( dnsClient *dns.DnsClient, err error) { if p.dnsClient != nil { dnsClient = p.dnsClient return } client, err := dns.NewDnsClientWithConfigurationProvider(p) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "secret: Failed to create oracle client"), } return } p.dnsClient = &client dnsClient = p.dnsClient return } func NewOracleProvider(secr *Secret) (prov *OracleProvider, err error) { privateKey, fingerprint, err := loadPrivateKey(secr) if err != nil { return } prov = &OracleProvider{ privateKey: privateKey, tenancy: secr.Key, user: secr.Value, fingerprint: fingerprint, region: secr.Region, compartment: secr.Key, } return } ================================================ FILE: secret/secret.go ================================================ package secret import ( "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Secret struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Organization bson.ObjectID `bson:"organization" json:"organization"` Type string `bson:"type" json:"type"` Key string `bson:"key" json:"key"` Value string `bson:"value" json:"value"` Region string `bson:"region" json:"region"` PublicKey string `bson:"public_key" json:"public_key"` Data string `bson:"data" json:"data"` PrivateKey string `bson:"private_key" json:"-"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` Type string `bson:"type" json:"type"` } func (c *Secret) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { c.Name = utils.FilterName(c.Name) switch c.Type { case AWS, "": c.Type = AWS if c.Region == "" { c.Region = "us-east-1" } break case Cloudflare: c.Value = "" c.Region = "" break case OracleCloud: break case GoogleCloud: c.Value = "" c.Region = "" break case Json: c.Key = "" c.Value = "" c.Region = "" if !JsonValid(c.Data) { errData = &errortypes.ErrorData{ Error: "invalid_secret_json", Message: "Secret json data invalid", } return } break default: errData = &errortypes.ErrorData{ Error: "invalid_secret_type", Message: "Secret type invalid", } return } if c.PrivateKey == "" { privKey, pubKey, e := utils.GenerateRsaKey() if e != nil { err = e return } c.PublicKey = strings.TrimSpace(string(pubKey)) c.PrivateKey = strings.TrimSpace(string(privKey)) } return } func (c *Secret) GetOracleProvider() (prov *OracleProvider, err error) { prov, err = NewOracleProvider(c) if err != nil { return } return } func (c *Secret) Commit(db *database.Database) (err error) { coll := db.Secrets() err = coll.Commit(c.Id, c) if err != nil { return } return } func (c *Secret) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Secrets() err = coll.CommitFields(c.Id, c, fields) if err != nil { return } return } func (c *Secret) Insert(db *database.Database) (err error) { coll := db.Secrets() if !c.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("secret: Secret already exists"), } return } _, err = coll.InsertOne(db, c) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: secret/utils.go ================================================ package secret import ( "bytes" "crypto/md5" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) func JsonValid(data string) bool { var dataMap map[string]any err := json.Unmarshal([]byte(data), &dataMap) if err != nil { return false } for _, value := range dataMap { switch value.(type) { case string, bool, float64, int, int64, nil: continue default: return false } } return true } func Get(db *database.Database, secrId bson.ObjectID) ( secr *Secret, err error) { coll := db.Secrets() secr = &Secret{} err = coll.FindOneId(secrId, secr) if err != nil { return } return } func GetOne(db *database.Database, query *bson.M) (secr *Secret, err error) { coll := db.Secrets() secr = &Secret{} err = coll.FindOne(db, query).Decode(secr) if err != nil { err = database.ParseError(err) return } return } func GetOrg(db *database.Database, orgId, secrId bson.ObjectID) ( secr *Secret, err error) { coll := db.Secrets() secr = &Secret{} err = coll.FindOne(db, &bson.M{ "_id": secrId, "organization": orgId, }).Decode(secr) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( secrs []*Secret, err error) { coll := db.Secrets() secrs = []*Secret{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { secr := &Secret{} err = cursor.Decode(secr) if err != nil { return } secrs = append(secrs, secr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllOrg(db *database.Database, orgId bson.ObjectID) ( secrs []*Secret, err error) { coll := db.Secrets() secrs = []*Secret{} cursor, err := coll.Find(db, &bson.M{ "organization": orgId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { secr := &Secret{} err = cursor.Decode(secr) if err != nil { return } secrs = append(secrs, secr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( secrs []*database.Named, err error) { coll := db.Certificates() secrs = []*database.Named{} cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetProjection(&bson.D{ {"name", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { secr := &database.Named{} err = cursor.Decode(secr) if err != nil { err = database.ParseError(err) return } secrs = append(secrs, secr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (secrs []*Secret, count int64, err error) { coll := db.Secrets() secrs = []*Secret{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { secr := &Secret{} err = cursor.Decode(secr) if err != nil { err = database.ParseError(err) return } secrs = append(secrs, secr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func ExistsOrg(db *database.Database, orgId, secrId bson.ObjectID) ( exists bool, err error) { coll := db.Secrets() n, err := coll.CountDocuments( db, &bson.M{ "_id": secrId, "organization": orgId, }, ) if err != nil { return } if n > 0 { exists = true } return } func Remove(db *database.Database, secrId bson.ObjectID) (err error) { coll := db.Secrets() _, err = coll.DeleteMany(db, &bson.M{ "_id": secrId, }) if err != nil { err = database.ParseError(err) return } return } func RemoveOrg(db *database.Database, orgId, secrId bson.ObjectID) ( err error) { coll := db.Secrets() _, err = coll.DeleteOne(db, &bson.M{ "_id": secrId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, secrIds []bson.ObjectID) ( err error) { coll := db.Secrets() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": secrIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, secrIds []bson.ObjectID) (err error) { coll := db.Secrets() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": secrIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } func loadPrivateKey(secr *Secret) ( key *rsa.PrivateKey, fingerprint string, err error) { block, _ := pem.Decode([]byte(secr.PrivateKey)) if block == nil { err = &errortypes.ParseError{ errors.New("secret: Failed to decode private key"), } return } if block.Type != "RSA PRIVATE KEY" { err = &errortypes.ParseError{ errors.New("secret: Invalid private key type"), } return } key, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secret: Failed to parse rsa key"), } return } pubKey, err := x509.MarshalPKIXPublicKey(key.Public()) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "secret: Failed to marshal public key"), } return } keyHash := md5.New() keyHash.Write(pubKey) fingerprint = fmt.Sprintf("%x", keyHash.Sum(nil)) fingerprintBuf := bytes.Buffer{} for i, run := range fingerprint { fingerprintBuf.WriteRune(run) if i%2 == 1 && i != len(fingerprint)-1 { fingerprintBuf.WriteRune(':') } } fingerprint = fingerprintBuf.String() return } ================================================ FILE: session/constants.go ================================================ package session const ( Admin = "admin" User = "user" ) ================================================ FILE: session/session.go ================================================ // Stores sessions in cookies. package session import ( "crypto/hmac" "crypto/sha512" "crypto/subtle" "encoding/base64" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/rokey" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/useragent" "github.com/pritunl/pritunl-cloud/utils" ) type Session struct { Id string `bson:"_id" json:"id"` Type string `bson:"type" json:"type"` User bson.ObjectID `bson:"user" json:"user"` Rokey bson.ObjectID `bson:"rokey" json:"-"` Secret string `bson:"secret" json:"-"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` LastActive time.Time `bson:"last_active" json:"last_active"` Removed bool `bson:"removed" json:"removed"` Agent *useragent.Agent `bson:"agent" json:"agent"` user *user.User `bson:"-" json:"-"` } func (s *Session) CheckSignature(db *database.Database, inSig string) ( valid bool, err error) { if s.Rokey.IsZero() || s.Secret == "" { return } rkey, err := rokey.GetId(db, s.Type, s.Rokey) if err != nil { return } if rkey == nil { return } if rkey.Secret == "" { err = &errortypes.ReadError{ errors.Wrap(err, "session: Empty secret"), } return } hash := hmac.New(sha512.New, []byte(rkey.Secret)) hash.Write([]byte(s.Secret)) outSig := base64.RawStdEncoding.EncodeToString(hash.Sum(nil)) if subtle.ConstantTimeCompare([]byte(inSig), []byte(outSig)) == 1 { valid = true } return } func (s *Session) GenerateSignature(db *database.Database) ( sig string, err error) { rkey, err := rokey.Get(db, s.Type) if err != nil { return } s.Rokey = rkey.Id s.Secret, err = utils.RandStr(64) if err != nil { return } if rkey.Secret == "" { err = &errortypes.ReadError{ errors.Wrap(err, "session: Empty secret"), } return } hash := hmac.New(sha512.New, []byte(rkey.Secret)) hash.Write([]byte(s.Secret)) sig = base64.RawStdEncoding.EncodeToString(hash.Sum(nil)) return } func (s *Session) Active() bool { if s.Removed { return false } expire := GetExpire(s.Type) maxDuration := GetMaxDuration(s.Type) if expire != 0 { if time.Since(s.LastActive) > expire { return false } } if maxDuration != 0 { if time.Since(s.Timestamp) > maxDuration { return false } } return true } func (s *Session) Update(db *database.Database) (err error) { coll := db.Sessions() err = coll.FindOneId(s.Id, s) if err != nil { return } return } func (s *Session) Remove(db *database.Database) (err error) { err = Remove(db, s.Id) if err != nil { return } return } func (s *Session) GetUser(db *database.Database) (usr *user.User, err error) { if s.user != nil || db == nil { usr = s.user return } usr, err = user.GetUpdate(db, s.User) if err != nil { return } s.user = usr return } ================================================ FILE: session/utils.go ================================================ package session import ( "net/http" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/useragent" "github.com/pritunl/pritunl-cloud/utils" ) func GetExpire(typ string) time.Duration { switch typ { case User: return time.Duration(settings.Auth.UserExpire) * time.Minute default: return time.Duration(settings.Auth.AdminExpire) * time.Minute } } func GetMaxDuration(typ string) time.Duration { switch typ { case User: return time.Duration(settings.Auth.UserMaxDuration) * time.Minute default: return time.Duration(settings.Auth.AdminMaxDuration) * time.Minute } } func Get(db *database.Database, sessId string) ( sess *Session, err error) { coll := db.Sessions() sess = &Session{} err = coll.FindOneId(sessId, sess) if err != nil { return } return } func GetUpdate(db *database.Database, sessId string, r *http.Request, typ, sig string) (sess *Session, err error) { query := bson.M{ "_id": sessId, "removed": &bson.M{ "$ne": true, }, } expire := GetExpire(typ) maxDuration := GetMaxDuration(typ) if expire != 0 { query["last_active"] = &bson.M{ "$gte": time.Now().Add(-expire), } } if maxDuration != 0 { query["timestamp"] = &bson.M{ "$gte": time.Now().Add(-maxDuration), } } coll := db.Sessions() sess = &Session{} timestamp := time.Now() err = coll.FindOneAndUpdate( db, query, &bson.M{ "$set": &bson.M{ "last_active": timestamp, }, }, ).Decode(sess) if err != nil { err = database.ParseError(err) return } sess.LastActive = timestamp valid, err := sess.CheckSignature(db, sig) if err != nil { return } if !valid { sess = nil return } agnt, err := useragent.Parse(db, r) if err != nil { return } if agnt != nil && (sess.Agent == nil || sess.Agent.Diff(agnt)) { sess.Agent = agnt err = coll.UpdateId(sess.Id, &bson.M{ "$set": &bson.M{ "agent": agnt, }, }) if err != nil { err = database.ParseError(err) return } } return } func GetAll(db *database.Database, userId bson.ObjectID, includeRemoved bool) (sessions []*Session, err error) { coll := db.Sessions() sessions = []*Session{} cursor, err := coll.Find(db, &bson.M{ "user": userId, }) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { sess := &Session{} err = cursor.Decode(sess) if err != nil { err = database.ParseError(err) return } if !sess.Active() { if !includeRemoved { continue } sess.Removed = true } sessions = append(sessions, sess) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func New(db *database.Database, r *http.Request, userId bson.ObjectID, typ string) (sess *Session, sig string, err error) { id, err := utils.RandStr(32) if err != nil { return } agnt, err := useragent.Parse(db, r) if err != nil { return } coll := db.Sessions() sess = &Session{ Id: id, Type: typ, User: userId, Timestamp: time.Now(), LastActive: time.Now(), Agent: agnt, } sig, err = sess.GenerateSignature(db) if err != nil { return } _, err = coll.InsertOne(db, sess) if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, id string) (err error) { coll := db.Sessions() err = coll.UpdateId(id, &bson.M{ "$set": &bson.M{ "removed": true, }, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveAll(db *database.Database, userId bson.ObjectID) (err error) { coll := db.Sessions() _, err = coll.UpdateMany(db, &bson.M{ "user": userId, }, &bson.M{ "$set": &bson.M{ "removed": true, }, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } ================================================ FILE: settings/acme.go ================================================ package settings var Acme *acme type acme struct { Id string `bson:"_id"` Url string `bson:"url" default:"https://acme-v01.api.letsencrypt.org"` DnsMaxConcurrent int `bson:"dns_max_concurrent" default:"10"` DnsRetryRate int `bson:"dns_retry_rate" default:"3"` DnsTimeout int `bson:"dns_timeout" default:"45"` DnsDelay int `bson:"dns_delay" default:"15"` DnsAwsTtl int `bson:"dns_aws_ttl" default:"10"` DnsCloudflareTtl int `bson:"dns_cloudflare_ttl" default:"60"` DnsOracleCloudTtl int `bson:"dns_oracle_cloud_ttl" default:"10"` DnsGoogleCloudTtl int `bson:"dns_google_cloud_ttl" default:"10"` } func newAcme() interface{} { return &acme{ Id: "acme", } } func updateAcme(data interface{}) { Acme = data.(*acme) } func init() { register("acme", newAcme, updateAcme) } ================================================ FILE: settings/auth.go ================================================ package settings import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) var Auth *auth const ( SetOnInsert = "set_on_insert" Merge = "merge" Overwrite = "overwrite" Azure = "azure" AuthZero = "authzero" Google = "google" OneLogin = "onelogin" Okta = "okta" JumpCloud = "jumpcloud" Duo = "duo" OneLogin2 = "one_login" ) type Provider struct { Id bson.ObjectID `bson:"id" json:"id"` Type string `bson:"type" json:"type"` Label string `bson:"label" json:"label"` DefaultRoles []string `bson:"default_roles" json:"default_roles"` AutoCreate bool `bson:"auto_create" json:"auto_create"` RoleManagement string `bson:"role_management" json:"role_management"` Region string `bson:"region" json:"region"` // azure Tenant string `bson:"tenant" json:"tenant"` // azure ClientId string `bson:"client_id" json:"client_id"` // azure + authzero ClientSecret string `bson:"client_secret" json:"client_secret"` // azure + authzero Domain string `bson:"domain" json:"domain"` // google + authzero GoogleKey string `bson:"google_key" json:"google_key"` // google GoogleEmail string `bson:"google_email" json:"google_email"` // google JumpCloudAppId string `bson:"jumpcloud_app_id" json:"jumpcloud_app_id"` // jumpcloud JumpCloudSecret string `bson:"jumpcloud_secret" json:"jumpcloud_secret"` // jumpcloud IssuerUrl string `bson:"issuer_url" json:"issuer_url"` // saml SamlUrl string `bson:"saml_url" json:"saml_url"` // saml SamlCert string `bson:"saml_cert" json:"saml_cert"` // saml } func (p *Provider) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { if p.Id.IsZero() { p.Id = bson.NewObjectID() } p.Label = utils.FilterStr(p.Label, 32) switch p.Type { case AuthZero: p.Region = "" p.Tenant = "" p.GoogleKey = "" p.GoogleEmail = "" p.JumpCloudAppId = "" p.JumpCloudSecret = "" p.IssuerUrl = "" p.SamlUrl = "" p.SamlCert = "" break case Azure: if p.Region == "" { p.Region = "global2" } p.Domain = "" p.GoogleKey = "" p.GoogleEmail = "" p.JumpCloudAppId = "" p.JumpCloudSecret = "" p.IssuerUrl = "" p.SamlUrl = "" p.SamlCert = "" break case Google: p.Region = "" p.Tenant = "" p.ClientId = "" p.ClientSecret = "" p.JumpCloudAppId = "" p.JumpCloudSecret = "" p.IssuerUrl = "" p.SamlUrl = "" p.SamlCert = "" break case OneLogin: p.Region = "" p.Tenant = "" p.ClientId = "" p.ClientSecret = "" p.Domain = "" p.GoogleKey = "" p.GoogleEmail = "" p.JumpCloudAppId = "" p.JumpCloudSecret = "" break case Okta: p.Region = "" p.Tenant = "" p.ClientId = "" p.ClientSecret = "" p.Domain = "" p.GoogleKey = "" p.GoogleEmail = "" p.JumpCloudAppId = "" p.JumpCloudSecret = "" break case JumpCloud: p.Region = "" p.Tenant = "" p.ClientId = "" p.ClientSecret = "" p.Domain = "" p.GoogleKey = "" p.GoogleEmail = "" break default: errData = &errortypes.ErrorData{ Error: "unknown_provider_type", Message: "Unknown authentication provider type", } return } switch p.RoleManagement { case SetOnInsert, "": break case Merge: break case Overwrite: break default: errData = &errortypes.ErrorData{ Error: "unknown_role_management", Message: "Unknown role management mode", } return } return } type SecondaryProvider struct { Id bson.ObjectID `bson:"id" json:"id"` Type string `bson:"type" json:"type"` Name string `bson:"name" json:"name"` Label string `bson:"label" json:"label"` DuoHostname string `bson:"duo_hostname" json:"duo_hostname"` // duo DuoKey string `bson:"duo_key" json:"duo_key"` // duo DuoSecret string `bson:"duo_secret" json:"duo_secret"` // duo OneLoginRegion string `bson:"one_login_region" json:"one_login_region"` // onelogin OneLoginId string `bson:"one_login_id" json:"one_login_id"` // onelogin OneLoginSecret string `bson:"one_login_secret" json:"one_login_secret"` // onelogin OktaDomain string `bson:"okta_domain" json:"okta_domain"` // okta OktaToken string `bson:"okta_token" json:"okta_token"` // okta PushFactor bool `bson:"push_factor" json:"push_factor"` // duo + onelogin + okta PhoneFactor bool `bson:"phone_factor" json:"phone_factor"` // duo + onelogin + okta PasscodeFactor bool `bson:"passcode_factor" json:"passcode_factor"` // duo + onelogin + okta SmsFactor bool `bson:"sms_factor" json:"sms_factor"` // duo + onelogin + okta } func (p *SecondaryProvider) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { if p.Id.IsZero() { p.Id = bson.NewObjectID() } p.Name = utils.FilterStr(p.Name, 32) p.Label = utils.FilterStr(p.Label, 32) switch p.Type { case Duo: p.OneLoginRegion = "" p.OneLoginId = "" p.OneLoginSecret = "" p.OktaDomain = "" p.OktaToken = "" break case OneLogin2: p.DuoHostname = "" p.DuoKey = "" p.DuoSecret = "" p.OktaDomain = "" p.OktaToken = "" if p.OneLoginRegion == "" { p.OneLoginRegion = "us" } break case Okta: p.DuoHostname = "" p.DuoKey = "" p.DuoSecret = "" p.OneLoginRegion = "" p.OneLoginId = "" p.OneLoginSecret = "" break default: errData = &errortypes.ErrorData{ Error: "unknown_secondary_provider_type", Message: "Unknown secondary authentication provider type", } return } return } type auth struct { Id string `bson:"_id"` Server string `bson:"server" default:"https://auth.pritunl.com"` Sync int `bson:"sync" json:"sync" default:"1800"` CookieAge int `bson:"cookie_age" json:"cookie_age" default:"63072000"` Providers []*Provider `bson:"providers"` SecondaryProviders []*SecondaryProvider `bson:"secondary_providers"` FastLogin bool `bson:"fast_login" json:"fast_login"` ForceFastUserLogin bool `bson:"force_fast_user_login" json:"force_fast_user_login"` Window int `bson:"window" json:"window" default:"60"` SecondaryExpire int `bson:"secondary_expire" json:"secondary_expire" default:"90"` AdminExpire int `bson:"admin_expire" json:"admin_expire" default:"1440"` AdminMaxDuration int `bson:"admin_max_duration" json:"admin_max_duration" default:"4320"` UserExpire int `bson:"user_expire" json:"user_expire" default:"1440"` UserMaxDuration int `bson:"user_max_duration" json:"user_max_duration" default:"4320"` } func (a *auth) GetProvider(id bson.ObjectID) *Provider { for _, provider := range a.Providers { if provider.Id == id { return provider } } return nil } func (a *auth) GetSecondaryProvider(id bson.ObjectID) *SecondaryProvider { for _, provider := range a.SecondaryProviders { if provider.Id == id { return provider } } return nil } func newAuth() interface{} { return &auth{ Id: "auth", Providers: []*Provider{}, SecondaryProviders: []*SecondaryProvider{}, } } func updateAuth(data interface{}) { Auth = data.(*auth) } func init() { register("auth", newAuth, updateAuth) } ================================================ FILE: settings/hypervisor.go ================================================ package settings var Hypervisor *hypervisor type hypervisor struct { Id string `bson:"_id"` SystemdPath string `bson:"systemd_path" default:"/etc/systemd/system"` LibPath string `bson:"lib_path" default:"/var/lib/pritunl-cloud"` RunPath string `bson:"run_path" default:"/var/run/pritunl-cloud"` AgentHostPath string `bson:"agent_host_path" default:"/usr/bin/pritunl-cloud-agent"` AgentBsdHostPath string `bson:"agent_bsd_host_path" default:"/usr/bin/pritunl-cloud-agent-bsd"` AgentGuestPath string `bson:"agent_guest_path" default:"/usr/bin/pci"` InitGuestPath string `bson:"init_guest_path" default:"/etc/pritunl-cloud-init"` HugepagesPath string `bson:"hugepages_path" default:"/dev/hugepages/pritunl"` LockCloudPass bool `bson:"lock_cloud_pass"` DesktopEnv string `bson:"desktop_env" default:"gnome"` OvmfCodePath string `bson:"ovmf_code_path"` OvmfVarsPath string `bson:"ovmf_vars_path"` OvmfSecureCodePath string `bson:"ovmf_secure_code_path"` OvmfSecureVarsPath string `bson:"ovmf_secure_vars_path"` NbdPath string `bson:"nbd_path" default:"/dev/nbd6"` DiskAio string `bson:"disk_aio"` NoSandbox bool `bson:"no_sandbox"` GlHostMem int `bson:"gl_host_mem" default:"2048"` BridgeIfaceName string `bson:"bridge_iface_name" default:"br0"` ImdsIfaceName string `bson:"imds_iface_name" default:"imds0"` NormalMtu int `bson:"normal_mtu" default:"1500"` JumboMtu int `bson:"jumbo_mtu" default:"9000"` DiskQueuesMin int `bson:"disk_queues_min" default:"1"` DiskQueuesMax int `bson:"disk_queues_max" default:"4"` NetworkQueuesMin int `bson:"network_queues_min" default:"1"` NetworkQueuesMax int `bson:"network_queues_max" default:"8"` CloudInitNetVer int `bson:"cloud_init_net_ver" default:"1"` HostNetwork string `bson:"host_network" default:"198.18.84.0/22"` HostNetworkName string `bson:"host_network_name" default:"pritunlhost0"` VirtRng bool `bson:"virt_rng"` VlanRanges string `bson:"vlan_ranges" default:"1001-3999"` VxlanId int `bson:"vxlan_id" default:"9417"` VxlanDestPort int `bson:"vxlan_dest_port" default:"4789"` IpTimeout int `bson:"ip_timeout" default:"30"` IpTimeout6 int `bson:"ip_timeout6" default:"15"` ActionRate int `bson:"action_rate" default:"3"` NodePortNetwork string `bson:"node_port_network" default:"198.19.96.0/23"` NodePortRanges string `bson:"node_port_ranges" default:"30000-32767"` NodePortNetworkName string `bson:"node_port_network_name" default:"pritunlport0"` AddressRefreshTtl int `bson:"address_refresh_ttl" default:"1800"` StartTimeout int `bson:"start_timeout" default:"45"` StopTimeout int `bson:"stop_timeout" default:"180"` RefreshRate int `bson:"refresh_rate" default:"90"` SplashTime int `bson:"splash_time" default:"60"` DhcpRenewTtl int `bson:"dhcp_renew_ttl" default:"60"` NoIpv6PingInit bool `bson:"no_ipv6_ping_init"` Ipv6PingHost string `bson:"ipv6_ping_host" default:"2001:4860:4860::8888"` ImdsAddress string `bson:"imds_address" default:"169.254.169.254/32"` ImdsPort int `bson:"imds_port" default:"80"` ImdsSyncLogTimeout int `bson:"imds_sync_log_timeout" default:"20"` ImdsSyncRestartTimeout int `bson:"imds_sync_log_timeout" default:"30"` InfoTtl int `bson:"info_ttl" default:"10"` NoGuiFullscreen bool `bson:"no_gui_fullscreen"` UsbHsPorts int `bson:"usb_hs_ports" default:"4"` UsbSsPorts int `bson:"usb_ss_ports" default:"4"` NoVirtioHid bool `bson:"no_virtio_hid"` JournalDisplayLimit int64 `bson:"journal_display_limit" default:"3000"` DhcpLifetime int `bson:"dhcp_lifetime" default:"3600"` NdpRaInterval int `bson:"ndp_ra_interval" default:"6"` DnsServerPrimary string `bson:"dns_server_primary" default:"8.8.8.8"` DnsServerSecondary string `bson:"dns_server_secondary" default:"8.8.4.4"` DnsServerPrimary6 string `bson:"dns_server_primary6" default:"2001:4860:4860::8888"` DnsServerSecondary6 string `bson:"dns_server_secondary6" default:"2001:4860:4860::8844"` NodePortMaxAttempts int `bson:"node_port_max_attempts" default:"10000"` MaxDeploymentFailures int `bson:"max_deployment_failures" default:"3"` } func newHypervisor() interface{} { return &hypervisor{ Id: "hypervisor", } } func updateHypervisor(data interface{}) { Hypervisor = data.(*hypervisor) } func init() { register("hypervisor", newHypervisor, updateHypervisor) } ================================================ FILE: settings/local.go ================================================ package settings var Local *local type local struct { AppId string Facets []string NoLocalAuth bool DisableWeb bool DisableMsg string } func init() { Local = &local{} } ================================================ FILE: settings/registry.go ================================================ package settings var ( registry = map[string]*group{} ) type newFunc func() interface{} type updateFunc func(interface{}) type group struct { New newFunc Update updateFunc } func register(name string, new newFunc, update updateFunc) { grp := &group{ New: new, Update: update, } registry[name] = grp } ================================================ FILE: settings/router.go ================================================ package settings var Router *router type router struct { Id string `bson:"_id"` ReadTimeout int `bson:"read_timeout" default:"300"` ReadHeaderTimeout int `bson:"read_header_timeout" default:"60"` WriteTimeout int `bson:"write_timeout" default:"300"` IdleTimeout int `bson:"idle_timeout" default:"60"` DialTimeout int `bson:"dial_timeout" default:"60"` DialKeepAlive int `bson:"dial_keep_alive" default:"60"` MaxIdleConns int `bson:"max_idle_conns" default:"1000"` MaxIdleConnsPerHost int `bson:"max_idle_conns_per_host" default:"100"` IdleConnTimeout int `bson:"idle_conn_timeout" default:"90"` HandshakeTimeout int `bson:"handshake_timeout" default:"10"` ContinueTimeout int `bson:"continue_timeout" default:"10"` MaxHeaderBytes int `bson:"max_header_bytes" default:"4194304"` ForceRedirectSystemd bool `bson:"force_redirect_systemd"` SkipVerify bool `bson:"skip_verify"` ProxyResolverRefresh int `bson:"proxy_resolver_refresh" default:"30"` ProxyResolverTtl int `bson:"proxy_resolver_ttl" default:"30"` } func newRouter() interface{} { return &router{ Id: "router", } } func updateRouter(data interface{}) { Router = data.(*router) } func init() { register("router", newRouter, updateRouter) } ================================================ FILE: settings/settings.go ================================================ package settings import ( "reflect" "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) func Commit(db *database.Database, group interface{}, fields set.Set) ( err error) { coll := db.Settings() selector := database.SelectFields(group, set.NewSet("_id")) updated := database.SelectFields(group, fields) _, err = coll.UpdateOne( db, selector, &bson.M{ "$set": updated, }, options.UpdateOne().SetUpsert(true), ) if err != nil { err = database.ParseError(err) return } return } func Get(db *database.Database, group string, key string) ( val interface{}, err error) { coll := db.Settings() grp := map[string]interface{}{} err = coll.FindOne( db, &bson.M{ "_id": group, }, options.FindOne().SetProjection(&bson.D{ {key, 1}, }), ).Decode(grp) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil return default: err = &errortypes.DatabaseError{ errors.Wrap(err, "settings: Database error"), } return } } val = grp[key] return } func Set(db *database.Database, group string, key string, val interface{}) ( err error) { coll := db.Settings() _, err = coll.UpdateOne( db, &bson.M{ "_id": group, }, &bson.M{ "$set": &bson.M{ key: val, }, }, options.UpdateOne().SetUpsert(true), ) if err != nil { err = database.ParseError(err) return } return } func Unset(db *database.Database, group string, key string) ( err error) { coll := db.Settings() _, err = coll.UpdateOne( db, &bson.M{ "_id": group, }, &bson.M{ "$unset": &bson.M{ key: 1, }, }, options.UpdateOne().SetUpsert(true), ) if err != nil { err = database.ParseError(err) return } return } func setDefaults(obj interface{}) { val := reflect.ValueOf(obj) elm := val.Elem() n := elm.NumField() for i := 0; i < n; i++ { fld := elm.Field(i) typ := elm.Type().Field(i) if typ.PkgPath != "" { continue } tag := typ.Tag.Get("default") if tag == "" { continue } switch fld.Kind() { case reflect.Bool: parVal, err := strconv.ParseBool(tag) if err != nil { panic(err) } fld.SetBool(parVal) break case reflect.Int, reflect.Int64: if fld.Int() != 0 { break } parVal, err := strconv.Atoi(tag) if err != nil { panic(err) } fld.SetInt(int64(parVal)) break case reflect.String: if fld.String() != "" { break } fld.SetString(tag) break case reflect.Slice: if fld.Len() != 0 { break } sliceType := reflect.TypeOf(fld.Interface()).Elem() vals := strings.Split(tag, ",") n := len(vals) slice := reflect.MakeSlice(reflect.SliceOf(sliceType), n, n) switch sliceType.Kind() { case reflect.Bool: for i, val := range vals { parVal, err := strconv.ParseBool(val) if err != nil { panic(err) } slice.Index(i).SetBool(parVal) } case reflect.Int: for i, val := range vals { parVal, err := strconv.Atoi(val) if err != nil { panic(err) } slice.Index(i).SetInt(int64(parVal)) } case reflect.String: for i, val := range vals { slice.Index(i).SetString(val) } } fld.Set(slice) break } } return } func Update(name string) (err error) { db := database.GetDatabase() defer db.Close() coll := db.Settings() group := registry[name] data := group.New() err = database.IgnoreNotFoundError(coll.FindOneId(name, data)) if err != nil { return } setDefaults(data) group.Update(data) return } func update() { for { time.Sleep(10 * time.Second) if constants.Shutdown { return } for name := range registry { err := Update(name) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("settings: Update error") return } } } } func init() { module := requires.New("settings") module.After("database") module.Handler = func() (err error) { for name := range registry { err = Update(name) if err != nil { return } } db := database.GetDatabase() defer db.Close() if System.DatabaseVersion == 0 { System.DatabaseVersion = constants.DatabaseVersion err = Commit(db, System, set.NewSet("database_version")) if err != nil { return } } if System.DatabaseVersion > constants.DatabaseVersion { logrus.WithFields(logrus.Fields{ "database_version": System.DatabaseVersion, "software_version": constants.DatabaseVersion, }).Error("settings: Database version newer then software") err = &errortypes.DatabaseError{ errors.New( "settings: Database version newer then software"), } return } else if System.DatabaseVersion != constants.DatabaseVersion { logrus.WithFields(logrus.Fields{ "database_version": System.DatabaseVersion, "new_database_version": constants.DatabaseVersion, }).Info("settings: Upgrading database version") System.DatabaseVersion = constants.DatabaseVersion err = Commit(db, System, set.NewSet("database_version")) if err != nil { return } } if System.Name == "" { System.Name = utils.RandName() err = Commit(db, System, set.NewSet("name")) if err != nil { return } } if Auth.Providers == nil { Auth.Providers = []*Provider{} err = Commit(db, Auth, set.NewSet("providers")) if err != nil { return } } if Auth.SecondaryProviders == nil { Auth.SecondaryProviders = []*SecondaryProvider{} err = Commit(db, Auth, set.NewSet("secondary_providers")) if err != nil { return } } go update() return } } ================================================ FILE: settings/system.go ================================================ package settings var System *system type system struct { Id string `bson:"_id"` Name string `bson:"name"` DatabaseVersion int `bson:"database_version"` Demo bool `bson:"demo"` License string `bson:"license"` AdminCookieAuthKey []byte `bson:"admin_cookie_auth_key"` AdminCookieCryptoKey []byte `bson:"admin_cookie_crypto_key"` UserCookieAuthKey []byte `bson:"user_cookie_auth_key"` UserCookieCryptoKey []byte `bson:"user_cookie_crypto_key"` NodeTimestampTtl int `bson:"node_timestamp_ttl" default:"15"` InstanceTimestampTtl int `bson:"instance_timestamp_ttl" default:"20"` DomainLockTtl int `bson:"domain_lock_ttl" default:"30"` DomainDeleteTtl int `bson:"domain_delete_ttl" default:"200"` DomainRefreshTtl int `bson:"domain_refresh_ttl" default:"90"` AcmeKeyAlgorithm string `bson:"acme_key_algorithm" default:"rsa"` DiskBackupWindow int `bson:"disk_backup_window" default:"6"` DiskBackupTime int `bson:"disk_backup_time" default:"10"` PlannerBatchSize int `bson:"planner_batch_size" default:"10"` NoMigrateRefresh bool `bson:"no_migrate_refresh"` OracleApiRetryRate int `bson:"oracle_api_retry_rate" default:"1"` OracleApiRetryCount int `bson:"oracle_api_retry_count" default:"120"` TwilioAccount string `bson:"twilio_account"` TwilioSecret string `bson:"twilio_secret"` TwilioNumber string `bson:"twilio_number"` } func newSystem() interface{} { return &system{ Id: "system", } } func updateSystem(data interface{}) { System = data.(*system) } func init() { register("system", newSystem, updateSystem) } ================================================ FILE: settings/telemetry.go ================================================ package settings var Telemetry *telemetry type telemetry struct { Id string `bson:"_id"` NvdTtl int `bson:"nvd_ttl" default:"21600"` NvdFinalTtl int `bson:"nvd_final_ttl" default:"604800"` NvdApiLimit int `bson:"nvd_api_limit" default:"8"` NvdApiAuthLimit int `bson:"nvd_api_auth_limit" default:"1"` NvdApiKey string `bson:"nvd_api_key"` } func newTelemetry() interface{} { return &telemetry{ Id: "telemetry", } } func updateTelemetry(data interface{}) { Telemetry = data.(*telemetry) } func init() { register("telemetry", newTelemetry, updateTelemetry) } ================================================ FILE: setup/iptables.go ================================================ package setup import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/ipset" "github.com/pritunl/pritunl-cloud/iptables" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" ) func Iptables() (err error) { db := database.GetDatabase() defer db.Close() namespaces, err := utils.GetNamespaces() if err != nil { return } nodeDatacenter, err := node.Self.GetDatacenter(db) if err != nil { return } vpcs := []*vpc.Vpc{} if !nodeDatacenter.IsZero() { vpcs, err = vpc.GetDatacenter(db, nodeDatacenter) if err != nil { return } } instances, err := instance.GetAllVirt(db, &bson.M{ "node": node.Self.Id, }, nil, nil) if err != nil { return } specRules, nodePortsMap, err := firewall.GetSpecRulesSlow( db, node.Self.Id, instances) if err != nil { return } nodeFirewall, firewalls, firewallMaps, _, err := firewall.GetAllIngress( db, node.Self, instances, specRules, nodePortsMap) if err != nil { return } err = ipset.Init(namespaces, instances, nodeFirewall, firewalls) if err != nil { return } err = iptables.Init(namespaces, vpcs, instances, nodeFirewall, firewalls, firewallMaps) if err != nil { return } err = ipset.InitNames(namespaces, instances, nodeFirewall, firewalls) if err != nil { return } return } ================================================ FILE: shape/constants.go ================================================ package shape const ( Instance = "instance" Qcow2 = "qcow2" Lvm = "lvm" ) ================================================ FILE: shape/node.go ================================================ package shape import ( "sort" "github.com/pritunl/pritunl-cloud/node" ) type Nodes []*node.Node func (n Nodes) Len() int { return len(n) } func (n Nodes) Less(i, j int) bool { return n[i].Usage() < n[j].Usage() } func (n Nodes) Swap(i, j int) { n[i], n[j] = n[j], n[i] } func (n Nodes) Sort() { sort.Sort(n) } ================================================ FILE: shape/shape.go ================================================ package shape import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" ) type Shape struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Type string `bson:"type" json:"type"` DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Roles []string `bson:"roles" json:"roles"` Flexible bool `bson:"flexible" json:"flexible"` DiskType string `bson:"disk_type" json:"disk_type"` DiskPool bson.ObjectID `bson:"disk_pool" json:"disk_pool"` Memory int `bson:"memory" json:"memory"` Processors int `bson:"processors" json:"processors"` NodeCount int `bson:"-" json:"node_count"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` Flexible bool `bson:"flexible" json:"flexible"` Memory int `bson:"memory" json:"memory"` Processors int `bson:"processors" json:"processors"` } func (s *Shape) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { s.Name = utils.FilterName(s.Name) if s.Type == "" { s.Type = Instance } switch s.Type { case Instance: break default: errData = &errortypes.ErrorData{ Error: "invalid_shape_type", Message: "Shape type invalid", } return } if s.Roles == nil { s.Roles = []string{} } switch s.DiskType { case "", Qcow2: s.DiskType = Qcow2 break case Lvm: break default: errData = &errortypes.ErrorData{ Error: "invalid_disk_type", Message: "Disk type invalid", } return } if s.Datacenter.IsZero() { errData = &errortypes.ErrorData{ Error: "missing_datacenter", Message: "Shape datacenter required", } return } return } func (s *Shape) FindNode(db *database.Database, processors, memory int) ( nde *node.Node, err error) { zones, err := zone.GetAllDatacenter(db, s.Datacenter) if err != nil { return } zoneIds := []bson.ObjectID{} for _, zne := range zones { zoneIds = append(zoneIds, zne.Id) } ndes, err := node.GetAllShape(db, zoneIds, s.Roles) if err != nil { return } Nodes(ndes).Sort() for _, nd := range ndes { nde = nd return } err = &errortypes.NotFoundError{ errors.New("shape: Failed to find available node"), } return } func (s *Shape) Commit(db *database.Database) (err error) { coll := db.Shapes() err = coll.Commit(s.Id, s) if err != nil { return } return } func (s *Shape) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Shapes() err = coll.CommitFields(s.Id, s, fields) if err != nil { return } return } func (s *Shape) Insert(db *database.Database) (err error) { coll := db.Shapes() _, err = coll.InsertOne(db, s) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: shape/utils.go ================================================ package shape import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, shapeId bson.ObjectID) ( shpe *Shape, err error) { coll := db.Shapes() shpe = &Shape{} err = coll.FindOneId(shapeId, shpe) if err != nil { return } return } func GetOne(db *database.Database, query *bson.M) (shpe *Shape, err error) { coll := db.Shapes() shpe = &Shape{} err = coll.FindOne(db, query).Decode(shpe) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( shapes []*Shape, err error) { coll := db.Shapes() shapes = []*Shape{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { nde := &Shape{} err = cursor.Decode(nde) if err != nil { err = database.ParseError(err) return } shapes = append(shapes, nde) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (shapes []*Shape, count int64, err error) { coll := db.Shapes() shapes = []*Shape{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { shpe := &Shape{} err = cursor.Decode(shpe) if err != nil { err = database.ParseError(err) return } shapes = append(shapes, shpe) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( shapes []*Shape, err error) { coll := db.Shapes() shapes = []*Shape{} cursor, err := coll.Find( db, query, options.Find(). SetSort(&bson.D{ {"name", 1}, }). SetProjection(&bson.D{ {"_id", 1}, {"name", 1}, {"type", 1}, {"zone", 1}, {"flexible", 1}, {"disk_type", 1}, {"disk_pool", 1}, {"memory", 1}, {"processors", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { shpe := &Shape{} err = cursor.Decode(shpe) if err != nil { err = database.ParseError(err) return } shapes = append(shapes, shpe) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, shapeId bson.ObjectID) (err error) { coll := db.Shapes() _, err = coll.DeleteOne(db, &bson.M{ "_id": shapeId, "delete_protection": false, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, shapeIds []bson.ObjectID) ( err error) { coll := db.Shapes() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": shapeIds, }, "delete_protection": false, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: signature/signature.go ================================================ package signature import ( "crypto/hmac" "crypto/sha512" "crypto/subtle" "encoding/base64" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/nonce" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/pritunl/pritunl-cloud/utils" ) type Signature struct { Token string Nonce string Timestamp time.Time Signature string Method string Path string user *user.User } func (s *Signature) GetUser(db *database.Database) ( usr *user.User, err error) { if s.user != nil || db == nil || s.Token == "" { usr = s.user return } usr, err = user.GetTokenUpdate(db, s.Token) if err != nil { return } s.user = usr return } func (s *Signature) Validate(db *database.Database) (err error) { if s.Token == "" { err = &errortypes.AuthenticationError{ errors.New("signature: Invalid authentication token"), } return } if len(s.Nonce) < 16 || len(s.Nonce) > 128 { err = &errortypes.AuthenticationError{ errors.New("signature: Invalid authentication nonce"), } return } if utils.SinceAbs(s.Timestamp) > time.Duration( settings.Auth.Window)*time.Second { err = &errortypes.AuthenticationError{ errors.New("signature: Authentication timestamp outside window"), } return } usr, err := s.GetUser(db) if err != nil { switch err.(type) { case *database.NotFoundError: usr = nil err = nil break default: return } } if usr == nil || usr.Type != user.Api || usr.Token == "" || usr.Secret == "" { err = &errortypes.AuthenticationError{ errors.New("signature: User not found"), } return } authString := strings.Join([]string{ usr.Token, strconv.FormatInt(s.Timestamp.Unix(), 10), s.Nonce, s.Method, s.Path, }, "&") err = nonce.Validate(db, s.Nonce) if err != nil { return } hashFunc := hmac.New(sha512.New, []byte(usr.Secret)) hashFunc.Write([]byte(authString)) rawSignature := hashFunc.Sum(nil) sig := base64.StdEncoding.EncodeToString(rawSignature) if subtle.ConstantTimeCompare([]byte(s.Signature), []byte(sig)) != 1 { err = &errortypes.AuthenticationError{ errors.New("signature: Invalid signature"), } return } return } ================================================ FILE: signature/utils.go ================================================ package signature import ( "strconv" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) func Parse(token, sigStr, timeStr, nonce, method, path string) ( sig *Signature, err error) { timestampInt, _ := strconv.ParseInt(timeStr, 10, 64) if timestampInt == 0 { err = &errortypes.AuthenticationError{ errors.New("signature: Invalid authentication timestamp"), } return } timestamp := time.Unix(timestampInt, 0) sig = &Signature{ Token: token, Nonce: nonce, Timestamp: timestamp, Signature: sigStr, Method: method, Path: path, } return } ================================================ FILE: spec/constants.go ================================================ package spec import ( "regexp" "github.com/pritunl/mongo-go-driver/v2/bson" ) var resourcesRe = regexp.MustCompile("(?s)```yaml(.*?)```") const ( All = "all" Icmp = "icmp" Tcp = "tcp" Udp = "udp" Multicast = "multicast" Broadcast = "broadcast" Host = "host" Private = "private" Private6 = "private6" Public = "public" Public6 = "public6" CloudPublic = "cloud_public" CloudPublic6 = "cloud_public6" CloudPrivate = "cloud_private" Systemd = "systemd" File = "file" TokenPrefix = "+/" Disk = "disk" HostPath = "host_path" ) type Base struct { Kind string `yaml:"kind"` } const ( Unit = "unit" ) type Refrence struct { Id bson.ObjectID `bson:"id" json:"id"` Realm bson.ObjectID `bson:"realm" json:"realm"` Kind string `bson:"kind" json:"kind"` Selector string `bson:"selector" json:"selector"` } ================================================ FILE: spec/domain.go ================================================ package spec import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Domain struct { Records []*Record `bson:"records" json:"records"` } func (d *Domain) Validate() (errData *errortypes.ErrorData, err error) { for _, rec := range d.Records { rec.Name = utils.FilterDomain(rec.Name) switch rec.Type { case Host: break case Private: break case Private6: break case Public: break case Public6: break case CloudPublic: break case CloudPrivate: break default: errData = &errortypes.ErrorData{ Error: "unknown_domain_record_type", Message: "Unknown domain record type", } return } } return } type Record struct { Name string `bson:"name" json:"name"` Domain bson.ObjectID `bson:"domain" json:"domain"` Type string `bson:"type" json:"type"` } type DomainYaml struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Records []DomainYamlRecord `yaml:"records"` } type DomainYamlRecord struct { Name string `yaml:"name"` Domain string `yaml:"domain"` Type string `yaml:"type"` } ================================================ FILE: spec/firewall.go ================================================ package spec import ( "net" "strconv" "strings" "github.com/pritunl/pritunl-cloud/errortypes" ) type Firewall struct { Ingress []*Rule `bson:"ingress" json:"ingress"` } type Rule struct { Protocol string `bson:"protocol" json:"protocol"` Port string `bson:"port" json:"port"` SourceIps []string `bson:"source_ips" json:"source_ips"` Sources []*Refrence `bson:"sources" json:"sources"` } func (f *Firewall) Validate() (errData *errortypes.ErrorData, err error) { if f.Ingress == nil { f.Ingress = []*Rule{} } for _, rule := range f.Ingress { switch rule.Protocol { case All: rule.Port = "" break case Icmp: rule.Port = "" break case Tcp, Udp, Multicast, Broadcast: ports := strings.Split(rule.Port, "-") portInt, e := strconv.Atoi(ports[0]) if e != nil { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } if portInt < 1 || portInt > 65535 { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } parsedPort := strconv.Itoa(portInt) if len(ports) > 1 { portInt2, e := strconv.Atoi(ports[1]) if e != nil { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } if portInt < 1 || portInt > 65535 || portInt2 <= portInt { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_port", Message: "Invalid ingress rule port", } return } parsedPort += "-" + strconv.Itoa(portInt2) } rule.Port = parsedPort break default: errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_protocol", Message: "Invalid ingress rule protocol", } return } if rule.Sources == nil { rule.Sources = []*Refrence{} } if rule.SourceIps == nil { rule.SourceIps = []string{} } for i, sourceIp := range rule.SourceIps { if sourceIp == "" { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_source_ip", Message: "Empty ingress rule source IP", } return } if !strings.Contains(sourceIp, "/") { if strings.Contains(sourceIp, ":") { sourceIp += "/128" } else { sourceIp += "/32" } } _, sourceCidr, e := net.ParseCIDR(sourceIp) if e != nil { errData = &errortypes.ErrorData{ Error: "invalid_ingress_rule_source_ip", Message: "Invalid ingress rule source IP", } return } rule.SourceIps[i] = sourceCidr.String() } if rule.Protocol == Multicast || rule.Protocol == Broadcast { rule.Sources = []*Refrence{} rule.SourceIps = []string{} } } return } type FirewallYaml struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Ingress []FirewallYamlIngress `yaml:"ingress"` } type FirewallYamlIngress struct { Protocol string `yaml:"protocol"` Port string `yaml:"port"` Source []string `yaml:"source"` } ================================================ FILE: spec/instance.go ================================================ package spec import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/nodeport" ) type Instance struct { Plan bson.ObjectID `bson:"plan,omitempty" json:"plan"` // clear Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` // hard Zone bson.ObjectID `bson:"zone" json:"zone"` // hard Node bson.ObjectID `bson:"node,omitempty" json:"node"` // hard Shape bson.ObjectID `bson:"shape,omitempty" json:"shape"` // hard Vpc bson.ObjectID `bson:"vpc" json:"vpc"` // hard Subnet bson.ObjectID `bson:"subnet" json:"subnet"` // hard Roles []string `bson:"roles" json:"roles"` // soft Processors int `bson:"processors" json:"processors"` // soft Memory int `bson:"memory" json:"memory"` // soft Uefi *bool `bson:"uefi,omitempty" json:"uefi"` // soft SecureBoot *bool `bson:"secure_boot,omitempty" json:"secure_boot"` // soft CloudType string `bson:"cloud_type" json:"cloud_type"` // soft Tpm bool `bson:"tpm" json:"tpm"` // soft Vnc bool `bson:"vnc" json:"vnc"` // soft DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` // soft SkipSourceDestCheck bool `bson:"skip_source_dest_check" json:"skip_source_dest_check"` // soft Gui bool `bson:"gui" json:"gui"` // soft HostAddress *bool `bson:"host_address,omitempty" json:"host_address"` // soft PublicAddress *bool `bson:"public_address,omitempty" json:"public_address"` // soft PublicAddress6 *bool `bson:"public_address6,omitempty" json:"public_address6"` // soft DhcpServer bool `bson:"dhcp_server" json:"dhcp_server"` // soft Image bson.ObjectID `bson:"image" json:"image"` // hard DiskSize int `bson:"disk_size" json:"disk_size"` // hard Mounts []Mount `bson:"mounts" json:"mounts"` // hard NodePorts []NodePort `bson:"node_ports" json:"node_ports"` // soft Certificates []bson.ObjectID `bson:"certificates" json:"certificates"` // soft Secrets []bson.ObjectID `bson:"secrets" json:"secrets"` // soft Pods []bson.ObjectID `bson:"pods" json:"pods"` // soft } type NodePort struct { Protocol string `bson:"protocol" json:"protocol"` ExternalPort int `bson:"external_port" json:"external_port"` InternalPort int `bson:"internal_port" json:"internal_port"` } func (m *NodePort) Validate() ( errData *errortypes.ErrorData, err error) { switch m.Protocol { case Tcp, Udp: break default: errData = &errortypes.ErrorData{ Error: "invalid_protocol", Message: "Invalid node port protocol", } return } portRanges, e := nodeport.GetPortRanges() if e != nil { err = e return } matched := false for _, ports := range portRanges { if ports.Contains(m.ExternalPort) { matched = true break } } if !matched { errData = &errortypes.ErrorData{ Error: "invalid_external_port", Message: "Invalid external node port", } return } if m.InternalPort <= 0 || m.InternalPort > 65535 { errData = &errortypes.ErrorData{ Error: "invalid_internal_port", Message: "Invalid internal node port", } return } return } func (i *Instance) DiffNodePorts(newNodePorts []NodePort) bool { if len(i.NodePorts) != len(newNodePorts) { return true } for x := range i.NodePorts { if i.NodePorts[x].Protocol != newNodePorts[x].Protocol || i.NodePorts[x].ExternalPort != newNodePorts[x].ExternalPort || i.NodePorts[x].InternalPort != newNodePorts[x].InternalPort { return true } } return false } func (i *Instance) MemoryUnits() float64 { return float64(i.Memory) / float64(1024) } type Mount struct { Name string `bson:"name" json:"name"` Type string `bson:"type" json:"type"` Path string `bson:"path" json:"path"` HostPath string `bson:"host_path" json:"host_path"` Disks []bson.ObjectID `bson:"disks" json:"disks"` } type InstanceYaml struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Count int `yaml:"count"` Plan string `yaml:"plan"` Zone string `yaml:"zone"` Node string `yaml:"node,omitempty"` Shape string `yaml:"shape,omitempty"` Vpc string `yaml:"vpc"` Subnet string `yaml:"subnet"` Roles []string `yaml:"roles"` Processors int `yaml:"processors"` Memory int `yaml:"memory"` Uefi *bool `yaml:"uefi"` SecureBoot *bool `yaml:"secureBoot"` CloudType string `yaml:"cloudType"` Tpm bool `yaml:"tpm"` Vnc bool `yaml:"vnc"` DeleteProtection bool `yaml:"deleteProtection"` SkipSourceDestCheck bool `yaml:"skipSourceDestCheck"` Gui bool `yaml:"gui"` HostAddress *bool `yaml:"hostAddress"` PublicAddress *bool `yaml:"publicAddress"` PublicAddress6 *bool `yaml:"publicAddress6"` DhcpServer bool `yaml:"dhcpServer"` Image string `yaml:"image"` Mounts []InstanceMountYaml `yaml:"mounts"` NodePorts []InstanceNodePortYaml `yaml:"nodePorts"` Certificates []string `yaml:"certificates"` Secrets []string `yaml:"secrets"` Pods []string `yaml:"pods"` DiskSize int `yaml:"diskSize"` } type InstanceMountYaml struct { Name string `yaml:"name"` Type string `yaml:"type"` Path string `yaml:"path"` HostPath string `yaml:"hostPath"` Disks []string `yaml:"disks"` } type InstanceNodePortYaml struct { Protocol string `yaml:"protocol"` ExternalPort int `yaml:"externalPort"` InternalPort int `yaml:"internalPort"` } ================================================ FILE: spec/journal.go ================================================ package spec import ( "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Journal struct { Inputs []*Input `bson:"inputs" json:"inputs"` } type Input struct { Index int32 `bson:"index" json:"index"` Key string `bson:"key" json:"key"` Type string `bson:"type" json:"type"` Unit string `bson:"unit" json:"unit"` Path string `bson:"path" json:"path"` } func (j *Journal) Validate() (errData *errortypes.ErrorData, err error) { for _, input := range j.Inputs { if input.Key == "" { errData = &errortypes.ErrorData{ Error: "journal_key_missing", Message: "Missing journal key", } return } key := utils.FilterName(input.Key) if input.Key != key { errData = &errortypes.ErrorData{ Error: "journal_key_invalid", Message: "Journal key invalid", } return } input.Key = key switch input.Type { case Systemd: input.Path = "" if input.Unit == "" { errData = &errortypes.ErrorData{ Error: "systemd_unit_missing", Message: "Missing systemd unit", } return } inputUnit := utils.FilterUnit(input.Unit) if input.Unit != inputUnit { errData = &errortypes.ErrorData{ Error: "systemd_unit_invalid", Message: "Invalid systemd unit", } return } input.Unit = inputUnit break case File: input.Unit = "" if input.Path == "" { errData = &errortypes.ErrorData{ Error: "log_path_missing", Message: "Missing log path", } return } input.Path = utils.FilterPath(input.Path) if input.Path == "" { errData = &errortypes.ErrorData{ Error: "log_path_invalid", Message: "Invalid log path", } return } break default: errData = &errortypes.ErrorData{ Error: "unknown_input_type", Message: "Unknown input type", } return } } return } type JournalYaml struct { Name string `yaml:"name"` Kind string `yaml:"kind"` Inputs []JournalYamlInput `yaml:"inputs"` } type JournalYamlInput struct { Key string `yaml:"key"` Type string `yaml:"type"` Unit string `yaml:"unit,omitempty"` Path string `yaml:"path,omitempty"` } ================================================ FILE: spec/node.go ================================================ package spec import ( "sort" "github.com/pritunl/pritunl-cloud/node" ) type Nodes []*node.Node func (n Nodes) Len() int { return len(n) } func (n Nodes) Less(i, j int) bool { return n[i].Usage() < n[j].Usage() } func (n Nodes) Swap(i, j int) { n[i], n[j] = n[j], n[i] } func (n Nodes) Sort() { sort.Sort(n) } ================================================ FILE: spec/spec.go ================================================ package spec import ( "crypto/sha1" "fmt" "io" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/finder" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" "gopkg.in/yaml.v2" ) type Spec struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Pod bson.ObjectID `bson:"pod" json:"pod"` Unit bson.ObjectID `bson:"unit" json:"unit"` Organization bson.ObjectID `bson:"organization" json:"organization"` Index int `bson:"index" json:"index"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Name string `bson:"name" json:"name"` Kind string `bson:"kind" json:"kind"` Count int `bson:"count" json:"count"` Hash string `bson:"hash" json:"hash"` Data string `bson:"data" json:"data"` Instance *Instance `bson:"instance,omitempty" json:"-"` Firewall *Firewall `bson:"firewall,omitempty" json:"-"` Domain *Domain `bson:"domain,omitempty" json:"-"` Journal *Journal `bson:"journal,omitempty" json:"-"` } func (s *Spec) GetAllNodes(db *database.Database) (ndes Nodes, offlineCount, noMountCount int, err error) { org, err := organization.Get(db, s.Organization) if err != nil { return } shpe, err := shape.Get(db, s.Instance.Shape) if err != nil { return } zones, err := zone.GetAllDatacenter(db, shpe.Datacenter) if err != nil { return } zoneIds := []bson.ObjectID{} for _, zne := range zones { zoneIds = append(zoneIds, zne.Id) } allNdes, err := node.GetAllShape(db, zoneIds, shpe.Roles) if err != nil { return } var mountNodes []set.Set if len(s.Instance.Mounts) > 0 { diskIds := []bson.ObjectID{} for _, mount := range s.Instance.Mounts { if mount.Type != Disk { continue } diskIds = append(diskIds, mount.Disks...) } disksMap := map[bson.ObjectID]*disk.Disk{} if len(diskIds) > 0 { disksMap, err = disk.GetAllMap(db, &bson.M{ "_id": &bson.M{ "$in": diskIds, }, "organization": s.Organization, }) if err != nil { return } } for _, mount := range s.Instance.Mounts { mountSet := set.NewSet() if mount.Type == Disk { for _, dskId := range mount.Disks { dsk := disksMap[dskId] if dsk == nil || !dsk.IsAvailable() { continue } mountSet.Add(dsk.Node) } } else if mount.Type == HostPath { for _, nde := range allNdes { for _, share := range nde.Shares { if share.MatchPath(mount.HostPath) && utils.HasMatchingItem(share.Roles, org.Roles) { mountSet.Add(nde.Id) } } } } mountNodes = append(mountNodes, mountSet) } } ndes = Nodes{} for _, nde := range allNdes { if !nde.IsOnline() { offlineCount += 1 continue } if mountNodes != nil { match := true for _, mountSet := range mountNodes { if !mountSet.Contains(nde.Id) { match = false break } } if !match { noMountCount += 1 continue } } ndes = append(ndes, nde) } return } func (s *Spec) ExtractResources() (resources string, err error) { matches := resourcesRe.FindStringSubmatch(s.Data) if len(matches) > 1 { resources = matches[1] resources = strings.TrimSpace(resources) return } return } func (s *Spec) parseFirewall(db *database.Database, orgId bson.ObjectID, dataYaml *FirewallYaml) ( errData *errortypes.ErrorData, err error) { data := &Firewall{ Ingress: []*Rule{}, } if dataYaml.Kind != finder.FirewallKind { errData = &errortypes.ErrorData{ Error: "unit_kind_mismatch", Message: "Unit kind unexpected", } return } resources := &finder.Resources{ Organization: orgId, } for _, ruleYaml := range dataYaml.Ingress { if ruleYaml.Source == nil { continue } rule := &Rule{ Protocol: ruleYaml.Protocol, Port: ruleYaml.Port, } refs := set.NewSet() for _, source := range ruleYaml.Source { if strings.HasPrefix(source, TokenPrefix) { kind, e := resources.Find(db, source) if e != nil { err = e return } if kind == finder.UnitKind && resources.Unit != nil { selector := resources.Selector if selector == "" { selector = "private_ips" } refs.Add(Refrence{ Id: resources.Unit.Id, Realm: resources.Unit.Pod, Kind: Unit, Selector: selector, }) } } else { rule.SourceIps = append(rule.SourceIps, source) } } for refInf := range refs.Iter() { ref := refInf.(Refrence) rule.Sources = append(rule.Sources, &ref) } data.Ingress = append(data.Ingress, rule) } errData, err = data.Validate() if err != nil || errData != nil { return } s.Firewall = data return } func (s *Spec) parseInstance(db *database.Database, orgId bson.ObjectID, dataYaml *InstanceYaml) ( errData *errortypes.ErrorData, err error) { data := &Instance{} var shpe *shape.Shape if dataYaml.Name == "" { errData = &errortypes.ErrorData{ Error: "unit_name_missing", Message: "Unit name is missing", } return } switch dataYaml.Kind { case finder.InstanceKind: break case finder.ImageKind: break default: errData = &errortypes.ErrorData{ Error: "unit_kind_mismatch", Message: "Unit kind unexpected", } return } resources := &finder.Resources{ Organization: orgId, } if dataYaml.Plan != "" { kind, e := resources.Find(db, dataYaml.Plan) if e != nil { err = e return } if kind == finder.PlanKind && resources.Plan != nil { data.Plan = resources.Plan.Id } } if dataYaml.Zone != "" { kind, e := resources.Find(db, dataYaml.Zone) if e != nil { err = e return } if kind == finder.ZoneKind && resources.Zone != nil { data.Datacenter = resources.Datacenter.Id data.Zone = resources.Zone.Id } } if data.Zone.IsZero() { errData = &errortypes.ErrorData{ Error: "unit_zone_missing", Message: "Unit zone is missing", } return } if dataYaml.Node != "" { kind, e := resources.Find(db, dataYaml.Node) if e != nil { err = e return } if kind == finder.NodeKind && resources.Node != nil { data.Node = resources.Node.Id } } if dataYaml.Shape != "" { kind, e := resources.Find(db, dataYaml.Shape) if e != nil { err = e return } if kind == finder.ShapeKind && resources.Shape != nil { shpe = resources.Shape data.Shape = resources.Shape.Id } } if data.Node.IsZero() && data.Shape.IsZero() { errData = &errortypes.ErrorData{ Error: "unit_node_missing", Message: "Unit node or shape is missing", } return } if dataYaml.Vpc != "" { kind, e := resources.Find(db, dataYaml.Vpc) if e != nil { err = e return } if kind == finder.VpcKind && resources.Vpc != nil { data.Vpc = resources.Vpc.Id } } if data.Vpc.IsZero() { errData = &errortypes.ErrorData{ Error: "unit_vpc_missing", Message: "Unit VPC is missing", } return } if dataYaml.Subnet != "" { kind, e := resources.Find(db, dataYaml.Subnet) if e != nil { err = e return } if kind == finder.SubnetKind && resources.Subnet != nil { data.Subnet = resources.Subnet.Id } } if data.Subnet.IsZero() { errData = &errortypes.ErrorData{ Error: "unit_subnet_missing", Message: "Unit subnet is missing", } return } if dataYaml.Image != "" { kind, e := resources.Find(db, dataYaml.Image) if e != nil { err = e return } if kind == finder.ImageKind && resources.Image != nil { data.Image = resources.Image.Id } if kind == finder.BuildKind && resources.Deployment != nil && resources.Deployment.ImageReady() { data.Image = resources.Deployment.Image } } if dataYaml.Mounts != nil { for _, mount := range dataYaml.Mounts { mnt := Mount{ Path: utils.FilterPath(mount.Path), Disks: []bson.ObjectID{}, } if mnt.Path == "" { errData = &errortypes.ErrorData{ Error: "mount_path_missing", Message: "Unit mount path is missing", } return } if mount.Type == HostPath { mnt.Name = mount.Name mnt.Type = HostPath mnt.HostPath = utils.FilterPath(mount.HostPath) if mnt.Name == "" { errData = &errortypes.ErrorData{ Error: "mount_name_missing", Message: "Unit mount name is missing", } return } if mnt.HostPath == "" { errData = &errortypes.ErrorData{ Error: "mount_host_path_missing", Message: "Unit mount hostPath is missing", } return } } else if mount.Type == Disk || mount.Type == "" { mnt.Type = Disk if mnt.Path == "" { errData = &errortypes.ErrorData{ Error: "mount_path_missing", Message: "Unit mount path is missing", } return } for _, dsk := range mount.Disks { kind, e := resources.Find(db, dsk) if e != nil { err = e return } if kind == finder.DiskKind && resources.Disks != nil { for _, dskRes := range resources.Disks { mnt.Disks = append(mnt.Disks, dskRes.Id) } } } } else { errData = &errortypes.ErrorData{ Error: "mount_type_invalid", Message: "Unit mount type is invalid", } return } data.Mounts = append(data.Mounts, mnt) } } if dataYaml.NodePorts != nil { externalNodePorts := set.NewSet() for _, nodePrt := range dataYaml.NodePorts { mapping := NodePort{ Protocol: nodePrt.Protocol, ExternalPort: nodePrt.ExternalPort, InternalPort: nodePrt.InternalPort, } extPortKey := fmt.Sprintf("%s:%d", mapping.Protocol, mapping.ExternalPort) if externalNodePorts.Contains(extPortKey) { errData = &errortypes.ErrorData{ Error: "node_port_external_duplicate", Message: "Duplicate external node port", } return } externalNodePorts.Add(extPortKey) errData, err = mapping.Validate() if err != nil || errData != nil { return } data.NodePorts = append(data.NodePorts, mapping) } } if dataYaml.Certificates != nil { for _, cert := range dataYaml.Certificates { kind, e := resources.Find(db, cert) if e != nil { err = e return } if kind == finder.CertificateKind && resources.Certificate != nil { data.Certificates = append( data.Certificates, resources.Certificate.Id, ) } } } if dataYaml.Secrets != nil { for _, cert := range dataYaml.Secrets { kind, e := resources.Find(db, cert) if e != nil { err = e return } if kind == finder.SecretKind && resources.Secret != nil { data.Secrets = append( data.Secrets, resources.Secret.Id, ) } } } if dataYaml.Pods != nil { for _, cert := range dataYaml.Pods { kind, e := resources.Find(db, cert) if e != nil { err = e return } if kind == finder.PodKind && resources.Pod != nil { data.Pods = append( data.Pods, resources.Pod.Id, ) } } } if data.Node.IsZero() && data.Shape.IsZero() { errData = &errortypes.ErrorData{ Error: "unit_image_missing", Message: "Unit image is missing", } return } if shpe != nil { data.Processors = shpe.Processors data.Memory = shpe.Memory if shpe.Flexible { if dataYaml.Processors != 0 { data.Processors = dataYaml.Processors } if dataYaml.Memory != 0 { data.Memory = dataYaml.Memory } } } else { data.Processors = dataYaml.Processors data.Memory = dataYaml.Memory } data.Uefi = dataYaml.Uefi data.SecureBoot = dataYaml.SecureBoot switch dataYaml.CloudType { case instance.Linux: data.CloudType = instance.Linux break case instance.LinuxLegacy: data.CloudType = instance.LinuxLegacy break case instance.BSD: data.CloudType = instance.BSD break case "": break default: errData = &errortypes.ErrorData{ Error: "invalid_unit_cloud_type", Message: "Unit instance cloud type is invalid", } return } data.Tpm = dataYaml.Tpm data.Vnc = dataYaml.Vnc data.DeleteProtection = dataYaml.DeleteProtection data.SkipSourceDestCheck = dataYaml.SkipSourceDestCheck data.Gui = dataYaml.Gui data.HostAddress = dataYaml.HostAddress data.PublicAddress = dataYaml.PublicAddress data.PublicAddress6 = dataYaml.PublicAddress6 data.DhcpServer = dataYaml.DhcpServer data.Roles = dataYaml.Roles data.DiskSize = dataYaml.DiskSize s.Name = dataYaml.Name s.Kind = dataYaml.Kind s.Count = dataYaml.Count s.Instance = data s.Count = dataYaml.Count if s.Kind == finder.ImageKind && s.Count != 0 { errData = &errortypes.ErrorData{ Error: "count_invalid", Message: "Count not valid for image kind", } return } return } func (s *Spec) parseDomain(db *database.Database, orgId bson.ObjectID, dataYaml *DomainYaml) ( errData *errortypes.ErrorData, err error) { data := &Domain{ Records: []*Record{}, } if dataYaml.Kind != finder.DomainKind { errData = &errortypes.ErrorData{ Error: "unit_kind_mismatch", Message: "Unit kind unexpected", } return } resources := &finder.Resources{ Organization: orgId, } for _, recordYaml := range dataYaml.Records { if recordYaml.Name == "" || recordYaml.Type == "" { continue } record := &Record{ Name: utils.FilterName(recordYaml.Name), Type: recordYaml.Type, } kind, e := resources.Find(db, recordYaml.Domain) if e != nil { err = e return } if kind == finder.DomainKind && resources.Domain != nil { record.Domain = resources.Domain.Id } data.Records = append(data.Records, record) } errData, err = data.Validate() if err != nil || errData != nil { return } s.Domain = data return } func (s *Spec) parseJournal(db *database.Database, jrnlKindGen journal.KindGenerator, dataYaml *JournalYaml) ( errData *errortypes.ErrorData, err error) { data := &Journal{ Inputs: []*Input{}, } if dataYaml.Kind != finder.JournalKind { errData = &errortypes.ErrorData{ Error: "unit_kind_mismatch", Message: "Unit kind unexpected", } return } curIndexes := map[string]int32{} if s.Journal != nil { for _, jrnl := range s.Journal.Inputs { curIndexes[jrnl.Key] = jrnl.Index } } for _, input := range dataYaml.Inputs { data.Inputs = append(data.Inputs, &Input{ Key: input.Key, Type: input.Type, Unit: input.Unit, Path: input.Path, }) } errData, err = data.Validate() if err != nil || errData != nil { return } for _, input := range data.Inputs { input.Index = curIndexes[input.Key] if input.Index == 0 { if jrnlKindGen == nil { errData = &errortypes.ErrorData{ Error: "journal_missing_index", Message: "Journal missing index", } return } index, e := jrnlKindGen.GetKind(db, input.Key) if e != nil { err = e return } input.Index = index } } s.Journal = data return } func (s *Spec) Refresh(db *database.Database) ( errData *errortypes.ErrorData, err error) { errData, err = s.Parse(db, nil) if err != nil || errData != nil { return } err = s.CommitData(db) if err != nil { return } return } func (s *Spec) Parse(db *database.Database, jrnlKindGen journal.KindGenerator) ( errData *errortypes.ErrorData, err error) { hash := sha1.New() hash.Write([]byte(filterSpecHash(s.Data))) hashBytes := hash.Sum(nil) s.Hash = fmt.Sprintf("%x", hashBytes) resourcesSpec, err := s.ExtractResources() if err != nil { return } if resourcesSpec == "" { errData = &errortypes.ErrorData{ Error: "unit_resources_block_missing", Message: "Unit missing yaml resources block", } return } baseDecode := yaml.NewDecoder(strings.NewReader(resourcesSpec)) decoder := yaml.NewDecoder(strings.NewReader(resourcesSpec)) for { baseDoc := &Base{} err = baseDecode.Decode(baseDoc) if err != nil { if err == io.EOF { err = nil break } err = &errortypes.ParseError{ errors.Wrap(err, "spec: Failed to decode yaml doc"), } return } switch baseDoc.Kind { case finder.InstanceKind, finder.ImageKind: instYaml := &InstanceYaml{} err = decoder.Decode(instYaml) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "spec: Failed to decode instance yaml doc"), } return } errData, err = s.parseInstance(db, s.Organization, instYaml) if err != nil || errData != nil { return } case finder.FirewallKind: fireYaml := &FirewallYaml{} err = decoder.Decode(fireYaml) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "spec: Failed to decode firewall yaml doc"), } return } errData, err = s.parseFirewall(db, s.Organization, fireYaml) if err != nil || errData != nil { return } case finder.DomainKind: domnYaml := &DomainYaml{} err = decoder.Decode(domnYaml) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "spec: Failed to decode domain yaml doc"), } return } errData, err = s.parseDomain(db, s.Organization, domnYaml) if err != nil || errData != nil { return } case finder.JournalKind: jrnlYaml := &JournalYaml{} err = decoder.Decode(jrnlYaml) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "spec: Failed to decode domain yaml doc"), } return } errData, err = s.parseJournal(db, jrnlKindGen, jrnlYaml) if err != nil || errData != nil { return } default: errData = &errortypes.ErrorData{ Error: "unit_kind_invalid", Message: "Unit kind is invalid", } return } } return } func (s *Spec) CanMigrate(db *database.Database, deply *deployment.Deployment, spc *Spec) ( errData *errortypes.ErrorData, err error) { var inst *instance.Instance if !deply.Instance.IsZero() { inst, err = instance.Get(db, deply.Instance) if err != nil { return } } if !settings.System.NoMigrateRefresh { errData, err = s.Parse(db, nil) if err != nil || errData != nil { return } } errData, err = spc.Parse(db, nil) if err != nil || errData != nil { return } if s.Pod != spc.Pod || s.Unit != spc.Unit { err = &errortypes.ParseError{ errors.Newf("spec: Invalid unit"), } return } if s.Kind != spc.Kind { errData = &errortypes.ErrorData{ Error: "unit_kind_conflict", Message: "Cannot migrate to different kind", } return } if s.Instance == nil || spc.Instance == nil { err = &errortypes.ParseError{ errors.Newf("spec: Instance not found"), } return } if s.Instance.Datacenter != spc.Instance.Datacenter { errData = &errortypes.ErrorData{ Error: "instance_datacenter_conflict", Message: "Cannot migrate to different instance datacenter", } return } if s.Instance.Zone != spc.Instance.Zone { errData = &errortypes.ErrorData{ Error: "instance_zone_conflict", Message: "Cannot migrate to different instance zone", } return } if s.Instance.Node != spc.Instance.Node && !spc.Instance.Node.IsZero() && inst.Node != spc.Instance.Node { errData = &errortypes.ErrorData{ Error: "instance_node_coflict", Message: "Cannot migrate to different instance node", } return } if s.Instance.Subnet != spc.Instance.Subnet { errData = &errortypes.ErrorData{ Error: "instance_subnet_coflict", Message: "Cannot migrate to different instance subnet", } return } curMountPaths := set.NewSet() for _, mnt := range s.Instance.Mounts { if mnt.Type == HostPath { continue } curMountPaths.Add(mnt.Path) } newMountPaths := set.NewSet() for _, mnt := range spc.Instance.Mounts { if mnt.Type == HostPath { continue } newMountPaths.Add(mnt.Path) } if !curMountPaths.IsEqual(newMountPaths) { errData = &errortypes.ErrorData{ Error: "instance_mount_coflict", Message: "Cannot migrate to different instance mounts", } return } return } func (s *Spec) Commit(db *database.Database) (err error) { coll := db.Specs() err = coll.Commit(s.Id, s) if err != nil { return } return } func (s *Spec) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Specs() err = coll.CommitFields(s.Id, s, fields) if err != nil { return } return } func (s *Spec) CommitData(db *database.Database) (err error) { coll := db.Specs() err = coll.CommitFields(s.Id, s, set.NewSet( "name", "count", "data", "instance", "firewall", "domain")) if err != nil { return } return } func (s *Spec) Insert(db *database.Database) (err error) { coll := db.Specs() resp, err := coll.InsertOne(db, s) if err != nil { err = database.ParseError(err) return } s.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: spec/utils.go ================================================ package spec import ( "regexp" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) type Named struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Unit bson.ObjectID `bson:"unit" json:"unit"` Index int `bson:"index" json:"index"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` } var ( yamlBlockRe = regexp.MustCompile("(?s)^```yaml\\n(.*?)```") ) func filterSpecHash(input string) string { return yamlBlockRe.ReplaceAllStringFunc(input, func(block string) string { lines := strings.Split(block, "\n") result := []string{} for _, line := range lines { line = strings.TrimSpace(line) if strings.HasPrefix(line, "name:") || strings.HasPrefix(line, "count:") { continue } result = append(result, line) } return strings.Join(result, "\n") }) } func New(podId, unitId, orgId bson.ObjectID, data string) (spc *Spec) { spc = &Spec{ Id: bson.NewObjectID(), Unit: unitId, Pod: podId, Organization: orgId, Data: data, } return } func Get(db *database.Database, commitId bson.ObjectID) ( spc *Spec, err error) { coll := db.Specs() spc = &Spec{} err = coll.FindOneId(commitId, spc) if err != nil { return } return } func GetOne(db *database.Database, query *bson.M) ( spc *Spec, err error) { coll := db.Specs() spc = &Spec{} err = coll.FindOne(db, query).Decode(spc) if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (spcs []*Named, count int64, err error) { coll := db.Specs() spcs = []*Named{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetProjection(bson.M{ "_id": 1, "unit": 1, "index": 1, "timestamp": 1, }). SetSort(bson.D{{"timestamp", -1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { spc := &Named{} err = cursor.Decode(spc) if err != nil { err = database.ParseError(err) return } spcs = append(spcs, spc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) ( spcs []*Spec, err error) { coll := db.Specs() spcs = []*Spec{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { spc := &Spec{} err = cursor.Decode(spc) if err != nil { err = database.ParseError(err) return } spcs = append(spcs, spc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllIndexes(db *database.Database, query *bson.M) ( spcs []*Spec, err error) { coll := db.Specs() spcs = []*Spec{} cursor, err := coll.Find( db, query, options.Find(). SetProjection(bson.M{ "_id": 1, "unit": 1, "index": 1, "timestamp": 1, }). SetSort(bson.D{{"timestamp", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { spc := &Spec{} err = cursor.Decode(spc) if err != nil { err = database.ParseError(err) return } spcs = append(spcs, spc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllProjectSorted(db *database.Database, query *bson.M) ( spcs []*Spec, err error) { coll := db.Specs() spcs = []*Spec{} cursor, err := coll.Find( db, query, options.Find(). SetProjection(bson.M{ "_id": 1, "unit": 1, "index": 1, "timestamp": 1, "hash": 1, "data": 1, }). SetSort(bson.D{{"timestamp", -1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { spc := &Spec{} err = cursor.Decode(spc) if err != nil { err = database.ParseError(err) return } spcs = append(spcs, spc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllIds(db *database.Database) (specIds set.Set, err error) { coll := db.Specs() specIds = set.NewSet() cursor, err := coll.Find( db, bson.M{}, options.Find(). SetProjection(bson.M{ "_id": 1, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { spc := &Spec{} err = cursor.Decode(spc) if err != nil { err = database.ParseError(err) return } specIds.Add(spc.Id) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, commitId bson.ObjectID) (err error) { coll := db.Specs() _, err = coll.DeleteOne(db, &bson.M{ "_id": commitId, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } return } func RemoveAll(db *database.Database, query *bson.M) (err error) { coll := db.Specs() _, err = coll.DeleteMany(db, query) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: state/arps.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/arp" "github.com/pritunl/pritunl-cloud/database" ) var ( Arps = &ArpsState{} ArpsPkg = NewPackage(Arps) ) type ArpsState struct { arpRecords map[string]set.Set } func (p *ArpsState) ArpRecords(namespace string) set.Set { return p.arpRecords[namespace] } func (p *ArpsState) Refresh(pkg *Package, db *database.Database) (err error) { p.arpRecords = arp.BuildState(Instances.Instances(), Vpcs.VpcsMap(), Vpcs.VpcIpsMap()) return } func (p *ArpsState) Apply(st *State) { st.ArpRecords = p.ArpRecords } func init() { ArpsPkg. After(Instances). After(Vpcs) } ================================================ FILE: state/authorities.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/database" ) var ( Authorities = &AuthoritiesState{} AuthoritiesPkg = NewPackage(Authorities) ) type AuthoritiesState struct { authoritiesMap map[string][]*authority.Authority } func (p *AuthoritiesState) GetInstaceAuthorities( orgId bson.ObjectID, roles []string) []*authority.Authority { authrSet := set.NewSet() authrs := []*authority.Authority{} for _, role := range roles { for _, authr := range p.authoritiesMap[role] { if authrSet.Contains(authr.Id) || authr.Organization != orgId { continue } authrSet.Add(authr.Id) authrs = append(authrs, authr) } } return authrs } func (p *AuthoritiesState) Refresh(pkg *Package, db *database.Database) (err error) { _, rolesSet := InstancesPreload.GetRoles() authorities := AuthoritiesPreload.Authorities() preloadRolesSet := AuthoritiesPreload.RolesSet() roles := rolesSet.Copy() roles.Subtract(preloadRolesSet) missRoles := []string{} for roleInf := range roles.Iter() { missRoles = append(missRoles, roleInf.(string)) } if len(missRoles) > 0 { missAuthorities, e := authority.GetMapRoles(db, &bson.M{ "roles": &bson.M{ "$in": missRoles, }, }) if e != nil { err = e return } for role, authrs := range missAuthorities { authorities[role] = authrs } } p.authoritiesMap = authorities return } func (p *AuthoritiesState) Apply(st *State) { st.GetInstaceAuthorities = p.GetInstaceAuthorities } func init() { AuthoritiesPkg. After(AuthoritiesPreload). After(InstancesPreload) } ================================================ FILE: state/authorities_preload.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/database" ) var ( AuthoritiesPreload = &AuthoritiesPreloadState{} AuthoritiesPreloadPkg = NewPackage(AuthoritiesPreload) ) type AuthoritiesPreloadState struct { authoritiesMap map[string][]*authority.Authority authoritiesRolesSet set.Set } func (p *AuthoritiesPreloadState) Authorities() map[string][]*authority.Authority { return p.authoritiesMap } func (p *AuthoritiesPreloadState) RolesSet() set.Set { return p.authoritiesRolesSet } func (p *AuthoritiesPreloadState) Refresh(pkg *Package, db *database.Database) (err error) { roles, rolesSet := InstancesPreload.GetRoles() if len(roles) == 0 { p.authoritiesMap = map[string][]*authority.Authority{} p.authoritiesRolesSet = set.NewSet() return } authrsMap, err := authority.GetMapRoles(db, &bson.M{ "roles": &bson.M{ "$in": roles, }, }) if err != nil { return } p.authoritiesMap = authrsMap p.authoritiesRolesSet = rolesSet return } func (p *AuthoritiesPreloadState) Apply(st *State) { } ================================================ FILE: state/datacenter.go ================================================ package state import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/node" ) var ( Datacenter = &DatacenterState{} DatacenterPkg = NewPackage(Datacenter) ) type DatacenterState struct { nodeDatacenter *datacenter.Datacenter } func (p *DatacenterState) NodeDatacenter() *datacenter.Datacenter { return p.nodeDatacenter } func (p *DatacenterState) Refresh(pkg *Package, db *database.Database) (err error) { dcId := node.Self.Datacenter if dcId.IsZero() { p.nodeDatacenter = nil pkg.Evict() return } dc, e := datacenter.Get(db, dcId) if e != nil { err = e return } p.nodeDatacenter = dc pkg.Cache(15 * time.Second) return } func (p *DatacenterState) Apply(st *State) { st.NodeDatacenter = p.NodeDatacenter } ================================================ FILE: state/deployments.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" ) var ( Deployments = &DeploymentsState{} DeploymentsPkg = NewPackage(Deployments) ) type DeploymentsResult struct { DeploymentIds []bson.ObjectID `bson:"deployment_ids"` PodIds []bson.ObjectID `bson:"pod_ids"` UnitIds []bson.ObjectID `bson:"unit_ids"` SpecIds []bson.ObjectID `bson:"spec_ids"` Deployments []*deployment.Deployment `bson:"deployments"` Pods []*pod.Pod `bson:"pods"` Units []*unit.Unit `bson:"units"` Specs []*spec.Spec `bson:"specs"` SpecPodIds []bson.ObjectID `bson:"spec_pod_ids"` SpecUnitIds []bson.ObjectID `bson:"spec_unit_ids"` SpecSecretIds []bson.ObjectID `bson:"spec_secret_ids"` SpecCertIds []bson.ObjectID `bson:"spec_cert_ids"` SpecDomainIds []bson.ObjectID `bson:"spec_domain_ids"` SpecIdUnits []*unit.Unit `bson:"spec_id_units"` SpecPodUnits []*unit.Unit `bson:"spec_pod_units"` SpecPods []*pod.Pod `bson:"spec_pods"` SpecSecrets []*secret.Secret `bson:"spec_secrets"` SpecCerts []*certificate.Certificate `bson:"spec_certs"` SpecDomains []*domain.Domain `bson:"spec_domains"` SpecDomainsRecords []*domain.Record `bson:"spec_domains_records"` LinkedDeployments []*deployment.Deployment `bson:"linked_deployments"` } type DeploymentsState struct { podsMap map[bson.ObjectID]*pod.Pod unitsMap map[bson.ObjectID]*unit.Unit specsMap map[bson.ObjectID]*spec.Spec specsPodsMap map[bson.ObjectID]*pod.Pod specsPodUnitsMap map[bson.ObjectID][]*unit.Unit specsUnitsMap map[bson.ObjectID]*unit.Unit specsDeploymentsMap map[bson.ObjectID]*deployment.Deployment specsDomainsMap map[bson.ObjectID]*domain.Domain specsSecretsMap map[bson.ObjectID]*secret.Secret specsCertsMap map[bson.ObjectID]*certificate.Certificate deploymentsNode map[bson.ObjectID]*deployment.Deployment deploymentsReservedMap map[bson.ObjectID]*deployment.Deployment deploymentsDeployedMap map[bson.ObjectID]*deployment.Deployment deploymentsInactiveMap map[bson.ObjectID]*deployment.Deployment } func (p *DeploymentsState) Pod(pdId bson.ObjectID) *pod.Pod { return p.podsMap[pdId] } func (p *DeploymentsState) PodsMap() map[bson.ObjectID]*pod.Pod { return p.podsMap } func (p *DeploymentsState) Unit(untId bson.ObjectID) *unit.Unit { return p.unitsMap[untId] } func (p *DeploymentsState) UnitsMap() map[bson.ObjectID]*unit.Unit { return p.unitsMap } func (p *DeploymentsState) Spec(commitId bson.ObjectID) *spec.Spec { return p.specsMap[commitId] } func (p *DeploymentsState) SpecsMap() map[bson.ObjectID]*spec.Spec { return p.specsMap } func (p *DeploymentsState) SpecPod(pdId bson.ObjectID) *pod.Pod { return p.specsPodsMap[pdId] } func (p *DeploymentsState) SpecPodUnits(pdId bson.ObjectID) []*unit.Unit { return p.specsPodUnitsMap[pdId] } func (p *DeploymentsState) SpecUnit(unitId bson.ObjectID) *unit.Unit { return p.specsUnitsMap[unitId] } func (p *DeploymentsState) SpecsUnitsMap() map[bson.ObjectID]*unit.Unit { return p.specsUnitsMap } func (p *DeploymentsState) SpecDomain(domnId bson.ObjectID) *domain.Domain { return p.specsDomainsMap[domnId] } func (p *DeploymentsState) SpecSecret(secrID bson.ObjectID) *secret.Secret { return p.specsSecretsMap[secrID] } func (p *DeploymentsState) SpecCert( certId bson.ObjectID) *certificate.Certificate { return p.specsCertsMap[certId] } func (p *DeploymentsState) SpecCertMap() map[bson.ObjectID]*certificate.Certificate { return p.specsCertsMap } func (p *DeploymentsState) DeploymentsNode() map[bson.ObjectID]*deployment.Deployment { return p.deploymentsNode } func (p *DeploymentsState) DeploymentReserved(deplyId bson.ObjectID) *deployment.Deployment { return p.deploymentsReservedMap[deplyId] } func (p *DeploymentsState) DeploymentsReserved() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = p.deploymentsReservedMap return } func (p *DeploymentsState) DeploymentDeployed(deplyId bson.ObjectID) *deployment.Deployment { return p.deploymentsDeployedMap[deplyId] } func (p *DeploymentsState) DeploymentsDeployed() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = p.deploymentsDeployedMap return } func (p *DeploymentsState) DeploymentsDestroy() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = p.deploymentsInactiveMap return } func (p *DeploymentsState) DeploymentInactive(deplyId bson.ObjectID) *deployment.Deployment { return p.deploymentsInactiveMap[deplyId] } func (p *DeploymentsState) DeploymentsInactive() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = p.deploymentsInactiveMap return } func (p *DeploymentsState) Deployment(deplyId bson.ObjectID) ( deply *deployment.Deployment) { deply = p.deploymentsDeployedMap[deplyId] if deply != nil { return } deply = p.deploymentsReservedMap[deplyId] if deply != nil { return } deply = p.deploymentsInactiveMap[deplyId] if deply != nil { return } return } func (p *DeploymentsState) Refresh(pkg *Package, db *database.Database) (err error) { pipeline := bson.A{ bson.M{ "$match": bson.M{ "node": node.Self.Id, }, }, bson.M{ "$group": bson.M{ "_id": nil, "deployments": bson.M{"$push": "$$ROOT"}, "deployment_ids": bson.M{"$addToSet": "$_id"}, "pod_ids": bson.M{"$addToSet": "$pod"}, "unit_ids": bson.M{"$addToSet": "$unit"}, "spec_ids": bson.M{"$addToSet": "$spec"}, }, }, bson.M{ "$lookup": bson.M{ "from": "specs", "localField": "spec_ids", "foreignField": "_id", "as": "specs", }, }, bson.M{ "$lookup": bson.M{ "from": "units", "localField": "unit_ids", "foreignField": "_id", "as": "units", }, }, bson.M{ "$lookup": bson.M{ "from": "pods", "localField": "pod_ids", "foreignField": "_id", "as": "pods", }, }, bson.M{ "$addFields": bson.M{ "specs_data": bson.M{ "$reduce": bson.M{ "input": "$specs", "initialValue": bson.M{ "pod_ids": bson.A{}, "secret_ids": bson.A{}, "cert_ids": bson.A{}, "domain_ids": bson.A{}, "unit_ids": bson.A{}, }, "in": bson.M{ "pod_ids": bson.M{ "$concatArrays": bson.A{ "$$value.pod_ids", bson.M{ "$ifNull": bson.A{ "$$this.instance.pods", bson.A{}, }, }, }, }, "secret_ids": bson.M{ "$concatArrays": bson.A{ "$$value.secret_ids", bson.M{ "$ifNull": bson.A{ "$$this.instance.secrets", bson.A{}, }, }, }, }, "cert_ids": bson.M{ "$concatArrays": bson.A{ "$$value.cert_ids", bson.M{ "$ifNull": bson.A{ "$$this.instance.certificates", bson.A{}, }, }, }, }, "domain_ids": bson.M{ "$concatArrays": bson.A{ "$$value.domain_ids", bson.M{ "$map": bson.M{ "input": bson.M{ "$ifNull": bson.A{ "$$this.domain.records", bson.A{}, }, }, "as": "record", "in": "$$record.domain", }, }, }, }, "unit_ids": bson.M{ "$concatArrays": bson.A{ "$$value.unit_ids", bson.M{ "$reduce": bson.M{ "input": bson.M{ "$ifNull": bson.A{ "$$this.firewall.ingress", bson.A{}, }, }, "initialValue": bson.A{}, "in": bson.M{ "$concatArrays": bson.A{ "$$value", bson.M{ "$ifNull": bson.A{ bson.M{ "$map": bson.M{ "input": bson.M{ "$ifNull": bson.A{ "$$this.sources", bson.A{}, }, }, "as": "source", "in": "$$source.id", }, }, bson.A{}, }, }, }, }, }, }, }, }, }, }, }, }, }, bson.M{ "$addFields": bson.M{ "spec_pod_ids": "$specs_data.pod_ids", "spec_secret_ids": "$specs_data.secret_ids", "spec_cert_ids": "$specs_data.cert_ids", "spec_domain_ids": "$specs_data.domain_ids", "spec_unit_ids": "$specs_data.unit_ids", }, }, bson.M{ "$lookup": bson.M{ "from": "units", "localField": "spec_unit_ids", "foreignField": "_id", "as": "spec_id_units", }, }, bson.M{ "$lookup": bson.M{ "from": "units", "localField": "spec_pod_ids", "foreignField": "pod", "as": "spec_pod_units", }, }, bson.M{ "$lookup": bson.M{ "from": "pods", "localField": "spec_pod_ids", "foreignField": "_id", "as": "spec_pods", }, }, bson.M{ "$lookup": bson.M{ "from": "secrets", "localField": "spec_secret_ids", "foreignField": "_id", "as": "spec_secrets", }, }, bson.M{ "$lookup": bson.M{ "from": "certificates", "localField": "spec_cert_ids", "foreignField": "_id", "as": "spec_certs", }, }, bson.M{ "$lookup": bson.M{ "from": "domains", "localField": "spec_domain_ids", "foreignField": "_id", "as": "spec_domains", }, }, bson.M{ "$lookup": bson.M{ "from": "domains_records", "localField": "spec_domain_ids", "foreignField": "domain", "as": "spec_domains_records", }, }, bson.M{ "$addFields": bson.M{ "spec_deployment_id_ids": bson.M{ "$reduce": bson.M{ "input": "$spec_id_units", "initialValue": bson.A{}, "in": bson.M{ "$concatArrays": bson.A{ "$$value", bson.M{ "$ifNull": bson.A{ "$$this.deployments", bson.A{}, }, }, }, }, }, }, "spec_deployment_pod_ids": bson.M{ "$reduce": bson.M{ "input": "$spec_pod_units", "initialValue": bson.A{}, "in": bson.M{ "$concatArrays": bson.A{ "$$value", bson.M{ "$ifNull": bson.A{ "$$this.deployments", bson.A{}, }, }, }, }, }, }, "pod_deployment_ids": bson.M{ "$reduce": bson.M{ "input": "$units", "initialValue": bson.A{}, "in": bson.M{ "$setUnion": bson.A{ "$$value", bson.M{ "$ifNull": bson.A{ "$$this.deployments", bson.A{}, }, }, }, }, }, }, }, }, bson.M{ "$addFields": bson.M{ "linked_deployment_ids": bson.M{ "$setDifference": bson.A{ bson.M{ "$setUnion": bson.A{ "$spec_deployment_id_ids", "$spec_deployment_pod_ids", "$pod_deployment_ids", }, }, "$deployment_ids", }, }, }, }, bson.M{ "$lookup": bson.M{ "from": "deployments", "localField": "linked_deployment_ids", "foreignField": "_id", "as": "linked_deployments", }, }, bson.M{ "$project": bson.M{ "deployment_ids": 1, "pod_ids": 1, "unit_ids": 1, "spec_ids": 1, "deployments": 1, "pods": 1, "units": 1, "specs": 1, "spec_pod_ids": 1, "spec_unit_ids": 1, "spec_secret_ids": 1, "spec_cert_ids": 1, "spec_domain_ids": 1, "spec_id_units": 1, "spec_pod_units": 1, "spec_pods": 1, "spec_secrets": 1, "spec_certs": 1, "spec_domains": 1, "spec_domains_records": 1, "linked_deployments": 1, }, }, } cursor, err := db.Deployments().Aggregate(db, pipeline) if err != nil { return } defer cursor.Close(db) result := &DeploymentsResult{} if cursor.Next(db) { err = cursor.Decode(result) if err != nil { return } } deploymentsNode := map[bson.ObjectID]*deployment.Deployment{} deploymentsReservedMap := map[bson.ObjectID]*deployment.Deployment{} deploymentsDeployedMap := map[bson.ObjectID]*deployment.Deployment{} deploymentsInactiveMap := map[bson.ObjectID]*deployment.Deployment{} for _, deply := range result.Deployments { deploymentsNode[deply.Id] = deply switch deply.State { case deployment.Reserved: deploymentsReservedMap[deply.Id] = deply case deployment.Deployed: switch deply.Action { case deployment.Destroy, deployment.Archive, deployment.Restore: deploymentsInactiveMap[deply.Id] = deply default: deploymentsDeployedMap[deply.Id] = deply } case deployment.Archived: deploymentsInactiveMap[deply.Id] = deply } } p.deploymentsNode = deploymentsNode specsMap := map[bson.ObjectID]*spec.Spec{} for _, spec := range result.Specs { specsMap[spec.Id] = spec } p.specsMap = specsMap specsCertsMap := map[bson.ObjectID]*certificate.Certificate{} for _, specCert := range result.SpecCerts { specsCertsMap[specCert.Id] = specCert } p.specsCertsMap = specsCertsMap specsSecretsMap := map[bson.ObjectID]*secret.Secret{} for _, specSecret := range result.SpecSecrets { specsSecretsMap[specSecret.Id] = specSecret } p.specsSecretsMap = specsSecretsMap specsPodsMap := map[bson.ObjectID]*pod.Pod{} for _, specPod := range result.SpecPods { specsPodsMap[specPod.Id] = specPod } p.specsPodsMap = specsPodsMap specDomains := domain.PreloadedRecords( result.SpecDomains, result.SpecDomainsRecords) specsDomainsMap := map[bson.ObjectID]*domain.Domain{} for _, specDomain := range specDomains { specsDomainsMap[specDomain.Id] = specDomain } p.specsDomainsMap = specsDomainsMap specUnitsIds := set.NewSet() specsUnitsMap := map[bson.ObjectID]*unit.Unit{} specsPodUnitsMap := map[bson.ObjectID][]*unit.Unit{} for _, specUnit := range result.SpecIdUnits { if specUnitsIds.Contains(specUnit.Id) { continue } specUnitsIds.Add(specUnit.Id) specsUnitsMap[specUnit.Id] = specUnit specsPodUnitsMap[specUnit.Pod] = append( specsPodUnitsMap[specUnit.Pod], specUnit) } for _, specUnit := range result.SpecPodUnits { if specUnitsIds.Contains(specUnit.Id) { continue } specUnitsIds.Add(specUnit.Id) specsUnitsMap[specUnit.Id] = specUnit specsPodUnitsMap[specUnit.Pod] = append( specsPodUnitsMap[specUnit.Pod], specUnit) } p.specsUnitsMap = specsUnitsMap p.specsPodUnitsMap = specsPodUnitsMap for _, deply := range result.LinkedDeployments { switch deply.State { case deployment.Reserved: deploymentsReservedMap[deply.Id] = deply case deployment.Deployed: switch deply.Action { case deployment.Destroy, deployment.Archive, deployment.Restore: deploymentsInactiveMap[deply.Id] = deply default: deploymentsDeployedMap[deply.Id] = deply } case deployment.Archived: deploymentsInactiveMap[deply.Id] = deply } } p.deploymentsReservedMap = deploymentsReservedMap p.deploymentsDeployedMap = deploymentsDeployedMap p.deploymentsInactiveMap = deploymentsInactiveMap podsMap := map[bson.ObjectID]*pod.Pod{} for _, pd := range result.Pods { podsMap[pd.Id] = pd } p.podsMap = podsMap unitsMap := map[bson.ObjectID]*unit.Unit{} for _, unt := range result.Units { unitsMap[unt.Id] = unt } p.unitsMap = unitsMap return } func (p *DeploymentsState) Apply(st *State) { st.Pod = p.Pod st.PodsMap = p.PodsMap st.Unit = p.Unit st.UnitsMap = p.UnitsMap st.Spec = p.Spec st.SpecPod = p.SpecPod st.SpecPodUnits = p.SpecPodUnits st.SpecUnit = p.SpecUnit st.SpecsUnitsMap = p.SpecsUnitsMap st.SpecDomain = p.SpecDomain st.SpecSecret = p.SpecSecret st.SpecCert = p.SpecCert st.DeploymentsNode = p.DeploymentsNode st.DeploymentReserved = p.DeploymentReserved st.DeploymentsReserved = p.DeploymentsReserved st.DeploymentDeployed = p.DeploymentDeployed st.DeploymentsDeployed = p.DeploymentsDeployed st.DeploymentsDestroy = p.DeploymentsDestroy st.DeploymentInactive = p.DeploymentInactive st.DeploymentsInactive = p.DeploymentsInactive st.Deployment = p.Deployment } ================================================ FILE: state/disks.go ================================================ package state import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/node" ) var ( Disks = &DisksState{} DisksPkg = NewPackage(Disks) ) type DisksState struct { disks []*disk.Disk instanceDisks map[bson.ObjectID][]*disk.Disk deploymentDisks map[bson.ObjectID][]*disk.Disk } func (p *DisksState) Disks() []*disk.Disk { return p.disks } func (p *DisksState) GetInstaceDisks(instId bson.ObjectID) []*disk.Disk { return p.instanceDisks[instId] } func (p *DisksState) GetDeploymentDisks( deplyId bson.ObjectID) []*disk.Disk { return p.deploymentDisks[deplyId] } func (p *DisksState) InstaceDisksMap() map[bson.ObjectID][]*disk.Disk { return p.instanceDisks } func (p *DisksState) Refresh(pkg *Package, db *database.Database) (err error) { ndeId := node.Self.Id ndePools := node.Self.Pools disks, err := disk.GetNode(db, ndeId, ndePools) if err != nil { return } p.disks = disks instanceDisks := map[bson.ObjectID][]*disk.Disk{} deploymentDisks := map[bson.ObjectID][]*disk.Disk{} for _, dsk := range disks { if !dsk.Instance.IsZero() { instanceDisks[dsk.Instance] = append( instanceDisks[dsk.Instance], dsk) } if !dsk.Deployment.IsZero() { deploymentDisks[dsk.Deployment] = append( deploymentDisks[dsk.Deployment], dsk) } } p.instanceDisks = instanceDisks p.deploymentDisks = deploymentDisks return } func (p *DisksState) Apply(st *State) { st.Disks = p.Disks st.GetInstaceDisks = p.GetInstaceDisks st.GetDeploymentDisks = p.GetDeploymentDisks st.InstaceDisksMap = p.InstaceDisksMap } ================================================ FILE: state/domains.go ================================================ package state import ( "net" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/imds/types" ) var ( Domains = &DomainsState{} DomainsPkg = NewPackage(Domains) ) type DomainsState struct { domains map[bson.ObjectID][]*types.Domain } func (p *DomainsState) GetDomains(orgId bson.ObjectID) []*types.Domain { return p.domains[orgId] } func (p *DomainsState) Refresh(pkg *Package, db *database.Database) (err error) { coll := db.Domains() rootDomains := map[bson.ObjectID]*domain.Domain{} records := map[bson.ObjectID][]*types.Domain{} cursor, err := coll.Find(db, bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { dmn := &domain.Domain{} err = cursor.Decode(dmn) if err != nil { err = database.ParseError(err) return } rootDomains[dmn.Id] = dmn } coll = db.DomainsRecords() cursor, err = coll.Find( db, bson.M{}, options.Find().SetSort(bson.D{{"_id", 1}}), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { rec := &domain.Record{} err = cursor.Decode(rec) if err != nil { err = database.ParseError(err) return } if rec.IsDeleted() { continue } dmn := rootDomains[rec.Domain] if dmn == nil { continue } dmnRec := &types.Domain{ Domain: rec.SubDomain + "." + dmn.RootDomain + ".", Type: rec.Type, } switch rec.Type { case domain.A: dmnRec.Ip = net.ParseIP(rec.Value) case domain.AAAA: dmnRec.Ip = net.ParseIP(rec.Value) case domain.CNAME: dmnRec.Target = rec.Value + "." default: continue } records[dmn.Organization] = append(records[dmn.Organization], dmnRec) } p.domains = records return } func (p *DomainsState) Apply(st *State) { st.GetDomains = p.GetDomains } ================================================ FILE: state/firewalls.go ================================================ package state import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/node" ) var ( Firewalls = &FirewallsState{} FirewallsPkg = NewPackage(Firewalls) ) type FirewallsState struct { nodeFirewall []*firewall.Rule firewalls map[string][]*firewall.Rule firewallMaps map[string][]*firewall.Mapping instanceNamespaces map[bson.ObjectID][]string } func (p *FirewallsState) NodeFirewall() []*firewall.Rule { return p.nodeFirewall } func (p *FirewallsState) Firewalls() map[string][]*firewall.Rule { return p.firewalls } func (p *FirewallsState) FirewallMaps() map[string][]*firewall.Mapping { return p.firewallMaps } func (p *FirewallsState) GetInstanceNamespaces( instId bson.ObjectID) []string { return p.instanceNamespaces[instId] } func (p *FirewallsState) Refresh(pkg *Package, db *database.Database) (err error) { specRules, err := firewall.GetSpecRules(Instances.Instances(), Deployments.DeploymentsNode(), Deployments.SpecsMap(), Deployments.SpecsUnitsMap(), Deployments.DeploymentsDeployed()) if err != nil { return } _, rolesSet := InstancesPreload.GetRoles() firesMap := FirewallsPreload.Firewalls() firewallRolesSet := FirewallsPreload.RolesSet() roles := rolesSet.Copy() roles.Subtract(firewallRolesSet) missRoles := []string{} for roleInf := range roles.Iter() { missRoles = append(missRoles, roleInf.(string)) } if len(missRoles) > 0 { missFiresMap, e := firewall.GetMapRoles(db, missRoles) if e != nil { err = e return } for role, fires := range missFiresMap { firesMap[role] = fires } } nodeFirewall, firewalls, firewallMaps, instNamespaces, err := firewall.GetAllIngressPreloaded(node.Self, Instances.Instances(), specRules, Instances.NodePortsMap(), firesMap) if err != nil { return } p.nodeFirewall = nodeFirewall p.firewalls = firewalls p.firewallMaps = firewallMaps p.instanceNamespaces = instNamespaces return } func (p *FirewallsState) Apply(st *State) { st.NodeFirewall = p.NodeFirewall st.Firewalls = p.Firewalls st.FirewallMaps = p.FirewallMaps st.GetInstanceNamespaces = p.GetInstanceNamespaces } func init() { FirewallsPkg. After(FirewallsPreload). After(Instances). After(Vpcs). After(Deployments) } ================================================ FILE: state/firewalls_preload.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/firewall" ) var ( FirewallsPreload = &FirewallsPreloadState{} FirewallsPreloadPkg = NewPackage(FirewallsPreload) ) type FirewallsPreloadState struct { firewalls map[string][]*firewall.Firewall firewallsRolesSet set.Set } func (p *FirewallsPreloadState) Firewalls() map[string][]*firewall.Firewall { return p.firewalls } func (p *FirewallsPreloadState) RolesSet() set.Set { return p.firewallsRolesSet } func (p *FirewallsPreloadState) Refresh(pkg *Package, db *database.Database) (err error) { roles, rolesSet := InstancesPreload.GetRoles() if len(roles) == 0 { p.firewalls = map[string][]*firewall.Firewall{} p.firewallsRolesSet = set.NewSet() return } firesMap, err := firewall.GetMapRoles(db, roles) if err != nil { return } p.firewalls = firesMap p.firewallsRolesSet = rolesSet return } func (p *FirewallsPreloadState) Apply(st *State) { } ================================================ FILE: state/instances.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/nodeport" ) var ( Instances = &InstancesState{} InstancesPkg = NewPackage(Instances) ) type InstancesState struct { instances []*instance.Instance instancesMap map[bson.ObjectID]*instance.Instance nodePortsMap map[string][]*nodeport.Mapping } func (p *InstancesState) GetInstace( instId bson.ObjectID) *instance.Instance { if instId.IsZero() { return nil } return p.instancesMap[instId] } func (p *InstancesState) Instances() []*instance.Instance { return p.instances } func (p *InstancesState) NodePortsMap() map[string][]*nodeport.Mapping { return p.nodePortsMap } func (p *InstancesState) Refresh(pkg *Package, db *database.Database) (err error) { instances := InstancesPreload.GetInstances() instances = instance.LoadAllVirt(instances, Pools.NodePools(), Disks.InstaceDisksMap()) p.instances = instances instId := set.NewSet() instancesMap := map[bson.ObjectID]*instance.Instance{} nodePortsMap := map[string][]*nodeport.Mapping{} for _, inst := range instances { instId.Add(inst.Id) instancesMap[inst.Id] = inst nodePortsMap[inst.NetworkNamespace] = append( nodePortsMap[inst.NetworkNamespace], inst.NodePorts...) } p.instancesMap = instancesMap p.nodePortsMap = nodePortsMap return } func (p *InstancesState) Apply(st *State) { st.GetInstace = p.GetInstace st.Instances = p.Instances st.NodePortsMap = p.NodePortsMap } func init() { InstancesPkg. After(InstancesPreload). After(Disks). After(Pools) } ================================================ FILE: state/instances_preload.go ================================================ package state import ( "sync" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" ) var ( InstancesPreload = &InstancesPreloadState{} InstancesPreloadPkg = NewPackage(InstancesPreload) ) type InstancesPreloadState struct { roles []string rolesSet set.Set rolesLock sync.Mutex instances []*instance.Instance } func (p *InstancesPreloadState) GetInstances() []*instance.Instance { return p.instances } func (p *InstancesPreloadState) GetRoles() (roles []string, rolesSet set.Set) { p.rolesLock.Lock() roles = p.roles rolesSet = p.rolesSet p.rolesLock.Unlock() return } func (p *InstancesPreloadState) setRoles(roles []string, rolesSet set.Set) { p.rolesLock.Lock() p.roles = roles p.rolesSet = rolesSet p.rolesLock.Unlock() return } func (p *InstancesPreloadState) Refresh(pkg *Package, db *database.Database) (err error) { ndeId := node.Self.Id instances, rolesSet, err := instance.GetAllRoles(db, &bson.M{ "node": ndeId, }) if err != nil { return } p.instances = instances nde := node.Self if nde.Firewall { roles := nde.Roles for _, role := range roles { rolesSet.Add(role) } } roles := []string{} for instRoleInf := range rolesSet.Iter() { roles = append(roles, instRoleInf.(string)) } p.setRoles(roles, rolesSet) return } func (p *InstancesPreloadState) Apply(st *State) { } ================================================ FILE: state/network.go ================================================ package state import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) var ( Network = &NetworkState{} NetworkPkg = NewPackage(Network) ) type NetworkState struct { namespaces []string interfaces []string interfacesSet set.Set } func (p *NetworkState) Namespaces() []string { return p.namespaces } func (p *NetworkState) Interfaces() []string { return p.interfaces } func (p *NetworkState) HasInterfaces(iface string) bool { return p.interfacesSet.Contains(iface) } func (p *NetworkState) Refresh(pkg *Package, db *database.Database) (err error) { namespaces, err := utils.GetNamespaces() if err != nil { return } p.namespaces = namespaces interfaces, interfacesSet, err := utils.GetInterfacesSet() if err != nil { return } p.interfaces = interfaces p.interfacesSet = interfacesSet return } func (p *NetworkState) Apply(st *State) { st.Namespaces = p.Namespaces st.Interfaces = p.Interfaces st.HasInterfaces = p.HasInterfaces } ================================================ FILE: state/package.go ================================================ package state import ( "reflect" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/database" ) var ( refCounter = 0 registry = map[PackageHandler]*Package{} ) type PackageHandler interface { Refresh(pkg *Package, db *database.Database) (err error) Apply(st *State) } type Package struct { name string ref int after set.Set ttl time.Duration handler PackageHandler } func (p *Package) Cache(d time.Duration) *Package { p.ttl = d return p } func (p *Package) Evict() *Package { p.ttl = 0 return p } func (p *Package) After(handler PackageHandler) *Package { p.after.Add(registry[handler].ref) return p } func NewPackage(handler PackageHandler) *Package { refCounter += 1 pkg := &Package{ name: reflect.TypeOf(handler).Elem().Name(), ref: refCounter, handler: handler, after: set.NewSet(), } registry[handler] = pkg return pkg } func RefreshAll(db *database.Database, runtimes *Runtimes) (err error) { inDegree := make(map[int]int) dependents := make(map[int][]*Package) refToPackage := make(map[int]*Package) for _, pkg := range registry { inDegree[pkg.ref] = 0 dependents[pkg.ref] = []*Package{} refToPackage[pkg.ref] = pkg } for _, pkg := range registry { for afterRef := range pkg.after.Iter() { afterRefInt := afterRef.(int) inDegree[pkg.ref]++ dependents[afterRefInt] = append(dependents[afterRefInt], pkg) } } ready := make(chan *Package, len(registry)) for ref, degree := range inDegree { if degree == 0 { ready <- refToPackage[ref] } } done := make(chan *Package, len(registry)) errors := make(chan error, len(registry)) completed := make(map[int]bool) processPackage := func(pkg *Package) { go func() { defer func() { done <- pkg }() start := time.Now() refreshErr := pkg.handler.Refresh(pkg, db) dur := time.Since(start) if refreshErr != nil { errors <- refreshErr } runtimes.SetState(pkg.name, dur) }() } processed := 0 totalPackages := len(registry) for processed < totalPackages { select { case pkg := <-ready: processPackage(pkg) case completedPkg := <-done: processed++ completed[completedPkg.ref] = true for _, dependent := range dependents[completedPkg.ref] { if !completed[dependent.ref] { allDepsCompleted := true for afterRef := range dependent.after.Iter() { if !completed[afterRef.(int)] { allDepsCompleted = false } } if allDepsCompleted { ready <- dependent } } } case refreshErr := <-errors: if err == nil { err = refreshErr } } } select { case refreshErr := <-errors: if err == nil { err = refreshErr } default: } return } func ApplyAll(st *State) { for _, pkg := range registry { pkg.handler.Apply(st) } } ================================================ FILE: state/pools.go ================================================ package state import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/pool" ) var ( Pools = &PoolsState{} PoolsPkg = NewPackage(Pools) ) type PoolsState struct { nodePools []*pool.Pool } func (p *PoolsState) NodePools() []*pool.Pool { return p.nodePools } func (p *PoolsState) Refresh(pkg *Package, db *database.Database) (err error) { zneId := node.Self.Zone if zneId.IsZero() { p.nodePools = nil return } pools, err := pool.GetAll(db, &bson.M{ "zone": zneId, }) if err != nil { return } p.nodePools = pools return } func (p *PoolsState) Apply(st *State) { st.NodePools = p.NodePools } ================================================ FILE: state/runtimes.go ================================================ package state import ( "fmt" "sync" "time" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) type Runtimes struct { State map[string]time.Duration Network time.Duration Ipset time.Duration Iptables time.Duration Disks time.Duration Instances time.Duration Namespaces time.Duration Pods time.Duration Deployments time.Duration Imds time.Duration Wait time.Duration Total time.Duration lock sync.Mutex } func (r *Runtimes) Init() { r.State = map[string]time.Duration{} } func (r *Runtimes) SetState(key string, dur time.Duration) { r.lock.Lock() r.State[key] = dur r.lock.Unlock() } func (r *Runtimes) Log() { fields := logrus.Fields{ "network": fmt.Sprintf("%v", r.Network), "ipset": fmt.Sprintf("%v", r.Ipset), "iptables": fmt.Sprintf("%v", r.Iptables), "disks": fmt.Sprintf("%v", r.Disks), "namespaces": fmt.Sprintf("%v", r.Namespaces), "pods": fmt.Sprintf("%v", r.Pods), "deployments": fmt.Sprintf("%v", r.Deployments), "imds": fmt.Sprintf("%v", r.Imds), "wait": fmt.Sprintf("%v", r.Wait), "total": fmt.Sprintf("%v", r.Total), } for key, dur := range r.State { key = utils.ToSnakeCase(key) fields[key] = fmt.Sprintf("%v", dur) } logrus.WithFields(fields).Warn("sync: High state sync runtime") } ================================================ FILE: state/schedulers.go ================================================ package state import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/scheduler" ) var ( Schedulers = &SchedulersState{} SchedulersPkg = NewPackage(Schedulers) ) type SchedulersState struct { schedulers []*scheduler.Scheduler } func (p *SchedulersState) Schedulers() []*scheduler.Scheduler { return p.schedulers } func (p *SchedulersState) Refresh(pkg *Package, db *database.Database) (err error) { schedulers, err := scheduler.GetAll(db) if err != nil { return } p.schedulers = schedulers return } func (p *SchedulersState) Apply(st *State) { st.Schedulers = p.Schedulers } ================================================ FILE: state/state.go ================================================ package state import ( "sync" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/scheduler" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) type State struct { waiter *sync.WaitGroup // Datacenter NodeDatacenter func() *datacenter.Datacenter // Zone NodeZone func() *zone.Zone // Zones VxLan func() bool GetZone func(zneId bson.ObjectID) *zone.Zone Nodes func() []*node.Node // Network Namespaces func() []string Interfaces func() []string HasInterfaces func(iface string) bool // Domains GetDomains func(orgId bson.ObjectID) []*types.Domain // Pools NodePools func() []*pool.Pool // Disks Disks func() []*disk.Disk GetInstaceDisks func(instId bson.ObjectID) []*disk.Disk GetDeploymentDisks func(deplyId bson.ObjectID) []*disk.Disk InstaceDisksMap func() map[bson.ObjectID][]*disk.Disk // Vpcs Vpc func(vpcId bson.ObjectID) *vpc.Vpc VpcsMap func() map[bson.ObjectID]*vpc.Vpc VpcIps func(vpcId bson.ObjectID) []*vpc.VpcIp VpcIpsMap func() map[bson.ObjectID][]*vpc.VpcIp Vpcs func() []*vpc.Vpc // Deployments Pod func(pdId bson.ObjectID) *pod.Pod PodsMap func() map[bson.ObjectID]*pod.Pod Unit func(unitId bson.ObjectID) *unit.Unit UnitsMap func() map[bson.ObjectID]*unit.Unit Spec func(commitId bson.ObjectID) *spec.Spec SpecsMap func() map[bson.ObjectID]*spec.Spec SpecPod func(pdId bson.ObjectID) *pod.Pod SpecPodUnits func(pdId bson.ObjectID) []*unit.Unit SpecUnit func(unitId bson.ObjectID) *unit.Unit SpecsUnitsMap func() map[bson.ObjectID]*unit.Unit SpecDomain func(domnId bson.ObjectID) *domain.Domain SpecSecret func(secrID bson.ObjectID) *secret.Secret SpecCert func(certId bson.ObjectID) *certificate.Certificate DeploymentsNode func() map[bson.ObjectID]*deployment.Deployment DeploymentReserved func(deplyId bson.ObjectID) *deployment.Deployment DeploymentsReserved func() map[bson.ObjectID]*deployment.Deployment DeploymentDeployed func(deplyId bson.ObjectID) *deployment.Deployment DeploymentsDeployed func() map[bson.ObjectID]*deployment.Deployment DeploymentsDestroy func() map[bson.ObjectID]*deployment.Deployment DeploymentInactive func(deplyId bson.ObjectID) *deployment.Deployment DeploymentsInactive func() map[bson.ObjectID]*deployment.Deployment Deployment func(deplyId bson.ObjectID) *deployment.Deployment // Instances GetInstace func(instId bson.ObjectID) *instance.Instance Instances func() []*instance.Instance NodePortsMap func() map[string][]*nodeport.Mapping GetInstaceAuthorities func(orgId bson.ObjectID, roles []string) []*authority.Authority // Virtuals DiskInUse func(instId, dskId bson.ObjectID) bool GetVirt func(instId bson.ObjectID) *vm.VirtualMachine VirtsMap func() map[bson.ObjectID]*vm.VirtualMachine // Schedulers Schedulers func() []*scheduler.Scheduler // Firewalls NodeFirewall func() []*firewall.Rule Firewalls func() map[string][]*firewall.Rule FirewallMaps func() map[string][]*firewall.Mapping ArpRecords func(namespace string) set.Set GetInstanceNamespaces func(instId bson.ObjectID) []string } func (s *State) Node() *node.Node { return node.Self } func (s *State) WaitAdd() { s.waiter.Add(1) } func (s *State) WaitDone() { s.waiter.Done() } func (s *State) Wait() { s.waiter.Wait() } func GetState(runtimes *Runtimes) (stat *State, err error) { db := database.GetDatabase() defer db.Close() err = RefreshAll(db, runtimes) if err != nil { return } stat = &State{ waiter: &sync.WaitGroup{}, } ApplyAll(stat) return } ================================================ FILE: state/state_old.go ================================================ package state import ( "io/ioutil" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/arp" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/qemu" "github.com/pritunl/pritunl-cloud/scheduler" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" "github.com/sirupsen/logrus" ) type StateOld struct { nodeSelf *node.Node nodes []*node.Node nodeDatacenter *datacenter.Datacenter nodeZone *zone.Zone vxlan bool zoneMap map[bson.ObjectID]*zone.Zone namespaces []string interfaces []string interfacesSet set.Set nodeFirewall []*firewall.Rule firewalls map[string][]*firewall.Rule firewallMaps map[string][]*firewall.Mapping pools []*pool.Pool disks []*disk.Disk schedulers []*scheduler.Scheduler deploymentsReservedMap map[bson.ObjectID]*deployment.Deployment deploymentsDeployedMap map[bson.ObjectID]*deployment.Deployment deploymentsInactiveMap map[bson.ObjectID]*deployment.Deployment podsMap map[bson.ObjectID]*pod.Pod unitsMap map[bson.ObjectID]*unit.Unit specsMap map[bson.ObjectID]*spec.Spec specsPodsMap map[bson.ObjectID]*pod.Pod specsPodUnitsMap map[bson.ObjectID][]*unit.Unit specsUnitsMap map[bson.ObjectID]*unit.Unit specsDeploymentsMap map[bson.ObjectID]*deployment.Deployment specsDomainsMap map[bson.ObjectID]*domain.Domain specsSecretsMap map[bson.ObjectID]*secret.Secret specsCertsMap map[bson.ObjectID]*certificate.Certificate virtsMap map[bson.ObjectID]*vm.VirtualMachine instances []*instance.Instance instancesMap map[bson.ObjectID]*instance.Instance instanceDisks map[bson.ObjectID][]*disk.Disk instanceNamespaces map[bson.ObjectID][]string authoritiesMap map[string][]*authority.Authority vpcs []*vpc.Vpc vpcsMap map[bson.ObjectID]*vpc.Vpc vpcIpsMap map[bson.ObjectID][]*vpc.VpcIp arpRecords map[string]set.Set addInstances set.Set remInstances set.Set running []string } func (s *StateOld) Node() *node.Node { return s.nodeSelf } func (s *StateOld) Nodes() []*node.Node { return s.nodes } func (s *StateOld) VxLan() bool { return s.vxlan } func (s *StateOld) NodeDatacenter() *datacenter.Datacenter { return s.nodeDatacenter } func (s *StateOld) NodeZone() *zone.Zone { return s.nodeZone } func (s *StateOld) GetZone(zneId bson.ObjectID) *zone.Zone { return s.zoneMap[zneId] } func (s *StateOld) Namespaces() []string { return s.namespaces } func (s *StateOld) Interfaces() []string { return s.interfaces } func (s *StateOld) HasInterfaces(iface string) bool { return s.interfacesSet.Contains(iface) } func (s *StateOld) Instances() []*instance.Instance { return s.instances } func (s *StateOld) Schedulers() []*scheduler.Scheduler { return s.schedulers } func (s *StateOld) NodeFirewall() []*firewall.Rule { return s.nodeFirewall } func (s *StateOld) Firewalls() map[string][]*firewall.Rule { return s.firewalls } func (s *StateOld) FirewallMaps() map[string][]*firewall.Mapping { return s.firewallMaps } func (s *StateOld) Running() []string { return s.running } func (s *StateOld) Disks() []*disk.Disk { return s.disks } func (s *StateOld) GetInstaceDisks(instId bson.ObjectID) []*disk.Disk { return s.instanceDisks[instId] } func (s *StateOld) GetInstanceNamespaces(instId bson.ObjectID) []string { return s.instanceNamespaces[instId] } func (s *StateOld) GetInstaceAuthorities(roles []string) []*authority.Authority { authrSet := set.NewSet() authrs := []*authority.Authority{} for _, role := range roles { for _, authr := range s.authoritiesMap[role] { if authrSet.Contains(authr.Id) { continue } authrSet.Add(authr.Id) authrs = append(authrs, authr) } } return authrs } func (s *StateOld) DeploymentReserved(deplyId bson.ObjectID) *deployment.Deployment { return s.deploymentsReservedMap[deplyId] } func (s *StateOld) DeploymentsReserved() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = s.deploymentsReservedMap return } func (s *StateOld) DeploymentDeployed(deplyId bson.ObjectID) *deployment.Deployment { return s.deploymentsDeployedMap[deplyId] } func (s *StateOld) DeploymentsDeployed() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = s.deploymentsDeployedMap return } func (s *StateOld) DeploymentsDestroy() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = s.deploymentsInactiveMap return } func (s *StateOld) DeploymentInactive(deplyId bson.ObjectID) *deployment.Deployment { return s.deploymentsInactiveMap[deplyId] } func (s *StateOld) DeploymentsInactive() ( deplys map[bson.ObjectID]*deployment.Deployment) { deplys = s.deploymentsInactiveMap return } func (s *StateOld) Deployment(deplyId bson.ObjectID) ( deply *deployment.Deployment) { deply = s.deploymentsDeployedMap[deplyId] if deply != nil { return } deply = s.deploymentsReservedMap[deplyId] if deply != nil { return } deply = s.deploymentsInactiveMap[deplyId] if deply != nil { return } return } func (s *StateOld) Pod(pdId bson.ObjectID) *pod.Pod { return s.podsMap[pdId] } func (s *StateOld) Unit(unitId bson.ObjectID) *unit.Unit { return s.unitsMap[unitId] } func (s *StateOld) Spec(commitId bson.ObjectID) *spec.Spec { return s.specsMap[commitId] } func (s *StateOld) SpecPod(pdId bson.ObjectID) *pod.Pod { return s.specsPodsMap[pdId] } func (s *StateOld) SpecPodUnits(pdId bson.ObjectID) []*unit.Unit { return s.specsPodUnitsMap[pdId] } func (s *StateOld) SpecUnit(unitId bson.ObjectID) *unit.Unit { return s.specsUnitsMap[unitId] } func (s *StateOld) SpecDomain(domnId bson.ObjectID) *domain.Domain { return s.specsDomainsMap[domnId] } func (s *StateOld) SpecSecret(secrID bson.ObjectID) *secret.Secret { return s.specsSecretsMap[secrID] } func (s *StateOld) SpecCert(certId bson.ObjectID) *certificate.Certificate { return s.specsCertsMap[certId] } func (s *StateOld) Vpc(vpcId bson.ObjectID) *vpc.Vpc { return s.vpcsMap[vpcId] } func (s *StateOld) VpcIps(vpcId bson.ObjectID) []*vpc.VpcIp { return s.vpcIpsMap[vpcId] } func (s *StateOld) VpcIpsMap() map[bson.ObjectID][]*vpc.VpcIp { return s.vpcIpsMap } func (s *StateOld) ArpRecords(namespace string) set.Set { return s.arpRecords[namespace] } func (s *StateOld) Vpcs() []*vpc.Vpc { return s.vpcs } func (s *StateOld) DiskInUse(instId, dskId bson.ObjectID) bool { curVirt := s.virtsMap[instId] if curVirt != nil { if curVirt.State != vm.Stopped && curVirt.State != vm.Failed { for _, vmDsk := range curVirt.Disks { if vmDsk.GetId() == dskId { return true } } } } return false } func (s *StateOld) GetVirt(instId bson.ObjectID) *vm.VirtualMachine { if instId.IsZero() { return nil } return s.virtsMap[instId] } func (s *StateOld) GetInstace(instId bson.ObjectID) *instance.Instance { if instId.IsZero() { return nil } return s.instancesMap[instId] } func (s *StateOld) init() (err error) { db := database.GetDatabase() defer db.Close() s.nodeSelf = node.Self.Copy() // Datacenter dcId := s.nodeSelf.Datacenter if !dcId.IsZero() { dc, e := datacenter.Get(db, dcId) if e != nil { err = e return } s.nodeDatacenter = dc } // Datacenter // Zone zneId := s.nodeSelf.Zone if !zneId.IsZero() { zne, e := zone.Get(db, zneId) if e != nil { err = e return } s.nodeZone = zne } // Zone // Zones if s.nodeDatacenter != nil && s.nodeDatacenter.Vxlan() { s.vxlan = true znes, e := zone.GetAllDatacenter(db, s.nodeDatacenter.Id) if e != nil { err = e return } zonesMap := map[bson.ObjectID]*zone.Zone{} for _, zne := range znes { zonesMap[zne.Id] = zne } s.zoneMap = zonesMap ndes, e := node.GetAllNet(db) if e != nil { err = e return } s.nodes = ndes } // Zones // Network namespaces, err := utils.GetNamespaces() if err != nil { return } s.namespaces = namespaces interfaces, interfacesSet, err := utils.GetInterfacesSet() if err != nil { return } s.interfaces = interfaces s.interfacesSet = interfacesSet // Network // Pools pools, err := pool.GetAll(db, &bson.M{ "zone": s.nodeSelf.Zone, }) if err != nil { return } s.pools = pools // Pools // Disks disks, err := disk.GetNode(db, s.nodeSelf.Id, s.nodeSelf.Pools) if err != nil { return } s.disks = disks instanceDisks := map[bson.ObjectID][]*disk.Disk{} for _, dsk := range disks { dsks := instanceDisks[dsk.Instance] if dsks == nil { dsks = []*disk.Disk{} } instanceDisks[dsk.Instance] = append(dsks, dsk) } s.instanceDisks = instanceDisks // Disks // Vpcs vpcs := []*vpc.Vpc{} vpcsId := []bson.ObjectID{} vpcsMap := map[bson.ObjectID]*vpc.Vpc{} if s.nodeDatacenter != nil { vpcs, err = vpc.GetDatacenter(db, s.nodeDatacenter.Id) if err != nil { return } for _, vc := range vpcs { vpcsId = append(vpcsId, vc.Id) vpcsMap[vc.Id] = vc } } s.vpcs = vpcs s.vpcsMap = vpcsMap vpcIpsMap := map[bson.ObjectID][]*vpc.VpcIp{} if s.nodeDatacenter != nil { vpcIpsMap, err = vpc.GetIpsMapped(db, vpcsId) if err != nil { return } } s.vpcIpsMap = vpcIpsMap // Vpcs // Deployments deployments, err := deployment.GetAll(db, &bson.M{ "node": node.Self.Id, }) if err != nil { return } deploymentsNode := map[bson.ObjectID]*deployment.Deployment{} deploymentsReservedMap := map[bson.ObjectID]*deployment.Deployment{} deploymentsDeployedMap := map[bson.ObjectID]*deployment.Deployment{} deploymentsInactiveMap := map[bson.ObjectID]*deployment.Deployment{} deploymentsIdSet := set.NewSet() podIdsSet := set.NewSet() unitIdsSet := set.NewSet() specIdsSet := set.NewSet() for _, deply := range deployments { deploymentsNode[deply.Id] = deply deploymentsIdSet.Add(deply.Id) switch deply.State { case deployment.Reserved: deploymentsReservedMap[deply.Id] = deply break case deployment.Deployed: switch deply.Action { case deployment.Destroy, deployment.Archive, deployment.Restore: deploymentsInactiveMap[deply.Id] = deply break default: deploymentsDeployedMap[deply.Id] = deply } break case deployment.Archived: deploymentsInactiveMap[deply.Id] = deply break } podIdsSet.Add(deply.Pod) unitIdsSet.Add(deply.Unit) specIdsSet.Add(deply.Spec) } specIds := []bson.ObjectID{} for specId := range specIdsSet.Iter() { specIds = append(specIds, specId.(bson.ObjectID)) } specs := []*spec.Spec{} if len(specIds) > 0 { specs, err = spec.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specIds, }, }) if err != nil { return } } specSecretsSet := set.NewSet() specCertsSet := set.NewSet() specPodsSet := set.NewSet() specUnitsSet := set.NewSet() specDomainsSet := set.NewSet() specsMap := map[bson.ObjectID]*spec.Spec{} for _, spc := range specs { specsMap[spc.Id] = spc if spc.Instance != nil { if spc.Instance.Pods != nil { for _, pdId := range spc.Instance.Pods { specPodsSet.Add(pdId) } } if spc.Instance.Secrets != nil { for _, secrId := range spc.Instance.Secrets { specSecretsSet.Add(secrId) } } if spc.Instance.Certificates != nil { for _, certId := range spc.Instance.Certificates { specCertsSet.Add(certId) } } } if spc.Firewall != nil { for _, rule := range spc.Firewall.Ingress { for _, ref := range rule.Sources { specUnitsSet.Add(ref.Id) } } } if spc.Domain != nil { for _, record := range spc.Domain.Records { specDomainsSet.Add(record.Domain) } } } s.specsMap = specsMap specCertIds := []bson.ObjectID{} for certId := range specCertsSet.Iter() { specCertIds = append(specCertIds, certId.(bson.ObjectID)) } specsCertsMap := map[bson.ObjectID]*certificate.Certificate{} specCerts := []*certificate.Certificate{} if len(specCertIds) > 0 { specCerts, err = certificate.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specCertIds, }, }) if err != nil { return } } for _, specCert := range specCerts { specsCertsMap[specCert.Id] = specCert } s.specsCertsMap = specsCertsMap specSecretIds := []bson.ObjectID{} for secrId := range specSecretsSet.Iter() { specSecretIds = append(specSecretIds, secrId.(bson.ObjectID)) } specsSecretsMap := map[bson.ObjectID]*secret.Secret{} specSecrets := []*secret.Secret{} if len(specSecretIds) > 0 { specSecrets, err = secret.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specSecretIds, }, }) if err != nil { return } } for _, specSecret := range specSecrets { specsSecretsMap[specSecret.Id] = specSecret } s.specsSecretsMap = specsSecretsMap specPodIds := []bson.ObjectID{} for pdId := range specPodsSet.Iter() { specPodIds = append(specPodIds, pdId.(bson.ObjectID)) } specPods := []*pod.Pod{} if len(specPodIds) > 0 { specPods, err = pod.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specPodIds, }, }) if err != nil { return } } specsPodsMap := map[bson.ObjectID]*pod.Pod{} for _, specPod := range specPods { specsPodsMap[specPod.Id] = specPod } s.specsPodsMap = specsPodsMap specUnitIds := []bson.ObjectID{} for unitId := range specUnitsSet.Iter() { specUnitIds = append(specUnitIds, unitId.(bson.ObjectID)) } specUnits := []*unit.Unit{} if len(specUnitIds) > 0 || len(specPodIds) > 0 { specUnits, err = unit.GetAll(db, &bson.M{ "$or": []*bson.M{ &bson.M{ "_id": &bson.M{ "$in": specUnitIds, }, }, &bson.M{ "pod": &bson.M{ "$in": specPodIds, }, }, }, }) if err != nil { return } } specDeploymentsSet := set.NewSet() specsUnitsMap := map[bson.ObjectID]*unit.Unit{} specsPodUnitsMap := map[bson.ObjectID][]*unit.Unit{} for _, specUnit := range specUnits { specsUnitsMap[specUnit.Id] = specUnit specsPodUnitsMap[specUnit.Pod] = append( specsPodUnitsMap[specUnit.Pod], specUnit) for _, deplyId := range specUnit.Deployments { specDeploymentsSet.Add(deplyId) } } s.specsUnitsMap = specsUnitsMap s.specsPodUnitsMap = specsPodUnitsMap specDomainIds := []bson.ObjectID{} for pdId := range specDomainsSet.Iter() { specDomainIds = append(specDomainIds, pdId.(bson.ObjectID)) } specsDomainsMap := map[bson.ObjectID]*domain.Domain{} specDomains, err := domain.GetLoadedAllIds(db, specDomainIds) if err != nil { return } for _, specDomain := range specDomains { specsDomainsMap[specDomain.Id] = specDomain } s.specsDomainsMap = specsDomainsMap specDeploymentIds := []bson.ObjectID{} for deplyIdInf := range specDeploymentsSet.Iter() { deplyId := deplyIdInf.(bson.ObjectID) if !deploymentsIdSet.Contains(deplyId) { specDeploymentIds = append(specDeploymentIds, deplyId) } } specDeployments := []*deployment.Deployment{} if len(specDeploymentIds) > 0 { specDeployments, err = deployment.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specDeploymentIds, }, }) if err != nil { return } } for _, specDeployment := range specDeployments { deploymentsIdSet.Add(specDeployment.Id) switch specDeployment.State { case deployment.Reserved: deploymentsReservedMap[specDeployment.Id] = specDeployment break case deployment.Deployed: switch specDeployment.Action { case deployment.Destroy, deployment.Archive, deployment.Restore: deploymentsInactiveMap[specDeployment.Id] = specDeployment break default: deploymentsDeployedMap[specDeployment.Id] = specDeployment } break case deployment.Archived: deploymentsInactiveMap[specDeployment.Id] = specDeployment break } } podIds := []bson.ObjectID{} for podId := range podIdsSet.Iter() { podIds = append(podIds, podId.(bson.ObjectID)) } pods := []*pod.Pod{} if len(podIds) > 0 { pods, err = pod.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": podIds, }, }) if err != nil { return } } podsMap := map[bson.ObjectID]*pod.Pod{} for _, pd := range pods { podsMap[pd.Id] = pd } s.podsMap = podsMap unitIds := []bson.ObjectID{} for unitId := range unitIdsSet.Iter() { unitIds = append(unitIds, unitId.(bson.ObjectID)) } units := []*unit.Unit{} if len(unitIds) > 0 { units, err = unit.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": unitIds, }, }) if err != nil { return } } unitsMap := map[bson.ObjectID]*unit.Unit{} podDeploymentsSet := set.NewSet() for _, unt := range units { unitsMap[unt.Id] = unt for _, deplyId := range unt.Deployments { podDeploymentsSet.Add(deplyId) } } s.unitsMap = unitsMap podDeploymentIds := []bson.ObjectID{} for deplyIdInf := range podDeploymentsSet.Iter() { deplyId := deplyIdInf.(bson.ObjectID) if !deploymentsIdSet.Contains(deplyId) { podDeploymentIds = append(podDeploymentIds, deplyId) } } podDeployments := []*deployment.Deployment{} if len(podDeploymentIds) > 0 { podDeployments, err = deployment.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": podDeploymentIds, }, }) if err != nil { return } } for _, podDeployment := range podDeployments { deploymentsIdSet.Add(podDeployment.Id) switch podDeployment.State { case deployment.Reserved: deploymentsReservedMap[podDeployment.Id] = podDeployment break case deployment.Deployed: switch podDeployment.Action { case deployment.Destroy, deployment.Archive, deployment.Restore: deploymentsInactiveMap[podDeployment.Id] = podDeployment break default: deploymentsDeployedMap[podDeployment.Id] = podDeployment } break case deployment.Archived: deploymentsInactiveMap[podDeployment.Id] = podDeployment break } } s.deploymentsReservedMap = deploymentsReservedMap s.deploymentsDeployedMap = deploymentsDeployedMap s.deploymentsInactiveMap = deploymentsInactiveMap // Deployments // Instances instances, err := instance.GetAllVirtMapped(db, &bson.M{ "node": s.nodeSelf.Id, }, s.pools, instanceDisks) if err != nil { return } s.instances = instances nodePortsMap := map[string][]*nodeport.Mapping{} instId := set.NewSet() instancesMap := map[bson.ObjectID]*instance.Instance{} instancesRolesSet := set.NewSet() for _, inst := range instances { instId.Add(inst.Id) instancesMap[inst.Id] = inst nodePortsMap[inst.NetworkNamespace] = append( nodePortsMap[inst.NetworkNamespace], inst.NodePorts...) for _, role := range inst.Roles { instancesRolesSet.Add(role) } } s.instancesMap = instancesMap instancesRoles := []string{} for instRoleInf := range instancesRolesSet.Iter() { instancesRoles = append(instancesRoles, instRoleInf.(string)) } authrsMap, err := authority.GetMapRoles(db, &bson.M{ "roles": &bson.M{ "$in": instancesRoles, }, }) if err != nil { return } s.authoritiesMap = authrsMap // Instances // Virtuals curVirts, err := qemu.GetVms(db) if err != nil { return } virtsMap := map[bson.ObjectID]*vm.VirtualMachine{} for _, virt := range curVirts { if !instId.Contains(virt.Id) { logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("sync: Unknown instance") } virtsMap[virt.Id] = virt } s.virtsMap = virtsMap // Virtuals // Firewalls s.arpRecords = arp.BuildState(s.instances, s.vpcsMap, s.vpcIpsMap) // Firewalls // Firewalls specRules, err := firewall.GetSpecRules(instances, deploymentsNode, specsMap, specsUnitsMap, deploymentsDeployedMap) if err != nil { return } nodeFirewall, firewalls, firewallMaps, instNamespaces, err := firewall.GetAllIngress(db, s.nodeSelf, instances, specRules, nodePortsMap) if err != nil { return } s.nodeFirewall = nodeFirewall s.firewalls = firewalls s.firewallMaps = firewallMaps s.instanceNamespaces = instNamespaces // Firewalls // Schedulers schedulers, err := scheduler.GetAll(db) if err != nil { return } s.schedulers = schedulers // Schedulers // Running items, err := ioutil.ReadDir("/var/run") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "state: Failed to read run directory"), } return } running := []string{} for _, item := range items { if !item.IsDir() { running = append(running, item.Name()) } } s.running = running // Running return } ================================================ FILE: state/virtuals.go ================================================ package state import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/qemu" "github.com/pritunl/pritunl-cloud/vm" ) var ( Virtuals = &VirtualsState{} VirtualsPkg = NewPackage(Virtuals) ) type VirtualsState struct { virtsMap map[bson.ObjectID]*vm.VirtualMachine } func (p *VirtualsState) DiskInUse(instId, dskId bson.ObjectID) bool { curVirt := p.virtsMap[instId] if curVirt != nil { if curVirt.State != vm.Stopped && curVirt.State != vm.Failed { for _, vmDsk := range curVirt.Disks { if vmDsk.GetId() == dskId { return true } } } } return false } func (p *VirtualsState) GetVirt(instId bson.ObjectID) *vm.VirtualMachine { if instId.IsZero() { return nil } return p.virtsMap[instId] } func (p *VirtualsState) VirtsMap() map[bson.ObjectID]*vm.VirtualMachine { return p.virtsMap } func (p *VirtualsState) Refresh(pkg *Package, db *database.Database) (err error) { curVirts, err := qemu.GetVms(db) if err != nil { return } virtsMap := map[bson.ObjectID]*vm.VirtualMachine{} for _, virt := range curVirts { virtsMap[virt.Id] = virt } p.virtsMap = virtsMap return } func (p *VirtualsState) Apply(st *State) { st.DiskInUse = p.DiskInUse st.GetVirt = p.GetVirt st.VirtsMap = p.VirtsMap } ================================================ FILE: state/vpcs.go ================================================ package state import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/vpc" ) var ( Vpcs = &VpcsState{} VpcsPkg = NewPackage(Vpcs) ) type VpcsState struct { vpcs []*vpc.Vpc vpcsMap map[bson.ObjectID]*vpc.Vpc vpcIpsMap map[bson.ObjectID][]*vpc.VpcIp } func (p *VpcsState) Vpc(vpcId bson.ObjectID) *vpc.Vpc { return p.vpcsMap[vpcId] } func (p *VpcsState) VpcsMap() map[bson.ObjectID]*vpc.Vpc { return p.vpcsMap } func (p *VpcsState) VpcIps(vpcId bson.ObjectID) []*vpc.VpcIp { return p.vpcIpsMap[vpcId] } func (p *VpcsState) VpcIpsMap() map[bson.ObjectID][]*vpc.VpcIp { return p.vpcIpsMap } func (p *VpcsState) Vpcs() []*vpc.Vpc { return p.vpcs } func (p *VpcsState) Refresh(pkg *Package, db *database.Database) (err error) { dcId := node.Self.Datacenter vpcsId := []bson.ObjectID{} vpcsMap := map[bson.ObjectID]*vpc.Vpc{} if dcId.IsZero() { p.vpcs = nil p.vpcsMap = map[bson.ObjectID]*vpc.Vpc{} p.vpcIpsMap = map[bson.ObjectID][]*vpc.VpcIp{} return } vpcs, err := vpc.GetDatacenter(db, dcId) if err != nil { return } for _, vc := range vpcs { vpcsId = append(vpcsId, vc.Id) vpcsMap[vc.Id] = vc } p.vpcs = vpcs p.vpcsMap = vpcsMap vpcIpsMap, err := vpc.GetIpsMapped(db, vpcsId) if err != nil { return } p.vpcIpsMap = vpcIpsMap return } func (p *VpcsState) Apply(st *State) { st.Vpc = p.Vpc st.VpcsMap = p.VpcsMap st.VpcIps = p.VpcIps st.VpcIpsMap = p.VpcIpsMap st.Vpcs = p.Vpcs } ================================================ FILE: state/zone.go ================================================ package state import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/zone" ) var ( Zone = &ZoneState{} ZonePkg = NewPackage(Zone) ) type ZoneState struct { nodeZone *zone.Zone } func (p *ZoneState) NodeZone() *zone.Zone { return p.nodeZone } func (p *ZoneState) Refresh(pkg *Package, db *database.Database) (err error) { zneId := node.Self.Zone if zneId.IsZero() { p.nodeZone = nil pkg.Evict() return } zne, e := zone.Get(db, zneId) if e != nil { err = e return } p.nodeZone = zne pkg.Cache(15 * time.Second) return } func (p *ZoneState) Apply(st *State) { st.NodeZone = p.NodeZone } ================================================ FILE: state/zones.go ================================================ package state import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/zone" ) var ( Zones = &ZonesState{} ZonesPkg = NewPackage(Zones) ) type ZonesState struct { vxlan bool zoneMap map[bson.ObjectID]*zone.Zone nodes []*node.Node } func (p *ZonesState) VxLan() bool { return p.vxlan } func (p *ZonesState) GetZone(zneId bson.ObjectID) *zone.Zone { return p.zoneMap[zneId] } func (p *ZonesState) Nodes() []*node.Node { return p.nodes } func (p *ZonesState) Refresh(pkg *Package, db *database.Database) (err error) { nodeDc := Datacenter.NodeDatacenter() if nodeDc == nil || !nodeDc.Vxlan() { p.vxlan = false p.zoneMap = nil p.nodes = nil pkg.Evict() return } p.vxlan = true znes, e := zone.GetAllDatacenter(db, nodeDc.Id) if e != nil { err = e return } zonesMap := map[bson.ObjectID]*zone.Zone{} for _, zne := range znes { zonesMap[zne.Id] = zne } p.zoneMap = zonesMap ndes, e := node.GetAllNet(db) if e != nil { err = e return } p.nodes = ndes pkg.Cache(10 * time.Second) return } func (p *ZonesState) Apply(st *State) { st.VxLan = p.VxLan st.GetZone = p.GetZone st.Nodes = p.Nodes } func init() { ZonesPkg. After(Datacenter) } ================================================ FILE: static/file.go ================================================ package static import ( "bytes" "compress/gzip" "crypto/md5" "encoding/base32" "io/ioutil" "path/filepath" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var ( mimeTypes = map[string]string{ ".js": "application/javascript", ".json": "application/json", ".css": "text/css", ".html": "text/html", ".jpg": "image/jpeg", ".png": "image/png", ".svg": "image/svg+xml", ".ico": "image/vnd.microsoft.icon", ".otf": "application/font-sfnt", ".ttf": "application/font-sfnt", ".woff": "application/font-woff", ".woff2": "font/woff2", ".ijmap": "text/plain", ".eot": "application/vnd.ms-fontobject", ".map": "application/json", } ) type File struct { Type string Hash string Data []byte GzipData []byte } func NewFile(path string) (file *File, err error) { ext := filepath.Ext(path) if len(ext) == 0 { return } typ, ok := mimeTypes[ext] if !ok { return } data, e := ioutil.ReadFile(path) if e != nil { err = &errortypes.ReadError{ errors.Wrap(e, "static: Read error"), } return } hash := md5.Sum(data) hashStr := base32.StdEncoding.EncodeToString(hash[:]) hashStr = strings.Replace(hashStr, "=", "", -1) hashStr = strings.ToLower(hashStr) file = &File{ Type: typ, Hash: hashStr, Data: data, } gzipData := &bytes.Buffer{} writer, err := gzip.NewWriterLevel(gzipData, gzip.BestCompression) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "static: Gzip error"), } return } writer.Write(file.Data) writer.Close() file.GzipData = gzipData.Bytes() return } ================================================ FILE: static/static.go ================================================ // Versions static files with hash, replaces references and stores in memory. package static import ( "io/ioutil" "path" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) type Store struct { Files map[string]*File root string } func (s *Store) addDir(dir string) (err error) { files, err := ioutil.ReadDir(dir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "static: Read directory error"), } return } for _, info := range files { name := info.Name() fullPath := path.Join(dir, name) if info.IsDir() { s.addDir(fullPath) continue } file, e := NewFile(fullPath) if e != nil { err = e return } if file != nil { s.Files[fullPath] = file } } return } func NewStore(root string) (store *Store, err error) { store = &Store{ Files: map[string]*File{}, root: root, } err = store.addDir(root) if err != nil { time.Sleep(3 * time.Second) err = store.addDir(root) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "static: Init error"), } return } } return } func GetMimeType(name string) string { return mimeTypes[path.Ext(name)] } ================================================ FILE: storage/constants.go ================================================ package storage const ( Public = "public" Private = "private" Web = "web" AwsStandard = "aws_standard" AwsInfrequentAccess = "aws_infrequent_access" AwsGlacier = "aws_glacier" OracleStandard = "oracle_standard" OracleArchive = "oracle_archive" ) ================================================ FILE: storage/storage.go ================================================ package storage import ( "net/url" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Storage struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` Type string `bson:"type" json:"type"` Endpoint string `bson:"endpoint" json:"endpoint"` Bucket string `bson:"bucket" json:"bucket"` AccessKey string `bson:"access_key" json:"access_key"` SecretKey string `bson:"secret_key" json:"secret_key"` Insecure bool `bson:"insecure" json:"insecure"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Type string `bson:"type" json:"type"` } func (s *Storage) IsOracle() bool { return strings.Contains(strings.ToLower(s.Endpoint), "oracle") } func (s *Storage) GetWebUrl() (u *url.URL) { u = &url.URL{} if s.Insecure { u.Scheme = "http" } else { u.Scheme = "https" } u.Host = s.Endpoint u.Path = "/" + s.Bucket return } func (s *Storage) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { s.Name = utils.FilterName(s.Name) switch s.Type { case Public: break case Private: break case Web: break case "": s.Type = Public break default: errData = &errortypes.ErrorData{ Error: "invalid_type", Message: "Storage type is invalid", } return } return } func (s *Storage) Commit(db *database.Database) (err error) { coll := db.Storages() err = coll.Commit(s.Id, s) if err != nil { return } return } func (s *Storage) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Storages() err = coll.CommitFields(s.Id, s, fields) if err != nil { return } return } func (s *Storage) Insert(db *database.Database) (err error) { coll := db.Storages() if !s.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("storage: Storage already exists"), } return } resp, err := coll.InsertOne(db, s) if err != nil { err = database.ParseError(err) return } s.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: storage/utils.go ================================================ package storage import ( "strings" minio "github.com/minio/minio-go/v7" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, storeId bson.ObjectID) ( store *Storage, err error) { coll := db.Storages() store = &Storage{} err = coll.FindOneId(storeId, store) if err != nil { return } return } func GetAll(db *database.Database) (stores []*Storage, err error) { coll := db.Storages() stores = []*Storage{} cursor, err := coll.Find( db, &bson.M{}, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { store := &Storage{} err = cursor.Decode(store) if err != nil { err = database.ParseError(err) return } stores = append(stores, store) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (stores []*Storage, count int64, err error) { coll := db.Storages() stores = []*Storage{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { store := &Storage{} err = cursor.Decode(store) if err != nil { err = database.ParseError(err) return } stores = append(stores, store) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, storeId bson.ObjectID) (err error) { coll := db.Images() _, err = coll.DeleteMany(db, &bson.M{ "storage": storeId, }) if err != nil { return } coll = db.Storages() _, err = coll.DeleteOne(db, &bson.M{ "_id": storeId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, storeIds []bson.ObjectID) ( err error) { coll := db.Images() for _, storeId := range storeIds { _, err = coll.DeleteMany(db, &bson.M{ "storage": storeId, }) if err != nil { return } } coll = db.Storages() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": storeIds, }, }) if err != nil { err = database.ParseError(err) return } return } func FormatStorageClass(class string) string { switch class { case AwsStandard: return "STANDARD" case AwsInfrequentAccess: return "STANDARD_IA" case AwsGlacier: return "GLACIER" } return "" } func ParseStorageClass(obj minio.ObjectInfo) string { opcRequestId := obj.Metadata.Get("Opc-Request-Id") archivalState := strings.ToLower(obj.Metadata.Get("Archival-State")) if archivalState != "" { return OracleArchive } else if opcRequestId != "" { return OracleStandard } switch obj.StorageClass { case "STANDARD": return AwsStandard case "STANDARD_IA": return AwsInfrequentAccess case "GLACIER": return AwsGlacier } return "" } ================================================ FILE: store/address.go ================================================ package store import ( "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" ) var ( addressStores = map[bson.ObjectID]*AddressStore{} addressStoresLock = sync.Mutex{} ) type AddressStore struct { Addr string Addr6 string Timestamp time.Time Refresh time.Duration } func GetAddress(virtId bson.ObjectID) ( addressStore *AddressStore, ok bool) { addressStoresLock.Lock() addressStore, ok = addressStores[virtId] addressStoresLock.Unlock() ttl := settings.Hypervisor.AddressRefreshTtl if ok && ttl != 0 && time.Since(addressStore.Timestamp) > time.Duration( ttl)*time.Second && node.Self.IsDhcp6() { ok = false } return } func SetAddress(virtId bson.ObjectID, addr, addr6 string) { addressStoresLock.Lock() now := time.Now() addressStore := addressStores[virtId] if addressStore != nil && addressStore.Refresh != 0 { refreshTtl := time.Duration( settings.Hypervisor.AddressRefreshTtl) * time.Second now = now.Add(-refreshTtl).Add(addressStore.Refresh) } addressStores[virtId] = &AddressStore{ Addr: addr, Addr6: addr6, Timestamp: now, } addressStoresLock.Unlock() } func SetAddressExpire(virtId bson.ObjectID, ttl time.Duration) { addressStoresLock.Lock() addressStore, ok := addressStores[virtId] if ok { refreshTtl := time.Duration( settings.Hypervisor.AddressRefreshTtl) * time.Second addressStore.Timestamp = time.Now().Add(-refreshTtl).Add(ttl) } addressStoresLock.Unlock() } func SetAddressExpireMulti(virtId bson.ObjectID, ttl, ttl2 time.Duration) { addressStoresLock.Lock() addressStore, ok := addressStores[virtId] if ok { refreshTtl := time.Duration( settings.Hypervisor.AddressRefreshTtl) * time.Second addressStore.Timestamp = time.Now().Add(-refreshTtl).Add(ttl) addressStore.Refresh = ttl2 } addressStoresLock.Unlock() } func RemAddress(addressId bson.ObjectID) { addressStoresLock.Lock() delete(addressStores, addressId) addressStoresLock.Unlock() } ================================================ FILE: store/arp.go ================================================ package store import ( "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" ) var ( arpStores = map[bson.ObjectID]ArpStore{} arpStoresLock = sync.Mutex{} ) type ArpStore struct { Records set.Set Timestamp time.Time } func GetArp(instId bson.ObjectID) (arpStore ArpStore, ok bool) { arpStoresLock.Lock() arpStore, ok = arpStores[instId] arpStoresLock.Unlock() if ok { arpStore.Records = arpStore.Records.Copy() } return } func SetArp(instId bson.ObjectID, records set.Set) { arpStoresLock.Lock() arpStores[instId] = ArpStore{ Records: records.Copy(), Timestamp: time.Now(), } arpStoresLock.Unlock() } func RemArp(instId bson.ObjectID) { arpStoresLock.Lock() delete(arpStores, instId) arpStoresLock.Unlock() } ================================================ FILE: store/disks.go ================================================ package store import ( "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/vm" ) var ( disksStores = map[bson.ObjectID]DisksStore{} disksStoresLock = sync.Mutex{} ) type DisksStore struct { Disks []vm.Disk Timestamp time.Time } func GetDisks(virtId bson.ObjectID) (disksStore DisksStore, ok bool) { disksStoresLock.Lock() disksStore, ok = disksStores[virtId] disksStoresLock.Unlock() if ok { disksStore.Disks = append([]vm.Disk{}, disksStore.Disks...) } return } func SetDisks(virtId bson.ObjectID, disks []*vm.Disk) { disksRef := []vm.Disk{} for _, dsk := range disks { disksRef = append(disksRef, *dsk) } disksStoresLock.Lock() disksStores[virtId] = DisksStore{ Disks: disksRef, Timestamp: time.Now(), } disksStoresLock.Unlock() } func RemDisks(virtId bson.ObjectID) { disksStoresLock.Lock() delete(disksStores, virtId) disksStoresLock.Unlock() } ================================================ FILE: store/routes.go ================================================ package store import ( "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/vpc" ) var ( routesStores = map[bson.ObjectID]RoutesStore{} routesStoresLock = sync.Mutex{} ) type RoutesStore struct { IcmpRedirects bool Routes []vpc.Route Routes6 []vpc.Route Timestamp time.Time } func GetRoutes(instId bson.ObjectID) (routesStore RoutesStore, ok bool) { routesStoresLock.Lock() routesStore, ok = routesStores[instId] routesStoresLock.Unlock() if ok { routesStore.Routes = append([]vpc.Route{}, routesStore.Routes...) } return } func SetRoutes(instId bson.ObjectID, icmpRedirects bool, routes, routes6 []vpc.Route) { routesStoresLock.Lock() routesStores[instId] = RoutesStore{ IcmpRedirects: icmpRedirects, Routes: append([]vpc.Route{}, routes...), Routes6: append([]vpc.Route{}, routes6...), Timestamp: time.Now(), } routesStoresLock.Unlock() } func RemRoutes(instId bson.ObjectID) { routesStoresLock.Lock() delete(routesStores, instId) routesStoresLock.Unlock() } ================================================ FILE: store/usb.go ================================================ package store import ( "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/vm" ) var ( usbsStores = map[bson.ObjectID]UsbsStore{} usbsStoresLock = sync.Mutex{} ) type UsbsStore struct { Usbs []vm.UsbDevice Timestamp time.Time } func GetUsbs(virtId bson.ObjectID) (usbsStore UsbsStore, ok bool) { usbsStoresLock.Lock() usbsStore, ok = usbsStores[virtId] usbsStoresLock.Unlock() if ok { usbsStore.Usbs = append([]vm.UsbDevice{}, usbsStore.Usbs...) } return } func SetUsbs(virtId bson.ObjectID, usbs []*vm.UsbDevice) { usbsRef := []vm.UsbDevice{} for _, dsk := range usbs { usbsRef = append(usbsRef, *dsk) } usbsStoresLock.Lock() usbsStores[virtId] = UsbsStore{ Usbs: usbsRef, Timestamp: time.Now(), } usbsStoresLock.Unlock() } func RemUsbs(virtId bson.ObjectID) { usbsStoresLock.Lock() delete(usbsStores, virtId) usbsStoresLock.Unlock() } ================================================ FILE: store/virt.go ================================================ package store import ( "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/vm" ) var ( virtStores = map[bson.ObjectID]VirtStore{} virtStoresLock = sync.Mutex{} ) type VirtStore struct { Virt vm.VirtualMachine Timestamp time.Time } func GetVirt(virtId bson.ObjectID) (virtStore VirtStore, ok bool) { virtStoresLock.Lock() virtStore, ok = virtStores[virtId] virtStoresLock.Unlock() return } func SetVirt(virtId bson.ObjectID, virt *vm.VirtualMachine) { virtRef := *virt virtRef.Disks = nil virtStoresLock.Lock() virtStores[virtId] = VirtStore{ Virt: virtRef, Timestamp: time.Now(), } virtStoresLock.Unlock() } func RemVirt(virtId bson.ObjectID) { virtStoresLock.Lock() delete(virtStores, virtId) virtStoresLock.Unlock() } ================================================ FILE: subscription/subscription.go ================================================ package subscription import ( "bytes" "encoding/json" "net/http" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" ) var ( Sub = &Subscription{} client = &http.Client{ Timeout: 30 * time.Second, } ) type Subscription struct { Active bool `json:"active"` Status string `json:"status"` Plan string `json:"plan"` Quantity int `json:"quantity"` Amount int `json:"amount"` PeriodEnd time.Time `json:"period_end"` TrialEnd time.Time `json:"trial_end"` CancelAtPeriodEnd bool `json:"cancel_at_period_end"` Balance int64 `json:"balance"` UrlKey string `json:"url_key"` } type subscriptionData struct { Active bool `json:"active"` Status string `json:"status"` Plan string `json:"plan"` Quantity int `json:"quantity"` Amount int `json:"amount"` PeriodEnd int64 `json:"period_end"` TrialEnd int64 `json:"trial_end"` CancelAtPeriodEnd bool `json:"cancel_at_period_end"` Balance int64 `json:"balance"` UrlKey string `json:"url_key"` } func Update() (errData *errortypes.ErrorData, err error) { sub := &Subscription{} if settings.System.License == "" { Sub = sub return } data, err := json.Marshal(struct { Id string `json:"id"` License string `json:"license"` }{ Id: settings.System.Name, License: settings.System.License, }) req, err := http.NewRequest( "GET", "https://app.pritunl.com/subscription", bytes.NewBuffer(data), ) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "subscription: Subscription request failed"), } return } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "subscription: Subscription request failed"), } return } defer resp.Body.Close() if resp.StatusCode != 200 { errData = &errortypes.ErrorData{} err = json.NewDecoder(resp.Body).Decode(errData) if err != nil { errData = nil } else { logrus.WithFields(logrus.Fields{ "error": errData.Error, "error_msg": errData.Message, }).Error("subscription: Subscription error") } err = &errortypes.RequestError{ errors.Wrap(err, "subscription: Subscription server error"), } return } subData := &subscriptionData{} err = json.NewDecoder(resp.Body).Decode(subData) if err != nil { err = &errortypes.ParseError{ errors.Wrap( err, "subscription: Failed to parse subscription response", ), } return } if !strings.Contains(subData.Plan, "zero") && !strings.Contains(subData.Plan, "cloud") { errData = &errortypes.ErrorData{ Error: "invalid_plan", Message: "Invalid subscription plan", } err = &errortypes.RequestError{ errors.Wrap(err, "subscription: Invalid plan"), } return } sub.Active = subData.Active sub.Status = subData.Status sub.Plan = subData.Plan sub.Quantity = subData.Quantity sub.Amount = subData.Amount sub.CancelAtPeriodEnd = subData.CancelAtPeriodEnd sub.Balance = subData.Balance sub.UrlKey = subData.UrlKey if subData.PeriodEnd != 0 { sub.PeriodEnd = time.Unix(subData.PeriodEnd, 0) } if subData.TrialEnd != 0 { sub.TrialEnd = time.Unix(subData.TrialEnd, 0) } Sub = sub return } func update() { for { time.Sleep(30 * time.Minute) if constants.Shutdown { return } err, _ := Update() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("subscription: Update error") return } } } func init() { module := requires.New("subscription") module.After("settings") module.Handler = func() (err error) { Update() go update() return } } ================================================ FILE: sync/auth.go ================================================ package sync import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/user" "github.com/sirupsen/logrus" ) func authSync() (err error) { db := database.GetDatabase() defer db.Close() coll := db.Users() count, err := coll.CountDocuments( db, &bson.M{ "type": user.Local, }, options.Count().SetLimit(1), ) if err != nil { err = database.ParseError(err) return } settings.Local.NoLocalAuth = count == 0 return } func authRunner() { time.Sleep(1 * time.Second) for { time.Sleep(10 * time.Second) if constants.Shutdown { return } err := authSync() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("sync: Failed to sync authentication status") } } } func initAuth() { go authRunner() } ================================================ FILE: sync/nodes.go ================================================ package sync import ( "fmt" "strconv" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" ) func nodeSync() (err error) { db := database.GetDatabase() defer db.Close() appId := "" facets := []string{} if node.Self.UserDomain != "" { domain := node.Self.UserDomain port := node.Self.Port if node.Self.Protocol == "https" && port != 443 { domain += ":" + strconv.Itoa(port) } appId = fmt.Sprintf("https://%s/auth/u2f/app.json", domain) } nodes, err := node.GetAll(db) if err != nil { return } domains := set.NewSet() for _, nde := range nodes { if appId == "" { appId = fmt.Sprintf("https://%s/auth/u2f/app.json", nde.UserDomain) } domain := nde.UserDomain port := nde.Port if domain != "" { if nde.Protocol == "https" && port != 443 { domain += ":" + strconv.Itoa(port) } if !domains.Contains(domain) { domains.Add(domain) facets = append(facets, fmt.Sprintf("https://%s", domain)) } } domain = nde.AdminDomain port = nde.Port if domain != "" { if nde.Protocol == "https" && port != 443 { domain += ":" + strconv.Itoa(port) } if !domains.Contains(domain) { domains.Add(domain) facets = append(facets, fmt.Sprintf("https://%s", domain)) } } } settings.Local.AppId = appId settings.Local.Facets = facets return } func nodeRunner() { time.Sleep(1 * time.Second) for { if constants.Shutdown { return } err := nodeSync() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("sync: Failed to sync node status") } time.Sleep(10 * time.Second) } } func initNode() { go nodeRunner() } ================================================ FILE: sync/sync.go ================================================ package sync func Init() { initAuth() initNode() initVm() } ================================================ FILE: sync/vm.go ================================================ package sync import ( "runtime/debug" "time" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deploy" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/iptables" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/vpc" "github.com/sirupsen/logrus" ) func deployState(runtimes *state.Runtimes) (err error) { defer func() { panc := recover() if panc != nil { logrus.WithFields(logrus.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in state deploy") } }() start := time.Now() stat, err := state.GetState(runtimes) if err != nil { return } err = deploy.Deploy(stat, runtimes) if err != nil { return } runtimes.Total = time.Since(start) return } func syncNodeFirewall() { defer func() { panc := recover() if panc != nil { logrus.WithFields(logrus.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in node firewall") } }() db := database.GetDatabase() defer db.Close() if !node.Self.Firewall { iptables.UpdateState(node.Self, []*vpc.Vpc{}, []*instance.Instance{}, []string{}, nil, map[string][]*firewall.Rule{}, map[string][]*firewall.Mapping{}) return } for i := 0; i < 2; i++ { fires, err := firewall.GetRoles(db, node.Self.Roles) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("sync: Failed to get node firewall rules") return } ingress := firewall.MergeIngress(fires) iptables.UpdateStateRecover(node.Self, []*vpc.Vpc{}, []*instance.Instance{}, []string{}, ingress, map[string][]*firewall.Rule{}, map[string][]*firewall.Mapping{}) break } } func vmRunner() { time.Sleep(1 * time.Second) for { time.Sleep(1 * time.Second) if constants.Shutdown { return } if !node.Self.IsHypervisor() { syncNodeFirewall() continue } break } logrus.WithFields(logrus.Fields{ "production": constants.Production, }).Info("sync: Starting hypervisor") runtimes := &state.Runtimes{} runtimes.Init() for { if runtimes.Total > 1500*time.Millisecond { runtimes.Log() } delay := (3000 * time.Millisecond) - runtimes.Total if delay < 50*time.Millisecond { delay = 50 * time.Millisecond } time.Sleep(delay) runtimes = &state.Runtimes{} runtimes.Init() if constants.Shutdown { return } if !node.Self.IsHypervisor() { syncNodeFirewall() continue } err := deployState(runtimes) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("sync: Failed to deploy state") continue } } } func initVm() { go vmRunner() } ================================================ FILE: systemd/systemd.go ================================================ package systemd import ( "strings" "sync" "time" "github.com/pritunl/pritunl-cloud/utils" ) var ( systemdLock = sync.Mutex{} ) func Reload() (err error) { systemdLock.Lock() defer systemdLock.Unlock() _, err = utils.ExecCombinedOutput("", "systemctl", "daemon-reload") if err != nil { return } time.Sleep(100 * time.Millisecond) return } func Start(unit string) (err error) { systemdLock.Lock() defer systemdLock.Unlock() _, err = utils.ExecCombinedOutputLogged(nil, "systemctl", "start", unit) if err != nil { return } time.Sleep(300 * time.Millisecond) return } func Restart(unit string) (err error) { systemdLock.Lock() defer systemdLock.Unlock() _, err = utils.ExecCombinedOutputLogged(nil, "systemctl", "restart", unit) if err != nil { return } time.Sleep(300 * time.Millisecond) return } func Stop(unit string) (err error) { systemdLock.Lock() defer systemdLock.Unlock() _, err = utils.ExecCombinedOutput("", "systemctl", "stop", unit) if err != nil { return } time.Sleep(300 * time.Millisecond) return } func GetState(unit string) (state string, timestamp time.Time, err error) { systemdLock.Lock() defer systemdLock.Unlock() output, _ := utils.ExecOutput("", "systemctl", "show", "--no-page", unit) timestampStr := "" exitCode := "" exitStatus := "" for _, line := range strings.Split(output, "\n") { n := len(line) if state == "" && n > 13 && line[:12] == "ActiveState=" { state = line[12:] } else if exitCode == "" && n > 13 && line[:13] == "ExecMainCode=" { exitCode = line[13:] } else if exitStatus == "" && n > 15 && line[:15] == "ExecMainStatus=" { exitStatus = line[15:] } else if timestampStr == "" && n > 24 && line[:23] == "ExecMainStartTimestamp=" { timestampStr = line[23:] } } if (state == "failed" && exitCode == "2" && exitStatus == "31") || (state == "failed" && exitCode == "3" && exitStatus == "31") { state = "inactive" } if timestampStr != "" && timestampStr != "0" && timestampStr != "n/a" { timestamp, _ = time.Parse("Mon 2006-01-02 15:04:05 MST", timestampStr) } return } ================================================ FILE: systemd/utils.go ================================================ package systemd import ( "fmt" "time" ) func FormatUptime(timestamp time.Time) (uptime string) { since := time.Since(timestamp) minutes := int64(since.Minutes()) days := minutes / 1440 hours := (minutes % 1440) / 60 minutes = (minutes % 1440) % 60 if days > 0 { uptime = fmt.Sprintf("%d days", days) } if hours > 0 || uptime != "" { if uptime != "" { uptime += " " } uptime += fmt.Sprintf("%d hours", hours) } if uptime != "" { uptime += " " } uptime += fmt.Sprintf("%d mins", minutes) return } func FormatUptimeShort(timestamp time.Time) (uptime string) { since := time.Since(timestamp) minutes := int64(since.Minutes()) days := minutes / 1440 hours := (minutes % 1440) / 60 minutes = (minutes % 1440) % 60 if days > 3 { uptime = fmt.Sprintf("%d days", days) } else { if days > 0 { hours += days * 24 } if hours > 0 { uptime += fmt.Sprintf("%d hr", hours) } if minutes > 0 { if uptime != "" { uptime += " " } uptime += fmt.Sprintf("%d mn", minutes) } } return } ================================================ FILE: task/acme.go ================================================ package task import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/acme" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/sirupsen/logrus" ) var acmeRenew = &Task{ Name: "acme_renew", Version: 1, Hours: []int{7}, Minutes: []int{45}, Handler: acmeRenewHandler, } func acmeRenewHandler(db *database.Database) (err error) { certs, err := certificate.GetAll(db, &bson.M{}) if err != nil { return } for _, cert := range certs { if cert.Type != certificate.LetsEncrypt { continue } err = acme.Renew(db, cert, false) if err != nil { logrus.WithFields(logrus.Fields{ "certificate_id": cert.Id.Hex(), "certificate_name": cert.Name, "error": err, }).Error("task: Failed to renew certificate") continue } } return } func init() { register(acmeRenew) } ================================================ FILE: task/advisory.go ================================================ package task import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/advisory" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/instance" "github.com/sirupsen/logrus" ) var advisoryData = &Task{ Name: "advisory", Version: 1, Hours: []int{0, 3, 6, 9, 12, 15, 18, 21}, Minutes: []int{22}, Handler: advisoryDataHandler, RunOnStart: true, } func advisoryDataHandler(db *database.Database) (err error) { advisories := map[string]*advisory.Advisory{} coll := db.Instances() cursor, err := coll.Find(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { inst := &instance.Instance{} err = cursor.Decode(inst) if err != nil { err = database.ParseError(err) return } if inst.Guest == nil { continue } for _, updt := range inst.Guest.Updates { details := []*advisory.Advisory{} for _, cve := range updt.Cves { adv, ok := advisories[cve] if !ok { adv, err = advisory.GetOneLimit(db, cve) if err != nil { logrus.WithFields(logrus.Fields{ "cve_id": cve, "error": err, }).Error("task: Failed to query CVE") err = nil adv = nil } advisories[cve] = adv } if adv != nil { details = append(details, adv) } } updt.Details = details } err = inst.CommitFields(db, set.NewSet("guest")) if err != nil { return } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func init() { register(advisoryData) } ================================================ FILE: task/backing.go ================================================ package task import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var backingClean = &Task{ Name: "backing_clean", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}, Handler: backingCleanHandler, } func backingCleanHandler(db *database.Database) (err error) { backingDir := paths.GetBackingPath() diskKeys, err := disk.GetAllKeys(db, node.Self.Id) if err != nil { return } exists, err := utils.ExistsDir(backingDir) if !exists { return } items, err := ioutil.ReadDir(backingDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "task: Failed to read backing directory"), } return } for _, item := range items { name := item.Name() pth := filepath.Join(backingDir, name) if strings.HasPrefix(name, "image-") { keys := strings.Split(name, "-") if len(keys) != 3 { continue } key := fmt.Sprintf("%s-%s", keys[1], keys[2]) if !diskKeys.Contains(key) { if time.Since(item.ModTime()) > 5*time.Minute { logrus.WithFields(logrus.Fields{ "key": key, "path": pth, }).Info("task: Removing unused backing image") os.Remove(pth) continue } } } } return } func init() { register(backingClean) } ================================================ FILE: task/balancer.go ================================================ package task import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/database" ) var balancerClean = &Task{ Name: "balancer_clean", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{35}, Handler: balancerCleanHandler, } func balancerCleanHandler(db *database.Database) (err error) { balcns, err := balancer.GetAll(db, &bson.M{}) for _, balnc := range balcns { err = balnc.Clean(db) if err != nil { return } } return } func init() { register(balancerClean) } ================================================ FILE: task/blocks.go ================================================ package task import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var blocksCheck = &Task{ Name: "blocks_check", Version: 1, Hours: []int{7}, Minutes: []int{30}, Handler: blocksCheckHandler, } func blocksCheckHandler(db *database.Database) (err error) { coll := db.Blocks() ipColl := db.BlocksIp() instColl := db.Instances() ipBlocks := []bson.ObjectID{} err = ipColl.Distinct(db, "block", &bson.M{ "type": &bson.M{ "$in": []string{ block.External, block.IPv4, block.IPv6, }, }, }).Decode(&ipBlocks) if err != nil { err = database.ParseError(err) return } blocks := set.NewSet() ipBlocksSet := set.NewSet() for _, ipBlock := range ipBlocks { ipBlocksSet.Add(ipBlock) } cursor, err := coll.Find(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { blck := &block.Block{} err = cursor.Decode(blck) if err != nil { err = database.ParseError(err) return } blocks.Add(blck.Id) err = blck.ValidateAddresses(db, nil) if err != nil { return } } err = cursor.Err() if err != nil { err = database.ParseError(err) return } ipBlocksSet.Subtract(blocks) for blckIdInf := range ipBlocksSet.Iter() { blckId := blckIdInf.(bson.ObjectID) cursor2, e := ipColl.Find(db, &bson.M{ "block": blckId, }) if e != nil { err = database.ParseError(e) return } defer cursor2.Close(db) for cursor2.Next(db) { blckIp := &block.BlockIp{} err = cursor2.Decode(blckIp) if err != nil { err = database.ParseError(err) return } logrus.WithFields(logrus.Fields{ "ip_address": utils.Int2IpAddress(blckIp.Ip).String(), "block_id": blckIp.Id.Hex(), "instance_id": blckIp.Instance.Hex(), }).Warn("task: Removing lost block IP") _, _ = instColl.UpdateOne(db, &bson.M{ "_id": blckIp.Instance, }, &bson.M{ "$set": &bson.M{ "restart_block_ip": true, }, }) _, err = ipColl.DeleteOne(db, &bson.M{ "_id": blckIp.Id, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } } err = cursor2.Err() if err != nil { err = database.ParseError(err) return } } return } func init() { register(blocksCheck) } ================================================ FILE: task/cache.go ================================================ package task import ( "fmt" "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var cacheClean = &Task{ Name: "cache_clean", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}, Handler: cacheCleanHandler, } func cacheCleanHandler(db *database.Database) (err error) { cacheDir := node.Self.GetCachePath() imageKeys, err := image.GetAllKeys(db) if err != nil { return } exists, err := utils.ExistsDir(cacheDir) if !exists { return } items, err := ioutil.ReadDir(cacheDir) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "task: Failed to read cache directory"), } return } for _, item := range items { name := item.Name() pth := filepath.Join(cacheDir, name) if strings.HasPrefix(name, "image-") { keys := strings.Split(name, "-") if len(keys) != 3 { logrus.WithFields(logrus.Fields{ "path": pth, }).Warning("task: Removing unknown image cache") os.Remove(pth) continue } key := fmt.Sprintf("%s-%s", keys[1], keys[2]) if !imageKeys.Contains(key) { if time.Since(item.ModTime()) > 5*time.Minute { logrus.WithFields(logrus.Fields{ "key": key, "path": pth, }).Info("task: Removing old image cache") os.Remove(pth) continue } } } } return } func init() { register(cacheClean) } ================================================ FILE: task/constants.go ================================================ package task const ( Running = "running" Failed = "failed" Finished = "finished" ) var ( AllHours = []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, } TwoHours = []int{ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, } SixHours = []int{ 0, 6, 12, 18, } AllMins = []int{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, } TwoMins = []int{ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, } FiveMins = []int{ 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, } TenMins = []int{ 0, 10, 20, 30, 40, 50, } FifteenMins = []int{ 0, 15, 30, 45, } ThirtyMins = []int{ 0, 30, } ) ================================================ FILE: task/deployments.go ================================================ package task import ( "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/planner" ) var deployments = &Task{ Name: "deployments", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}, Seconds: 3 * time.Second, Handler: deploymentsHandler, } func deploymentsHandler(db *database.Database) (err error) { plnr := &planner.Planner{} err = plnr.ApplyPlans(db) if err != nil { return } return } func init() { register(deployments) } ================================================ FILE: task/domains.go ================================================ package task import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/settings" ) var domains = &Task{ Name: "domains", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}, Handler: domainsHandler, } func domainsHandler(db *database.Database) (err error) { refreshTtl := time.Duration( settings.System.DomainRefreshTtl) * time.Second domns, err := domain.GetAll(db, &bson.M{ "last_update": &bson.M{ "$gte": time.Now().Add(-refreshTtl), }, }) if err != nil { return } for _, domn := range domns { domain.Refresh(db, domn.Id) } return } func init() { register(domains) } ================================================ FILE: task/imds.go ================================================ package task import ( "math/rand" "sync" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/imds" "github.com/pritunl/pritunl-cloud/imds/types" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" ) var imdsSync = &Task{ Name: "imds_sync", Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}, Seconds: 3 * time.Second, Local: true, Handler: imdsSyncHandler, } var ( failTime = map[bson.ObjectID]failTimeData{} ) type failTimeData struct { timestamp time.Time logged bool } func test() { test := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} for _, val := range test { go func() { time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond) print(val) }() } } func imdsSyncHandler(db *database.Database) (err error) { confs := imds.GetConfigs() logTtl := time.Duration( settings.Hypervisor.ImdsSyncLogTimeout) * time.Second restartTtl := time.Duration( settings.Hypervisor.ImdsSyncRestartTimeout) * time.Second newFailTime := map[bson.ObjectID]failTimeData{} newFailTimeLock := sync.Mutex{} waiter := &sync.WaitGroup{} for _, conf := range confs { if conf.Instance == nil || conf.Instance.NetworkNamespace == "" { continue } waiter.Add(1) go func(conf *types.Config) { defer waiter.Done() err := imds.Sync(db, conf.Instance.NetworkNamespace, conf.Instance.Id, conf.Instance.Deployment, conf) if err != nil { newFailTimeLock.Lock() ttlData := failTime[conf.Instance.Id] if ttlData.timestamp.IsZero() { newFailTime[conf.Instance.Id] = failTimeData{ timestamp: time.Now(), } } else if time.Since(ttlData.timestamp) > logTtl && !ttlData.logged { logrus.WithFields(logrus.Fields{ "action": conf.Instance.Action, "instance": conf.Instance.Id.Hex(), "error": err, }).Error("task: Failed to sync imds") newFailTime[conf.Instance.Id] = failTimeData{ timestamp: ttlData.timestamp, logged: true, } } else if time.Since(ttlData.timestamp) > restartTtl { logrus.WithFields(logrus.Fields{ "action": conf.Instance.Action, "instance": conf.Instance.Id.Hex(), "error": err, }).Error("task: Failed to sync imds, restarting...") e := imds.Restart(conf.Instance.Id) if e != nil { logrus.WithFields(logrus.Fields{ "action": conf.Instance.Action, "instance": conf.Instance.Id.Hex(), "error": e, }).Error("task: Failed to restart imds") } } else { newFailTime[conf.Instance.Id] = failTime[conf.Instance.Id] } newFailTimeLock.Unlock() } }(conf) } waiter.Wait() failTime = newFailTime return } func init() { register(imdsSync) } ================================================ FILE: task/job.go ================================================ package task import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) type Job struct { Id string `bson:"_id"` Name string `bson:"name"` State string `bson:"state"` Retry bool `bson:"retry"` Node bson.ObjectID `bson:"node"` Timestamp time.Time `bson:"timestamp"` } func (j *Job) Reserve(db *database.Database) (reserved bool, err error) { coll := db.Tasks() _, err = coll.InsertOne(db, j) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.DuplicateKeyError: err = nil break } return } reserved = true return } func (j *Job) Failed(db *database.Database) (err error) { coll := db.Tasks() err = coll.UpdateId(j.Id, &bson.M{ "$set": &bson.M{ "state": Failed, }, }) return } func (j *Job) Finished(db *database.Database) (err error) { coll := db.Tasks() err = coll.UpdateId(j.Id, &bson.M{ "$set": &bson.M{ "state": Finished, }, }) return } ================================================ FILE: task/notification.go ================================================ package task import ( "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/notification" "github.com/sirupsen/logrus" ) var notificationCheck = &Task{ Name: "notification_check", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{15}, Handler: notificationCheckHandler, RunOnStart: true, } func notificationCheckHandler(db *database.Database) (err error) { err = notification.Check() if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("task: Failed to check vulnerability alerts") } return } func init() { register(notificationCheck) } ================================================ FILE: task/scheduler.go ================================================ package task import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/scheduler" "github.com/pritunl/pritunl-cloud/unit" "github.com/sirupsen/logrus" ) var schedule = &Task{ Name: "schedule", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}, Seconds: 5 * time.Second, Handler: scheduleHandler, } func scheduleUnits(db *database.Database) (err error) { units, err := unit.GetAll(db, &bson.M{ "kind": bson.M{ "$in": []string{deployment.Instance, deployment.Image}, }, }) if err != nil { return } deploymentIds, err := deployment.GetAllActiveIds(db) if err != nil { return } for _, unt := range units { for _, deplyId := range unt.Deployments { if !deploymentIds.Contains(deplyId) { logrus.WithFields(logrus.Fields{ "pod": unt.Pod.Hex(), "unit": unt.Id.Hex(), "deployment": deplyId.Hex(), }).Info("deploy: Removing deployment") err = unt.RemoveDeployement(db, deplyId) if err != nil { return } } } } for _, unt := range units { if len(unt.Deployments) >= unt.Count { continue } err = scheduler.Schedule(db, unt) if err != nil { return } } return } func scheduleHandler(db *database.Database) (err error) { err = scheduleUnits(db) if err != nil { return } schds, err := scheduler.GetAll(db) if err != nil { return } for _, schd := range schds { if schd.Consumed >= schd.Count { _, err = scheduler.Remove(db, schd.Id) if err != nil { return } } } return } func init() { register(schedule) } ================================================ FILE: task/spec.go ================================================ package task import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/spec" "github.com/sirupsen/logrus" ) var specs = &Task{ Name: "specs", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55}, Handler: specsHandler, } func specsHandler(db *database.Database) (err error) { deplys, err := deployment.GetAll(db, &bson.M{}) if err != nil { return } specIdsSet := set.NewSet() for _, deply := range deplys { if deply.Kind != deployment.Instance { continue } specIdsSet.Add(deply.Spec) } specIds := []bson.ObjectID{} for specId := range specIdsSet.Iter() { specIds = append(specIds, specId.(bson.ObjectID)) } specs, err := spec.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": specIds, }, }) if err != nil { return } for _, spec := range specs { errData, e := spec.Refresh(db) if e != nil || errData != nil { err = e logrus.WithFields(logrus.Fields{ "spec_id": spec.Id.Hex(), "error": err, "error_data": errData, }).Error("deploy: Failed to refresh active spec") err = nil errData = nil } } return } func init() { register(specs) } ================================================ FILE: task/specindex.go ================================================ package task import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" ) var specIndex = &Task{ Name: "spec_index", Version: 1, Hours: []int{6}, Minutes: []int{32}, Handler: specIndexHandler, } func specIndexSyncUnit(db *database.Database, unt *unit.Unit) (err error) { specs, err := spec.GetAllIndexes(db, &bson.M{ "unit": unt.Id, }) index := 0 for i, spc := range specs { index = i + 1 if spc.Index != index { spc.Index = index err = spc.CommitFields(db, set.NewSet("index")) if err != nil { return } } } if unt.SpecIndex != index { unt.SpecIndex = index err = unt.CommitFields(db, set.NewSet("spec_index")) if err != nil { return } } return } func specIndexHandler(db *database.Database) (err error) { units, err := unit.GetAll(db, &bson.M{}) if err != nil { return } for _, unt := range units { err = specIndexSyncUnit(db, unt) if err != nil { return } } return } func init() { register(specIndex) } ================================================ FILE: task/storage.go ================================================ package task import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/storage" "github.com/sirupsen/logrus" ) var storageSync = &Task{ Name: "storage_renew", Version: 1, Hours: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, Minutes: []int{0, 14, 29, 44, 59}, Handler: storageSyncHandler, RunOnStart: true, } func storageSyncHandler(db *database.Database) (err error) { coll := db.Images() imgStoreIdsList := []bson.ObjectID{} err = coll.Distinct( db, "storage", &bson.M{}, ).Decode(&imgStoreIdsList) if err != nil { err = database.ParseError(err) return } imgStoreIds := set.NewSet() for _, storeId := range imgStoreIdsList { imgStoreIds.Add(storeId) } storeIds := set.NewSet() stores, err := storage.GetAll(db) if err != nil { return } for _, store := range stores { storeIds.Add(store.Id) err = data.Sync(db, store) if err != nil { logrus.WithFields(logrus.Fields{ "storage_id": store.Id.Hex(), "storage_name": store.Name, "error": err, }).Error("task: Failed to sync storage") } } imgStoreIds.Subtract(storeIds) remStoreIds := []bson.ObjectID{} for storeIdInf := range imgStoreIds.Iter() { storeId := storeIdInf.(bson.ObjectID) logrus.WithFields(logrus.Fields{ "storage_id": storeId.Hex(), }).Warning("task: Cleaning unknown images") remStoreIds = append(remStoreIds, storeId) } if len(remStoreIds) > 0 { _, err = coll.DeleteMany(db, &bson.M{ "storage": &bson.M{ "$in": remStoreIds, }, }) if err != nil { return } } event.PublishDispatch(db, "image.change") return } func init() { register(storageSync) } ================================================ FILE: task/task.go ================================================ package task import ( "fmt" "runtime/debug" "time" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/version" "github.com/sirupsen/logrus" ) var ( registry = []*Task{} ) type Task struct { Name string Version int Hours []int Minutes []int Seconds time.Duration Retry bool Handler func(*database.Database) error RunOnStart bool Local bool DebugNodes []string timestamp time.Time } func (t *Task) scheduled(hour, min int) bool { for _, h := range t.Hours { if h == hour { for _, m := range t.Minutes { if m == min { return true } } } } return false } func (t *Task) runShared(db *database.Database, now time.Time) { defer func() { panc := recover() if panc != nil { logrus.WithFields(logrus.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in run task") } }() if t.Seconds == 0 { time.Sleep(time.Duration(utils.RandInt(0, 1000)) * time.Millisecond) } else { time.Sleep(time.Duration(utils.RandInt(0, 300)) * time.Millisecond) } if t.DebugNodes != nil { matched := false for _, ndeName := range t.DebugNodes { if node.Self.Name == ndeName { matched = true } } if !matched { return } } id := fmt.Sprintf("%s-%d", t.Name, now.Unix()-int64(now.Second())) if t.Seconds != 0 { id += fmt.Sprintf("-%d", GetBlock(now, t.Seconds)) } job := &Job{ Id: id, Name: t.Name, State: Running, Retry: t.Retry, Node: node.Self.Id, Timestamp: time.Now(), } reserved, err := job.Reserve(db) if err != nil { logrus.WithFields(logrus.Fields{ "task": t.Name, "error": err, }).Error("task: Task reserve failed") return } if !reserved { return } err = t.Handler(db) if err != nil { logrus.WithFields(logrus.Fields{ "task": t.Name, "error": err, }).Error("task: Task failed") _ = job.Failed(db) return } _ = job.Finished(db) } func (t *Task) runLocal(db *database.Database, now time.Time) { defer func() { panc := recover() if panc != nil { logrus.WithFields(logrus.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in run local task") } }() if t.DebugNodes != nil { matched := false for _, ndeName := range t.DebugNodes { if node.Self.Name == ndeName { matched = true } } if !matched { return } } id := fmt.Sprintf("%s-%d", t.Name, now.Unix()-int64(now.Second())) if t.Seconds != 0 { id += fmt.Sprintf("-%d", GetBlock(now, t.Seconds)) } err := t.Handler(db) if err != nil { logrus.WithFields(logrus.Fields{ "task": t.Name, "error": err, }).Error("task: Local task failed") return } } func (t *Task) run(now time.Time) { go func() { db := database.GetDatabase() defer db.Close() if t.Version != 0 { supported, err := version.Check(db, t.Name, t.Version) if err != nil { logrus.WithFields(logrus.Fields{ "task": t.Name, "error": err, }).Error("task: Version check failed") return } if !supported { logrus.WithFields(logrus.Fields{ "task": t.Name, "version": t.Version, }).Info("task: Skipping incompatible task") return } } curTimestamp := t.timestamp if !curTimestamp.IsZero() { if time.Since(curTimestamp) > 10*time.Minute { logrus.WithFields(logrus.Fields{ "task_name": t.Name, "runtime": time.Since(curTimestamp), }).Error("task: Task stuck running") } return } t.timestamp = time.Now() defer func() { t.timestamp = time.Time{} }() if t.Local { t.runLocal(db, now) } else { t.runShared(db, now) } }() } func runScheduler() { now := time.Now() curHour := now.Hour() curMin := now.Minute() curSecBlocks := map[time.Duration]int{} for _, task := range registry { if task.Seconds != 0 { curSecBlocks[task.Seconds] = GetBlock(now, task.Seconds) } if task.RunOnStart { go task.run(now) } } for { time.Sleep(1 * time.Second) now = time.Now() hour := now.Hour() min := now.Minute() for block, curSecBlock := range curSecBlocks { secBlock := GetBlock(now, block) if curSecBlock != secBlock { for _, task := range registry { if task.Seconds != 0 && task.Seconds == block && task.scheduled(hour, min) { task.run(now) } } } curSecBlocks[block] = secBlock } if curHour == hour && curMin == min { continue } curHour = hour curMin = min for _, task := range registry { if task.Seconds == 0 && task.scheduled(hour, min) { task.run(now) } } } } func register(task *Task) { registry = append(registry, task) } func Init() (err error) { for _, task := range registry { if task.Version == 0 { continue } err = version.Set(database.GetDatabase(), task.Name, task.Version) if err != nil { logrus.WithFields(logrus.Fields{ "task": task.Name, "version": task.Version, "error": err, }).Error("task: Failed to set task version") return } } go runScheduler() return } func GetBlock(n time.Time, d time.Duration) int { s := int(d.Seconds()) return (n.Second() / s) * s } ================================================ FILE: telemetry/constants.go ================================================ package telemetry const ( Moderate = "moderate" Important = "important" Critical = "critical" ) ================================================ FILE: telemetry/telemetry.go ================================================ package telemetry import ( "fmt" "sync" "time" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) var ( registry []handler ) type handler interface { getName() string Refresh() error } type Telemetry[Data any] struct { name string Default Data lock sync.Mutex lastTransmit time.Time TransmitRate time.Duration lastRefresh time.Time RefreshRate time.Duration Refresher func() (Data, error) Validate func(Data) Data data Data } func (r *Telemetry[Data]) getName() string { return r.name } func (r *Telemetry[Data]) Refresh() (err error) { r.lock.Lock() lastRefresh := r.lastRefresh r.lock.Unlock() if time.Since(lastRefresh) < r.RefreshRate { return } var data Data func() { defer utils.RecoverLog("telemetry: Panic in refresh") data, err = r.Refresher() if err != nil { return } }() r.Set(data) return } func (r *Telemetry[Data]) Set(data Data) { r.lock.Lock() r.data = data r.lastRefresh = time.Now() r.lock.Unlock() } func (r *Telemetry[Data]) Get() (Data, bool) { r.lock.Lock() lastRefresh := r.lastRefresh lastTransmit := r.lastTransmit r.lock.Unlock() if lastRefresh.IsZero() || time.Since(lastTransmit) < r.TransmitRate { var x Data return x, false } r.lock.Lock() r.lastTransmit = time.Now() r.lock.Unlock() if r.Validate != nil { return r.Validate(r.data), true } else { return r.data, true } } func Register[Data any](telm *Telemetry[Data]) { telm.name = fmt.Sprintf("%T", telm) registry = append(registry, telm) } func Refresh() { for _, telm := range registry { err := telm.Refresh() if err != nil { logrus.WithFields(logrus.Fields{ "kind": telm.getName(), }).Error("telemetry: Telemetry refresh failed") } } } ================================================ FILE: telemetry/updates.go ================================================ package telemetry import ( "regexp" "sort" "strings" "time" "github.com/pritunl/pritunl-cloud/advisory" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) var ( cveReg = regexp.MustCompile(`CVE-\d{4}-\d+`) ) var Updates = &Telemetry[[]*Update]{ TransmitRate: 6 * time.Minute, RefreshRate: 6 * time.Hour, Refresher: UpdatesRefresh, Validate: func(data []*Update) []*Update { if len(data) > 50 { return data[:50] } return data }, } type Update struct { Advisory string `bson:"advisory" json:"advisory"` Cves []string `bson:"cves" json:"cves"` Severity string `bson:"severity" json:"severity"` Description string `bson:"description" json:"description"` Packages []string `bson:"packages" json:"packages"` Details []*advisory.Advisory `bson:"details" json:"details"` } func parseRecord(lines []string) (update *Update) { updt := &Update{} descLines := []string{} currentField := "" for _, line := range lines { colonIdx := strings.Index(line, ":") if colonIdx < 0 { continue } prefix := line[:colonIdx] value := strings.TrimSpace(line[colonIdx+1:]) if strings.TrimSpace(prefix) == "" { switch currentField { case "Description": descLines = append(descLines, value) case "CVEs": if value != "" { updt.Cves = append(updt.Cves, cveReg.FindAllString(value, -1)...) } } continue } field := strings.TrimSpace(prefix) currentField = field switch field { case "Update ID", "Name": updt.Advisory = value case "Severity": updt.Severity = parseSeverity(value) case "Description": if value != "" { descLines = append(descLines, value) } case "CVEs": if value != "" { updt.Cves = append(updt.Cves, cveReg.FindAllString(value, -1)...) } } } if !matchAdvisory(updt.Advisory) { return } if updt.Severity == "" { return } for len(descLines) > 0 && descLines[len(descLines)-1] == "" { descLines = descLines[:len(descLines)-1] } updt.Description = strings.Join(descLines, "\n") fullText := strings.Join(lines, "\n") cveSet := map[string]bool{} deduped := []string{} for _, c := range updt.Cves { if !cveSet[c] { cveSet[c] = true deduped = append(deduped, c) } } for _, c := range cveReg.FindAllString(fullText, -1) { if !cveSet[c] { cveSet[c] = true deduped = append(deduped, c) } } sort.Strings(deduped) updt.Cves = deduped update = updt return } func updatesList() (advisories map[string][]string, err error) { if !IsDnf() { return } resp, err := commander.Exec(&commander.Opt{ Name: "dnf", Args: []string{ "updateinfo", "list", }, Timeout: 90 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { if resp != nil { logrus.WithFields( resp.Map(), ).Error("telemetry: Failed to get dnf security update list") } return } advisories = map[string][]string{} seen := map[string]map[string]bool{} for _, line := range strings.Split(string(resp.Output), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "Last metadata") || strings.HasPrefix(line, "Updating") || strings.HasPrefix(line, "Repositories") { continue } parts := strings.Fields(line) if len(parts) < 3 { continue } adv := parts[0] if !matchAdvisory(adv) { continue } pkg := "" part1 := strings.ToLower(parts[1]) part2 := strings.ToLower(parts[2]) if strings.Contains(part1, Moderate) || strings.Contains(part1, Important) || strings.Contains(part1, Critical) { pkg = parts[2] } else if len(parts) >= 4 && (strings.Contains(part2, Moderate) || strings.Contains(part2, Important) || strings.Contains(part2, Critical)) { pkg = parts[3] } else { continue } if pkg == "" { continue } pkgSet, ok := seen[adv] if !ok { pkgSet = map[string]bool{} seen[adv] = pkgSet } if !pkgSet[pkg] { pkgSet[pkg] = true advisories[adv] = append(advisories[adv], pkg) } } for adv := range advisories { sort.Strings(advisories[adv]) } return } func UpdatesRefresh() (updates []*Update, err error) { if !IsDnf() { return } pkgMap, err := updatesList() if err != nil { return } resp, err := commander.Exec(&commander.Opt{ Name: "dnf", Args: []string{ "updateinfo", "info", }, Timeout: 120 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { if resp != nil { logrus.WithFields( resp.Map(), ).Error("telemetry: Failed to get dnf updateinfo report") } return } updates = []*Update{} seen := map[string]bool{} var current []string flush := func() { if len(current) == 0 { return } record := current current = nil upd := parseRecord(record) if upd == nil { return } if seen[upd.Advisory] { return } pkgs, ok := pkgMap[upd.Advisory] if !ok { return } seen[upd.Advisory] = true upd.Packages = pkgs updates = append(updates, upd) } for _, line := range strings.Split(string(resp.Output), "\n") { if isSeparatorLine(line) { flush() continue } if strings.HasPrefix( strings.ReplaceAll(line, " ", ""), "Name:") { flush() } current = append(current, line) } flush() return } func init() { Register(Updates) } ================================================ FILE: telemetry/utils.go ================================================ package telemetry import ( "bytes" "os" "strings" "time" "github.com/pritunl/tools/commander" ) var ( hasSevs = 0 ) func IsDnf() bool { _, err := os.Stat("/usr/bin/dnf") return err == nil } func HasSevs() bool { if hasSevs == 1 { return false } else if hasSevs == 2 { return true } resp, err := commander.Exec(&commander.Opt{ Name: "dnf", Args: []string{ "updateinfo", "list", "--help", }, Timeout: 8 * time.Second, PipeOut: true, PipeErr: true, }) if err != nil { hasSevs = 1 return false } if bytes.Contains(resp.Output, []byte("--advisory-severities")) { hasSevs = 2 return true } return false } func matchAdvisory(id string) bool { return strings.HasPrefix(id, "RHSA-") || strings.HasPrefix(id, "ALSA-") || strings.HasPrefix(id, "RLSA-") || strings.HasPrefix(id, "ELSA-") || strings.HasPrefix(id, "FEDORA-") } func parseSeverity(value string) string { switch strings.ToLower(strings.TrimSpace(value)) { case "critical": return Critical case "important": return Important case "moderate": return Moderate } return "" } func isSeparatorLine(line string) bool { trimmed := strings.TrimSpace(line) if len(trimmed) == 0 { return false } for _, r := range trimmed { if r != '=' { return false } } return true } ================================================ FILE: tools/autoindex.py ================================================ import os import sys from datetime import datetime, timezone ROOT_DIR = "/mnt/images" PREFIX = "" if len(sys.argv) > 1: PREFIX = sys.argv[1] + "/" HTML_HEADER = """Index of /{relative_path}

Index of /{relative_path}


../
"""
HTML_FOOTER = """

""" def limit_filename_length(filename, max_length=50): if len(filename) > max_length: return filename[:max_length - 3] + '..>' return filename def generate_directory_listing_html(current_dir, relative_path, dirs, files): html = HTML_HEADER.format(relative_path=PREFIX + relative_path) for directory in sorted(dirs): dir_path = os.path.join(current_dir, directory) stat_info = os.stat(dir_path) modified_time = datetime.fromtimestamp(stat_info.st_mtime, tz=timezone.utc).strftime("%d-%b-%Y %H:%M") label = limit_filename_length(directory + '/') html += f"{label}" + \ f"{' ' * (51 - len(label))}{modified_time}{' ' * 20}-\n" for file_name in sorted(files): if file_name == "index.html": continue file_path = os.path.join(current_dir, file_name) stat_info = os.stat(file_path) modified_time = datetime.fromtimestamp(stat_info.st_mtime, tz=timezone.utc).strftime("%d-%b-%Y %H:%M") file_size = f"{stat_info.st_size:21}" label = limit_filename_length(file_name) html += f"{label}" + \ f"{' ' * (51 - len(label))}{modified_time}{file_size}\n" html += HTML_FOOTER return html def generate_index_files(root_dir): for current_dir, subdirs, files in os.walk(root_dir): relative_path = os.path.relpath(current_dir, root_dir) if relative_path == ".": relative_path = "" else: relative_path += "/" html_content = generate_directory_listing_html(current_dir, relative_path, sorted(subdirs), sorted(files)) index_path = os.path.join(current_dir, "index.html") with open(index_path, "w") as f: f.write(html_content) print(f"Generated index.html for: {current_dir}") generate_index_files(ROOT_DIR) ================================================ FILE: tools/build_run.sh ================================================ #!/bin/bash set -e NO_AGENT=false STABLE=false ARGS=() while [[ $# -gt 0 ]]; do case $1 in --no-agent) NO_AGENT=true shift ;; --stable) STABLE=true shift ;; *) ARGS+=("$1") shift ;; esac done build_pritunl_agent() { CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-extldflags "-static"' -v -o agent-static sudo cp -f ./agent-static /usr/bin/pritunl-cloud-agent rm -f ./agent-static CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -v -o agent-bsd sudo cp -f ./agent-bsd /usr/bin/pritunl-cloud-agent-bsd rm -f ./agent-bsd } if [ "$NO_AGENT" = false ]; then cd agent output=$(go install -v 2>&1 | tee /dev/tty) || exit 1 if [ -n "$output" ]; then build_pritunl_agent fi cd .. fi cd redirect go install -v sudo cp -f ~/go/bin/redirect /usr/bin/pritunl-cloud-redirect cd .. go install -v sudo cp -f ~/go/bin/pritunl-cloud /usr/bin/pritunl-cloud if [ "$STABLE" = true ]; then sudo /usr/bin/pritunl-cloud start --fast-exit elif [ ${#ARGS[@]} -eq 0 ]; then sudo /usr/bin/pritunl-cloud start --debug else sudo /usr/bin/pritunl-cloud "${ARGS[@]}" fi ================================================ FILE: tools/builder.py ================================================ import optparse import datetime import re import sys import subprocess import time import math import json import requests import os import getpass import base64 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import ( Cipher, algorithms, modes ) from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC USAGE = """Usage: builder [command] [options] Command Help: builder [command] --help Commands: version Print the version and exit set-version Set current version build Build and release""" CONSTANTS_PATH = 'constants/constants.go' CHANGES_PATH = 'CHANGES' STABLE_PACUR_PATH = '../pritunl-pacur' TEST_PACUR_PATH = '../pritunl-pacur-test' BUILD_KEYS_PATH = os.path.expanduser('~/data/build/pritunl_build.json') BUILD_TARGETS = ('pritunl-cloud',) REPO_NAME = 'pritunl-cloud' cur_date = datetime.datetime.utcnow() pacur_path = None def wget(url, cwd=None, output=None): if output: args = ['wget', '-O', output, url] else: args = ['wget', url] subprocess.check_call(args, cwd=cwd) def post_git_asset(release_id, file_name, file_path): file_size = os.path.getsize(file_path) response = requests.post( 'https://uploads.github.com/repos/%s/%s/releases/%s/assets' % ( github_owner, REPO_NAME, release_id), headers={ 'Authorization': 'token %s' % github_token, 'Content-Type': 'application/octet-stream', 'Content-Size': str(file_size), }, params={ 'name': file_name, }, data=open(file_path, 'rb').read(), ) if response.status_code != 201: print('Failed to create asset on github') print(response.json()) sys.exit(1) def get_ver(version): day_num = (cur_date - datetime.datetime(2015, 11, 24)).days min_num = int(math.floor(((cur_date.hour * 60) + cur_date.minute) / 14.4)) ver = re.findall(r'\d+', version) ver_str = '.'.join((ver[0], ver[1], str(day_num), str(min_num))) ver_str += ''.join(re.findall('[a-z]+', version)) return ver_str def get_int_ver(version): ver = re.findall(r'\d+', version) if 'snapshot' in version: pass elif 'alpha' in version: ver[-1] = str(int(ver[-1]) + 1000) elif 'beta' in version: ver[-1] = str(int(ver[-1]) + 2000) elif 'rc' in version: ver[-1] = str(int(ver[-1]) + 3000) else: ver[-1] = str(int(ver[-1]) + 4000) return int(''.join([x.zfill(4) for x in ver])) def iter_packages(): for target in BUILD_TARGETS: target_path = os.path.join(pacur_path, target) for name in os.listdir(target_path): if cur_version not in name: continue elif name.endswith(".pkg.tar.zst"): pass elif name.endswith(".rpm"): pass elif name.endswith(".deb"): pass else: continue path = os.path.join(target_path, name) yield name, path # Parse args if len(sys.argv) > 1: cmd = sys.argv[1] else: cmd = 'version' def aes_encrypt(passphrase, data): enc_salt = os.urandom(32) enc_iv = os.urandom(12) kdf = PBKDF2HMAC( algorithm=hashes.SHA512(), length=32, salt=enc_salt, iterations=10000000, backend=default_backend(), ) enc_key = kdf.derive(passphrase.encode()) cipher = Cipher( algorithms.AES(enc_key), modes.GCM(enc_iv), backend=default_backend() ).encryptor() enc_data = cipher.update(data.encode('utf-8')) + cipher.finalize() auth_tag = cipher.tag return '\n'.join([ base64.b64encode(enc_salt).decode('utf-8'), base64.b64encode(enc_iv).decode('utf-8'), base64.b64encode(enc_data).decode('utf-8'), base64.b64encode(auth_tag).decode('utf-8'), ]) def aes_decrypt(passphrase, data): data = data.split('\n') if len(data) < 4: raise ValueError('Invalid encryption data') enc_salt = base64.b64decode(data[0]) enc_iv = base64.b64decode(data[1]) enc_data = base64.b64decode(data[2]) auth_tag = base64.b64decode(data[3]) kdf = PBKDF2HMAC( algorithm=hashes.SHA512(), length=32, salt=enc_salt, iterations=10000000, backend=default_backend(), ) enc_key = kdf.derive(passphrase.encode()) cipher = Cipher( algorithms.AES(enc_key), modes.GCM(enc_iv, auth_tag), backend=default_backend() ).decryptor() decrypted_data = cipher.update(enc_data) + cipher.finalize() return decrypted_data.decode('utf-8') passphrase = getpass.getpass('Enter passphrase: ') if cmd == 'encrypt': passphrase2 = getpass.getpass('Enter passphrase: ') if passphrase != passphrase2: print('ERROR: Passphrase mismatch') sys.exit(1) with open(BUILD_KEYS_PATH, 'r') as build_keys_file: data = build_keys_file.read().strip() enc_data = aes_encrypt(passphrase, data) with open(BUILD_KEYS_PATH, 'w') as build_keys_file: build_keys_file.write(enc_data) sys.exit(0) if cmd == 'decrypt': with open(BUILD_KEYS_PATH, 'r') as build_keys_file: enc_data = build_keys_file.read().strip() data = aes_decrypt(passphrase, enc_data) with open(BUILD_KEYS_PATH, 'w') as build_keys_file: build_keys_file.write(data) sys.exit(0) # Load build keys with open(BUILD_KEYS_PATH, 'r') as build_keys_file: enc_data = build_keys_file.read() data = aes_decrypt(passphrase, enc_data) build_keys = json.loads(data.strip()) github_owner = build_keys['github_owner'] github_token = build_keys['github_token'] gitlab_token = build_keys['gitlab_token'] gitlab_host = build_keys['gitlab_host'] # Get package info with open(CONSTANTS_PATH, 'r') as constants_file: cur_version = re.findall('= "(.*?)"', constants_file.read())[0] parser = optparse.OptionParser(usage=USAGE) (options, args) = parser.parse_args() build_num = 0 # Run cmd if cmd == 'version': print('%s v%s' % (REPO_NAME, cur_version)) sys.exit(0) if cmd == 'sync-releases': next_url = 'https://api.github.com/repos/%s/%s/releases' % ( github_owner, REPO_NAME) while True: # Get github release response = requests.get( next_url, headers={ 'Authorization': 'token %s' % github_token, 'Content-type': 'application/json', }, ) if response.status_code != 200: print('Failed to get repo releases on github') print(response.json()) sys.exit(1) for release in response.json(): print(release['tag_name']) # Create gitlab release resp = requests.post( ('https://%s/api/v4/projects' + '/%s%%2F%s/repository/tags/%s/release') % ( gitlab_host, github_owner, REPO_NAME, release['tag_name']), headers={ 'Private-Token': gitlab_token, 'Content-type': 'application/json', }, data=json.dumps({ 'tag_name': release['tag_name'], 'description': release['body'], }), ) if resp.status_code not in (201, 409): print('Failed to create releases on gitlab') print(resp.json()) sys.exit(1) if 'Link' not in response.headers or \ 'rel="next"' not in response.headers['Link']: break next_url = response.headers['Link'].split(';')[0][1:-1] if cmd == 'get-version': new_version_orig = args[1] new_version = get_ver(new_version_orig) print(new_version) if cmd == 'set-version': new_version_orig = args[1] new_version = get_ver(new_version_orig) is_snapshot = 'snapshot' in new_version pacur_path = TEST_PACUR_PATH if is_snapshot else STABLE_PACUR_PATH # Update changes if not is_snapshot: with open(CHANGES_PATH, 'r') as changes_file: changes_data = changes_file.read() with open(CHANGES_PATH, 'w') as changes_file: ver_date_str = 'Version ' + new_version.replace( 'v', '') + cur_date.strftime(' %Y-%m-%d') changes_file.write(changes_data.replace( '<%= version %>', '%s\n%s' % (ver_date_str, '-' * len(ver_date_str)), )) # Check for duplicate version response = requests.get( 'https://api.github.com/repos/%s/%s/releases' % ( github_owner, REPO_NAME), headers={ 'Authorization': 'token %s' % github_token, 'Content-type': 'application/json', }, ) if response.status_code != 200: print('Failed to get repo releases on github') print(response.json()) sys.exit(1) for release in response.json(): if release['tag_name'] == new_version: print('Version already exists in github') sys.exit(1) # Generate changelog version = None release_body = '' if not is_snapshot: with open(CHANGES_PATH, 'r') as changelog_file: for line in changelog_file.readlines()[2:]: line = line.strip() if not line or line[0] == '-': continue if line[:7] == 'Version': if version: break version = line.split(' ')[1] elif version: release_body += '* %s\n' % line if not is_snapshot and version != new_version: print('New version does not exist in changes') sys.exit(1) if is_snapshot: release_body = '* Snapshot release' elif not release_body: print('Failed to generate github release body') sys.exit(1) release_body = release_body.rstrip('\n') # Update constants with open(CONSTANTS_PATH, 'r') as constants_file: constants_data = constants_file.read() with open(CONSTANTS_PATH, 'w') as constants_file: constants_file.write(re.sub( '(= ".*?")', '= "%s"' % new_version, constants_data, count=1, )) # Git commit subprocess.check_call(['git', 'reset', 'HEAD', '.']) subprocess.check_call(['git', 'add', CHANGES_PATH]) subprocess.check_call(['git', 'add', CONSTANTS_PATH]) subprocess.check_call(['git', 'commit', '-S', '-m', 'Create new release']) subprocess.check_call(['git', 'push']) # Create branch if not is_snapshot: subprocess.check_call(['git', 'branch', new_version]) subprocess.check_call(['git', 'push', '-u', 'origin', new_version]) time.sleep(6) # Create tag subprocess.check_call(['git', 'tag', new_version]) subprocess.check_call(['git', 'push', '--tags']) time.sleep(1) # Create release response = requests.post( 'https://api.github.com/repos/%s/%s/releases' % ( github_owner, REPO_NAME), headers={ 'Authorization': 'token %s' % github_token, 'Content-type': 'application/json', }, data=json.dumps({ 'tag_name': new_version, 'name': '%s v%s' % (REPO_NAME, new_version), 'body': release_body, 'prerelease': is_snapshot, 'target_commitish': 'master' if is_snapshot else new_version, }), ) if response.status_code != 201: print('Failed to create release on github') print(response.json()) sys.exit(1) subprocess.check_call(['git', 'pull']) subprocess.check_call(['git', 'push', '--tags']) time.sleep(6) # Create gitlab release response = requests.post( ('https://%s/api/v4/projects' + '/%s%%2F%s/releases') % ( gitlab_host, github_owner, REPO_NAME), headers={ 'Private-Token': gitlab_token, 'Content-type': 'application/json', }, data=json.dumps({ 'tag_name': new_version, 'name': '%s v%s' % (REPO_NAME, new_version), 'description': release_body, }), ) if response.status_code != 201: print('Failed to create release on gitlab') print(response.json()) sys.exit(1) if cmd == 'build' or cmd == 'build-upload': is_snapshot = 'snapshot' in cur_version pacur_path = TEST_PACUR_PATH if is_snapshot else STABLE_PACUR_PATH # Get sha256 sum archive_name = '%s.tar.gz' % cur_version archive_path = os.path.join(os.path.sep, 'tmp', archive_name) if os.path.isfile(archive_path): os.remove(archive_path) wget('https://github.com/%s/%s/archive/refs/tags/%s' % ( github_owner, REPO_NAME, archive_name), output=archive_name, cwd=os.path.join(os.path.sep, 'tmp'), ) archive_sha256_sum = subprocess.check_output( ['sha256sum', archive_path]).split()[0] os.remove(archive_path) # Update sha256 sum and pkgver in PKGBUILD for target in BUILD_TARGETS: pkgbuild_path = os.path.join(pacur_path, target, 'PKGBUILD') with open(pkgbuild_path, 'r') as pkgbuild_file: pkgbuild_data = re.sub( 'pkgver="(.*)"', 'pkgver="%s"' % cur_version, pkgbuild_file.read(), count=1, ) pkgbuild_data = re.sub( '"[a-f0-9]{64}"', '"%s"' % archive_sha256_sum.decode('utf-8'), pkgbuild_data, count=1, ) with open(pkgbuild_path, 'w') as pkgbuild_file: pkgbuild_file.write(pkgbuild_data) # Run pacur project build for build_target in BUILD_TARGETS: subprocess.check_call( ['sudo', 'pacur', 'project', 'build', build_target], cwd=pacur_path, ) if cmd == 'upload' or cmd == 'build-upload': is_snapshot = 'snapshot' in cur_version pacur_path = TEST_PACUR_PATH if is_snapshot else STABLE_PACUR_PATH # Get release id release_id = None response = requests.get( 'https://api.github.com/repos/%s/%s/releases' % ( github_owner, REPO_NAME), headers={ 'Authorization': 'token %s' % github_token, 'Content-type': 'application/json', }, ) for release in response.json(): if release['tag_name'] == cur_version: release_id = release['id'] if not release_id: print('Version does not exists in github') sys.exit(1) # Run pacur project build subprocess.check_call( ['sudo', 'pacur', 'project', 'repo'], cwd=pacur_path, ) # Add to github for name, path in iter_packages(): post_git_asset(release_id, name, path) # Sync mirror subprocess.check_call([ 'sh', 'upload-unstable.sh', ], cwd=pacur_path) if cmd == 'upload-github': is_snapshot = 'snapshot' in cur_version # Get release id release_id = None response = requests.get( 'https://api.github.com/repos/%s/%s/releases' % ( github_owner, REPO_NAME), headers={ 'Authorization': 'token %s' % github_token, 'Content-type': 'application/json', }, ) for release in response.json(): if release['tag_name'] == cur_version: release_id = release['id'] if not release_id: print('Version does not exists in github') sys.exit(1) # Add to github for name, path in iter_packages(): post_git_asset(release_id, name, path) ================================================ FILE: tools/generate_demo_data.py ================================================ #!/usr/bin/env python3 import random import string import hashlib ORGANIZATION_ID = "5a3245a50accad1a8a53bc82" DATACENTER_ID = "689733b7a7a35eae0dbaea1b" ZONE_ID = "689733b7a7a35eae0dbaea1e" VPC_ID = "689733b7a7a35eae0dbaea23" NODE_IDS = [ "689733b2a7a35eae0dbaea0a", "689733b2a7a35eae0dbaea0b", "689733b2a7a35eae0dbaea0c", "689733b2a7a35eae0dbaea0d", "689733b2a7a35eae0dbaea0e", "689733b2a7a35eae0dbaea0f", ] NODE_NAMES = [ "pritunl-east0", "pritunl-east1", "pritunl-east2", "pritunl-east3", "pritunl-east4", "pritunl-east5", ] SHAPE_IDS = { "small": "65e6e303ceeebbb3dabaec96", "medium": "65e6e2ecceeebbb3dabaec79", "large": "66f63282aac06d53e8c9c435", } IMAGE_IDS = [ "650a2c36aed15f1f1f5e96e1", "650a2c36aed15f1f1f5e96e2", ] POD_ID = "688bf358d978631566998ffc" UNIT_IDS = { "web": "688c716d9da165ffad4b3682", "database": "68b67d1aee12c08a1f39f88b", } SPEC_IDS = { "web": "688c7cde9da165ffad4b52e4", "database": "688c7cde9da165ffad4b34f2", } def generate_ip(subnet_base="10.196"): third_octet = random.randint(1, 8) fourth_octet = random.randint(2, 254) return f"{subnet_base}.{third_octet}.{fourth_octet}" def generate_pub_ip(subnet_base="1.253.67"): fourth_octet = random.randint(2, 254) return f"{subnet_base}.{fourth_octet}" def generate_priv_ip6(id): hash_obj = hashlib.sha256(str(id).encode()) hash_hex = hash_obj.hexdigest() return f"fd97:30bf:d456:a3bc:{hash_hex[0:4]}:{hash_hex[4:8]}:{hash_hex[8:12]}:{hash_hex[12:16]}" def generate_pub_ip6(id): hash_obj = hashlib.sha256(str(id).encode() + str(id).encode()) hash_hex = hash_obj.hexdigest() return f"2001:db8:85a3:4d2f:{hash_hex[0:4]}:{hash_hex[4:8]}:{hash_hex[8:12]}:{hash_hex[12:16]}" def generate_network_namespace(): characters = string.ascii_lowercase + string.digits return ''.join(random.choice(characters) for _ in range(14)) def get_instance_spec(instance_type): specs = { "web": {"name": "web-app", "shape": "small", "memory": 2048, "processors": 2, "disk": 20}, "database": {"name": "database", "shape": "large", "memory": 8192, "processors": 4, "disk": 100}, "search": {"name": "search", "shape": "large", "memory": 8192, "processors": 4, "disk": 200}, "vpn": {"name": "vpn", "shape": "small", "memory": 2048, "processors": 2, "disk": 20}, } return specs.get(instance_type) def generate_instances(count=20): instances = [] used_priv_ips = set() used_host_ips = set() used_pub_ips = set() instance_types = ( ["web"] * 10 + ["database"] * 6 + ["search"] * 2 + ["vpn"] * 2 )[:count] for i in range(count): instance_id = f"651d8e7c4cf9e2e3e4d56a{i:02x}" priv_ip = generate_ip() while priv_ip in used_priv_ips: priv_ip = generate_ip() used_priv_ips.add(priv_ip) pub_ip = generate_pub_ip() while pub_ip in used_pub_ips: pub_ip = generate_pub_ip() used_pub_ips.add(pub_ip) host_ip = generate_pub_ip("198.18.84") while host_ip in used_host_ips: host_ip = generate_pub_ip("198.18.84") used_host_ips.add(host_ip) instance_type = instance_types[i] spec = get_instance_spec(instance_type) node_id = NODE_IDS[i % len(NODE_IDS)] node_name = NODE_NAMES[i % len(NODE_NAMES)] image_id = IMAGE_IDS[i % len(IMAGE_IDS)] load1 = round(random.uniform(10, 60), 2) load5 = round(load1 + random.uniform(1, 10), 2) load15 = round(load5 + random.uniform(1, 10), 2) instance = { "id": instance_id, "type": instance_type, "organization": ORGANIZATION_ID, "datacenter": DATACENTER_ID, "zone": ZONE_ID, "vpc": VPC_ID, "image": image_id, "image_backing": False, "status": "Running", "state": "running", "action": "start", "public_ips": [pub_ip], "public_ips6": [generate_pub_ip6(instance_id)], "private_ips": [priv_ip], "private_ips6": [generate_priv_ip6(instance_id)], "host_ips": [host_ip], "node": node_id, "node_name": node_name, "shape": SHAPE_IDS[spec["shape"]], "name": spec["name"], "comment": "", "init_disk_size": spec["disk"], "memory": spec["memory"], "processors": spec["processors"], "network_namespace": generate_network_namespace(), "mem": round(random.uniform(30, 80), 2), "load1": load1, "load5": load5, "load15": load15, } instances.append(instance) return instances def generate_disks(instances): disks = [] for i, instance in enumerate(instances): disk_id = f"651d8e7c4cf9e2e3e4d34f{i:02x}" disk = { "id": disk_id, "name": instance['name'], "comment": "", "state": "attached", "type": "qcow2", "datacenter": instance["datacenter"], "zone": instance["zone"], "node": instance["node"], "organization": instance["organization"], "instance": instance["id"], "image": instance["image"], "index": "0", "size": instance["init_disk_size"], } disks.append(disk) return disks def generate_deployments(instances): deployments = [] for i, instance in enumerate(instances): if instance["type"] != "web" and instance["type"] != "database": continue deployment_id = f"651d8e7c4cf91e3b53d62d{i:02x}" deployment = { "id": deployment_id, "name": instance['name'], "type": instance['type'], "datacenter": instance["datacenter"], "zone": instance["zone"], "node": instance["node"], "node_name": instance["node_name"], "organization": instance["organization"], "instance": instance["id"], "public_ips": instance["public_ips"], "public_ips6": instance["public_ips6"], "private_ips": instance["private_ips"], "private_ips6": instance["private_ips6"], "host_ips": instance["host_ips"], "memory": instance["memory"], "processors": instance["processors"], "mem": instance["mem"], "load1": instance["load1"], "load5": instance["load5"], "load15": instance["load15"], } deployments.append(deployment) return deployments def format_go_instance(instance): go_code = f""" {{ Id: utils.ObjectIdHex("{instance['id']}"), Organization: utils.ObjectIdHex("{instance['organization']}"), Datacenter: utils.ObjectIdHex("{instance['datacenter']}"), Zone: utils.ObjectIdHex("{instance['zone']}"), Vpc: utils.ObjectIdHex("{instance['vpc']}"), Image: utils.ObjectIdHex("{instance['image']}"), ImageBacking: {str(instance['image_backing']).lower()}, Status: "{instance['status']}", State: "{instance['state']}", Action: "{instance['action']}", Uptime: "5 days 11 hours 34 mins", PublicIps: []string{{"{instance['public_ips'][0]}"}}, PublicIps6: []string{{"{instance['public_ips6'][0]}"}}, PrivateIps: []string{{"{instance['private_ips'][0]}"}}, PrivateIps6: []string{{"{instance['private_ips6'][0]}"}}, HostIps: []string{{"{instance['host_ips'][0]}"}}, Node: utils.ObjectIdHex("{instance['node']}"), Shape: utils.ObjectIdHex("{instance['shape']}"), Name: "{instance['name']}", Comment: "", InitDiskSize: {instance['init_disk_size']}, Memory: {instance['memory']}, Processors: {instance['processors']}, NetworkNamespace: "{instance['network_namespace']}", Created: time.Now(), Timestamp: time.Now(), Guest: &instance.GuestData{{ Status: "running", Timestamp: time.Now(), Heartbeat: time.Now(), Memory: {instance['mem']}, Load1: {instance['load1']}, Load5: {instance['load5']}, Load15: {instance['load15']}, }}, }},""" return go_code def format_go_disk(disk): go_code = f""" {{ Disk: disk.Disk{{ Id: utils.ObjectIdHex("{disk['id']}"), Name: "{disk['name']}", Comment: "", State: "{disk['state']}", Type: "{disk['type']}", Datacenter: utils.ObjectIdHex("{disk['datacenter']}"), Zone: utils.ObjectIdHex("{disk['zone']}"), Node: utils.ObjectIdHex("{disk['node']}"), Organization: utils.ObjectIdHex("{disk['organization']}"), Instance: utils.ObjectIdHex("{disk['instance']}"), Image: utils.ObjectIdHex("{disk['image']}"), Index: "{disk['index']}", Size: {disk['size']}, Created: time.Now(), }}, }},""" return go_code def format_go_deployment(deployment): go_code = f""" {{ Id: utils.ObjectIdHex("{deployment['id']}"), Pod: utils.ObjectIdHex("{POD_ID}"), Unit: utils.ObjectIdHex("{UNIT_IDS[deployment['type']]}"), Spec: utils.ObjectIdHex("{SPEC_IDS[deployment['type']]}"), SpecOffset: 0, SpecIndex: 2, SpecTimestamp: time.Now(), Timestamp: time.Now(), Tags: []string{{}}, Kind: "instance", State: "deployed", Action: "", Status: "healthy", Node: utils.ObjectIdHex("{deployment['node']}"), Instance: utils.ObjectIdHex("{deployment['instance']}"), InstanceData: &deployment.InstanceData{{ HostIps: []string{{"{deployment['host_ips'][0]}"}}, PublicIps: []string{{"{deployment['public_ips'][0]}"}}, PublicIps6: []string{{"{deployment['public_ips6'][0]}"}}, PrivateIps: []string{{"{deployment['private_ips'][0]}"}}, PrivateIps6: []string{{"{deployment['private_ips6'][0]}"}}, }}, ZoneName: "us-west-1a", NodeName: "{deployment['node_name']}", InstanceName: "{deployment['name']}", InstanceRoles: []string{{"instance"}}, InstanceMemory: {deployment['memory']}, InstanceProcessors: {deployment['processors']}, InstanceStatus: "Running", InstanceUptime: "5 days", InstanceState: "running", InstanceAction: "start", InstanceGuestStatus: "running", InstanceTimestamp: time.Now(), InstanceHeartbeat: time.Now(), InstanceMemoryUsage: {deployment['mem']}, InstanceHugePages: 0, InstanceLoad1: {deployment['load1']}, InstanceLoad5: {deployment['load5']}, InstanceLoad15: {deployment['load15']}, }},""" return go_code def main(): instances = generate_instances(20) disks = generate_disks(instances) deployments = generate_deployments(instances) print("// Instances") print("var Instances = []*instance.Instance{") for i, instance in enumerate(instances): print(format_go_instance(instance)) print("}") print("") print("// Disks") print("var Disks = []*aggregate.DiskAggregate{") for i, disk in enumerate(disks): print(format_go_disk(disk)) print("}") print("") print("// Deployments") print("var Deployments = []*aggregate.Deployment{") for i, deployment in enumerate(deployments): print(format_go_deployment(deployment)) print("}") if __name__ == "__main__": main() ================================================ FILE: tools/generate_files.py ================================================ import os import shutil import subprocess import json from datetime import datetime, timezone def md5_hash(filepath): result = subprocess.run(["md5sum", filepath], stdout=subprocess.PIPE) return result.stdout.split()[0].decode("utf-8") def last_modified_time(filepath): timestamp = os.path.getmtime(filepath) return datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") + "Z" def create_files_json(directory, output_file): existing_entries = {} if os.path.exists(output_file): backup_file = "{}.{}.bak".format( output_file, datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ"), ) shutil.copy2(output_file, backup_file) print("Backed up {} to {}".format(output_file, backup_file)) with open(output_file, "r") as f: existing_data = json.load(f) for entry in existing_data.get("files", []): existing_entries[entry["name"]] = entry files_data = { "version": 1, "files": [], } seen_names = set() for root, _, filenames in os.walk(directory): for filename in sorted(filenames): if not filename.endswith(".qcow2"): continue if filename in seen_names: continue seen_names.add(filename) if filename in existing_entries: files_data["files"].append(existing_entries[filename]) continue filepath = os.path.join(root, filename) print("Hashing new file {}".format(filename)) file_data = { "name": filename, "signed": True, "hash": md5_hash(filepath), "last_modified": last_modified_time(filepath), } files_data["files"].append(file_data) for name, entry in existing_entries.items(): if name not in seen_names: files_data["files"].append(entry) files_data["files"].sort(key=lambda e: e["name"]) with open(output_file, "w") as f: json.dump(files_data, f, indent=4) create_files_json(os.getcwd(), "files.json") ================================================ FILE: tools/package/PKGBUILD ================================================ targets=( "oraclelinux-10" ) pkgname="pritunl-cloud" pkgver="2.0.3665.99" pkgrel="2" pkgdesc="Pritunl Cloud" pkgdesclong=( "Pritunl Cloud" ) maintainer="Pritunl " arch="amd64" license=("custom") section="utils" priority="optional" url="https://github.com/pritunl/${pkgname}" depends:yum=( "iptables" "net-tools" "ipset" "ipvsadm" "xorriso" ) depends:apt=( "iptables" "net-tools" "ipset" "ipvsadm" "xorriso" ) optdepends:yum=( "qemu-kvm" "qemu-img" "swtpm" "swtpm-tools" ) makedepends:yum=( "git" ) makedepends:apt=( "git" ) provides=("${pkgname}") conflicts=( "${pkgname}" ) sources=( "pritunl-cloud.tar" ) hashsums=( "690c2aab9a0ea6a940b4390b6e95ec55c10951e3634e51115a36051402d686bc" ) backup=( "etc/${pkgname}.json" "var/log/${pkgname}.log" "var/log/${pkgname}.log.1" ) build() { mkdir -p /go/src/github.com/pritunl/${pkgname}/ mv "${srcdir}"/* /go/src/github.com/pritunl/${pkgname}/ cd /go/src/github.com/pritunl/${pkgname} go get go install cd /go/src/github.com/pritunl/${pkgname}/agent CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-extldflags "-static"' -v -o agent-static mv ./agent-static /go/bin/${pkgname}-agent CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -v -o agent-bsd mv ./agent-bsd /go/bin/${pkgname}-agent-bsd cd /go/src/github.com/pritunl/${pkgname}/redirect CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -o redirect mv ./redirect /go/bin/${pkgname}-redirect } package() { mkdir -p ${pkgdir}/usr/bin cp /go/bin/${pkgname} ${pkgdir}/usr/bin/${pkgname} chmod 755 ${pkgdir}/usr/bin/${pkgname} cp /go/bin/${pkgname}-redirect ${pkgdir}/usr/bin/${pkgname}-redirect chmod 755 ${pkgdir}/usr/bin/${pkgname}-redirect cp /go/bin/${pkgname}-agent ${pkgdir}/usr/bin/${pkgname}-agent chmod 755 ${pkgdir}/usr/bin/${pkgname}-agent cp /go/bin/${pkgname}-agent-bsd ${pkgdir}/usr/bin/${pkgname}-agent-bsd chmod 755 ${pkgdir}/usr/bin/${pkgname}-agent-bsd mkdir -p ${pkgdir}/usr/share/${pkgname}/www cp -r /go/src/github.com/pritunl/${pkgname}/www/dist/. ${pkgdir}/usr/share/${pkgname}/www/ mkdir -p ${pkgdir}/etc echo "{}" > ${pkgdir}/etc/${pkgname}.json chmod 600 ${pkgdir}/etc/${pkgname}.json mkdir -p ${pkgdir}/etc/systemd/system cp /go/src/github.com/pritunl/${pkgname}/tools/pritunl-cloud.service ${pkgdir}/etc/systemd/system cp /go/src/github.com/pritunl/${pkgname}/tools/pritunl-cloud-redirect.socket ${pkgdir}/etc/systemd/system cp /go/src/github.com/pritunl/${pkgname}/tools/pritunl-cloud-redirect.service ${pkgdir}/etc/systemd/system mkdir -p ${pkgdir}/var/log touch ${pkgdir}/var/log/${pkgname}.log touch ${pkgdir}/var/log/${pkgname}.log.1 } postinst() { useradd -r -s /sbin/nologin -c 'Pritunl Cloud web server' pritunl-cloud-web &> /dev/null || true systemctl daemon-reload &> /dev/null || true systemctl is-active --quiet pritunl-cloud && systemctl restart pritunl-cloud || true } postrm() { systemctl daemon-reload &> /dev/null || true } ================================================ FILE: tools/package/README.md ================================================ # pritunl-cloud: package Build test package ## install pacur ```bash sudo dnf -y install git-core podman sudo rm -rf /usr/local/go wget https://go.dev/dl/go1.25.4.linux-amd64.tar.gz echo "9fa5ffeda4170de60f67f3aa0f824e426421ba724c21e133c1e35d6159ca1bec go1.25.4.linux-amd64.tar.gz" | sha256sum -c - && sudo tar -C /usr/local -xf go1.25.4.linux-amd64.tar.gz rm -f go1.25.4.linux-amd64.tar.gz tee -a ~/.bashrc << EOF export GO111MODULE=on export GOPATH=\$HOME/go export GOROOT=/usr/local/go export PATH=/usr/local/go/bin:\$PATH:\$HOME/go/bin EOF chown cloud:cloud ~/.bashrc source ~/.bashrc go install github.com/pacur/pacur@latest cd "$(ls -d ~/go/pkg/mod/github.com/pacur/pacur@*/docker/ | sort -V | tail -n 1)" sudo find . -maxdepth 1 -type d -name "*" ! -name "." ! -name ".." ! -name "oraclelinux-10" -exec rm -rf {} + sh clean.sh sh build.sh cd ``` ## build package ```bash git clone https://github.com/pritunl/pritunl-cloud.git cd pritunl-cloud/tools/package sudo podman run --rm -t -v `pwd`:/pacur:Z localhost/pacur/oraclelinux-10 ``` ================================================ FILE: tools/pritunl-cloud-redirect.service ================================================ [Unit] Description=Pritunl Cloud Redirect Server Daemon Requires=pritunl-cloud-redirect.socket After=pritunl-cloud-redirect.socket [Service] ExecStart=/usr/bin/pritunl-cloud-redirect EnvironmentFile=/var/lib/pritunl-cloud/redirect.conf User=pritunl-cloud-web Group=pritunl-cloud-web PrivateTmp=true PrivateDevices=true ProtectSystem=strict ProtectHome=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true PrivateNetwork=true RestrictAddressFamilies=AF_INET AF_INET6 RestrictNamespaces=true RestrictRealtime=true MemoryDenyWriteExecute=true LockPersonality=true SystemCallFilter=@system-service SystemCallArchitectures=native RestrictSUIDSGID=true DevicePolicy=closed CapabilityBoundingSet= AmbientCapabilities= NoNewPrivileges=true IPAddressDeny=any SocketBindDeny=any ReadOnlyPaths=/ InaccessiblePaths=/home /root /boot /opt /mnt /media [Install] WantedBy=multi-user.target ================================================ FILE: tools/pritunl-cloud-redirect.socket ================================================ [Unit] Description=Pritunl Cloud Redirect Server Socket [Socket] ListenStream=80 Accept=no [Install] WantedBy=sockets.target ================================================ FILE: tools/pritunl-cloud.service ================================================ [Unit] Description=Pritunl Cloud Daemon [Service] LimitNOFILE=50000 ExecStart=/usr/bin/pritunl-cloud start [Install] WantedBy=multi-user.target ================================================ FILE: tools/tsc_run.sh ================================================ #!/bin/bash set -e npx tsc --watch ================================================ FILE: tools/virt-install/README.md ================================================ # pritunl-cloud: virt-install scripts Scripts used to build base images for pritunl-cloud ```bash sudo tee /etc/security/limits.conf << EOF * soft memlock 2048000000 * hard memlock 2048000000 root soft memlock 2048000000 root hard memlock 2048000000 * hard nofile 500000 * soft nofile 500000 root hard nofile 500000 root soft nofile 500000 EOF sudo tee /etc/systemd/system/disable-thp.service << EOF [Unit] Description=Disable Transparent Huge Pages [Service] Type=simple ExecStart=/bin/sh -c "echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled && echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag" [Install] WantedBy=multi-user.target EOF sudo systemctl daemon-reload sudo systemctl start disable-thp sudo systemctl enable disable-thp sudo sed -i 's/^SELINUX=.*/SELINUX=permissive/g' /etc/selinux/config sudo setenforce 0 sudo dnf -y install qemu-kvm qemu-img libguestfs-tools xorriso edk2-ovmf libvirt virt-install sudo systemctl enable --now libvirtd cd ./setup sudo firewall-cmd --zone=libvirt --add-port=8000/tcp --permanent python3 -m http.server # alpine linux setup-alpine curl -o /root/setup.sh http://192.168.122.1:8000/alpine.sh echo "c502a8b650d2b60f61414ea2f286577732ab7fc96bac487ebf024cd2120244ca /root/setup.sh" | sha256sum -c && sudo sh /root/setup.sh # arch linux mkdir /mnt/config mount /dev/sr1 /mnt/config cp /mnt/config/archinstall.json /root umount /mnt/config rmdir /mnt/config archinstall --silent --config /root/archinstall.json reboot curl -o /root/setup.sh http://192.168.122.1:8000/arch.sh echo "412aacb35f882d09ad7390124f2e3f52a7ae8deb6aaf2825a8775912dfb058fd /root/setup.sh" | sha256sum -c && bash /root/setup.sh # debian sudo curl -o /root/setup.sh http://192.168.122.1:8000/debian.sh echo "5fe9beb585bc434a8ebc8a32fbca347d8180ebc2cf6aef014b06b8c82a1f802a /root/setup.sh" | sudo sha256sum -c && sudo bash /root/setup.sh # fedora curl -o /root/setup.sh http://192.168.122.1:8000/fedora.sh echo "18032b049b0410f6c78ad5cfcd5f9b65fa3955e58268dc86ca41d7dbfb66a465 /root/setup.sh" | sha256sum -c && bash /root/setup.sh # freebsd fetch -o /root/setup.sh http://192.168.122.1:8000/freebsd.sh [ "$(sha256sum /root/setup.sh)" = "6aab203e3ba7c8aa31ad9dc7da38f701f3871a1ae339904e1f1e6f774ec58238 /root/setup.sh" ] && sh /root/setup.sh # rhel7 curl -o /root/setup.sh http://192.168.122.1:8000/rhel7.sh echo "da5f9518e45a71f1348b7fffd14e496a64cf2bb4a73fc763ec8e97d8f4c2e6d6 /root/setup.sh" | sha256sum -c && bash /root/setup.sh # rhel8 curl -o /root/setup.sh http://192.168.122.1:8000/rhel8.sh echo "dd277240c6d5b573f34e98241c67caec8c0b3c855d13fbb8ccfdfda7f7e726fa /root/setup.sh" | sha256sum -c && bash /root/setup.sh # rhel9 curl -o /root/setup.sh http://192.168.122.1:8000/rhel9.sh echo "23e0b0191270db7e09ade9afce206ac6a455aa7e91bf9eda6b6c677dfb78d994 /root/setup.sh" | sha256sum -c && bash /root/setup.sh # rhel10 curl -o /root/setup.sh http://192.168.122.1:8000/rhel10.sh echo "49cd8fd80e0a3badfbfa1b62de270e62e8f33e831e7f4780fa2f38bd89b9ffe5 /root/setup.sh" | sha256sum -c && bash /root/setup.sh find /var/lib/virt/images/ -name "*_$(date +%y%m%d).qcow2" -type f -exec sudo GPG_TTY=$(tty) gpg --default-key 055C08A4 --armor --output {}.sig --detach-sig {} \; sudo mkdir -p /mnt/images sudo chown cloud:cloud /mnt/images mkdir -p /mnt/images/stable mkdir -p /mnt/images/unstable rsync --human-readable --archive --xattrs --progress 127.0.0.1:/var/lib/virt/images/ /mnt/images/unstable/ sudo wget -P /tmp https://raw.githubusercontent.com/pritunl/toolbox/73aacb9e22b09a34f87d389b3dc301d6c450b0e8/s3c/s3c.py echo "7d14fa361e47ff328bbadac302a06a995f6ab65abbe4efce7d8cde6657ba8dde /tmp/s3c.py" | sha256sum -c - && sudo cp /tmp/s3c.py /usr/local/bin/s3c && sudo chmod +x /usr/local/bin/s3c sudo rm /tmp/s3c.py cd /mnt/images/unstable python3 ~/git/pritunl-cloud/tools/generate_files.py python3 ~/git/pritunl-cloud/tools/autoindex.py s3c cp almalinux8_$(date +%y%m%d).qcow2 pritunl-images:/unstable/almalinux8_$(date +%y%m%d).qcow2 s3c cp almalinux8_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/almalinux8_$(date +%y%m%d).qcow2.sig s3c cp almalinux9_$(date +%y%m%d).qcow2 pritunl-images:/unstable/almalinux9_$(date +%y%m%d).qcow2 s3c cp almalinux9_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/almalinux9_$(date +%y%m%d).qcow2.sig s3c cp almalinux10_$(date +%y%m%d).qcow2 pritunl-images:/unstable/almalinux10_$(date +%y%m%d).qcow2 s3c cp almalinux10_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/almalinux10_$(date +%y%m%d).qcow2.sig s3c cp alpinelinux_$(date +%y%m%d).qcow2 pritunl-images:/unstable/alpinelinux_$(date +%y%m%d).qcow2 s3c cp alpinelinux_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/alpinelinux_$(date +%y%m%d).qcow2.sig s3c cp archlinux_$(date +%y%m%d).qcow2 pritunl-images:/unstable/archlinux_$(date +%y%m%d).qcow2 s3c cp archlinux_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/archlinux_$(date +%y%m%d).qcow2.sig s3c cp fedora43_$(date +%y%m%d).qcow2 pritunl-images:/unstable/fedora43_$(date +%y%m%d).qcow2 s3c cp fedora43_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/fedora43_$(date +%y%m%d).qcow2.sig s3c cp fedora44_$(date +%y%m%d).qcow2 pritunl-images:/unstable/fedora44_$(date +%y%m%d).qcow2 s3c cp fedora44_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/fedora44_$(date +%y%m%d).qcow2.sig s3c cp freebsd_$(date +%y%m%d).qcow2 pritunl-images:/unstable/freebsd_$(date +%y%m%d).qcow2 s3c cp freebsd_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/freebsd_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux7_$(date +%y%m%d).qcow2 pritunl-images:/unstable/oraclelinux7_$(date +%y%m%d).qcow2 s3c cp oraclelinux7_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/oraclelinux7_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux8_$(date +%y%m%d).qcow2 pritunl-images:/unstable/oraclelinux8_$(date +%y%m%d).qcow2 s3c cp oraclelinux8_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/oraclelinux8_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux9_$(date +%y%m%d).qcow2 pritunl-images:/unstable/oraclelinux9_$(date +%y%m%d).qcow2 s3c cp oraclelinux9_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/oraclelinux9_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux10_$(date +%y%m%d).qcow2 pritunl-images:/unstable/oraclelinux10_$(date +%y%m%d).qcow2 s3c cp oraclelinux10_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/oraclelinux10_$(date +%y%m%d).qcow2.sig s3c cp rockylinux8_$(date +%y%m%d).qcow2 pritunl-images:/unstable/rockylinux8_$(date +%y%m%d).qcow2 s3c cp rockylinux8_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/rockylinux8_$(date +%y%m%d).qcow2.sig s3c cp rockylinux9_$(date +%y%m%d).qcow2 pritunl-images:/unstable/rockylinux9_$(date +%y%m%d).qcow2 s3c cp rockylinux9_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/rockylinux9_$(date +%y%m%d).qcow2.sig s3c cp rockylinux10_$(date +%y%m%d).qcow2 pritunl-images:/unstable/rockylinux10_$(date +%y%m%d).qcow2 s3c cp rockylinux10_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/rockylinux10_$(date +%y%m%d).qcow2.sig s3c cp ubuntu2404_$(date +%y%m%d).qcow2 pritunl-images:/unstable/ubuntu2404_$(date +%y%m%d).qcow2 s3c cp ubuntu2404_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/ubuntu2404_$(date +%y%m%d).qcow2.sig s3c cp ubuntu2604_$(date +%y%m%d).qcow2 pritunl-images:/unstable/ubuntu2604_$(date +%y%m%d).qcow2 s3c cp ubuntu2604_$(date +%y%m%d).qcow2.sig pritunl-images:/unstable/ubuntu2604_$(date +%y%m%d).qcow2.sig s3c cp files.json pritunl-images:/unstable/files.json s3c cp index.html pritunl-images:/unstable/index.html rsync --human-readable --archive --progress --delete /mnt/images/unstable/ /mnt/images/stable/ cd /mnt/images/stable python3 ~/git/pritunl-cloud/tools/generate_files.py python3 ~/git/pritunl-cloud/tools/autoindex.py s3c cp almalinux8_$(date +%y%m%d).qcow2 pritunl-images:/stable/almalinux8_$(date +%y%m%d).qcow2 s3c cp almalinux8_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/almalinux8_$(date +%y%m%d).qcow2.sig s3c cp almalinux9_$(date +%y%m%d).qcow2 pritunl-images:/stable/almalinux9_$(date +%y%m%d).qcow2 s3c cp almalinux9_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/almalinux9_$(date +%y%m%d).qcow2.sig s3c cp almalinux10_$(date +%y%m%d).qcow2 pritunl-images:/stable/almalinux10_$(date +%y%m%d).qcow2 s3c cp almalinux10_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/almalinux10_$(date +%y%m%d).qcow2.sig s3c cp alpinelinux_$(date +%y%m%d).qcow2 pritunl-images:/stable/alpinelinux_$(date +%y%m%d).qcow2 s3c cp alpinelinux_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/alpinelinux_$(date +%y%m%d).qcow2.sig s3c cp archlinux_$(date +%y%m%d).qcow2 pritunl-images:/stable/archlinux_$(date +%y%m%d).qcow2 s3c cp archlinux_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/archlinux_$(date +%y%m%d).qcow2.sig s3c cp fedora43_$(date +%y%m%d).qcow2 pritunl-images:/stable/fedora43_$(date +%y%m%d).qcow2 s3c cp fedora43_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/fedora43_$(date +%y%m%d).qcow2.sig s3c cp fedora44_$(date +%y%m%d).qcow2 pritunl-images:/stable/fedora44_$(date +%y%m%d).qcow2 s3c cp fedora44_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/fedora44_$(date +%y%m%d).qcow2.sig s3c cp freebsd_$(date +%y%m%d).qcow2 pritunl-images:/stable/freebsd_$(date +%y%m%d).qcow2 s3c cp freebsd_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/freebsd_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux7_$(date +%y%m%d).qcow2 pritunl-images:/stable/oraclelinux7_$(date +%y%m%d).qcow2 s3c cp oraclelinux7_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/oraclelinux7_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux8_$(date +%y%m%d).qcow2 pritunl-images:/stable/oraclelinux8_$(date +%y%m%d).qcow2 s3c cp oraclelinux8_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/oraclelinux8_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux9_$(date +%y%m%d).qcow2 pritunl-images:/stable/oraclelinux9_$(date +%y%m%d).qcow2 s3c cp oraclelinux9_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/oraclelinux9_$(date +%y%m%d).qcow2.sig s3c cp oraclelinux10_$(date +%y%m%d).qcow2 pritunl-images:/stable/oraclelinux10_$(date +%y%m%d).qcow2 s3c cp oraclelinux10_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/oraclelinux10_$(date +%y%m%d).qcow2.sig s3c cp rockylinux8_$(date +%y%m%d).qcow2 pritunl-images:/stable/rockylinux8_$(date +%y%m%d).qcow2 s3c cp rockylinux8_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/rockylinux8_$(date +%y%m%d).qcow2.sig s3c cp rockylinux9_$(date +%y%m%d).qcow2 pritunl-images:/stable/rockylinux9_$(date +%y%m%d).qcow2 s3c cp rockylinux9_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/rockylinux9_$(date +%y%m%d).qcow2.sig s3c cp rockylinux10_$(date +%y%m%d).qcow2 pritunl-images:/stable/rockylinux10_$(date +%y%m%d).qcow2 s3c cp rockylinux10_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/rockylinux10_$(date +%y%m%d).qcow2.sig s3c cp ubuntu2404_$(date +%y%m%d).qcow2 pritunl-images:/stable/ubuntu2404_$(date +%y%m%d).qcow2 s3c cp ubuntu2404_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/ubuntu2404_$(date +%y%m%d).qcow2.sig s3c cp ubuntu2604_$(date +%y%m%d).qcow2 pritunl-images:/stable/ubuntu2604_$(date +%y%m%d).qcow2 s3c cp ubuntu2604_$(date +%y%m%d).qcow2.sig pritunl-images:/stable/ubuntu2604_$(date +%y%m%d).qcow2.sig s3c cp files.json pritunl-images:/stable/files.json s3c cp index.html pritunl-images:/stable/index.html ``` ================================================ FILE: tools/virt-install/download.sh ================================================ #!/bin/bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" INSTALL_DIR="$SCRIPT_DIR/install" if [ ! -d "$INSTALL_DIR" ]; then echo "Error: install directory not found at: $INSTALL_DIR" exit 1 fi if ! command -v wget &> /dev/null; then echo "Error: wget is not installed" exit 1 fi processed=0 downloaded=0 for script in "$INSTALL_DIR"/*.sh; do if [ ! -f "$script" ]; then echo "No .sh files found in $INSTALL_DIR" exit 0 fi echo "Processing: $script" ((processed++)) iso_url=$(grep -E "^[[:space:]]*ISO_URL=" "$script" | tail -1 | cut -d'=' -f2- | sed 's/^["'\'']//' | sed 's/["'\'']$//') if [ -z "$iso_url" ]; then echo " No ISO_URL found in $script" continue fi iso_url=$(echo "$iso_url" | sed 's/\${\([^}]*\)}/\1/g' | sed 's/\$\([A-Za-z_][A-Za-z0-9_]*\)/\1/g') if [ -f "/var/lib/virt/iso/$(basename ${iso_url})" ]; then echo " File already exists, skipping" continue fi echo " Downloading: $iso_url" if sudo wget -P /var/lib/virt/iso "$iso_url"; then echo " Successfully downloaded: $iso_url" ((downloaded++)) else echo " Failed to download from: $iso_url" fi echo "" done echo "Summary:" echo " Processed $processed .sh files" echo " Successfully downloaded $downloaded files" ================================================ FILE: tools/virt-install/install/almalinux10.sh ================================================ #!/bin/bash set -ev NAME="almalinux10" ISO_URL="https://den.aws.repo.almalinux.org/10.1/isos/x86_64/AlmaLinux-10.1-x86_64-dvd.iso" ISO_HASH="4597a7483fd7b49bbb7a46958fe2574b7c575531e5ee64ad4d6c1d2779494400" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard -kexec-tools %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud %post grubby --update-kernel=ALL --remove-args=crashkernel %end EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ${NAME} \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/almalinux8.sh ================================================ #!/bin/bash set -ev NAME="almalinux8" ISO_URL="https://den.aws.repo.almalinux.org/8.10/isos/x86_64/AlmaLinux-8.10-x86_64-dvd.iso" ISO_HASH="463fa92155b886e31627f6713e1c2824343762245a914715ffd6f2efc300b7a1" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ${NAME} \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/almalinux9.sh ================================================ #!/bin/bash set -ev NAME="almalinux9" ISO_URL="https://den.aws.repo.almalinux.org/9.7/isos/x86_64/AlmaLinux-9.7-x86_64-dvd.iso" ISO_HASH="56f8bf5e44d293a040203b73b70f08bb7dc52f27654b047c20be8598f63ec1f8" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard -kexec-tools %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud %post grubby --update-kernel=ALL --remove-args=crashkernel %end EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ${NAME} \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/alpinelinux.sh ================================================ #!/bin/bash set -ev NAME="alpinelinux" ISO_URL="https://dl-cdn.alpinelinux.org/alpine/v3.22/releases/x86_64/alpine-virt-3.22.2-x86_64.iso" ISO_HASH="b6c45d69829b1b0416ada798353805099d57b8bef9093b85a8319fe5373595d5" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo mkdir -p /var/lib/virt/init sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo mkdir /var/lib/virt/init/${NAME} sudo tee /var/lib/virt/init/${NAME}/user-data << EOF #alpine-config apk: repositories: - base_url: https://dl-cdn.alpinelinux.org/alpine repos: - main - community packages: - curl - dosfstools - grub-efi - xfsprogs - xfsprogs-extra - sudo - chrony - openssh - qemu-guest-agent - cloud-init - cloud-utils-growpart runcmd: - rm /etc/runlevels/*/tiny-cloud* - lbu include /root/.ssh /home/alpine/.ssh - ERASE_DISKS=/dev/vda USE_EFI=1 DISKLABEL=gpt ROOTFS=xfs BOOT_SIZE=100 SWAP_SIZE=0 setup-disk -m sys /dev/vda - reboot EOF sudo tee /var/lib/virt/init/${NAME}/meta-data << EOF hostname: cloud EOF sudo rm -f /var/lib/virt/init/${NAME}.iso sudo xorriso -as mkisofs \ -output /var/lib/virt/init/${NAME}.iso \ -volid cidata \ -joliet \ -rock \ -input-charset utf-8 \ /var/lib/virt/init/${NAME}/user-data \ /var/lib/virt/init/${NAME}/meta-data sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant alpinelinux3.20 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}),kernel=boot/vmlinuz-virt,initrd=boot/initramfs-virt \ --cloud-init meta-data=/var/lib/virt/init/${NAME}/meta-data,user-data=/var/lib/virt/init/${NAME}/user-data \ --extra-args="console=ttyS0 autoinstall" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} sudo rm -f /var/lib/virt/init/${NAME}.iso echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/archlinux.sh ================================================ #!/bin/bash set -ev NAME="archlinux" ISO_URL="https://cofractal-sea.mm.fcix.net/archlinux/iso/latest/archlinux-2026.04.01-x86_64.iso" ISO_HASH="f14bf46afbe782d28835aed99bfa2fe447903872cb9f4b21153196d6ed1d48ae" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/images sudo mkdir -p /var/lib/virt/init sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo mkdir /var/lib/virt/init/${NAME} sudo tee /var/lib/virt/init/${NAME}/archinstall.json << EOF { "additional-repositories": null, "archinstall-language": "English", "audio_config": null, "bootloader": "Systemd-boot", "debug": false, "disk_config": { "config_type": "manual_partitioning", "device_modifications": [ { "device": "/dev/vda", "partitions": [ { "btrfs": [], "dev_path": null, "flags": ["boot", "esp"], "fs_type": "fat32", "size": { "sector_size": { "unit": "B", "value": 512 }, "unit": "MiB", "value": 512 }, "mount_options": [], "mountpoint": "/boot", "obj_id": "$(uuidgen)", "start": { "sector_size": { "unit": "B", "value": 512 }, "unit": "MiB", "value": 1 }, "status": "create", "type": "primary" }, { "btrfs": [], "dev_path": null, "flags": [], "fs_type": "xfs", "size": { "sector_size": { "unit": "B", "value": 512 }, "unit": "MiB", "value": 7678 }, "mount_options": [], "mountpoint": "/", "obj_id": "$(uuidgen)", "start": { "sector_size": { "unit": "B", "value": 512 }, "unit": "MiB", "value": 513 }, "status": "create", "type": "primary" } ], "wipe": true } ] }, "hostname": "cloud", "kernels": ["linux"], "locale_config": { "kb_layout": "us", "sys_enc": "UTF-8", "sys_lang": "en_US" }, "network_config": { "type": "nm" }, "no_pkg_lookups": false, "ntp": true, "offline": false, "packages": [ "base", "base-devel", "linux", "linux-firmware", "networkmanager", "openssh", "efibootmgr", "vi" ], "parallel downloads": 0, "profile_config": { "gfx_driver": null, "greeter": null, "profile": { "custom_settings": {}, "details": [], "main": "Server" } }, "mirror_config": { "custom_mirrors": [], "mirror_regions": { "Worldwide": [ "https://geo.mirror.pkgbuild.com/\$repo/os/\$arch", "https://mirror.rackspace.com/archlinux/\$repo/os/\$arch" ] } }, "custom_commands": [ "sed -i 's/rootfstype=xfs\$/rootfstype=xfs console=ttyS0/' /boot/loader/entries/*.conf", "systemctl enable serial-getty@ttyS0.service", "systemctl set-default multi-user.target", "mkinitcpio -P" ], "save_config": null, "script": "guided", "silent": true, "swap": false, "timezone": "UTC", "version": "2.8.6", "root_enc_password": "\$y\$j9T\$KsJ3WRqoGvcjGsQNis/oG0\$0zE1DqJ4NJn6pEN3VhnaUIA/nIBSeIYNR8yShbphLW1" } EOF sudo rm -f /var/lib/virt/init/${NAME}/archinstall.iso sudo xorriso -as mkisofs \ -output /var/lib/virt/init/${NAME}/archinstall.iso \ -volid CONFIGDATA \ -joliet \ -rock \ -input-charset utf-8 \ /var/lib/virt/init/${NAME}/archinstall.json sudo tee /var/lib/virt/init/${NAME}/archinstall-auto.service << 'EOF' [Unit] Description=Automated Arch Installation After=multi-user.target [Service] Type=oneshot ExecStart=/usr/bin/archinstall --silent --config /archinstall-config.json ExecStartPost=/usr/bin/systemctl poweroff [Install] WantedBy=multi-user.target EOF sudo tee /var/lib/virt/init/${NAME}/archinstall-auto.service << 'EOF' [Unit] Description=Automated Arch Installation After=multi-user.target [Service] Type=oneshot ExecStart=/usr/bin/archinstall --config /archinstall-config.json --silent ExecStartPost=/usr/bin/systemctl poweroff [Install] WantedBy=multi-user.target EOF sudo tee /var/lib/virt/init/${NAME}/autoinstall.sh << 'EOF' #!/bin/bash set -ex mkdir /mnt/config mount /dev/sr1 /mnt/config cp /mnt/config/archinstall.json /root/ umount /mnt/config rmdir /mnt/config archinstall --silent --config /root/archinstall.json reboot EOF sudo chmod +x /var/lib/virt/init/${NAME}/autoinstall.sh sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --disk path=/var/lib/virt/init/${NAME}/archinstall.iso,device=cdrom,bus=sata \ --os-variant archlinux \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}),kernel=arch/boot/x86_64/vmlinuz-linux,initrd=arch/boot/x86_64/initramfs-linux.img \ --extra-args="console=ttyS0 archisobasedir=arch archisosearchuuid=$(blkid -s UUID -o value /var/lib/virt/iso/$(basename ${ISO_URL})) cow_spacesize=1G" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/fedora43.sh ================================================ #!/bin/bash set -ev NAME="fedora43" ISO_URL="https://cofractal-sea.mm.fcix.net/fedora/linux/releases/43/Server/x86_64/iso/Fedora-Server-dvd-x86_64-43-1.6.iso" ISO_HASH="aca06983bef83da9b43144c1a2ff4c8483e4745167c17f53725c16a16742e643" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^server-product-environment %end firstboot --enable skipx ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant fedora40 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/fedora44.sh ================================================ #!/bin/bash set -ev NAME="fedora44" ISO_URL="https://cofractal-sea.mm.fcix.net/fedora/linux/releases/44/Server/x86_64/iso/Fedora-Server-dvd-x86_64-44-1.7.iso" ISO_HASH="85837793bfa36db6bc709b4cecd2ec116951b87d9c53c3d95eb2fac8dcf7cf1f" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^server-product-environment %end firstboot --enable skipx ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant fedora40 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/freebsd.sh ================================================ #!/bin/bash set -ev NAME="freebsd" ISO_URL="https://download.freebsd.org/releases/amd64/amd64/ISO-IMAGES/15.0/FreeBSD-15.0-RELEASE-amd64-dvd1.iso" ISO_HASH="8cf8e03d8df16401fd5a507480a3270091aa30b59ecf79a9989f102338e359aa" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi # Console type [vt100]: Enter # Welcome: Enter # Keymap Selection: Enter # Set Hostname: cloud # Select Installation Type: Distribution Sets # Distribution Select: Enter # Partitioning: Auto (UFS) # Partition: Enter # Partition Scheme: Enter # Partition Editor: Delete freebsd-swap # Partition Editor: Enter # New Password: cloud # Retype New Password: cloud # Network Configuration: Manual # Network Configuration: Enter # Network Configuration: Enter # Network Configuration: Enter # Network Configuration: No # IPv4 DNS #1: 8.8.8.8 # IPv4 DNS #2: 8.8.4.4 # Time Zone Selector: Enter # Time Zone Confirmation: Enter # Time & Date: Skip # Time & Date: Skip # System Configuration: +ntpd +ntpd_sync_on_start # System Hardening: Enter # Add User Accounts: Enter # Username: cloud # Full name: Cloud # Uid: 1000 # Login group: Enter # Invite cloud into other groups: wheel # Login class: Enter # Shell: tcsh # Home directory: Enter # Home directory permissions: Enter # Use password-based authentication: no # Lock out the account after creation: Enter # OK: yes # Add another user: Enter # Final Configuration: Exit # Manual Configuration: Enter # Complete: Enter sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant freebsd14.0 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --cdrom=/var/lib/virt/iso/$(basename ${ISO_URL}) while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/oraclelinux10.sh ================================================ #!/bin/bash set -ev NAME="oraclelinux10" ISO_URL="https://yum.oracle.com/ISOS/OracleLinux/OL10/u1/x86_64/OracleLinux-R10-U1-x86_64-dvd.iso" ISO_HASH="82fa2b70a18fb268c5ef013e298f85bba0d0e6c7ae882c49a3f67c02ee6d68de" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard -kexec-tools %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud %post grubby --update-kernel=ALL --remove-args=crashkernel %end EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ol9.5 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/oraclelinux7.sh ================================================ #!/bin/bash set -ev NAME="oraclelinux7" ISO_URL="https://yum.oracle.com/ISOS/OracleLinux/OL7/u9/x86_64/OracleLinux-R7-U9-Server-x86_64-dvd.iso" ISO_HASH="28d2928ded40baddcd11884b9a6a611429df12897784923c346057ec5cdd1012" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal @core %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ol7.9 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/oraclelinux8.sh ================================================ #!/bin/bash set -ev NAME="oraclelinux8" ISO_URL="https://yum.oracle.com/ISOS/OracleLinux/OL8/u10/x86_64/OracleLinux-R8-U10-x86_64-dvd.iso" ISO_HASH="7676a80eeaafa16903eebb2abba147a3afe230b130cc066d56fdd6854d8da900" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ol8.10 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/oraclelinux9.sh ================================================ #!/bin/bash set -ev NAME="oraclelinux9" ISO_URL="https://yum.oracle.com/ISOS/OracleLinux/OL9/u7/x86_64/OracleLinux-R9-U7-x86_64-dvd.iso" ISO_HASH="895751f157727bca8437607e8edb32b7c10d815aba954f5b3dc24c28ac8a10aa" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard -kexec-tools %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud %post grubby --update-kernel=ALL --remove-args=crashkernel %end EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant ol9.5 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/rockylinux10.sh ================================================ #!/bin/bash set -ev NAME="rockylinux10" ISO_URL="https://sjc.mirror.rackspace.com/rocky/10/isos/x86_64/Rocky-10.1-x86_64-dvd1.iso" ISO_HASH="55f96d45a052c0ed4f06309480155cb66281a008691eb7f3f359957205b1849a" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard -kexec-tools %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud %post grubby --update-kernel=ALL --remove-args=crashkernel %end EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant rocky9 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/rockylinux8.sh ================================================ #!/bin/bash set -ev NAME="rockylinux8" ISO_URL="https://sjc.mirror.rackspace.com/rocky/8/isos/x86_64/Rocky-8.10-x86_64-dvd1.iso" ISO_HASH="642ada8a49dbeca8cca6543b31196019ee3d649a0163b5db0e646c7409364eeb" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant rocky8 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/rockylinux9.sh ================================================ #!/bin/bash set -ev NAME="rockylinux9" ISO_URL="https://sjc.mirror.rackspace.com/rocky/9/isos/x86_64/Rocky-9.7-x86_64-dvd.iso" ISO_HASH="d48e902325dce6793935b4e13672a0d9a4f958e02d4e23fcf0a8a34c49ef03da" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo tee /var/lib/virt/ks/${NAME}.ks << EOF text cdrom %addon com_redhat_kdump --disable %end keyboard --xlayouts='us' lang en_US.UTF-8 network --bootproto=dhcp --hostname=cloud --activate %packages @^minimal-environment @standard -kexec-tools %end firstboot --enable ignoredisk --only-use=vda clearpart --all --initlabel part /boot/efi --fstype="efi" --ondisk=vda --size=100 --fsoptions="umask=0077,shortname=winnt" part / --fstype="xfs" --ondisk=vda --grow timezone Etc/UTC --utc rootpw --plaintext cloud %post grubby --update-kernel=ALL --remove-args=crashkernel %end EOF sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --os-variant rocky9 \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}) \ --initrd-inject=/var/lib/virt/ks/${NAME}.ks \ --extra-args="console=ttyS0 inst.ks=file:/${NAME}.ks inst.text" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/ubuntu24.sh ================================================ #!/bin/bash set -ev NAME="ubuntu2404" ISO_URL="https://cofractal-sea.mm.fcix.net/ubuntu-releases/24.04/ubuntu-24.04.3-live-server-amd64.iso" ISO_HASH="c3514bf0056180d09376462a7a1b4f213c1d6e8ea67fae5c25099c6fd3d8274b" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo mkdir -p /var/lib/virt/init sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo mkdir /var/lib/virt/init/${NAME} sudo tee /var/lib/virt/init/${NAME}/user-data << EOF #cloud-config autoinstall: version: 1 timezone: "Etc/UTC" identity: realname: "Cloud" username: cloud password: "\$6\$x7YEknTyUuNSTTVK\$nq4xoSTrYp7a/Kb1EvtpH97GxG02CFBqELznybQv4XrA7sskq9PI0Y5KADhp9KiwVdrwR6v2IP6wqoxyXj4SP/" hostname: cloud storage: layout: name: direct config: - type: disk id: disk0 match: size: largest - type: partition id: efi-partition device: disk0 size: 100M flag: boot grub_device: true - type: partition id: root-partition device: disk0 size: -1 - type: format id: efi-format volume: efi-partition fstype: fat32 - type: format id: root-format volume: root-partition fstype: xfs - type: mount id: efi-mount device: efi-format path: /boot/efi - type: mount id: root-mount device: root-format path: / EOF sudo tee /var/lib/virt/init/${NAME}/meta-data << EOF instance-id: ${NAME} local-hostname: cloud EOF sudo rm -f /var/lib/virt/init/${NAME}.iso sudo xorriso -as mkisofs \ -output /var/lib/virt/init/${NAME}.iso \ -volid cidata \ -joliet \ -rock \ -input-charset utf-8 \ /var/lib/virt/init/${NAME}/user-data \ /var/lib/virt/init/${NAME}/meta-data sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --disk path=/var/lib/virt/init/${NAME}.iso,device=cdrom \ --os-variant ubuntu-lts-latest \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}),kernel=casper/hwe-vmlinuz,initrd=casper/hwe-initrd \ --extra-args="console=ttyS0 serial autoinstall" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} sudo rm -rf /var/lib/virt/init/${NAME}.iso echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/install/ubuntu26.sh ================================================ #!/bin/bash set -ev NAME="ubuntu2604" ISO_URL="https://cofractal-sea.mm.fcix.net/ubuntu-releases/26.04/ubuntu-26.04-live-server-amd64.iso" ISO_HASH="dec49008a71f6098d0bcfc822021f4d042d5f2db279e4d75bdd981304f1ca5d9" sudo mkdir -p /var/lib/virt/iso sudo mkdir -p /var/lib/virt/ks sudo mkdir -p /var/lib/virt/images sudo mkdir -p /var/lib/virt/init sudo virsh destroy ${NAME} || true sudo virsh undefine ${NAME} --nvram || true sudo rm -f /var/lib/virt/${NAME}.qcow2 if [ ! -f "/var/lib/virt/iso/$(basename ${ISO_URL})" ]; then sudo wget -P /var/lib/virt/iso ${ISO_URL} fi echo "${ISO_HASH} /var/lib/virt/iso/$(basename ${ISO_URL})" | sha256sum --check if [ $? -ne 0 ]; then echo "Checksum for ISO failed" exit 1 fi sudo mkdir /var/lib/virt/init/${NAME} sudo tee /var/lib/virt/init/${NAME}/user-data << EOF #cloud-config autoinstall: version: 1 timezone: "Etc/UTC" identity: realname: "Cloud" username: cloud password: "\$6\$x7YEknTyUuNSTTVK\$nq4xoSTrYp7a/Kb1EvtpH97GxG02CFBqELznybQv4XrA7sskq9PI0Y5KADhp9KiwVdrwR6v2IP6wqoxyXj4SP/" hostname: cloud storage: layout: name: direct config: - type: disk id: disk0 match: size: largest - type: partition id: efi-partition device: disk0 size: 100M flag: boot grub_device: true - type: partition id: root-partition device: disk0 size: -1 - type: format id: efi-format volume: efi-partition fstype: fat32 - type: format id: root-format volume: root-partition fstype: xfs - type: mount id: efi-mount device: efi-format path: /boot/efi - type: mount id: root-mount device: root-format path: / EOF sudo tee /var/lib/virt/init/${NAME}/meta-data << EOF instance-id: ${NAME} local-hostname: cloud EOF sudo rm -f /var/lib/virt/init/${NAME}.iso sudo xorriso -as mkisofs \ -output /var/lib/virt/init/${NAME}.iso \ -volid cidata \ -joliet \ -rock \ -input-charset utf-8 \ /var/lib/virt/init/${NAME}/user-data \ /var/lib/virt/init/${NAME}/meta-data sudo virt-install \ --name ${NAME} \ --vcpus 8 \ --memory 8192 \ --boot uefi \ --disk path=/var/lib/virt/${NAME}.qcow2,size=8,format=qcow2,bus=virtio \ --disk path=/var/lib/virt/init/${NAME}.iso,device=cdrom \ --os-variant ubuntu-lts-latest \ --network network=default \ --graphics=none \ --console pty,target_type=serial \ --location=/var/lib/virt/iso/$(basename ${ISO_URL}),kernel=casper/vmlinuz,initrd=casper/initrd \ --extra-args="console=ttyS0 serial autoinstall" while ! sudo virsh domstate ${NAME} 2>/dev/null | grep -q "shut off"; do sleep 1 done sudo rm -rf /var/lib/virt/init/${NAME} sudo rm -rf /var/lib/virt/init/${NAME}.iso echo "Compressing image..." sudo rm -f /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sudo qemu-img convert -f qcow2 -O qcow2 -c /var/lib/virt/${NAME}.qcow2 /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 sha256sum /var/lib/virt/images/${NAME}_$(date +%y%m%d).qcow2 ================================================ FILE: tools/virt-install/setup/alpine.sh ================================================ #!/bin/sh set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting alpine setup ############################################################# tee /etc/motd << EOF Welcome to Alpine! The Alpine Wiki contains a large amount of how-to guides and general information about administrating Alpine systems. See . EOF echo "iso9660" > /etc/filesystems sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/g' /etc/default/grub grub-mkconfig -o /boot/grub/grub.cfg rc-update add sshd default rc-update add chronyd default rc-update add qemu-guest-agent default setup-cloud-init tee /etc/init.d/cloud-fix << EOF #!/sbin/openrc-run description="cloud-init final fix stage" depend() { after cloud-config provide cloud-fix } start() { if grep -q 'cloud-init=disabled' /proc/cmdline; then ewarn "\$RC_SVCNAME is disabled via /proc/cmdline." elif test -e /etc/cloud/cloud-init.disabled; then ewarn "\$RC_SVCNAME is disabled via cloud-init.disabled file" else ebegin "cloud-init fix" cloud-init modules --mode final eend \$? fi } EOF chmod +x /etc/init.d/cloud-fix rc-update add cloud-fix default sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals mkdir -p /home/alpine chown alpine:alpine /home/alpine chmod 700 /home/alpine usermod -l cloud alpine usermod -m -d /home/cloud cloud groupmod -n cloud alpine usermod -aG adm,wheel cloud passwd -d root passwd -l root passwd -d cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF cloud_password=$(tr -dc 'A-Za-z0-9!@#$%^&*()_+~' < /dev/urandom | head -c 64) echo "cloud:$cloud_password" | chpasswd passwd -u cloud cloud-init clean --machine-id tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.ash_history || true shred -u /home/cloud/.ash_history || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed sync fstrim -v / sync ############################################################# # finished alpine setup, clear history and shutdown: # unset HISTFILE && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/arch.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting arch setup ############################################################# sed -i 's/^timeout.*/timeout 0/' /boot/loader/loader.conf pacman -Syu pacman -Sy --noconfirm chrony qemu-guest-agent cloud-init cloud-guest-utils dhcpcd systemctl enable sshd systemctl enable chronyd systemctl enable cloud-init-local systemctl enable cloud-init-main systemctl enable cloud-config systemctl enable cloud-final systemctl disable systemd-networkd-wait-online systemctl mask systemd-networkd-wait-online sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals useradd -m -G wheel,systemd-journal cloud passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF cloud-init clean --machine-id tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub 2>/dev/null || true shred -u /root/.ssh/authorized_keys 2>/dev/null || true shred -u /root/.bash_history 2>/dev/null || true shred -u /home/cloud/.bash_history 2>/dev/null || true shred -u /var/log/lastlog 2>/dev/null || true shred -u /var/log/secure 2>/dev/null || true shred -u /var/log/utmp 2>/dev/null || true shred -u /var/log/wtmp 2>/dev/null || true shred -u /var/log/btmp 2>/dev/null || true shred -u /var/log/dmesg 2>/dev/null || true shred -u /var/log/dmesg.old 2>/dev/null || true shred -u /var/lib/systemd/random-seed 2>/dev/null || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed sync fstrim -v / sync ############################################################# # finished arch setup, clear history and shutdown: # unset HISTFILE && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/debian.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting debian setup ############################################################# tee /etc/modprobe.d/floppy-blacklist.conf << EOF blacklist floppy EOF apt update apt -y upgrade apt -y autoremove apt -y install bash-completion qemu-guest-agent cloud-init cloud-initramfs-growroot chrony openssh-server systemctl daemon-reload systemctl enable qemu-guest-agent.service systemctl enable cloud-init-local.service if systemctl list-unit-files cloud-init-main.service >/dev/null 2>&1; then systemctl enable cloud-init-main.service else systemctl enable cloud-init.service fi systemctl enable cloud-config.service systemctl enable cloud-final.service sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%sudo/d' /etc/sudoers tee -a /etc/sudoers << EOF %sudo ALL=(ALL) NOPASSWD:ALL EOF systemctl enable ssh.service ufw disable apt clean rm -f /etc/cloud/cloud.cfg.d/90-installer-network.cfg rm -f /etc/cloud/cloud.cfg.d/99-installer.cfg cloud-init clean --machine-id rm -rf /etc/NetworkManager/system-connections/* sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed rm -f /etc/machine-id touch /etc/machine-id sync fstrim -av sync ############################################################# # finished debian setup, clear history and shutdown: # unset HISTFILE && history -c && sudo poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/fedora.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting fedora setup ############################################################# tee /etc/modprobe.d/floppy-blacklist.conf << EOF blacklist floppy EOF dnf clean all dnf -y update dnf -y install bash-completion qemu-guest-agent dnf-utils cloud-init cloud-utils-growpart chrony openssh-server dnf -y update dnf -y remove cockpit-ws sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/g' /etc/default/grub grub2-mkconfig -o /boot/grub2/grub.cfg # cloud-init fix if [[ "$(cloud-init --version 2>&1)" == *"25.2"* ]]; then wget https://dl.fedoraproject.org/pub/fedora/linux/releases/44/Everything/x86_64/os/Packages/c/cloud-init-25.3-3.fc44.noarch.rpm echo "877c3b272f2202d46a7c1d06185eea262feb5bda637fa49575ae8c9a96e62652 cloud-init-25.3-3.fc44.noarch.rpm" | dnf -y install cloud-init-25.3-3.fc44.noarch.rpm rm -f cloud-init-25.3-3.fc44.noarch.rpm fi systemctl daemon-reload systemctl enable qemu-guest-agent.service systemctl enable cloud-init-local.service if systemctl list-unit-files cloud-init-main.service >/dev/null 2>&1; then systemctl enable cloud-init-main.service else systemctl enable cloud-init.service fi systemctl enable cloud-config.service systemctl enable cloud-final.service sed -i 's/^installonly_limit=.*/installonly_limit=2/g' /etc/dnf/dnf.conf sed -i 's/^SELINUX=.*/SELINUX=enforcing/g' /etc/selinux/config || true sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals restorecon -v /etc/ssh/trusted restorecon -v /etc/ssh/principals useradd -m -G adm,video,wheel,systemd-journal cloud || true passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh restorecon -v /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys restorecon -v /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF systemctl enable sshd systemctl restart sshd systemctl disable firewalld systemctl stop firewalld systemctl start chronyd systemctl enable chronyd dnf clean all rm -rf /var/cache/dnf cloud-init clean --machine-id rm -rf /etc/NetworkManager/system-connections/* sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed rm -f /etc/machine-id touch /etc/machine-id sync fstrim -av sync ############################################################# # finished fedora setup, clear history and shutdown: # unset HISTFILE && history -c && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/freebsd.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting freebsd setup ############################################################# env PAGER=/bin/cat freebsd-update fetch freebsd-update install || true env ASSUME_ALWAYS_YES=yes pkg update pkg upgrade -y sysrc -f /boot/loader.conf autoboot_delay=0 sysrc ifconfig_vtnet0="" sysrc ifconfig_vtnet0_ipv6="" pkg search cloud-init pkg install -y dual-dhclient py311-cloud-init sysrc dhclient_program="/usr/local/sbin/dual-dhclient" pw mod user root -w no pw mod user cloud -w no mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i "" '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i "" '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i "" '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i "" '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i "" '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i "" '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals tee /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF chmod 600 /etc/sudoers sysrc swapoff="YES" sysrc ifconfig_vtnet0="" sysrc cloudinit_enable="YES" tee /usr/local/etc/cloud/cloud.cfg.d/99_cloud.cfg << EOF datasource_list: [ NoCloud ] EOF cloud-init clean --machine-id tee /usr/local/etc/rc.d/cloudinitfix << EOF #!/bin/sh # PROVIDE: cloudinitfix # REQUIRE: FILESYSTEMS NETWORKING ldconfig devd # BEFORE: LOGIN cloudconfig cloudinit . /etc/rc.subr PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" name="cloudinitfix" start_cmd="cloudinitfix_start" stop_cmd=":" rcvar="cloudinit_enable" cloudinitfix_start() { rm -rf /var/lib/cloud/instances } load_rc_config \$name : \${cloudinitfix_enable="NO"} run_rc_command "\$1" EOF chmod 755 /usr/local/etc/rc.d/cloudinitfix tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF rm -f /etc/resolv.conf.bak sync rm -f /var/db/dhclient.leases.vtnet0 rm -f /var/db/dhclient6.leases rm -f setup.sh rm -rf /root/.cache rm -rf /home/cloud/.cache rm -f /etc/ssh/*_key* rm -f /root/.ssh/authorized_keys rm -f /root/.history rm -f /home/cloud/.history rm -f /root/.bash_history rm -f /home/cloud/.bash_history find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; sync ############################################################# # finished freebsd setup, clear history and shutdown: # unset history && unset HISTFILE && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/rhel10.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting rhel10 setup ############################################################# tee /etc/modprobe.d/floppy-blacklist.conf << EOF blacklist floppy EOF truncate -s 0 /etc/yum/vars/ociregion || true dnf clean all dnf -y update dnf -y install bash-completion qemu-guest-agent cloud-init cloud-utils-growpart chrony openssh-server dnf -y remove cockpit-ws sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/g' /etc/default/grub grub2-mkconfig -o /boot/grub2/grub.cfg systemctl daemon-reload systemctl enable qemu-guest-agent.service sed -i 's/^installonly_limit=.*/installonly_limit=2/g' /etc/yum.conf sed -i 's/^SELINUX=.*/SELINUX=enforcing/g' /etc/selinux/config || true sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals restorecon -v /etc/ssh/trusted restorecon -v /etc/ssh/principals useradd -m -G adm,video,wheel,systemd-journal cloud || true passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh restorecon -v /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys restorecon -v /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF systemctl enable sshd systemctl restart sshd systemctl disable firewalld systemctl stop firewalld systemctl start chronyd systemctl enable chronyd dnf clean all rm -rf /var/cache/dnf cloud-init clean --machine-id rm -rf /etc/NetworkManager/system-connections/* tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed rm -f /etc/machine-id touch /etc/machine-id sync fstrim -av sync ############################################################# # finished rhel10 setup, clear history and shutdown: # unset HISTFILE && history -c && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/rhel7.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting rhel7 setup ############################################################# yum-config-manager --disable ol7_ociyum_config || true truncate -s 0 /etc/yum/vars/ociregion || true tee /etc/modprobe.d/floppy-blacklist.conf << EOF blacklist floppy EOF yum clean all yum -y update yum -y install bash-completion qemu-guest-agent cloud-init cloud-utils-growpart chrony openssh-server yum -y remove cockpit-ws sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/g' /etc/default/grub grub2-mkconfig -o /boot/grub2/grub.cfg systemctl daemon-reload systemctl enable qemu-guest-agent.service rm -f /etc/sysconfig/network-scripts/ifcfg-eth* sed -i 's/^installonly_limit=.*/installonly_limit=2/g' /etc/yum.conf sed -i 's/^SELINUX=.*/SELINUX=enforcing/g' /etc/selinux/config || true sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals restorecon -v /etc/ssh/trusted restorecon -v /etc/ssh/principals useradd -m -G adm,video,wheel,systemd-journal cloud || true passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh restorecon -v /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys restorecon -v /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF systemctl enable sshd systemctl restart sshd systemctl disable firewalld systemctl stop firewalld systemctl start chronyd systemctl enable chronyd yum clean all rm -rf /var/cache/yum cloud-init clean tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed rm -f /etc/machine-id touch /etc/machine-id sync fstrim -av sync ############################################################# # finished rhel7 setup, clear history and shutdown: # unset HISTFILE && history -c && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/rhel8.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting rhel8 setup ############################################################# tee /etc/modprobe.d/floppy-blacklist.conf << EOF blacklist floppy EOF truncate -s 0 /etc/yum/vars/ociregion || true dnf clean all dnf -y update dnf -y install bash-completion qemu-guest-agent cloud-init cloud-utils-growpart chrony openssh-server dnf -y remove cockpit-ws sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/g' /etc/default/grub grub2-mkconfig -o /boot/grub2/grub.cfg systemctl daemon-reload systemctl enable qemu-guest-agent.service rm -f /etc/sysconfig/network-scripts/ifcfg-eth* sed -i 's/^installonly_limit=.*/installonly_limit=2/g' /etc/yum.conf sed -i 's/^SELINUX=.*/SELINUX=enforcing/g' /etc/selinux/config || true sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals restorecon -v /etc/ssh/trusted restorecon -v /etc/ssh/principals useradd -m -G adm,video,wheel,systemd-journal cloud || true passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh restorecon -v /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys restorecon -v /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF systemctl enable sshd systemctl restart sshd systemctl disable firewalld systemctl stop firewalld systemctl start chronyd systemctl enable chronyd cloud-init clean --machine-id rm -rf /etc/NetworkManager/system-connections/* tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF dnf clean all rm -rf /var/cache/dnf sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed rm -f /etc/machine-id touch /etc/machine-id sync fstrim -av sync ############################################################# # finished rhel8 setup, clear history and shutdown: # unset HISTFILE && history -c && poweroff ############################################################# ================================================ FILE: tools/virt-install/setup/rhel9.sh ================================================ #!/bin/bash set -ev if [ $(whoami) != "root" ]; then echo "Must be run as root" exit 1 fi ############################################################# # starting rhel9 setup ############################################################# tee /etc/modprobe.d/floppy-blacklist.conf << EOF blacklist floppy EOF truncate -s 0 /etc/yum/vars/ociregion || true dnf clean all dnf -y update dnf -y install bash-completion qemu-guest-agent cloud-init cloud-utils-growpart chrony openssh-server dnf -y remove cockpit-ws sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=0/g' /etc/default/grub grub2-mkconfig -o /boot/grub2/grub.cfg systemctl daemon-reload systemctl enable qemu-guest-agent.service sed -i 's/^installonly_limit=.*/installonly_limit=2/g' /etc/yum.conf sed -i 's/^SELINUX=.*/SELINUX=enforcing/g' /etc/selinux/config || true sed -i '/^PermitRootLogin/d' /etc/ssh/sshd_config sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config sed -i '/^ChallengeResponseAuthentication/d' /etc/ssh/sshd_config sed -i '/^KbdInteractiveAuthentication/d' /etc/ssh/sshd_config sed -i '/^TrustedUserCAKeys/d' /etc/ssh/sshd_config sed -i '/^AuthorizedPrincipalsFile/d' /etc/ssh/sshd_config tee -a /etc/ssh/sshd_config << EOF PermitRootLogin no PasswordAuthentication no ChallengeResponseAuthentication no KbdInteractiveAuthentication no TrustedUserCAKeys /etc/ssh/trusted AuthorizedPrincipalsFile /etc/ssh/principals EOF touch /etc/ssh/trusted touch /etc/ssh/principals restorecon -v /etc/ssh/trusted restorecon -v /etc/ssh/principals useradd -m -G adm,video,wheel,systemd-journal cloud || true passwd -d root passwd -l root passwd -d cloud passwd -l cloud mkdir -p /home/cloud/.ssh chown cloud:cloud /home/cloud/.ssh restorecon -v /home/cloud/.ssh touch /home/cloud/.ssh/authorized_keys chown cloud:cloud /home/cloud/.ssh/authorized_keys restorecon -v /home/cloud/.ssh/authorized_keys chmod 700 /home/cloud/.ssh chmod 600 /home/cloud/.ssh/authorized_keys sed -i '/^%wheel/d' /etc/sudoers tee -a /etc/sudoers << EOF %wheel ALL=(ALL) NOPASSWD:ALL EOF systemctl enable sshd systemctl restart sshd systemctl disable firewalld systemctl stop firewalld systemctl start chronyd systemctl enable chronyd dnf clean all rm -rf /var/cache/dnf cloud-init clean --machine-id rm -rf /etc/NetworkManager/system-connections/* tee /etc/resolv.conf << EOF nameserver 8.8.8.8 nameserver 8.8.4.4 EOF sync sleep 1 find /var/log -mtime -1 -type f -exec truncate -s 0 {} \; rm -rf /var/tmp/dnf-* rm -rf /home/cloud/.cache shred -u /etc/ssh/*_key /etc/ssh/*_key.pub || true shred -u /root/.ssh/authorized_keys || true shred -u /root/.bash_history || true shred -u /home/cloud/.bash_history || true shred -u /var/log/lastlog || true shred -u /var/log/secure || true shred -u /var/log/utmp || true shred -u /var/log/wtmp || true shred -u /var/log/btmp || true shred -u /var/log/dmesg || true shred -u /var/log/dmesg.old || true shred -u /var/lib/systemd/random-seed || true rm -rf /var/log/*.gz rm -rf /var/log/*.[0-9] rm -rf /var/log/*-???????? rm -rf /var/lib/cloud/instances/* rm -f /var/lib/systemd/random-seed rm -f /etc/machine-id touch /etc/machine-id sync fstrim -av sync ############################################################# # finished rhel9 setup, clear history and shutdown: # unset HISTFILE && history -c && poweroff ############################################################# ================================================ FILE: tools/webpack_run.sh ================================================ #!/bin/bash set -e npx webpack-cli --config webpack.dev.config --progress --color --watch ================================================ FILE: tpm/tpm.go ================================================ package tpm import ( "fmt" "os" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/permission" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) const systemdTemplate = `[Unit] Description=Pritunl Cloud TPM After=network.target [Service] Type=simple User=%s ExecStart=swtpm socket --tpm2 --key pwdfile=%s,mode=aes-256-cbc,remove=true,kdf=pbkdf2 --tpmstate dir=%s --ctrl type=unixio,path=%s --log level=5 TimeoutStopSec=5 PrivateTmp=true ProtectHome=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true NetworkNamespacePath=/var/run/netns/%s ` func WriteService(vmId bson.ObjectID, namespace string) (err error) { unitPath := paths.GetUnitPathTpm(vmId) tpmPath := paths.GetTpmPath(vmId) pwdPath := paths.GetTpmPwdPath(vmId) sockPath := paths.GetTpmSockPath(vmId) output := fmt.Sprintf( systemdTemplate, permission.GetUserName(vmId), pwdPath, tpmPath, sockPath, namespace, ) err = utils.CreateWrite(unitPath, output, 0644) if err != nil { return } return } func Start(db *database.Database, virt *vm.VirtualMachine) (err error) { namespace := vm.GetNamespace(virt.Id, 0) logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), }).Info("tpm: Starting virtual machine tpm") tpmsPath := paths.GetTpmsPath() tpmPath := paths.GetTpmPath(virt.Id) unit := paths.GetUnitNameTpm(virt.Id) pwdPath := paths.GetTpmPwdPath(virt.Id) err = utils.ExistsMkdir(tpmsPath, 0755) if err != nil { return } err = utils.ExistsMkdir(tpmPath, 0700) if err != nil { return } err = permission.InitTpm(virt) if err != nil { return } _ = systemd.Stop(unit) secret, err := GetSecret(db, virt.Id) if err != nil { return } if secret == "" { err = &errortypes.NotFoundError{ errors.New("tpm: Missing instance tpm secret"), } return } err = WriteService(virt.Id, namespace) if err != nil { return } err = systemd.Reload() if err != nil { return } go func() { time.Sleep(15 * time.Second) _ = os.Remove(pwdPath) }() err = utils.CreateWrite(pwdPath, secret, 0600) if err != nil { return } err = permission.InitTpmPwd(virt) if err != nil { _ = os.Remove(pwdPath) return } err = systemd.Start(unit) if err != nil { _ = os.Remove(pwdPath) return } return } func Stop(virt *vm.VirtualMachine) (err error) { unit := paths.GetUnitNameTpm(virt.Id) _ = systemd.Stop(unit) return } ================================================ FILE: tpm/utils.go ================================================ package tpm import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) type instanceData struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` TpmSecret string `bson:"tpm_secret" json:"-"` } func GenerateSecret() (secret string, err error) { secret, err = utils.RandPasswd(128) if err != nil { return } return } func GetSecret(db *database.Database, vmId bson.ObjectID) ( secret string, err error) { coll := db.Instances() data := &instanceData{} err = coll.FindOne( db, &bson.M{ "_id": vmId, }, options.FindOne(). SetProjection(bson.D{{"tpm_secret", 1}}), ).Decode(data) secret = data.TpmSecret return } ================================================ FILE: twilio/twilio.go ================================================ package twilio import ( "encoding/xml" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" "github.com/twilio/twilio-go" openapi "github.com/twilio/twilio-go/rest/api/v2010" ) type TwimlSay struct { XMLName xml.Name `xml:"Say"` Voice string `xml:"voice,attr"` Loop string `xml:"loop,attr"` Message string `xml:",chardata"` } type TwimlResponse struct { XMLName xml.Name `xml:"Response"` Say *TwimlSay `xml:"Say"` } func PhoneCall(number, message string) (err error) { client := twilio.NewRestClientWithParams(twilio.ClientParams{ Username: settings.System.TwilioAccount, Password: settings.System.TwilioSecret, }) params := &openapi.CreateCallParams{} params.SetFrom(settings.System.TwilioNumber) params.SetTo(number) twiml := &TwimlResponse{ Say: &TwimlSay{ Voice: "alice", Loop: "3", Message: FilterStrPhone(message, 160), }, } twimlData, err := xml.Marshal(twiml) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "twilio: Failed to marshal twiml message"), } return } params.SetTwiml(string(twimlData)) resp, err := client.Api.CreateCall(params) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "twilio: Twilio call error"), } return } respSid := *resp.Sid if respSid == "" { err = &errortypes.RequestError{ errors.Wrap(err, "twilio: Invalid call sid"), } return } return } func TextMessage(number, message string) (err error) { client := twilio.NewRestClientWithParams(twilio.ClientParams{ Username: settings.System.TwilioAccount, Password: settings.System.TwilioSecret, }) params := &openapi.CreateMessageParams{} params.SetFrom(settings.System.TwilioNumber) params.SetTo(number) params.SetBody("Pritunl Alert: " + FilterStrMessage(message, 800)) resp, err := client.Api.CreateMessage(params) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "twilio: Twilio message error"), } return } respSid := *resp.Sid if respSid == "" { err = &errortypes.RequestError{ errors.Wrap(err, "twilio: Invalid message sid"), } return } if resp.ErrorCode != nil && resp.ErrorMessage != nil && *resp.ErrorMessage != "" { logrus.WithFields(logrus.Fields{ "number": number, "message": message, "source_number": settings.System.TwilioNumber, "error_code": resp.ErrorCode, "error_message": resp.ErrorMessage, }).Error("twilio: Text message error") err = &errortypes.RequestError{ errors.Wrap(err, "twilio: Twilio message error"), } return } return } ================================================ FILE: twilio/utils.go ================================================ package twilio import ( "github.com/dropbox/godropbox/container/set" ) var safeCharsPhone = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '=', '_', '/', ',', '.', ':', '%', '@', '!', ' ', ) var safeCharsMessage = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '=', '_', '/', ',', '.', ':', '#', '@', '%', '!', '[', ']', '(', ')', ' ', ) func filterStr(s string, n int, safe set.Set) string { if len(s) == 0 { return "" } if len(s) > n { s = s[:n] } ns := "" for _, c := range s { if safe.Contains(c) { ns += string(c) } } return ns } func FilterStrPhone(s string, n int) string { return filterStr(s, n, safeCharsPhone) } func FilterStrMessage(s string, n int) string { return filterStr(s, n, safeCharsMessage) } ================================================ FILE: uhandlers/alert.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/alert" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type alertData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Roles []string `json:"roles"` Resource string `json:"resource"` Level int `json:"level"` Frequency int `bson:"frequency" json:"frequency"` Ignores []string `bson:"ignores" json:"ignores"` ValueInt int `json:"value_int"` ValueStr string `json:"value_str"` } type alertsData struct { Alerts []*alert.Alert `json:"alerts"` Count int64 `json:"count"` } func alertPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &alertData{} alertId, ok := utils.ParseObjectId(c.Param("alert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } alrt, err := alert.GetOrg(db, userOrg, alertId) if err != nil { utils.AbortWithError(c, 500, err) return } alrt.Name = data.Name alrt.Comment = data.Comment alrt.Roles = data.Roles alrt.Resource = data.Resource alrt.Level = data.Level alrt.Frequency = data.Frequency alrt.Ignores = data.Ignores alrt.ValueInt = data.ValueInt alrt.ValueStr = data.ValueStr fields := set.NewSet( "name", "comment", "roles", "resource", "level", "frequency", "ignores", "value_int", "value_str", ) errData, err := alrt.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = alrt.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, alrt) } func alertPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &alertData{ Name: "new-alert", Resource: alert.InstanceOffline, Level: alert.Medium, } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } alrt := &alert.Alert{ Name: data.Name, Comment: data.Comment, Organization: userOrg, Roles: data.Roles, Resource: data.Resource, Level: data.Level, Frequency: data.Frequency, Ignores: data.Ignores, ValueInt: data.ValueInt, ValueStr: data.ValueStr, } errData, err := alrt.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = alrt.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, alrt) } func alertDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) alertId, ok := utils.ParseObjectId(c.Param("alert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := alert.RemoveOrg(db, userOrg, alertId) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, nil) } func alertsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { utils.AbortWithError(c, 500, err) return } err = alert.RemoveMultiOrg(db, userOrg, dta) if err != nil { utils.AbortWithError(c, 500, err) return } _ = event.PublishDispatch(db, "alert.change") c.JSON(200, nil) } func alertGet(c *gin.Context) { if demo.IsDemo() { alrt := demo.Alerts[0] c.JSON(200, alrt) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) alertId, ok := utils.ParseObjectId(c.Query("id")) if !ok { utils.AbortWithStatus(c, 400) return } alrt, err := alert.GetOrg(db, userOrg, alertId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, alrt) } func alertsGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } alertId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = alertId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["$or"] = []*bson.M{ &bson.M{ "name": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", }, }, } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } alerts, count, err := alert.GetAllPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } dta := &alertsData{ Alerts: alerts, Count: count, } c.JSON(200, dta) } ================================================ FILE: uhandlers/auth.go ================================================ package uhandlers import ( "strings" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/auth" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/cookie" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/secondary" "github.com/pritunl/pritunl-cloud/session" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/validator" ) func authStateGet(c *gin.Context) { data := auth.GetState() if demo.IsDemo() { provider := &auth.StateProvider{ Id: "demo", Type: "demo", Label: "demo", } data.Providers = append(data.Providers, provider) } c.JSON(200, data) } type authData struct { Username string `json:"username"` Password string `json:"password"` } func authSessionPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) data := &authData{} err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } usr, errData, err := auth.Local(db, data.Username, data.Password) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.UserPrimaryApprove, audit.Fields{ "method": "local", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } devAuth, secProviderId, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "local" err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } if devAuth { deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } secType := "" var secProvider bson.ObjectID if deviceCount == 0 { if secProviderId.IsZero() { secType = secondary.UserDeviceRegister secProvider = secondary.DeviceProvider } else { secType = secondary.User secProvider = secProviderId } } else { secType = secondary.UserDevice secProvider = secondary.DeviceProvider } secd, err := secondary.New(db, usr.Id, secType, secProvider) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } else if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.User, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } err = audit.New( db, c.Request, usr.Id, audit.UserLogin, audit.Fields{ "method": "local", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewUser(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.User) if err != nil { utils.AbortWithError(c, 500, err) return } redirectQueryJson(c, c.Request.URL.RawQuery) } type secondaryData struct { Token string `json:"token"` Factor string `json:"factor"` Passcode string `json:"passcode"` } func authSecondaryPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) data := &secondaryData{} err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.User) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := secd.Handle(db, c.Request, data.Factor, data.Passcode) if err != nil { if _, ok := err.(*secondary.IncompleteError); ok { c.Status(206) } else { utils.AbortWithError(c, 500, err) } return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, audit.Fields{ "method": "secondary", "provider_id": secd.ProviderId, "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.UserSecondaryApprove, audit.Fields{ "provider_id": secd.ProviderId, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } deviceAuth, _, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "secondary" errAudit["provider_id"] = secd.ProviderId err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } if deviceAuth { deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } if deviceCount == 0 { secd, err := secondary.New(db, usr.Id, secondary.UserDeviceRegister, secondary.DeviceProvider) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } } err = audit.New( db, c.Request, usr.Id, audit.UserLogin, audit.Fields{ "method": "secondary", "provider_id": secd.ProviderId, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewUser(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.User) if err != nil { utils.AbortWithError(c, 500, err) return } redirectQueryJson(c, c.Request.URL.RawQuery) } func logoutGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if authr.IsValid() { err := authr.Remove(db) if err != nil { utils.AbortWithError(c, 500, err) return } } usr, _ := authr.GetUser(db) if usr != nil { err := audit.New( db, c.Request, usr.Id, audit.UserLogout, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } } c.Redirect(302, "/") } func logoutAllGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } sessions, err := session.GetAll(db, usr.Id, false) if err != nil { utils.AbortWithError(c, 500, err) return } for _, sess := range sessions { err = sess.Remove(db) if err != nil { utils.AbortWithError(c, 500, err) return } } if authr.IsValid() { err := authr.Remove(db) if err != nil { utils.AbortWithError(c, 500, err) return } } err = audit.New( db, c.Request, usr.Id, audit.UserLogoutAll, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.Redirect(302, "/") } func authRequestGet(c *gin.Context) { auth.Request(c, auth.User) } func authCallbackGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) sig := c.Query("sig") query := strings.Split(c.Request.URL.RawQuery, "&sig=")[0] usr, tokn, errAudit, errData, err := auth.Callback(db, sig, query) if err != nil { switch err.(type) { case *auth.InvalidState: c.Redirect(302, "/") break default: utils.AbortWithError(c, 500, err) } return } if errData != nil { if usr != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "callback" err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.UserPrimaryApprove, audit.Fields{ "method": "callback", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } devAuth, secProviderId, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "callback" err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } if devAuth { deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } secType := "" var secProvider bson.ObjectID if deviceCount == 0 { if secProviderId.IsZero() { secType = secondary.UserDeviceRegister secProvider = secondary.DeviceProvider } else { secType = secondary.User secProvider = secProviderId } } else { secType = secondary.UserDevice secProvider = secondary.DeviceProvider } secd, err := secondary.New(db, usr.Id, secType, secProvider) if err != nil { utils.AbortWithError(c, 500, err) return } urlQuery, err := secd.GetQuery() if err != nil { utils.AbortWithError(c, 500, err) return } if tokn.Query != "" { urlQuery += "&" + tokn.Query } c.Redirect(302, "/login?"+urlQuery) return } else if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.User, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } urlQuery, err := secd.GetQuery() if err != nil { utils.AbortWithError(c, 500, err) return } if tokn.Query != "" { urlQuery += "&" + tokn.Query } c.Redirect(302, "/login?"+urlQuery) return } err = audit.New( db, c.Request, usr.Id, audit.UserLogin, audit.Fields{ "method": "callback", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewUser(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.User) if err != nil { utils.AbortWithError(c, 500, err) return } redirectQuery(c, tokn.Query) } func authU2fAppGet(c *gin.Context) { c.JSON(200, device.GetFacets()) } func authWanRegisterGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) token := c.Query("token") if node.Self.WebauthnDomain == "" { errData := &errortypes.ErrorData{ Error: "webauthn_domain_unavailable", Message: "WebAuthn domain must be configured", } c.JSON(400, errData) return } secd, err := secondary.Get(db, token, secondary.UserDeviceRegister) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = audit.New( db, c.Request, usr.Id, audit.UserDeviceRegisterRequest, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } resp, errData, err := secd.DeviceRegisterRequest(db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, audit.Fields{ "method": "device_register", "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } c.JSON(200, resp) } type devicesRegisterData struct { Token string `json:"token"` Name string `json:"name"` } func authWanRegisterPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) data := &devicesRegisterData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.UserDeviceRegister) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } _, _, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "device_register" err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } devc, errData, err := secd.DeviceRegisterResponse( db, utils.GetOrigin(c.Request), body, data.Name) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.DeviceRegisterFailed, audit.Fields{ "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.UserDeviceRegister, audit.Fields{ "device_id": devc.Id, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") err = audit.New( db, c.Request, usr.Id, audit.UserLogin, audit.Fields{ "method": "device_register", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewUser(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.User) if err != nil { utils.AbortWithError(c, 500, err) return } redirectQueryJson(c, c.Request.URL.RawQuery) } func authWanRequestGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) token := c.Query("token") secd, err := secondary.Get(db, token, secondary.UserDevice) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } resp, errData, err := secd.DeviceRequest( db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, audit.Fields{ "method": "device", "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } c.JSON(200, resp) } type authWanRespondData struct { Token string `json:"token"` } func authWanRespondPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) data := &authWanRespondData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.UserDevice) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(401, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := secd.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } _, secProviderId, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "device" err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } errData, err = secd.DeviceRespond( db, utils.GetOrigin(c.Request), body) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { err = audit.New( db, c.Request, usr.Id, audit.UserLoginFailed, audit.Fields{ "method": "device", "error": errData.Error, "message": errData.Message, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(401, errData) return } err = audit.New( db, c.Request, usr.Id, audit.UserDeviceApprove, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.User, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } err = audit.New( db, c.Request, usr.Id, audit.UserLogin, audit.Fields{ "method": "device", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } cook := cookie.NewUser(c.Writer, c.Request) _, err = cook.NewSession(db, c.Request, usr.Id, true, session.User) if err != nil { utils.AbortWithError(c, 500, err) return } redirectQueryJson(c, c.Request.URL.RawQuery) } ================================================ FILE: uhandlers/authority.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authority" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type authorityData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` Roles []string `json:"roles"` Key string `json:"key"` Principals []string `json:"principals"` Certificate string `json:"certificate"` } type authoritiesData struct { Authorities []*authority.Authority `json:"authorities"` Count int64 `json:"count"` } func authorityPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &authorityData{} authorityId, ok := utils.ParseObjectId(c.Param("authority_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } authr, err := authority.GetOrg(db, userOrg, authorityId) if err != nil { utils.AbortWithError(c, 500, err) return } authr.Name = data.Name authr.Comment = data.Comment authr.Type = data.Type authr.Roles = data.Roles authr.Key = data.Key authr.Principals = data.Principals authr.Certificate = data.Certificate fields := set.NewSet( "name", "comment", "type", "organization", "roles", "key", "principals", "certificate", ) errData, err := authr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = authr.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, authr) } func authorityPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &authorityData{ Name: "new-authority", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } fire := &authority.Authority{ Name: data.Name, Comment: data.Comment, Type: data.Type, Organization: userOrg, Roles: data.Roles, Key: data.Key, Principals: data.Principals, Certificate: data.Certificate, } errData, err := fire.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = fire.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, fire) } func authorityDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) authorityId, ok := utils.ParseObjectId(c.Param("authority_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := authority.RemoveOrg(db, userOrg, authorityId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, nil) } func authoritiesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = authority.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "authority.change") c.JSON(200, nil) } func authorityGet(c *gin.Context) { if demo.IsDemo() { authr := demo.Authorities[0] c.JSON(200, authr) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) authorityId, ok := utils.ParseObjectId(c.Param("authority_id")) if !ok { utils.AbortWithStatus(c, 400) return } fire, err := authority.GetOrg(db, userOrg, authorityId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, fire) } func authoritiesGet(c *gin.Context) { if demo.IsDemo() { data := &authoritiesData{ Authorities: demo.Authorities, Count: int64(len(demo.Authorities)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } authrId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = authrId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } principal := strings.TrimSpace(c.Query("principal")) if principal != "" { query["principals"] = principal } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } authorities, count, err := authority.GetAllPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &authoritiesData{ Authorities: authorities, Count: count, } c.JSON(200, data) } ================================================ FILE: uhandlers/balancer.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/balancer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type balancerData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` State bool `json:"state"` Type string `json:"type"` Datacenter bson.ObjectID `json:"datacenter"` Certificates []bson.ObjectID `json:"certificates"` WebSockets bool `json:"websockets"` Domains []*balancer.Domain `json:"domains"` Backends []*balancer.Backend `json:"backends"` CheckPath string `json:"check_path"` } type balancersData struct { Balancers []*balancer.Balancer `json:"balancers"` Count int64 `json:"count"` } func balancerPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &balancerData{} balancerId, ok := utils.ParseObjectId(c.Param("balancer_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } balnc, err := balancer.GetOrg(db, userOrg, balancerId) if err != nil { utils.AbortWithError(c, 500, err) return } balnc.Name = data.Name balnc.Comment = data.Comment balnc.State = data.State balnc.Type = data.Type balnc.Datacenter = data.Datacenter balnc.Certificates = data.Certificates balnc.WebSockets = data.WebSockets balnc.Domains = data.Domains balnc.Backends = data.Backends balnc.CheckPath = data.CheckPath exists, err := datacenter.ExistsOrg(db, userOrg, balnc.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } fields := set.NewSet( "name", "comment", "state", "type", "datacenter", "certificates", "websockets", "domains", "backends", "check_path", ) errData, err := balnc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = balnc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") balnc.Json() c.JSON(200, balnc) } func balancerPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &balancerData{ Name: "new-balancer", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } balnc := &balancer.Balancer{ Name: data.Name, Comment: data.Comment, State: data.State, Type: data.Type, Organization: userOrg, Datacenter: data.Datacenter, Certificates: data.Certificates, WebSockets: data.WebSockets, Domains: data.Domains, Backends: data.Backends, CheckPath: data.CheckPath, } exists, err := datacenter.ExistsOrg(db, userOrg, balnc.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } errData, err := balnc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = balnc.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") balnc.Json() c.JSON(200, balnc) } func balancerDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) balancerId, ok := utils.ParseObjectId(c.Param("balancer_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := balancer.RemoveOrg(db, userOrg, balancerId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") c.JSON(200, nil) } func balancersDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = balancer.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "balancer.change") c.JSON(200, nil) } func balancerGet(c *gin.Context) { if demo.IsDemo() { balnc := demo.Balancers[0] c.JSON(200, balnc) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) balancerId, ok := utils.ParseObjectId(c.Param("balancer_id")) if !ok { utils.AbortWithStatus(c, 400) return } balnc, err := balancer.GetOrg(db, userOrg, balancerId) if err != nil { utils.AbortWithError(c, 500, err) return } balnc.Json() c.JSON(200, balnc) } func balancersGet(c *gin.Context) { if demo.IsDemo() { data := &balancersData{ Balancers: demo.Balancers, Count: int64(len(demo.Balancers)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } balancerId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = balancerId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } datacenter, ok := utils.ParseObjectId(c.Query("datacenter")) if ok { query["datacenter"] = datacenter } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } balncs, count, err := balancer.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, balnc := range balncs { balnc.Json() } data := &balancersData{ Balancers: balncs, Count: count, } c.JSON(200, data) } ================================================ FILE: uhandlers/certificate.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/acme" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/utils" ) type certificateData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` Key string `json:"key"` Certificate string `json:"certificate"` AcmeDomains []string `json:"acme_domains"` AcmeAuth string `json:"acme_auth"` AcmeSecret bson.ObjectID `json:"acme_secret"` Refresh bool `json:"refresh"` } type certificatesData struct { Certificates []*certificate.Certificate `json:"certificates"` Count int64 `json:"count"` } func certificatePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &certificateData{} certId, ok := utils.ParseObjectId(c.Param("cert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } cert, err := certificate.GetOrg(db, userOrg, certId) if err != nil { utils.AbortWithError(c, 500, err) return } if cert.Type == certificate.LetsEncrypt && cert.AcmeType != certificate.AcmeDNS || cert.AcmeType == certificate.AcmeHTTP { errData := &errortypes.ErrorData{ Error: "acme_type_blocked", Message: "Cannot modify LetsEncrypt HTTP verified certificates", } c.JSON(400, errData) return } if !data.AcmeSecret.IsZero() { exists, err := secret.ExistsOrg(db, userOrg, data.AcmeSecret) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } } else { data.AcmeSecret = bson.NilObjectID } cert.Name = data.Name cert.Comment = data.Comment cert.Key = data.Key cert.Certificate = data.Certificate cert.Type = data.Type cert.AcmeDomains = data.AcmeDomains cert.AcmeType = certificate.AcmeDNS cert.AcmeAuth = data.AcmeAuth cert.AcmeSecret = data.AcmeSecret fields := set.NewSet( "name", "comment", "type", "acme_domains", "acme_type", "acme_auth", "acme_secret", "info", ) if cert.Type != certificate.LetsEncrypt { cert.Key = data.Key fields.Add("key") cert.Certificate = data.Certificate fields.Add("certificate") } errData, err := cert.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = cert.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } if cert.Type == certificate.LetsEncrypt { acme.RenewBackground(cert, data.Refresh) } event.PublishDispatch(db, "certificate.change") c.JSON(200, cert) } func certificatePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &certificateData{ Name: "new-certificate", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } cert := &certificate.Certificate{ Name: data.Name, Comment: data.Comment, Organization: userOrg, Type: data.Type, AcmeDomains: data.AcmeDomains, AcmeType: certificate.AcmeDNS, AcmeAuth: data.AcmeAuth, AcmeSecret: data.AcmeSecret, } if cert.Type != certificate.LetsEncrypt { cert.Key = data.Key cert.Certificate = data.Certificate } if !cert.AcmeSecret.IsZero() { _, err = secret.GetOrg(db, userOrg, cert.AcmeSecret) if err != nil { utils.AbortWithError(c, 500, err) return } } else { cert.AcmeSecret = bson.NilObjectID } errData, err := cert.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = cert.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } if cert.Type == certificate.LetsEncrypt { acme.RenewBackground(cert, false) } event.PublishDispatch(db, "certificate.change") c.JSON(200, cert) } func certificateDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) certId, ok := utils.ParseObjectId(c.Param("cert_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := certificate.RemoveOrg(db, userOrg, certId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "certificate.change") c.JSON(200, nil) } func certificatesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } err = certificate.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "certificate.change") c.JSON(200, nil) } func certificateGet(c *gin.Context) { if demo.IsDemo() { cert := demo.Certificates[0] c.JSON(200, cert) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) if c.Query("names") == "true" { certs, err := certificate.GetAllNames(db, &bson.M{ "organization": userOrg, }) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, certs) return } certId, ok := utils.ParseObjectId(c.Param("cert_id")) if !ok { utils.AbortWithStatus(c, 400) return } cert, err := certificate.GetOrg(db, userOrg, certId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { cert.Key = "demo" cert.AcmeAccount = "demo" } c.JSON(200, cert) } func certificatesGet(c *gin.Context) { if demo.IsDemo() { data := &certificatesData{ Certificates: demo.Certificates, Count: int64(len(demo.Certificates)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } certificateId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = certificateId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } certs, count, err := certificate.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &certificatesData{ Certificates: certs, Count: count, } c.JSON(200, data) } ================================================ FILE: uhandlers/check.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" ) func checkGet(c *gin.Context) { c.String(200, "ok") } ================================================ FILE: uhandlers/completion.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/block" "github.com/pritunl/pritunl-cloud/certificate" "github.com/pritunl/pritunl-cloud/completion" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) func completionGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if demo.IsDemo() { data := &completion.Completion{} for _, item := range demo.Organizations { data.Organizations = append(data.Organizations, &database.Named{ Id: item.Id, Name: item.Name, }) } for _, item := range demo.Authorities { data.Authorities = append(data.Authorities, &database.Named{ Id: item.Id, Name: item.Name, }) } for _, item := range demo.Policies { data.Policies = append(data.Policies, &database.Named{ Id: item.Id, Name: item.Name, }) } for _, item := range demo.Domains { data.Domains = append(data.Domains, &domain.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, }) } for _, item := range demo.Vpcs { data.Vpcs = append(data.Vpcs, &vpc.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, VpcId: item.VpcId, Network: item.Network, Subnets: item.Subnets, Datacenter: item.Datacenter, }) } for _, item := range demo.Datacenters { data.Datacenters = append(data.Datacenters, &datacenter.Completion{ Id: item.Id, Name: item.Name, NetworkMode: item.NetworkMode, }) } for _, item := range demo.Blocks { data.Blocks = append(data.Blocks, &block.Completion{ Id: item.Id, Name: item.Name, Type: item.Type, }) } for _, item := range demo.Nodes { data.Nodes = append(data.Nodes, &node.Completion{ Id: item.Id, Name: item.Name, Zone: item.Zone, Types: item.Types, }) } for _, item := range demo.Pools { data.Pools = append(data.Pools, &pool.Completion{ Id: item.Id, Name: item.Name, Zone: item.Zone, }) } for _, item := range demo.Zones { data.Zones = append(data.Zones, &zone.Completion{ Id: item.Id, Datacenter: item.Datacenter, Name: item.Name, }) } for _, item := range demo.Shapes { data.Shapes = append(data.Shapes, &shape.Completion{ Id: item.Id, Name: item.Name, Datacenter: item.Datacenter, Flexible: item.Flexible, Memory: item.Memory, Processors: item.Processors, }) } imgs, err := image.GetAllCompletion(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } data.Images = imgs for _, item := range demo.Storages { data.Storages = append(data.Storages, &storage.Completion{ Id: item.Id, Name: item.Name, Type: item.Type, }) } for _, item := range demo.Instances { data.Instances = append(data.Instances, &instance.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, Zone: item.Zone, Vpc: item.Vpc, Subnet: item.Subnet, Node: item.Node, }) } for _, item := range demo.Plans { data.Plans = append(data.Plans, &plan.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, }) } for _, item := range demo.Certificates { data.Certificates = append( data.Certificates, &certificate.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, Type: item.Type, }, ) } for _, item := range demo.Secrets { data.Secrets = append(data.Secrets, &secret.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, Type: item.Type, }) } for _, item := range demo.Pods { data.Pods = append(data.Pods, &pod.Completion{ Id: item.Id, Name: item.Name, Organization: item.Organization, }) } for _, item := range demo.Units { data.Units = append(data.Units, &unit.Completion{ Id: item.Id, Pod: item.Pod, Organization: item.Organization, Name: item.Name, Kind: item.Kind, }) } c.JSON(200, data) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } var userOrg bson.ObjectID orgIdStr := c.GetHeader("Organization") if orgIdStr != "" { orgId, ok := utils.ParseObjectId(orgIdStr) if !ok { utils.AbortWithStatus(c, 400) return } org, err := organization.Get(db, orgId) if err != nil { utils.AbortWithError(c, 500, err) return } match := usr.RolesMatch(org.Roles) if !match { utils.AbortWithStatus(c, 401) return } userOrg = org.Id } else { orgs, err := organization.GetAll(db, &bson.M{ "roles": &bson.M{ "$in": usr.Roles, }, }) if err != nil { utils.AbortWithError(c, 500, err) return } if len(orgs) > 0 { org := orgs[0] match := usr.RolesMatch(org.Roles) if !match { utils.AbortWithStatus(c, 401) return } userOrg = org.Id } } if userOrg.IsZero() { utils.AbortWithStatus(c, 400) return } cmpl, err := completion.GetCompletion(db, userOrg, usr.Roles) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, cmpl) } ================================================ FILE: uhandlers/csrf.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/csrf" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/utils" ) type csrfData struct { Token string `json:"token"` Theme string `json:"theme"` EditorTheme string `json:"editor_theme"` OracleLicense bool `json:"oracle_license"` } func csrfGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } token, err := csrf.NewToken(db, authr.SessionId()) if err != nil { utils.AbortWithError(c, 500, err) return } oracleLicense := usr.OracleLicense if demo.IsDemo() { oracleLicense = true } data := &csrfData{ Token: token, Theme: usr.Theme, EditorTheme: usr.EditorTheme, OracleLicense: oracleLicense, } c.JSON(200, data) } ================================================ FILE: uhandlers/datacenter.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/utils" ) func datacentersGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dcs, err := datacenter.GetAllNamesOrg(db, userOrg) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, dcs) } ================================================ FILE: uhandlers/devices.go ================================================ package uhandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/secondary" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/validator" ) type deviceData struct { Name string `json:"name"` } func devicePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &deviceData{} devcId, ok := utils.ParseObjectId(c.Param("device_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } devc, err := device.GetUser(db, devcId, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } devc.Name = data.Name fields := set.NewSet( "name", ) errData, err := devc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = devc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, devc) } func deviceDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } devcId, ok := utils.ParseObjectId(c.Param("device_id")) if !ok { utils.AbortWithStatus(c, 400) return } count, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } if count <= 1 { usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { utils.AbortWithError(c, 500, err) return } } err = device.RemoveUser(db, devcId, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } count, err = device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } if count == 0 { if !usr.Disabled { usr.Disabled = true err = usr.CommitFields(db, set.NewSet("disabled")) if err != nil { utils.AbortWithError(c, 500, err) return } } err = audit.New( db, c.Request, usr.Id, audit.UserAccountDisable, audit.Fields{ "reason": "All authentication devices removed", }, ) if err != nil { utils.AbortWithError(c, 500, err) return } errData := &errortypes.ErrorData{ Error: "device_empty", Message: "Account disabled contact an administrator", } c.JSON(401, errData) return } event.PublishDispatch(db, "device.change") c.JSON(200, nil) } func devicesGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } devices, err := device.GetAllSorted(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, devices) } type devicesWanRegisterRespData struct { Token string `json:"token"` Options interface{} `json:"options"` } func deviceWanRegisterGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) if node.Self.WebauthnDomain == "" { errData := &errortypes.ErrorData{ Error: "webauthn_domain_unavailable", Message: "WebAuthn domain must be configured", } c.JSON(400, errData) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } _, secProviderId, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "add_device_register" err = audit.New( db, c.Request, usr.Id, audit.UserAuthFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(400, errData) return } deviceCount, err := device.CountSecondary(db, usr.Id) if err != nil { utils.AbortWithError(c, 500, err) return } if deviceCount > 0 || !secProviderId.IsZero() { secType := "" var secProvider bson.ObjectID if deviceCount == 0 { secType = secondary.UserManage secProvider = secProviderId } else { secType = secondary.UserManageDevice secProvider = secondary.DeviceProvider } secd, err := secondary.New(db, usr.Id, secType, secProvider) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } secd, err := secondary.New(db, usr.Id, secondary.UserManageDeviceRegister, secondary.DeviceProvider) if err != nil { utils.AbortWithError(c, 500, err) return } err = audit.New( db, c.Request, usr.Id, audit.UserDeviceRegisterRequest, audit.Fields{}, ) if err != nil { utils.AbortWithError(c, 500, err) return } jsonResp, errData, err := secd.DeviceRegisterRequest(db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } resp := &devicesWanRegisterRespData{ Token: secd.Id, Options: jsonResp, } c.JSON(200, resp) } type devicesWanRegisterData struct { Token string `json:"token"` Name string `json:"name"` } func deviceWanRegisterPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &devicesWanRegisterData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.UserManageDeviceRegister) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } devc, errData, err := secd.DeviceRegisterResponse( db, utils.GetOrigin(c.Request), body, data.Name) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = audit.New( db, c.Request, usr.Id, audit.DeviceRegister, audit.Fields{ "device_id": devc.Id, }, ) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "device.change") c.JSON(200, nil) } type deviceSecondaryData struct { Type string `json:"type"` Token string `json:"token"` Factor string `json:"factor"` Passcode string `json:"passcode"` } func deviceSecondaryPut(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &deviceSecondaryData{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.UserManage) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } errData, err := secd.Handle(db, c.Request, data.Factor, data.Passcode) if err != nil { if _, ok := err.(*secondary.IncompleteError); ok { c.Status(206) } else { utils.AbortWithError(c, 500, err) } return } if errData != nil { c.JSON(400, errData) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } secd, err = secondary.New(db, usr.Id, secondary.UserManageDeviceRegister, secondary.DeviceProvider) if err != nil { utils.AbortWithError(c, 500, err) return } jsonResp, errData, err := secd.DeviceRegisterRequest(db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } resp := &devicesWanRegisterRespData{ Token: secd.Id, Options: jsonResp, } c.JSON(200, resp) } func deviceWanRequestGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) token := c.Query("token") secd, err := secondary.Get(db, token, secondary.UserManageDevice) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } resp, errData, err := secd.DeviceRequest( db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } c.JSON(200, resp) } type deviceWanRespondData struct { Token string `json:"token"` } func deviceWanRespondPost(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &deviceWanRespondData{} body, err := utils.CopyBody(c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } err = c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } secd, err := secondary.Get(db, data.Token, secondary.UserManageDevice) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "secondary_expired", Message: "Secondary authentication has expired", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } _, secProviderId, errAudit, errData, err := validator.ValidateUser( db, usr, false, c.Request) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } errData, err = secd.DeviceRespond( db, utils.GetOrigin(c.Request), body) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { if errAudit == nil { errAudit = audit.Fields{ "error": errData.Error, "message": errData.Message, } } errAudit["method"] = "add_device_register" err = audit.New( db, c.Request, usr.Id, audit.UserAuthFailed, errAudit, ) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(400, errData) return } if !secProviderId.IsZero() { secd, err := secondary.New(db, usr.Id, secondary.UserManage, secProviderId) if err != nil { utils.AbortWithError(c, 500, err) return } data, err := secd.GetData() if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(201, data) return } secd, err = secondary.New(db, usr.Id, secondary.UserManageDeviceRegister, secondary.DeviceProvider) if err != nil { utils.AbortWithError(c, 500, err) return } jsonResp, errData, err := secd.DeviceRegisterRequest(db, utils.GetOrigin(c.Request)) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } resp := &devicesWanRegisterRespData{ Token: secd.Id, Options: jsonResp, } c.JSON(200, resp) } ================================================ FILE: uhandlers/disk.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/utils" ) type diskData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Instance bson.ObjectID `json:"instance"` Index string `json:"index"` Type string `json:"type"` Node bson.ObjectID `json:"node"` Pool bson.ObjectID `json:"pool"` DeleteProtection bool `json:"delete_protection"` FileSystem string `json:"file_system"` Image bson.ObjectID `json:"image"` RestoreImage bson.ObjectID `json:"restore_image"` Backing bool `json:"backing"` Action string `json:"action"` Size int `json:"size"` LvSize int `json:"lv_size"` NewSize int `json:"new_size"` Backup bool `json:"backup"` } type disksMultiData struct { Ids []bson.ObjectID `json:"ids"` Action string `json:"action"` } type disksData struct { Disks []*aggregate.DiskAggregate `json:"disks"` Count int64 `json:"count"` } func diskPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := &diskData{} diskId, ok := utils.ParseObjectId(c.Param("disk_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } dsk, err := disk.GetOrg(db, userOrg, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } fields := set.NewSet( "name", "comment", "type", "instance", "delete_protection", "index", "backup", "new_size", ) if !dta.Instance.IsZero() { exists, err := instance.ExistsOrg(db, userOrg, dta.Instance) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } } dsk.PreCommit() if dta.Action != "" && dsk.Action != "" { errData := &errortypes.ErrorData{ Error: "disk_actin_active", Message: "Disk action already active", } c.JSON(400, errData) return } if dsk.IsActive() && dta.Action == disk.Snapshot { dsk.Action = disk.Snapshot fields.Add("action") } else if dsk.IsActive() && dta.Action == disk.Backup { dsk.Action = disk.Backup fields.Add("action") } else if dsk.IsActive() && dta.Action == disk.Expand { dsk.Action = disk.Expand dsk.NewSize = dta.NewSize fields.Add("action") } else if dsk.IsActive() && dta.Action == disk.Restore { img, err := image.GetOrg(db, userOrg, dta.RestoreImage) if err != nil { utils.AbortWithError(c, 500, err) return } if img.Disk != dsk.Id { errData := &errortypes.ErrorData{ Error: "invalid_restore_image", Message: "Invalid restore image", } c.JSON(400, errData) return } dsk.Action = disk.Restore dsk.RestoreImage = img.Id fields.Add("action") fields.Add("restore_image") } errData, err := dsk.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = dsk.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, dsk) } func diskPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := &diskData{ Name: "new-disk", } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } if !dta.Instance.IsZero() { exists, err := instance.ExistsOrg(db, userOrg, dta.Instance) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } } nde, err := node.Get(db, dta.Node) if err != nil { utils.AbortWithError(c, 500, err) return } exists, err := datacenter.ExistsOrg(db, userOrg, nde.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } imgSystemType := "" imgSystemKind := "" if !dta.Image.IsZero() { img, err := image.GetOrgPublic(db, userOrg, dta.Image) if err != nil { utils.AbortWithError(c, 500, err) return } imgSystemType = img.GetSystemType() imgSystemKind = img.GetSystemKind() store, err := storage.Get(db, img.Storage) if err != nil { utils.AbortWithError(c, 500, err) return } available, err := data.ImageAvailable(store, img) if err != nil { utils.AbortWithError(c, 500, err) return } if !available { if store.IsOracle() { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from archive", } c.JSON(400, errData) } else { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from glacier", } c.JSON(400, errData) } return } } dsk := &disk.Disk{ Name: dta.Name, Comment: dta.Comment, Organization: userOrg, Instance: dta.Instance, Datacenter: nde.Datacenter, Zone: nde.Zone, Index: dta.Index, Type: dta.Type, SystemType: imgSystemType, SystemKind: imgSystemKind, Node: dta.Node, Pool: dta.Pool, Image: dta.Image, DeleteProtection: dta.DeleteProtection, FileSystem: dta.FileSystem, Backing: dta.Backing, Size: dta.Size, LvSize: dta.LvSize, Backup: dta.Backup, } errData, err := dsk.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = dsk.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, dsk) } func disksPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &disksMultiData{} err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } if data.Action != disk.Snapshot && data.Action != disk.Backup { errData := &errortypes.ErrorData{ Error: "invalid_action", Message: "Invalid disk action", } c.JSON(400, errData) return } doc := bson.M{ "action": data.Action, } err = disk.UpdateMultiOrg(db, userOrg, data.Ids, &doc) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, nil) } func diskDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) diskId, ok := utils.ParseObjectId(c.Param("disk_id")) if !ok { utils.AbortWithStatus(c, 400) return } dsk, err := disk.Get(db, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } if dsk.DeleteProtection { errData := &errortypes.ErrorData{ Error: "delete_protection", Message: "Cannot delete disk with delete protection", } c.JSON(400, errData) return } if !dsk.Instance.IsZero() { inst, e := instance.Get(db, dsk.Instance) if e != nil { err = e return } if inst.DeleteProtection { errData := &errortypes.ErrorData{ Error: "instance_delete_protection", Message: "Cannot delete disk attached to " + "instance with delete protection", } c.JSON(400, errData) return } } err = disk.DeleteOrg(db, userOrg, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, nil) } func disksDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } err = disk.DeleteMultiOrg(db, userOrg, dta) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "disk.change") c.JSON(200, nil) } func diskGet(c *gin.Context) { if demo.IsDemo() { dsk := demo.Disks[0] c.JSON(200, dsk) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) diskId, ok := utils.ParseObjectId(c.Param("disk_id")) if !ok { utils.AbortWithStatus(c, 400) return } dsk, err := disk.GetOrg(db, userOrg, diskId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, dsk) } func disksGet(c *gin.Context) { if demo.IsDemo() { data := &disksData{ Disks: demo.Disks, Count: int64(len(demo.Disks)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } diskId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = diskId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } inst, ok := utils.ParseObjectId(c.Query("instance")) if ok { query["instance"] = inst } nodeId, ok := utils.ParseObjectId(c.Query("node")) if ok { query["node"] = nodeId } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } disks, count, err := aggregate.GetDiskPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } dta := &disksData{ Disks: disks, Count: count, } c.JSON(200, dta) } ================================================ FILE: uhandlers/domain.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/domain" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" ) type domainData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` Secret bson.ObjectID `json:"secret"` RootDomain string `json:"root_domain"` Records []*domain.Record `json:"records"` } type domainsData struct { Domains []*domain.Domain `json:"domains"` Count int64 `json:"count"` } func domainPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &domainData{} domainId, ok := utils.ParseObjectId(c.Param("domain_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } domn, err := domain.GetOrg(db, userOrg, domainId) if err != nil { utils.AbortWithError(c, 500, err) return } err = domn.LoadRecords(db, true) if err != nil { return } domn.PreCommit() domn.Name = data.Name domn.Comment = data.Comment domn.Type = data.Type domn.Secret = data.Secret domn.RootDomain = data.RootDomain domn.Records = data.Records fields := set.NewSet( "name", "comment", "type", "secret", "root_domain", ) errData, err := domn.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = domn.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } err = domn.CommitRecords(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, domn) } func domainPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &domainData{ Name: "new.domain", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } domn := &domain.Domain{ Name: data.Name, Comment: data.Comment, Organization: userOrg, Type: data.Type, Secret: data.Secret, RootDomain: data.RootDomain, } errData, err := domn.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = domn.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, domn) } func domainDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) domainId, ok := utils.ParseObjectId(c.Param("domain_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := domain.RemoveOrg(db, userOrg, domainId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, nil) } func domainsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = domain.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "domain.change") c.JSON(200, nil) } func domainGet(c *gin.Context) { if demo.IsDemo() { domn := demo.Domains[0] c.JSON(200, domn) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) domainId, ok := utils.ParseObjectId(c.Param("domain_id")) if !ok { utils.AbortWithStatus(c, 400) return } domn, err := domain.GetOrg(db, userOrg, domainId) if err != nil { utils.AbortWithError(c, 500, err) return } err = domn.LoadRecords(db, true) if err != nil { return } domn.Json() c.JSON(200, domn) } func domainsGet(c *gin.Context) { if demo.IsDemo() { data := &domainsData{ Domains: demo.Domains, Count: int64(len(demo.Domains)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) if c.Query("names") == "true" { domns, err := domain.GetAllName(db, &bson.M{ "organization": userOrg, }) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, domns) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } domainId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = domainId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } domains, count, err := aggregate.GetDomainPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &domainsData{ Domains: domains, Count: count, } c.JSON(200, data) } } ================================================ FILE: uhandlers/event.go ================================================ package uhandlers import ( "context" "fmt" "time" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" ) const ( writeTimeout = 10 * time.Second pingInterval = 30 * time.Second pingWait = 40 * time.Second ) func eventGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) socket := &event.WebSocket{} defer func() { socket.Close() event.WebSocketsLock.Lock() event.WebSockets.Remove(socket) event.WebSocketsLock.Unlock() }() event.WebSocketsLock.Lock() event.WebSockets.Add(socket) event.WebSocketsLock.Unlock() ctx, cancel := context.WithCancel(db) socket.Cancel = cancel conn, err := event.Upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to upgrade request"), } utils.AbortWithError(c, 500, err) return } socket.Conn = conn err = conn.SetReadDeadline(time.Now().Add(pingWait)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to set read deadline"), } utils.AbortWithError(c, 500, err) return } conn.SetPongHandler(func(x string) (err error) { err = conn.SetReadDeadline(time.Now().Add(pingWait)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to set read deadline"), } utils.AbortWithError(c, 500, err) return } return }) lst, err := event.SubscribeListener(db, []string{"dispatch"}) if err != nil { utils.AbortWithError(c, 500, err) return } socket.Listener = lst ticker := time.NewTicker(pingInterval) socket.Ticker = ticker sub := lst.Listen() defer lst.Close() go func() { defer func() { r := recover() if r != nil && !socket.Closed { logrus.WithFields(logrus.Fields{ "error": errors.New(fmt.Sprintf("%s", r)), }).Error("mhandlers: Event panic") } }() for { _, _, err := conn.NextReader() if err != nil { conn.Close() return } } }() for { select { case <-ctx.Done(): return case msg, ok := <-sub: if !ok { err = conn.WriteControl(websocket.CloseMessage, []byte{}, time.Now().Add(writeTimeout)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to set write control"), } return } return } err = conn.SetWriteDeadline(time.Now().Add(writeTimeout)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to set write deadline"), } return } err = conn.WriteJSON(msg) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to set write json"), } return } case <-ticker.C: err = conn.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeTimeout)) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "uhandlers: Failed to set write control"), } return } } } } ================================================ FILE: uhandlers/firewall.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/firewall" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type firewallData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Roles []string `json:"roles"` Ingress []*firewall.Rule `json:"ingress"` } type firewallsData struct { Firewalls []*firewall.Firewall `json:"firewalls"` Count int64 `json:"count"` } func firewallPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &firewallData{} firewallId, ok := utils.ParseObjectId(c.Param("firewall_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } fire, err := firewall.GetOrg(db, userOrg, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } fire.Name = data.Name fire.Comment = data.Comment fire.Roles = data.Roles fire.Ingress = data.Ingress fields := set.NewSet( "name", "comment", "roles", "ingress", ) errData, err := fire.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = fire.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, fire) } func firewallPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &firewallData{ Name: "new-firewall", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } fire := &firewall.Firewall{ Name: data.Name, Comment: data.Comment, Organization: userOrg, Roles: data.Roles, Ingress: data.Ingress, } errData, err := fire.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = fire.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, fire) } func firewallDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) firewallId, ok := utils.ParseObjectId(c.Param("firewall_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDeleteOrg(db, "firewall", userOrg, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = firewall.RemoveOrg(db, userOrg, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, nil) } func firewallsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteOrgAll(db, "firewall", userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = firewall.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "firewall.change") c.JSON(200, nil) } func firewallGet(c *gin.Context) { if demo.IsDemo() { fire := demo.Firewalls[0] c.JSON(200, fire) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) firewallId, ok := utils.ParseObjectId(c.Param("firewall_id")) if !ok { utils.AbortWithStatus(c, 400) return } fire, err := firewall.GetOrg(db, userOrg, firewallId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, fire) } func firewallsGet(c *gin.Context) { if demo.IsDemo() { data := &firewallsData{ Firewalls: demo.Firewalls, Count: int64(len(demo.Firewalls)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } firewallId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = firewallId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } firewalls, count, err := firewall.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &firewallsData{ Firewalls: firewalls, Count: count, } c.JSON(200, data) } ================================================ FILE: uhandlers/handlers.go ================================================ package uhandlers import ( "net/http" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/middlewear" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/static" ) var ( store *static.Store fileServer http.Handler ) func Register(engine *gin.Engine) { engine.Use(middlewear.Limiter) engine.Use(middlewear.Counter) engine.Use(middlewear.Recovery) engine.Use(middlewear.Headers) dbGroup := engine.Group("") dbGroup.Use(middlewear.Database) sessGroup := dbGroup.Group("") sessGroup.Use(middlewear.SessionUser) authGroup := sessGroup.Group("") authGroup.Use(middlewear.AuthUser) csrfGroup := authGroup.Group("") csrfGroup.Use(middlewear.CsrfToken) orgGroup := csrfGroup.Group("") orgGroup.Use(middlewear.UserOrg) engine.NoRoute(middlewear.NotFound) orgGroup.GET("/alert", alertsGet) orgGroup.GET("/alert/:alert_id", alertGet) orgGroup.PUT("/alert/:alert_id", alertPut) orgGroup.POST("/alert", alertPost) orgGroup.DELETE("/alert", alertsDelete) orgGroup.DELETE("/alert/:alert_id", alertDelete) engine.GET("/auth/state", authStateGet) dbGroup.POST("/auth/session", authSessionPost) dbGroup.POST("/auth/secondary", authSecondaryPost) dbGroup.GET("/auth/request", authRequestGet) dbGroup.GET("/auth/callback", authCallbackGet) engine.GET("/auth/u2f/app.json", authU2fAppGet) dbGroup.GET("/auth/webauthn/request", authWanRequestGet) dbGroup.POST("/auth/webauthn/respond", authWanRespondPost) dbGroup.GET("/auth/webauthn/register", authWanRegisterGet) dbGroup.POST("/auth/webauthn/register", authWanRegisterPost) sessGroup.GET("/logout", logoutGet) sessGroup.GET("/logout_all", logoutAllGet) orgGroup.GET("/authority", authoritiesGet) orgGroup.GET("/authority/:authority_id", authorityGet) orgGroup.PUT("/authority/:authority_id", authorityPut) orgGroup.POST("/authority", authorityPost) orgGroup.DELETE("/authority", authoritiesDelete) orgGroup.DELETE("/authority/:authority_id", authorityDelete) orgGroup.GET("/balancer", balancersGet) orgGroup.GET("/balancer/:balancer_id", balancerGet) orgGroup.PUT("/balancer/:balancer_id", balancerPut) orgGroup.POST("/balancer", balancerPost) orgGroup.DELETE("/balancer", balancersDelete) orgGroup.DELETE("/balancer/:balancer_id", balancerDelete) orgGroup.GET("/certificate", certificatesGet) orgGroup.GET("/certificate/:cert_id", certificateGet) orgGroup.PUT("/certificate/:cert_id", certificatePut) orgGroup.POST("/certificate", certificatePost) orgGroup.DELETE("/certificate/", certificatesDelete) orgGroup.DELETE("/certificate/:cert_id", certificateDelete) engine.GET("/check", checkGet) authGroup.GET("/csrf", csrfGet) csrfGroup.GET("/completion", completionGet) orgGroup.GET("/datacenter", datacentersGet) csrfGroup.GET("/device", devicesGet) csrfGroup.PUT("/device/:device_id", devicePut) csrfGroup.DELETE("/device/:device_id", deviceDelete) csrfGroup.PUT("/device/:device_id/secondary", deviceSecondaryPut) csrfGroup.GET("/device/:device_id/request", deviceWanRequestGet) csrfGroup.POST("/device/:device_id/respond", deviceWanRespondPost) csrfGroup.GET("/device/:device_id/register", deviceWanRegisterGet) csrfGroup.POST("/device/:device_id/register", deviceWanRegisterPost) orgGroup.GET("/domain", domainsGet) orgGroup.GET("/domain/:domain_id", domainGet) orgGroup.PUT("/domain/:domain_id", domainPut) orgGroup.POST("/domain", domainPost) orgGroup.DELETE("/domain", domainsDelete) orgGroup.DELETE("/domain/:domain_id", domainDelete) orgGroup.GET("/disk", disksGet) orgGroup.GET("/disk/:disk_id", diskGet) orgGroup.PUT("/disk", disksPut) orgGroup.PUT("/disk/:disk_id", diskPut) orgGroup.POST("/disk", diskPost) orgGroup.DELETE("/disk", disksDelete) orgGroup.DELETE("/disk/:disk_id", diskDelete) csrfGroup.GET("/event", eventGet) orgGroup.GET("/firewall", firewallsGet) orgGroup.GET("/firewall/:firewall_id", firewallGet) orgGroup.PUT("/firewall/:firewall_id", firewallPut) orgGroup.POST("/firewall", firewallPost) orgGroup.DELETE("/firewall", firewallsDelete) orgGroup.DELETE("/firewall/:firewall_id", firewallDelete) orgGroup.GET("/image", imagesGet) orgGroup.GET("/image/:image_id", imageGet) orgGroup.PUT("/image/:image_id", imagePut) orgGroup.DELETE("/image", imagesDelete) orgGroup.DELETE("/image/:image_id", imageDelete) orgGroup.GET("/instance", instancesGet) orgGroup.PUT("/instance", instancesPut) orgGroup.GET("/instance/:instance_id", instanceGet) orgGroup.GET("/instance/:instance_id/vnc", instanceVncGet) orgGroup.PUT("/instance/:instance_id", instancePut) orgGroup.POST("/instance", instancePost) orgGroup.DELETE("/instance", instancesDelete) orgGroup.DELETE("/instance/:instance_id", instanceDelete) csrfGroup.PUT("/license", licensePut) orgGroup.GET("/node", nodesGet) orgGroup.GET("/plan", plansGet) orgGroup.GET("/plan/:plan_id", planGet) orgGroup.PUT("/plan/:plan_id", planPut) orgGroup.POST("/plan", planPost) orgGroup.DELETE("/plan", plansDelete) orgGroup.DELETE("/plan/:plan_id", planDelete) csrfGroup.GET("/pool", poolsGet) orgGroup.GET("/relations/:kind/:id", relationsGet) orgGroup.GET("/secret", secretsGet) orgGroup.GET("/secret/:secr_id", secretGet) orgGroup.PUT("/secret/:secr_id", secretPut) orgGroup.POST("/secret", secretPost) orgGroup.DELETE("/secret", secretsDelete) orgGroup.DELETE("/secret/:secr_id", secretDelete) orgGroup.GET("/pod", podsGet) orgGroup.GET("/pod/:pod_id", podGet) orgGroup.PUT("/pod/:pod_id", podPut) orgGroup.PUT("/pod/:pod_id/drafts", podDraftsPut) orgGroup.PUT("/pod/:pod_id/deploy", podDeployPut) orgGroup.POST("/pod", podPost) orgGroup.DELETE("/pod", podsDelete) orgGroup.DELETE("/pod/:pod_id", podDelete) orgGroup.GET("/pod/:pod_id/unit/:unit_id", podUnitGet) orgGroup.PUT("/pod/:pod_id/unit/:unit_id/deployment", podUnitDeploymentsPut) orgGroup.PUT("/pod/:pod_id/unit/:unit_id/deployment/:deployment_id", podUnitDeploymentPut) orgGroup.POST("/pod/:pod_id/unit/:unit_id/deployment", podUnitDeploymentPost) orgGroup.GET( "/pod/:pod_id/unit/:unit_id/deployment/:deployment_id/log", podUnitDeploymentLogGet, ) orgGroup.GET("/pod/:pod_id/unit/:unit_id/spec", podUnitSpecsGet) orgGroup.GET("/pod/:pod_id/unit/:unit_id/spec/:spec_id", podUnitSpecGet) csrfGroup.GET("/shape", shapesGet) csrfGroup.GET("/organization", organizationsGet) csrfGroup.PUT("/theme", themePut) orgGroup.GET("/vpc", vpcsGet) orgGroup.GET("/vpc/:vpc_id", vpcGet) orgGroup.PUT("/vpc/:vpc_id", vpcPut) orgGroup.GET("/vpc/:vpc_id/routes", vpcRoutesGet) orgGroup.PUT("/vpc/:vpc_id/routes", vpcRoutesPut) orgGroup.POST("/vpc", vpcPost) orgGroup.DELETE("/vpc", vpcsDelete) orgGroup.DELETE("/vpc/:vpc_id", vpcDelete) orgGroup.GET("/zone", zonesGet) engine.GET("/robots.txt", middlewear.RobotsGet) if constants.Production { sessGroup.GET("/", staticIndexGet) engine.GET("/login", staticLoginGet) engine.GET("/logo.png", staticLogoGet) authGroup.GET("/static/*path", staticGet) } else { fs := gin.Dir(config.StaticTestingRoot, false) fileServer = http.FileServer(fs) sessGroup.GET("/", staticTestingGet) engine.GET("/login", staticTestingGet) engine.GET("/logo.png", staticTestingGet) authGroup.GET("/static/*path", staticTestingGet) } } func init() { module := requires.New("uhandlers") module.After("settings") module.Handler = func() (err error) { if constants.Production { store, err = static.NewStore(config.StaticRoot) if err != nil { return } } return } } ================================================ FILE: uhandlers/image.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/utils" ) type imageData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` } type imagesData struct { Images []*image.Image `json:"images"` Count int64 `json:"count"` } func imagePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := &imageData{} imageId, ok := utils.ParseObjectId(c.Param("image_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } img, err := image.GetOrg(db, userOrg, imageId) if err != nil { utils.AbortWithError(c, 500, err) return } img.Name = dta.Name img.Comment = dta.Comment fields := set.NewSet( "name", "comment", ) errData, err := img.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = img.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "image.change") c.JSON(200, img) } func imageDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) imageId, ok := utils.ParseObjectId(c.Param("image_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := data.DeleteImageOrg(db, userOrg, imageId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "image.change") event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func imagesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { utils.AbortWithError(c, 500, err) return } err = data.DeleteImagesOrg(db, userOrg, dta) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "image.change") event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func imageGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) imageId, ok := utils.ParseObjectId(c.Param("image_id")) if !ok { utils.AbortWithStatus(c, 400) return } img, err := image.GetOrgPublic(db, userOrg, imageId) if err != nil { utils.AbortWithError(c, 500, err) return } img.Json() c.JSON(200, img) } func imagesGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dcId, _ := utils.ParseObjectId(c.Query("datacenter")) if !dcId.IsZero() { dc, err := datacenter.Get(db, dcId) if err != nil { return } storages := dc.PublicStorages if storages == nil { storages = []bson.ObjectID{} } if len(storages) == 0 { c.JSON(200, []bson.ObjectID{}) return } query := &bson.M{ "organization": image.Global, "storage": &bson.M{ "$in": dc.PublicStorages, }, } if demo.IsDemo() { query = &bson.M{ "organization": image.Global, } } images, err := image.GetAllNames(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } for _, img := range images { img.Json() } if !dc.PrivateStorage.IsZero() { query = &bson.M{ "organization": userOrg, "storage": dc.PrivateStorage, } images2, err := image.GetAllNames(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } for _, img := range images2 { img.Json() images = append(images, img) } } c.JSON(200, images) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": &bson.M{ "$in": []bson.ObjectID{ image.Global, userOrg, }, }, } imageId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = imageId } name := strings.TrimSpace(c.Query("name")) if name != "" { query = bson.M{ "$and": []*bson.M{ &bson.M{ "organization": &bson.M{ "$in": []bson.ObjectID{ image.Global, userOrg, }, }, }, &bson.M{ "$or": []*bson.M{ &bson.M{ "name": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", }, }, &bson.M{ "key": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", }, }, }, }, }, } } typ := strings.TrimSpace(c.Query("type")) if typ != "" { query["type"] = typ } images, count, err := image.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, img := range images { img.Json() } dta := &imagesData{ Images: images, Count: count, } c.JSON(200, dta) } } ================================================ FILE: uhandlers/instance.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/data" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/drive" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/image" "github.com/pritunl/pritunl-cloud/instance" "github.com/pritunl/pritunl-cloud/iscsi" "github.com/pritunl/pritunl-cloud/iso" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/nodeport" "github.com/pritunl/pritunl-cloud/pci" "github.com/pritunl/pritunl-cloud/storage" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/pritunl/pritunl-cloud/vpc" "github.com/pritunl/pritunl-cloud/zone" ) type instanceData struct { Id bson.ObjectID `json:"id"` Zone bson.ObjectID `json:"zone"` Vpc bson.ObjectID `json:"vpc"` Subnet bson.ObjectID `json:"subnet"` CloudSubnet string `json:"cloud_subnet"` Shape bson.ObjectID `json:"shape"` Node bson.ObjectID `json:"node"` DiskType string `json:"disk_type"` DiskPool bson.ObjectID `json:"disk_pool"` Image bson.ObjectID `json:"image"` ImageBacking bool `json:"image_backing"` Name string `json:"name"` Comment string `json:"comment"` Action string `json:"action"` RootEnabled bool `json:"root_enabled"` Uefi bool `json:"uefi"` SecureBoot bool `json:"secure_boot"` Tpm bool `json:"tpm"` DhcpServer bool `json:"dhcp_server"` CloudType string `json:"cloud_type"` CloudScript string `json:"cloud_script"` DeleteProtection bool `json:"delete_protection"` SkipSourceDestCheck bool `json:"skip_source_dest_check"` InitDiskSize int `json:"init_disk_size"` Memory int `json:"memory"` Processors int `json:"processors"` Roles []string `json:"roles"` Isos []*iso.Iso `json:"isos"` UsbDevices []*usb.Device `json:"usb_devices"` PciDevices []*pci.Device `json:"pci_devices"` DriveDevices []*drive.Device `json:"drive_devices"` IscsiDevices []*iscsi.Device `json:"iscsi_devices"` Mounts []*instance.Mount `json:"mounts"` Vnc bool `json:"vnc"` Spice bool `json:"spice"` Gui bool `json:"gui"` NodePorts []*nodeport.Mapping `json:"node_ports"` NoPublicAddress bool `json:"no_public_address"` NoPublicAddress6 bool `json:"no_public_address6"` NoHostAddress bool `json:"no_host_address"` Count int `json:"count"` } type instanceMultiData struct { Ids []bson.ObjectID `json:"ids"` Action string `json:"action"` } type instancesData struct { Instances []*instance.Instance `json:"instances"` Count int64 `json:"count"` } func instancePut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := &instanceData{} instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } inst, err := instance.GetOrg(db, userOrg, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } exists, err := vpc.ExistsOrg(db, userOrg, dta.Vpc) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } inst.PreCommit() inst.Name = dta.Name inst.Comment = dta.Comment inst.Vpc = dta.Vpc inst.Subnet = dta.Subnet inst.CloudSubnet = dta.CloudSubnet if dta.Action != "" { inst.Action = dta.Action } inst.Uefi = dta.Uefi inst.SecureBoot = dta.SecureBoot inst.Tpm = dta.Tpm inst.DhcpServer = dta.DhcpServer inst.CloudType = dta.CloudType inst.CloudScript = dta.CloudScript inst.DeleteProtection = dta.DeleteProtection inst.SkipSourceDestCheck = dta.SkipSourceDestCheck inst.Memory = dta.Memory inst.Processors = dta.Processors inst.Roles = dta.Roles inst.Isos = dta.Isos inst.UsbDevices = dta.UsbDevices inst.PciDevices = dta.PciDevices inst.DriveDevices = dta.DriveDevices inst.IscsiDevices = dta.IscsiDevices inst.Mounts = dta.Mounts inst.RootEnabled = dta.RootEnabled inst.Vnc = dta.Vnc inst.Spice = dta.Spice inst.Gui = dta.Gui inst.NodePorts = dta.NodePorts inst.NoPublicAddress = dta.NoPublicAddress inst.NoPublicAddress6 = dta.NoPublicAddress6 inst.NoHostAddress = dta.NoHostAddress fields := set.NewSet( "unix_id", "name", "comment", "datacenter", "vpc", "subnet", "dhcp_ip", "dhcp_ip6", "cloud_subnet", "state", "restart", "restart_block_ip", "uefi", "secure_boot", "tpm", "tpm_secret", "dhcp_server", "cloud_type", "cloud_script", "delete_protection", "skip_source_dest_check", "memory", "processors", "roles", "isos", "usb_devices", "pci_devices", "drive_devices", "iscsi_devices", "mounts", "root_enabled", "root_passwd", "vnc", "vnc_display", "vnc_password", "spice", "spice_port", "spice_password", "gui", "node_ports", "no_public_address", "no_public_address6", "no_host_address", ) errData, err := inst.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } dskChange, err := inst.PostCommit(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = inst.CommitFields(db, fields) if err != nil { _ = inst.Cleanup(db) utils.AbortWithError(c, 500, err) return } err = inst.Cleanup(db) if err != nil { return } event.PublishDispatch(db, "instance.change") if dskChange { event.PublishDispatch(db, "disk.change") } c.JSON(200, inst) } func instancePost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := &instanceData{ Name: "new-instance", } err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } zne, err := zone.Get(db, dta.Zone) if err != nil { utils.AbortWithError(c, 500, err) return } exists, err := datacenter.ExistsOrg(db, userOrg, zne.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } if !dta.Shape.IsZero() { dta.Node = bson.NilObjectID dta.DiskType = "" dta.DiskPool = bson.NilObjectID } else { nde, err := node.Get(db, dta.Node) if err != nil { utils.AbortWithError(c, 500, err) return } if nde.Zone != zne.Id { utils.AbortWithStatus(c, 405) return } if dta.DiskType == disk.Lvm { poolMatch := false for _, plId := range nde.Pools { if plId == dta.DiskPool { poolMatch = true } } if !poolMatch { errData := &errortypes.ErrorData{ Error: "pool_not_found", Message: "Pool not found", } c.JSON(400, errData) return } } } exists, err = vpc.ExistsOrg(db, userOrg, dta.Vpc) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } if !dta.Image.IsZero() { img, err := image.GetOrgPublic(db, userOrg, dta.Image) if err != nil { if _, ok := err.(*database.NotFoundError); ok { errData := &errortypes.ErrorData{ Error: "image_not_found", Message: "Image not found", } c.JSON(400, errData) } else { utils.AbortWithError(c, 500, err) } return } stre, err := storage.Get(db, img.Storage) if err != nil { utils.AbortWithError(c, 500, err) return } available, err := data.ImageAvailable(stre, img) if err != nil { utils.AbortWithError(c, 500, err) return } if !available { if stre.IsOracle() { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from archive", } c.JSON(400, errData) } else { errData := &errortypes.ErrorData{ Error: "image_not_available", Message: "Image not restored from glacier", } c.JSON(400, errData) } return } } insts := []*instance.Instance{} if dta.Count == 0 { dta.Count = 1 } for i := 0; i < dta.Count; i++ { name := "" if strings.Contains(dta.Name, "%") { name = fmt.Sprintf(dta.Name, i+1) } else { name = dta.Name } inst := &instance.Instance{ Action: dta.Action, Organization: userOrg, Zone: dta.Zone, Vpc: dta.Vpc, Subnet: dta.Subnet, CloudSubnet: dta.CloudSubnet, Shape: dta.Shape, Node: dta.Node, DiskType: dta.DiskType, DiskPool: dta.DiskPool, Image: dta.Image, ImageBacking: dta.ImageBacking, Uefi: dta.Uefi, SecureBoot: dta.SecureBoot, Tpm: dta.Tpm, DhcpServer: dta.DhcpServer, CloudType: dta.CloudType, CloudScript: dta.CloudScript, DeleteProtection: dta.DeleteProtection, SkipSourceDestCheck: dta.SkipSourceDestCheck, Name: name, Comment: dta.Comment, InitDiskSize: dta.InitDiskSize, Memory: dta.Memory, Processors: dta.Processors, Roles: dta.Roles, Isos: dta.Isos, UsbDevices: dta.UsbDevices, PciDevices: dta.PciDevices, DriveDevices: dta.DriveDevices, IscsiDevices: dta.IscsiDevices, Mounts: dta.Mounts, RootEnabled: dta.RootEnabled, Vnc: dta.Vnc, Spice: dta.Spice, Gui: dta.Gui, NodePorts: dta.NodePorts, NoPublicAddress: dta.NoPublicAddress, NoHostAddress: dta.NoHostAddress, } errData, err := inst.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = inst.SyncNodePorts(db) if err != nil { return } err = inst.Insert(db) if err != nil { _ = inst.Cleanup(db) utils.AbortWithError(c, 500, err) return } err = inst.Cleanup(db) if err != nil { return } insts = append(insts, inst) } event.PublishDispatch(db, "instance.change") if len(insts) == 1 { c.JSON(200, insts[0]) } else { c.JSON(200, insts) } } func instancesPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := &instanceMultiData{} err := c.Bind(dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } if !instance.ValidActions.Contains(dta.Action) { errData := &errortypes.ErrorData{ Error: "invalid_action", Message: "Invalid instance action", } c.JSON(400, errData) return } doc := bson.M{ "action": dta.Action, } if dta.Action != instance.Start { doc["restart"] = false doc["restart_block_ip"] = false } err = instance.UpdateMultiOrg(db, userOrg, dta.Ids, &doc) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "instance.change") c.JSON(200, nil) } func instanceDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } inst, err := instance.Get(db, instanceId) if err != nil { return } if inst.DeleteProtection { errData := &errortypes.ErrorData{ Error: "delete_protection", Message: "Cannot delete instance with delete protection", } c.JSON(400, errData) return } err = instance.DeleteOrg(db, userOrg, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "instance.change") c.JSON(200, nil) } func instancesDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dta := []bson.ObjectID{} err := c.Bind(&dta) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } err = instance.DeleteMultiOrg(db, userOrg, dta) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "instance.change") c.JSON(200, nil) } func instanceGet(c *gin.Context) { if demo.IsDemo() { inst := demo.Instances[0] inst.Guest.Timestamp = time.Now() inst.Guest.Heartbeat = time.Now() c.JSON(200, inst) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } inst, err := instance.GetOrg(db, userOrg, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { inst.State = vm.Running inst.Action = instance.Start inst.State = vm.Running inst.Status = "Running" inst.PublicIps = []string{ demo.RandIp(inst.Id), } inst.PublicIps6 = []string{ demo.RandIp6(inst.Id), } inst.PrivateIps = []string{ demo.RandPrivateIp(inst.Id), } inst.PrivateIps6 = []string{ demo.RandPrivateIp6(inst.Id), } inst.NetworkNamespace = vm.GetNamespace(inst.Id, 0) } c.JSON(200, inst) } func instancesGet(c *gin.Context) { if demo.IsDemo() { for _, inst := range demo.Instances { inst.Guest.Timestamp = time.Now() inst.Guest.Heartbeat = time.Now() } data := &instancesData{ Instances: demo.Instances, Count: int64(len(demo.Instances)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) ndeId, _ := utils.ParseObjectId(c.Query("node_names")) plId, _ := utils.ParseObjectId(c.Query("pool_names")) if !ndeId.IsZero() { query := &bson.M{ "node": ndeId, "organization": userOrg, } insts, err := instance.GetAllName(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, insts) } else if !plId.IsZero() { nodes, err := node.GetAllPool(db, plId) if err != nil { return } ndeIds := []bson.ObjectID{} for _, nde := range nodes { ndeIds = append(ndeIds, nde.Id) } query := &bson.M{ "node": &bson.M{ "$in": ndeIds, }, "organization": userOrg, } insts, err := instance.GetAllName(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, insts) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } instId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = instId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } networkNamespace := strings.TrimSpace(c.Query("network_namespace")) if networkNamespace != "" { query["network_namespace"] = networkNamespace } nodeId, ok := utils.ParseObjectId(c.Query("node")) if ok { query["node"] = nodeId } zoneId, ok := utils.ParseObjectId(c.Query("zone")) if ok { query["zone"] = zoneId } vpcId, ok := utils.ParseObjectId(c.Query("vpc")) if ok { query["vpc"] = vpcId } subnetId, ok := utils.ParseObjectId(c.Query("subnet")) if ok { query["subnet"] = subnetId } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } instances, count, err := instance.GetAllPaged( db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, inst := range instances { inst.Json(false) if demo.IsDemo() { inst.State = vm.Running inst.Action = instance.Start inst.Status = "Running" inst.PublicIps = []string{ demo.RandIp(inst.Id), } inst.PublicIps6 = []string{ demo.RandIp6(inst.Id), } inst.PrivateIps = []string{ demo.RandPrivateIp(inst.Id), } inst.PrivateIps6 = []string{ demo.RandPrivateIp6(inst.Id), } inst.NetworkNamespace = vm.GetNamespace(inst.Id, 0) } } dta := &instancesData{ Instances: instances, Count: count, } c.JSON(200, dta) } } func instanceVncGet(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) instanceId, ok := utils.ParseObjectId(c.Param("instance_id")) if !ok { utils.AbortWithStatus(c, 400) return } inst, err := instance.GetOrg(db, userOrg, instanceId) if err != nil { utils.AbortWithError(c, 500, err) return } err = inst.VncConnect(db, c.Writer, c.Request) if err != nil { if _, ok := err.(*instance.VncDialError); ok { utils.AbortWithStatus(c, 504) } else { utils.AbortWithError(c, 500, err) } return } } ================================================ FILE: uhandlers/license.go ================================================ package uhandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type licenseData struct { Oracle bool `json:"oracle"` } func licensePut(c *gin.Context) { if demo.IsDemo() { c.JSON(200, nil) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &licenseData{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } usr.OracleLicense = data.Oracle err = usr.CommitFields(db, set.NewSet("oracle_licese")) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, data) return } ================================================ FILE: uhandlers/node.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" ) func nodesGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) zoneStr := c.Query("zone") if zoneStr == "" { c.JSON(200, []interface{}{}) return } zneId, _ := utils.ParseObjectId(zoneStr) zne, err := zone.Get(db, zneId) if err != nil { utils.AbortWithError(c, 500, err) return } exists, err := datacenter.ExistsOrg(db, userOrg, zne.Datacenter) if err != nil { return } if !exists { utils.AbortWithStatus(c, 405) return } query := &bson.M{ "zone": zneId, } nodes, err := node.GetAllHypervisors(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, nodes) } ================================================ FILE: uhandlers/organization.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/utils" ) func organizationsGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } orgs, err := organization.GetAllNameRoles(db, usr.Roles) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, orgs) } ================================================ FILE: uhandlers/plan.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/plan" "github.com/pritunl/pritunl-cloud/utils" ) type planData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Statements []*plan.Statement `json:"statements"` } type plansData struct { Plans []*plan.Plan `json:"plans"` Count int64 `json:"count"` } func planPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &planData{} planId, ok := utils.ParseObjectId(c.Param("plan_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } pln, err := plan.GetOrg(db, userOrg, planId) if err != nil { utils.AbortWithError(c, 500, err) return } pln.Name = data.Name pln.Comment = data.Comment err = pln.UpdateStatements(data.Statements) if err != nil { utils.AbortWithError(c, 500, err) return } fields := set.NewSet( "name", "comment", "statements", ) errData, err := pln.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pln.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, pln) } func planPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &planData{ Name: "new-plan", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } pln := &plan.Plan{ Name: data.Name, Comment: data.Comment, Organization: userOrg, } err = pln.UpdateStatements(data.Statements) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := pln.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pln.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, pln) } func planDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) planId, ok := utils.ParseObjectId(c.Param("plan_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := plan.RemoveOrg(db, userOrg, planId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, nil) } func plansDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } err = plan.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "plan.change") c.JSON(200, nil) } func planGet(c *gin.Context) { if demo.IsDemo() { pln := demo.Plans[0] c.JSON(200, pln) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) planId, ok := utils.ParseObjectId(c.Param("plan_id")) if !ok { utils.AbortWithStatus(c, 400) return } pln, err := plan.GetOrg(db, userOrg, planId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, pln) } func plansGet(c *gin.Context) { if demo.IsDemo() { data := &plansData{ Plans: demo.Plans, Count: int64(len(demo.Plans)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) if c.Query("names") == "true" { query := bson.M{ "organization": userOrg, } plns, err := plan.GetAllName(db, &query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, plns) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } planId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = planId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } plans, count, err := plan.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &plansData{ Plans: plans, Count: count, } c.JSON(200, data) } } ================================================ FILE: uhandlers/pod.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/aggregate" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/journal" "github.com/pritunl/pritunl-cloud/pod" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/scheduler" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/pritunl-cloud/unit" "github.com/pritunl/pritunl-cloud/utils" ) type podData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Organization bson.ObjectID `json:"organization"` DeleteProtection bool `json:"delete_protection"` Units []*unit.UnitInput `json:"units"` Drafts []*pod.UnitDraft `json:"drafts"` Count int `json:"count"` } type podsData struct { Pods []*aggregate.PodAggregate `json:"pods"` Count int64 `json:"count"` } type podsDeployData struct { Count int `json:"count"` Spec bson.ObjectID `json:"spec"` } type deploymentData struct { Id bson.ObjectID `json:"id"` Tags []string `json:"tags"` } type specsData struct { Specs []*spec.Named `json:"specs"` Count int64 `json:"count"` } func podPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &podData{} podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } pd, err := pod.GetOrg(db, userOrg, podId) if err != nil { utils.AbortWithError(c, 500, err) return } pd.Name = data.Name pd.Comment = data.Comment pd.DeleteProtection = data.DeleteProtection fields := set.NewSet( "name", "comment", "delete_protection", ) errData, err := pd.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } errData, err = pd.CommitFieldsUnits(db, data.Units, fields) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = pod.UpdateDrafts(db, podId, usr.Id, []*pod.UnitDraft{}) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, pd) } func podDraftsPut(c *gin.Context) { if demo.BlockedSilent(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &podData{} podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } err = pod.UpdateDraftsOrg(db, userOrg, podId, usr.Id, data.Drafts) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, nil) } func podDeployPut(c *gin.Context) { if demo.BlockedSilent(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &podData{} podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } units, err := unit.GetAll(db, &bson.M{ "pod": podId, "organization": userOrg, }) if err != nil { return } unitsDataMap := map[bson.ObjectID]*unit.UnitInput{} for _, unitData := range data.Units { unitsDataMap[unitData.Id] = unitData } for _, unt := range units { unitData := unitsDataMap[unt.Id] if unitData == nil || unitData.DeploySpec.IsZero() { continue } deploySpec, e := spec.Get(db, unitData.DeploySpec) if e != nil || deploySpec.Unit != unt.Id { errData := &errortypes.ErrorData{ Error: "unit_deploy_spec_invalid", Message: "Invalid unit deployment commit", } c.JSON(400, errData) return } unt.DeploySpec = unitData.DeploySpec err = unt.CommitFields(db, set.NewSet("deploy_spec")) if err != nil { utils.AbortWithError(c, 500, err) return } } c.JSON(200, nil) } func podPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &podData{ Name: "new-pod", } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } pd := &pod.Pod{ Name: data.Name, Comment: data.Comment, Organization: userOrg, DeleteProtection: data.DeleteProtection, } errData, err := pd.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pd.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err = pd.InitUnits(db, data.Units) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, pd) } func podDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDeleteOrg(db, "pod", userOrg, podId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pod.RemoveOrg(db, userOrg, podId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteOrgAll(db, "pod", userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = pod.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podGet(c *gin.Context) { if demo.IsDemo() { pd := demo.Pods[0] c.JSON(200, pd) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) userOrg := c.MustGet("organization").(bson.ObjectID) podId, ok := utils.ParseObjectId(c.Param("pod_id")) if !ok { utils.AbortWithStatus(c, 400) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } pd, err := aggregate.GetPod(db, usr.Id, &bson.M{ "_id": podId, "organization": userOrg, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, pd) } func podsGet(c *gin.Context) { if demo.IsDemo() { data := &podsData{ Pods: demo.Pods, Count: int64(len(demo.Pods)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } podId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = podId } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } role := strings.TrimSpace(c.Query("role")) if role != "" { if strings.HasPrefix(role, "~") { role := role[1:] if strings.HasPrefix(role, "!") { query["roles"] = &bson.M{ "$not": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role[1:])), "$options": "i", }, } } else { query["$or"] = []*bson.M{ &bson.M{ "roles": &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(role)), "$options": "i", }, }, } } } else { if strings.HasPrefix(role, "!") { role = strings.TrimLeft(role, "!") query["roles"] = &bson.M{ "$ne": role, } } else { query["roles"] = role } } } pods, count, err := aggregate.GetPodsPaged(db, usr.Id, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &podsData{ Pods: pods, Count: count, } c.JSON(200, data) } type PodUnit struct { Id bson.ObjectID `json:"id"` Pod bson.ObjectID `json:"pod"` Kind string `json:"kind"` Deployments []*aggregate.Deployment `json:"deployments"` } func podUnitGet(c *gin.Context) { if demo.IsDemo() { unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } var unit *unit.Unit for _, unt := range demo.Units { if unt.Id == unitId { unit = unt break } } deplys := []*aggregate.Deployment{} for _, deply := range demo.Deployments { if deply.Unit == unit.Id { deplys = append(deplys, deply) } } data := &PodUnit{ Id: unit.Id, Pod: demo.Pods[0].Id, Kind: unit.Kind, Deployments: deplys, } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } unt, err := unit.GetOrg(db, userOrg, unitId) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } deploys, err := aggregate.GetDeployments(db, unt) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } pdUnit := &PodUnit{ Id: unt.Id, Pod: unt.Pod, Kind: unt.Kind, Deployments: deploys, } c.JSON(200, pdUnit) } func podUnitDeploymentsPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } unt, err := unit.GetOrg(db, userOrg, unitId) if err != nil { utils.AbortWithError(c, 500, err) return } action := c.Query("action") switch action { case deployment.Archive: err = deployment.ArchiveMulti(db, unt.Id, data) if err != nil { utils.AbortWithError(c, 500, err) return } break case deployment.Restore: err = deployment.RestoreMulti(db, unt.Id, data) if err != nil { utils.AbortWithError(c, 500, err) return } break case deployment.Destroy: err = deployment.RemoveMulti(db, unt.Id, data) if err != nil { utils.AbortWithError(c, 500, err) return } break case deployment.Migrate: commitId, ok := utils.ParseObjectId(c.Query("commit")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := unt.MigrateDeployements(db, commitId, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } break } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podUnitDeploymentPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &podsDeployData{} unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } unt, err := unit.GetOrg(db, userOrg, unitId) if err != nil { utils.AbortWithError(c, 500, err) return } errData, err := scheduler.ManualSchedule(db, unt, data.Spec, data.Count) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "pod.change") event.PublishDispatch(db, "unit.change") c.JSON(200, nil) } func podUnitDeploymentPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &deploymentData{} unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } deplyId, ok := utils.ParseObjectId(c.Param("deployment_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } deply, err := deployment.GetUnitOrg(db, userOrg, unitId, deplyId) if err != nil { utils.AbortWithError(c, 500, err) return } deply.Tags = data.Tags fields := set.NewSet( "tags", ) errData, err := deply.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = deply.CommitFields(db, fields) if err != nil { return } event.PublishDispatch(db, "instance.change") event.PublishDispatch(db, "pod.change") c.JSON(200, nil) } func podUnitDeploymentLogGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.DeploymentLogs) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } deplyId, ok := utils.ParseObjectId(c.Param("deployment_id")) if !ok { utils.AbortWithStatus(c, 400) return } deply, err := deployment.GetUnitOrg(db, userOrg, unitId, deplyId) if err != nil { utils.AbortWithError(c, 500, err) return } kind := int32(0) resource := c.Query("resource") if resource == "agent" { kind = journal.DeploymentAgent } for _, jrnl := range deply.Journals { if jrnl.Key == resource { kind = jrnl.Index } } if kind == 0 { utils.AbortWithStatus(c, 404) return } data, err := journal.GetOutput(c, db, deply.Id, kind) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, data) } func podUnitSpecsGet(c *gin.Context) { if demo.IsDemo() { unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } specs := []*spec.Named{} for _, spc := range demo.SpecsNamed { if spc.Unit == unitId { specs = append(specs, spc) } } data := &specsData{ Specs: specs, Count: int64(len(specs)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } specs, count, err := spec.GetAllPaged(db, &bson.M{ "unit": unitId, "organization": userOrg, }, page, pageCount) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } data := &specsData{ Specs: specs, Count: count, } c.JSON(200, data) } func podUnitSpecGet(c *gin.Context) { if demo.IsDemo() { specId, ok := utils.ParseObjectId(c.Param("spec_id")) if !ok { utils.AbortWithStatus(c, 400) return } for _, spc := range demo.Specs { if spc.Id == specId { c.JSON(200, spc) return } } c.AbortWithStatus(404) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) unitId, ok := utils.ParseObjectId(c.Param("unit_id")) if !ok { utils.AbortWithStatus(c, 400) return } specId, ok := utils.ParseObjectId(c.Param("spec_id")) if !ok { utils.AbortWithStatus(c, 400) return } spec, err := spec.GetOne(db, &bson.M{ "_id": specId, "unit": unitId, "organization": userOrg, }) if err != nil { if _, ok := err.(*database.NotFoundError); ok { c.AbortWithStatus(404) } else { utils.AbortWithError(c, 500, err) } return } c.JSON(200, spec) } ================================================ FILE: uhandlers/pool.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/pool" "github.com/pritunl/pritunl-cloud/utils" ) func poolsGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.Pools) return } db := c.MustGet("db").(*database.Database) pools, err := pool.GetAllNames(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, pools) } ================================================ FILE: uhandlers/relations.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" ) type relationsData struct { Id any `json:"id"` Kind string `json:"kind"` Data string `json:"data"` } func relationsGet(c *gin.Context) { if demo.IsDemo() { kind := c.Param("kind") resourceId, ok := utils.ParseObjectId(c.Param("id")) if !ok { utils.AbortWithStatus(c, 400) return } data := &relationsData{ Id: resourceId, Kind: kind, Data: "demo", } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) kind := c.Param("kind") resourceId, ok := utils.ParseObjectId(c.Param("id")) if !ok { utils.AbortWithStatus(c, 400) return } resp, err := relations.AggregateOrg(db, kind, userOrg, resourceId) if err != nil { utils.AbortWithError(c, 500, err) return } if resp == nil { utils.AbortWithStatus(c, 404) return } data := &relationsData{ Id: resp.Id, Kind: kind, Data: resp.Yaml(), } c.JSON(200, data) } ================================================ FILE: uhandlers/secret.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/secret" "github.com/pritunl/pritunl-cloud/utils" ) type secretData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Type string `json:"type"` Key string `json:"key"` Value string `json:"value"` Data string `json:"data"` Region string `json:"region"` } type secretsData struct { Secrets []*secret.Secret `json:"secrets"` Count int64 `json:"count"` } func secretPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &secretData{} secrId, ok := utils.ParseObjectId(c.Param("secr_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } secr, err := secret.GetOrg(db, userOrg, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } secr.Name = data.Name secr.Comment = data.Comment secr.Type = data.Type secr.Key = data.Key secr.Value = data.Value secr.Data = data.Data secr.Region = data.Region fields := set.NewSet( "name", "comment", "type", "key", "value", "data", "region", "public_key", "private_key", ) errData, err := secr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secr.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, secr) } func secretPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &secretData{ Name: "new-secret", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } secr := &secret.Secret{ Name: data.Name, Comment: data.Comment, Organization: userOrg, Type: data.Type, Key: data.Key, Value: data.Value, Data: data.Data, Region: data.Region, } errData, err := secr.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secr.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, secr) } func secretDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) secrId, ok := utils.ParseObjectId(c.Param("secr_id")) if !ok { utils.AbortWithStatus(c, 400) return } errData, err := relations.CanDeleteOrg(db, "secret", userOrg, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secret.RemoveOrg(db, userOrg, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, nil) } func secretsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Bind error"), } utils.AbortWithError(c, 500, err) return } errData, err := relations.CanDeleteOrgAll(db, "secret", userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = secret.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "secret.change") c.JSON(200, nil) } func secretGet(c *gin.Context) { if demo.IsDemo() { secr := demo.Secrets[0] c.JSON(200, secr) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) secrId, ok := utils.ParseObjectId(c.Param("secr_id")) if !ok { utils.AbortWithStatus(c, 400) return } secr, err := secret.GetOrg(db, userOrg, secrId) if err != nil { utils.AbortWithError(c, 500, err) return } if demo.IsDemo() { secr.Key = "demo" secr.Value = "demo" } c.JSON(200, secr) } func secretsGet(c *gin.Context) { if demo.IsDemo() { data := &secretsData{ Secrets: demo.Secrets, Count: int64(len(demo.Secrets)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } secretId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = secretId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } comment := strings.TrimSpace(c.Query("comment")) if comment != "" { query["comment"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", comment), "$options": "i", } } secrs, count, err := secret.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } data := &secretsData{ Secrets: secrs, Count: count, } c.JSON(200, data) } ================================================ FILE: uhandlers/shape.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/shape" "github.com/pritunl/pritunl-cloud/utils" ) func shapesGet(c *gin.Context) { if demo.IsDemo() { c.JSON(200, demo.Shapes) return } db := c.MustGet("db").(*database.Database) shapes, err := shape.GetAllNames(db, &bson.M{}) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, shapes) } ================================================ FILE: uhandlers/static.go ================================================ package uhandlers import ( "strings" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/auth" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/config" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/middlewear" "github.com/pritunl/pritunl-cloud/static" "github.com/pritunl/pritunl-cloud/utils" ) func staticPath(c *gin.Context, pth string, cache bool) { pth = config.StaticRoot + pth file, ok := store.Files[pth] if !ok { utils.AbortWithStatus(c, 404) return } if constants.StaticCache && cache { c.Writer.Header().Add("Cache-Control", "public, max-age=86400") c.Writer.Header().Add("ETag", file.Hash) } else { c.Writer.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") c.Writer.Header().Add("Pragma", "no-cache") c.Writer.Header().Add("Expires", "0") } if strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") { c.Writer.Header().Add("Content-Encoding", "gzip") c.Data(200, file.Type, file.GzipData) } else { c.Data(200, file.Type, file.Data) } } func staticIndexGet(c *gin.Context) { authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { fastPth := auth.GetFastUserPath() if fastPth != "" { c.Redirect(302, fastPth) return } c.Redirect(302, "/login") return } staticPath(c, "/uindex.html", false) } func staticLoginGet(c *gin.Context) { fastPth := auth.GetFastUserPath() if fastPth != "" { c.Redirect(302, fastPth) return } staticPath(c, "/login.html", false) } func staticLogoGet(c *gin.Context) { staticPath(c, "/logo.png", true) } func staticGet(c *gin.Context) { staticPath(c, "/static"+c.Params.ByName("path"), true) } func staticTestingGet(c *gin.Context) { pth := c.Params.ByName("path") if pth == "" { if c.Request.URL.Path == "/config.js" { pth = "config.js" } else if c.Request.URL.Path == "/logo.png" { pth = "logo.png" } else if c.Request.URL.Path == "/build.js" { pth = "build.js" } else if c.Request.URL.Path == "/login" { fastPth := auth.GetFastUserPath() if fastPth != "" { c.Redirect(302, fastPth) return } c.Request.URL.Path = "/login.html" pth = "login.html" } else { authr := c.MustGet("authorizer").(*authorizer.Authorizer) if !authr.IsValid() { fastPth := auth.GetFastUserPath() if fastPth != "" { c.Redirect(302, fastPth) return } c.Redirect(302, "/login") return } pth = "uindex.html" } } if strings.HasPrefix(c.Request.URL.Path, "/node_modules/") || strings.HasPrefix(c.Request.URL.Path, "/jspm_packages/") { c.Writer.Header().Add("Cache-Control", "public, max-age=86400") } else { c.Writer.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") c.Writer.Header().Add("Pragma", "no-cache") c.Writer.Header().Add("Expires", "0") } if c.Request.URL.Path == "/" { c.Request.URL.Path = "/uindex.html" } c.Writer.Header().Add("Content-Type", static.GetMimeType(pth)) gzipWriter := middlewear.NewGzipWriter(c) defer gzipWriter.Close() fileServer.ServeHTTP(gzipWriter, c.Request) } ================================================ FILE: uhandlers/theme.go ================================================ package uhandlers import ( "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/pritunl-cloud/authorizer" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/utils" ) type themeData struct { Theme string `json:"theme"` EditorTheme string `json:"editor_theme"` } func themePut(c *gin.Context) { if demo.IsDemo() { c.JSON(200, nil) return } db := c.MustGet("db").(*database.Database) authr := c.MustGet("authorizer").(*authorizer.Authorizer) data := &themeData{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } usr, err := authr.GetUser(db) if err != nil { utils.AbortWithError(c, 500, err) return } usr.Theme = data.Theme usr.EditorTheme = data.EditorTheme err = usr.CommitFields(db, set.NewSet("theme", "editor_theme")) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, data) return } ================================================ FILE: uhandlers/utils.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" ) type redirectData struct { Redirect string `json:"redirect"` } func redirectQuery(c *gin.Context, query string) { if query != "" { c.Redirect(302, "/?"+query) } else { c.Redirect(302, "/"+query) } } func redirectQueryJson(c *gin.Context, query string) { data := redirectData{ Redirect: "/", } if query != "" { data.Redirect += "?" + query } c.JSON(202, data) } ================================================ FILE: uhandlers/vpc.go ================================================ package uhandlers import ( "fmt" "regexp" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/demo" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/relations" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vpc" ) type vpcData struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Comment string `json:"comment"` Network string `json:"network"` IcmpRedirects bool `json:"icmp_redirects"` Subnets []*vpc.Subnet `json:"subnets"` Datacenter bson.ObjectID `json:"datacenter"` Routes []*vpc.Route `json:"routes"` Maps []*vpc.Map `json:"maps"` } type vpcsData struct { Vpcs []*vpc.Vpc `json:"vpcs"` Count int64 `json:"count"` } func vpcPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &vpcData{} vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } exists, err := datacenter.ExistsOrg(db, userOrg, data.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } vc, err := vpc.GetOrg(db, userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } if vc.Organization != userOrg { utils.AbortWithStatus(c, 405) return } vc.PreCommit() vc.Name = data.Name vc.Comment = data.Comment vc.IcmpRedirects = data.IcmpRedirects vc.Routes = data.Routes vc.Maps = data.Maps vc.Subnets = data.Subnets fields := set.NewSet( "name", "comment", "icmp_redirects", "routes", "maps", "subnets", ) errData, err := vc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } errData, err = vc.PostCommit(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") vc.Json() c.JSON(200, vc) } func vpcPost(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := &vpcData{ Name: "new-vpc", } err := c.Bind(data) if err != nil { utils.AbortWithError(c, 500, err) return } exists, err := datacenter.ExistsOrg(db, userOrg, data.Datacenter) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } vc := &vpc.Vpc{ Name: data.Name, Comment: data.Comment, Network: data.Network, Subnets: data.Subnets, Organization: userOrg, Datacenter: data.Datacenter, IcmpRedirects: data.IcmpRedirects, Routes: data.Routes, Maps: data.Maps, } vc.InitVpc() errData, err := vc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vc.Insert(db) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") vc.Json() c.JSON(200, vc) } func vpcDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } exists, err := vpc.ExistsOrg(db, userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } if !exists { utils.AbortWithStatus(c, 405) return } errData, err := relations.CanDeleteOrg(db, "vpc", userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vpc.RemoveOrg(db, userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") c.JSON(200, nil) } func vpcsDelete(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []bson.ObjectID{} err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } for _, vpcId := range data { exists, e := vpc.ExistsOrg(db, userOrg, vpcId) if e != nil { utils.AbortWithError(c, 500, e) return } if !exists { utils.AbortWithStatus(c, 405) return } } errData, err := relations.CanDeleteOrgAll(db, "vpc", userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vpc.RemoveMultiOrg(db, userOrg, data) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") c.JSON(200, nil) } func vpcGet(c *gin.Context) { if demo.IsDemo() { vc := demo.Vpcs[0] c.JSON(200, vc) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } vc, err := vpc.GetOrg(db, userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } vc.Json() c.JSON(200, vc) } func vpcRoutesGet(c *gin.Context) { if demo.IsDemo() { vc := demo.Vpcs[0] c.JSON(200, vc.Routes) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } vc, err := vpc.GetOrg(db, userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, vc.Routes) } func vpcRoutesPut(c *gin.Context) { if demo.Blocked(c) { return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) data := []*vpc.Route{} vpcId, ok := utils.ParseObjectId(c.Param("vpc_id")) if !ok { utils.AbortWithStatus(c, 400) return } err := c.Bind(&data) if err != nil { utils.AbortWithError(c, 500, err) return } vc, err := vpc.GetOrg(db, userOrg, vpcId) if err != nil { utils.AbortWithError(c, 500, err) return } vc.Routes = data fields := set.NewSet( "routes", ) errData, err := vc.Validate(db) if err != nil { utils.AbortWithError(c, 500, err) return } if errData != nil { c.JSON(400, errData) return } err = vc.CommitFields(db, fields) if err != nil { utils.AbortWithError(c, 500, err) return } event.PublishDispatch(db, "vpc.change") vc.Json() c.JSON(200, vc) } func vpcsGet(c *gin.Context) { if demo.IsDemo() { data := &vpcsData{ Vpcs: demo.Vpcs, Count: int64(len(demo.Vpcs)), } c.JSON(200, data) return } db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) if c.Query("names") == "true" { query := &bson.M{ "organization": userOrg, } vpcs, err := vpc.GetAllNames(db, query) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, vpcs) } else { page, _ := strconv.ParseInt(c.Query("page"), 10, 0) pageCount, _ := strconv.ParseInt(c.Query("page_count"), 10, 0) query := bson.M{ "organization": userOrg, } vpcId, ok := utils.ParseObjectId(c.Query("id")) if ok { query["_id"] = vpcId } name := strings.TrimSpace(c.Query("name")) if name != "" { query["name"] = &bson.M{ "$regex": fmt.Sprintf(".*%s.*", regexp.QuoteMeta(name)), "$options": "i", } } network := strings.TrimSpace(c.Query("network")) if network != "" { query["network"] = network } dc, ok := utils.ParseObjectId(c.Query("datacenter")) if ok { query["datacenter"] = dc } vpcs, count, err := vpc.GetAllPaged(db, &query, page, pageCount) if err != nil { utils.AbortWithError(c, 500, err) return } for _, vc := range vpcs { vc.Json() } data := &vpcsData{ Vpcs: vpcs, Count: count, } c.JSON(200, data) } } ================================================ FILE: uhandlers/zone.go ================================================ package uhandlers import ( "github.com/gin-gonic/gin" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/datacenter" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/zone" ) func zonesGet(c *gin.Context) { db := c.MustGet("db").(*database.Database) userOrg := c.MustGet("organization").(bson.ObjectID) dcIds, err := datacenter.DistinctOrg(db, userOrg) if err != nil { utils.AbortWithError(c, 500, err) return } zones, err := zone.GetAllNamedDc(db, dcIds) if err != nil { utils.AbortWithError(c, 500, err) return } c.JSON(200, zones) } ================================================ FILE: unit/unit.go ================================================ package unit import ( "sync" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/deployment" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/spec" "github.com/pritunl/tools/errors" ) type Unit struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Pod bson.ObjectID `bson:"pod" json:"pod"` Organization bson.ObjectID `bson:"organization" json:"organization"` Name string `bson:"name" json:"name"` Kind string `bson:"kind" json:"kind"` Count int `bson:"count" json:"count"` Deployments []bson.ObjectID `bson:"deployments" json:"deployments"` Spec string `bson:"spec" json:"spec"` SpecIndex int `bson:"spec_index" json:"spec_index"` SpecTimestamp time.Time `bson:"spec_timestamp" json:"-"` LastSpec bson.ObjectID `bson:"last_spec" json:"last_spec"` DeploySpec bson.ObjectID `bson:"deploy_spec" json:"deploy_spec"` Hash string `bson:"hash" json:"hash"` Journals map[string]int32 `bson:"journals" json:"-"` JournalsIndex int32 `bson:"journals_index" json:"-"` journalsLock sync.Mutex `bson:"-" json:"-"` newUnit bool `bson:"-" json:"-"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Pod bson.ObjectID `bson:"pod" json:"pod"` Organization bson.ObjectID `bson:"organization" json:"organization"` Name string `bson:"name" json:"name"` Kind string `bson:"kind" json:"kind"` } type UnitInput struct { Id bson.ObjectID `json:"id"` Name string `json:"name"` Spec string `json:"spec"` DeploySpec bson.ObjectID `json:"deploy_spec"` Delete bool `json:"delete"` } func (u *Unit) Refresh(db *database.Database) (err error) { coll := db.Units() unt := &Unit{} err = coll.FindOne(db, &bson.M{ "_id": u.Id, }).Decode(unt) if err != nil { err = database.ParseError(err) return } u.Id = unt.Id u.Pod = unt.Pod u.Organization = unt.Organization u.Name = unt.Name u.Kind = unt.Kind u.Count = unt.Count u.Deployments = unt.Deployments u.Spec = unt.Spec u.SpecIndex = unt.SpecIndex u.SpecTimestamp = unt.SpecTimestamp u.LastSpec = unt.LastSpec u.DeploySpec = unt.DeploySpec u.Hash = unt.Hash u.Journals = unt.Journals u.JournalsIndex = unt.JournalsIndex return } func (u *Unit) RefreshJournals(db *database.Database) (err error) { coll := db.Units() unt := &Unit{} err = coll.FindOne(db, &bson.M{ "_id": u.Id, }).Decode(unt) if err != nil { err = database.ParseError(err) return } u.journalsLock.Lock() u.Journals = unt.Journals u.JournalsIndex = unt.JournalsIndex u.journalsLock.Unlock() return } func (u *Unit) HasDeployment(deployId bson.ObjectID) bool { if u.Deployments != nil { for _, deplyId := range u.Deployments { if deplyId == deployId { return true } } } return false } func (u *Unit) Reserve(db *database.Database, deployId bson.ObjectID, overrideCount int) (reserved bool, err error) { coll := db.Units() if overrideCount == 0 { if len(u.Deployments) >= u.Count { return } } else { if len(u.Deployments) >= overrideCount { return } } resp, err := coll.UpdateOne(db, bson.M{ "_id": u.Id, "pod": u.Pod, "count": u.Count, "deployments": bson.M{ "$size": len(u.Deployments), }, }, bson.M{ "$push": bson.M{ "deployments": deployId, }, }) if err != nil { err = database.ParseError(err) return } if resp.MatchedCount == 1 && resp.ModifiedCount == 1 { reserved = true } return } func (u *Unit) RestoreDeployment(db *database.Database, deployId bson.ObjectID) (err error) { coll := db.Units() _, err = coll.UpdateOne(db, bson.M{ "_id": u.Id, }, bson.M{ "$push": bson.M{ "deployments": deployId, }, }) if err != nil { err = database.ParseError(err) return } return } func (u *Unit) RemoveDeployement(db *database.Database, deployId bson.ObjectID) (err error) { coll := db.Units() _, err = coll.UpdateOne(db, bson.M{ "_id": u.Id, }, bson.M{ "$pull": bson.M{ "deployments": deployId, }, }) if err != nil { err = database.ParseError(err) return } return } func (u *Unit) MigrateDeployements(db *database.Database, newSpecId bson.ObjectID, deplyIds []bson.ObjectID) ( errData *errortypes.ErrorData, err error) { coll := db.Deployments() newSpc, err := spec.Get(db, newSpecId) if err != nil { return } if newSpc.Pod != u.Pod || newSpc.Unit != u.Id { err = &errortypes.ParseError{ errors.Newf("spec: Invalid unit"), } return } deplys, err := deployment.GetAll(db, &bson.M{ "_id": &bson.M{ "$in": deplyIds, }, "pod": u.Pod, "unit": u.Id, }) if err != nil { return } spcMap := map[bson.ObjectID]*spec.Spec{} for _, deply := range deplys { oldSpc := spcMap[deply.Spec] if oldSpc == nil { oldSpc, err = spec.Get(db, deply.Spec) if err != nil { return } spcMap[oldSpc.Id] = oldSpc } errData, err = oldSpc.CanMigrate(db, deply, newSpc) if err != nil || errData != nil { return } } _, err = coll.UpdateMany(db, &bson.M{ "_id": &bson.M{ "$in": deplyIds, }, "pod": u.Pod, "unit": u.Id, }, &bson.M{ "$set": &bson.M{ "action": deployment.Migrate, "new_spec": newSpc.Id, }, }) if err != nil { err = database.ParseError(err) return } return } func (u *Unit) newSpec(db *database.Database, spc *spec.Spec, newUnit bool) ( newSpec *spec.Spec, errData *errortypes.ErrorData, err error) { u.Name = spc.Name u.Count = spc.Count u.Spec = spc.Data if u.Kind == "" { u.Kind = spc.Kind } else if u.Kind != spc.Kind { errData = &errortypes.ErrorData{ Error: "spec_kind_invalid", Message: "Cannot change spec kind", } return } if newUnit { spc.Index = 1 spc.Timestamp = time.Now() } else { timestamp, index, e := NewSpec(db, u.Pod, u.Id) if e != nil { err = e return } spc.Index = index spc.Timestamp = timestamp } newSpec = spc u.Hash = spc.Hash u.LastSpec = spc.Id if u.DeploySpec.IsZero() { u.DeploySpec = spc.Id } return } func (u *Unit) updateSpec(db *database.Database, spc *spec.Spec) ( updateSpec *spec.Spec, errData *errortypes.ErrorData, err error) { curSpc, e := spec.Get(db, u.LastSpec) if e != nil { err = e return } curSpc.Name = spc.Name curSpc.Count = spc.Count curSpc.Data = spc.Data updateSpec = curSpc u.Name = curSpc.Name u.Count = curSpc.Count u.Spec = curSpc.Data if u.Kind == "" { u.Kind = spc.Kind } else if u.Kind != spc.Kind { errData = &errortypes.ErrorData{ Error: "spec_kind_invalid", Message: "Cannot change spec kind", } return } u.Hash = curSpc.Hash u.LastSpec = curSpc.Id if u.DeploySpec.IsZero() { u.DeploySpec = curSpc.Id } return } func (u *Unit) getKind(db *database.Database, key string) ( kind int32, err error) { u.journalsLock.Lock() defer u.journalsLock.Unlock() if u.Journals == nil { u.Journals = map[string]int32{} } kind, ok := u.Journals[key] if ok && kind != 0 { return } jrnls := map[string]int32{} for key, index := range u.Journals { jrnls[key] = index } index := u.JournalsIndex if index == 0 { index = 248000 } index += 1 jrnls[key] = index coll := db.Units() query := bson.M{ "_id": u.Id, } if u.JournalsIndex == 0 { query["$or"] = []bson.M{ {"journals_index": bson.M{"$exists": false}}, {"journals_index": 0}, } } else { query["journals_index"] = u.JournalsIndex } if !u.newUnit { resp, e := coll.UpdateOne(db, query, bson.M{ "$set": bson.M{ "journals_index": index, "journals": jrnls, }, }) if e != nil { err = database.ParseError(e) return } if resp.ModifiedCount < 1 { kind = 0 return } } u.Journals = jrnls u.JournalsIndex = index kind = index return } func (u *Unit) GetKind(db *database.Database, key string) ( kind int32, err error) { for i := 0; i < 3; i++ { kind, err = u.getKind(db, key) if err != nil { return } if kind != 0 { break } err = u.RefreshJournals(db) if err != nil { return } } if kind == 0 { err = &errortypes.ParseError{ errors.New("unit: Failed to get journal kind index"), } return } return } func (u *Unit) Parse(db *database.Database, newUnit bool) ( newSpec *spec.Spec, updateSpec *spec.Spec, errData *errortypes.ErrorData, err error) { if newUnit { u.newUnit = true } spc := spec.New(u.Pod, u.Id, u.Organization, u.Spec) errData, err = spc.Parse(db, u) if err != nil { return } if errData != nil { return } isNewSpec := u.Hash != spc.Hash if !isNewSpec && u.Name != spc.Name || u.Count != spc.Count { updateSpec, errData, err = u.updateSpec(db, spc) if err != nil { if _, ok := err.(*database.NotFoundError); ok { err = nil isNewSpec = true } else { return } } if errData != nil { return } } if isNewSpec { newSpec, errData, err = u.newSpec(db, spc, newUnit) if err != nil { return } if errData != nil { return } } return } func (u *Unit) Commit(db *database.Database) (err error) { coll := db.Units() err = coll.Commit(u.Id, u) if err != nil { return } return } func (u *Unit) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Units() err = coll.CommitFields(u.Id, u, fields) if err != nil { return } return } func (u *Unit) Insert(db *database.Database) (err error) { coll := db.Units() if u.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("unit: Cannot insert unit without id"), } return } resp, err := coll.InsertOne(db, u) if err != nil { err = database.ParseError(err) return } u.Id = resp.InsertedID.(bson.ObjectID) return } ================================================ FILE: unit/utils.go ================================================ package unit import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" ) func Get(db *database.Database, unitId bson.ObjectID) ( unt *Unit, err error) { coll := db.Units() unt = &Unit{} err = coll.FindOneId(unitId, unt) if err != nil { return } return } func GetOrg(db *database.Database, orgId, unitId bson.ObjectID) ( unt *Unit, err error) { coll := db.Units() unt = &Unit{} err = coll.FindOne(db, &bson.M{ "_id": unitId, "organization": orgId, }).Decode(unt) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M) (units []*Unit, err error) { coll := db.Units() units = []*Unit{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { unt := &Unit{} err = cursor.Decode(unt) if err != nil { err = database.ParseError(err) return } units = append(units, unt) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllMap(db *database.Database, query *bson.M) ( unitsMap map[bson.ObjectID]*Unit, err error) { coll := db.Units() unitsMap = map[bson.ObjectID]*Unit{} cursor, err := coll.Find(db, query) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { unt := &Unit{} err = cursor.Decode(unt) if err != nil { err = database.ParseError(err) return } unitsMap[unt.Id] = unt } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func NewSpec(db *database.Database, podId, unitId bson.ObjectID) (timestamp time.Time, index int, err error) { coll := db.Units() updateOpts := options.FindOneAndUpdate(). SetProjection(&bson.M{ "spec_index": 1, "spec_timestamp": 1, }). SetReturnDocument(options.After) unit := &Unit{} err = coll.FindOneAndUpdate(db, &bson.M{ "_id": unitId, "pod": podId, }, &bson.M{ "$inc": &bson.M{ "spec_index": 1, }, "$currentDate": &bson.M{ "spec_timestamp": true, }, }, updateOpts).Decode(unit) if err != nil { err = database.ParseError(err) return } index = unit.SpecIndex timestamp = unit.SpecTimestamp return } func Remove(db *database.Database, untId bson.ObjectID) (err error) { coll := db.Schedulers() _, err = coll.DeleteOne(db, &bson.M{ "_id": untId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } coll = db.Units() _, err = coll.DeleteOne(db, &bson.M{ "_id": untId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, untId bson.ObjectID) ( err error) { coll := db.Schedulers() _, err = coll.DeleteOne(db, &bson.M{ "_id": untId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } coll = db.Units() _, err = coll.DeleteOne(db, &bson.M{ "_id": untId, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveAll(db *database.Database, query *bson.M) (err error) { coll := db.Schedulers() _, err = coll.DeleteMany(db, query) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } coll = db.Units() _, err = coll.DeleteMany(db, query) if err != nil { err = database.ParseError(err) return } return } func RemoveMulti(db *database.Database, untIds []bson.ObjectID) (err error) { coll := db.Schedulers() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": untIds, }, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } coll = db.Units() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": untIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, untIds []bson.ObjectID) (err error) { coll := db.Schedulers() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": untIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } coll = db.Units() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": untIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/created.go ================================================ package upgrade import ( "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/disk" "github.com/pritunl/pritunl-cloud/instance" ) func createdUpgrade(db *database.Database) (err error) { insts, err := instance.GetAll(db, &bson.M{ "created": &bson.M{ "$exists": false, }, }) if err != nil { return } for _, inst := range insts { inst.Created = inst.Id.Timestamp() err = inst.CommitFields(db, set.NewSet("created")) if err != nil { return } } disks, err := disk.GetAll(db, &bson.M{ "created": &bson.M{ "$exists": false, }, }) if err != nil { return } for _, disk := range disks { disk.Created = disk.Id.Timestamp() err = disk.CommitFields(db, set.NewSet("created")) if err != nil { return } } return } ================================================ FILE: upgrade/instance.go ================================================ package upgrade import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) func instanceUpgrade(db *database.Database) (err error) { coll := db.Instances() _, err = coll.UpdateMany(db, bson.M{ "virt_timestamp": bson.M{ "$exists": true, }, }, []bson.M{ bson.M{ "$set": bson.M{ "timestamp": "$virt_timestamp", }, }, bson.M{ "$unset": "virt_timestamp", }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/journal.go ================================================ package upgrade import ( "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/journal" ) func journalUpgrade(db *database.Database) (err error) { coll := db.Journal() cursor, err := coll.Find(db, &bson.M{ "c": &bson.M{ "$exists": false, }, }, options.Find(). SetSort(bson.D{ {"t", 1}, {"_id", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) var count int32 var lastTime time.Time i := 0 for cursor.Next(db) { jrnl := &journal.Journal{} err = cursor.Decode(jrnl) if err != nil { err = database.ParseError(err) return } if jrnl.Timestamp.Unix() != lastTime.Unix() { count = 1 } lastTime = jrnl.Timestamp if i%1000 == 0 { println(count) } i += 1 _, err = coll.UpdateOne(db, &bson.M{ "_id": jrnl.Id, }, &bson.M{ "$set": &bson.M{ "c": count, }, }) if err != nil { err = database.ParseError(err) return } count += 1 } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/node.go ================================================ package upgrade import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) func nodeUpgrade(db *database.Database) (err error) { coll := db.Nodes() _, err = coll.UpdateMany(db, bson.M{ "available_interfaces": bson.M{ "$exists": true, }, "available_interfaces.0": bson.M{ "$type": "string", }, }, []bson.M{ bson.M{ "$unset": "available_interfaces", }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "available_bridges": bson.M{ "$exists": true, }, "available_bridges.0": bson.M{ "$type": "string", }, }, []bson.M{ bson.M{ "$unset": "available_bridges", }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/objectid.go ================================================ package upgrade import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) func objectIdUpgrade(db *database.Database) (err error) { nilObjectID := bson.NilObjectID coll := db.Alerts() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Authorities() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Balancers() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "datacenter": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "datacenter": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Certificates() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "acme_secret": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "acme_secret": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Deployments() _, err = coll.UpdateMany(db, bson.M{ "datacenter": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "datacenter": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "zone": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "zone": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "node": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "node": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "instance": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "instance": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "image": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "image": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Disks() _, err = coll.UpdateMany(db, bson.M{ "node": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "node": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "pool": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "pool": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "instance": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "instance": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "source_instance": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "source_instance": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "deployment": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "deployment": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "image": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "image": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "restore_image": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "restore_image": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Domains() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "lock_id": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "lock_id": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Firewalls() _, err = coll.UpdateMany(db, bson.M{ "$or": []*bson.M{ &bson.M{ "organization": nil, }, &bson.M{ "organization": &bson.M{ "$exists": false, }, }, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Images() _, err = coll.UpdateMany(db, bson.M{ "deployment": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "deployment": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Instances() _, err = coll.UpdateMany(db, bson.M{ "disk_pool": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "disk_pool": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "node": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "node": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "shape": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "shape": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } _, err = coll.UpdateMany(db, bson.M{ "deployment": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "deployment": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Plans() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Secrets() _, err = coll.UpdateMany(db, bson.M{ "organization": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "organization": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Zones() _, err = coll.UpdateMany(db, bson.M{ "datacenter": bson.M{ "$exists": false, }, }, bson.M{ "$set": bson.M{ "datacenter": nilObjectID, }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/roles.go ================================================ package upgrade import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) func rolesUpgrade(db *database.Database) (err error) { coll := db.Instances() _, err = coll.UpdateMany(db, bson.M{ "network_roles": bson.M{ "$exists": true, }, }, []bson.M{ bson.M{ "$set": bson.M{ "roles": "$network_roles", }, }, bson.M{ "$unset": "network_roles", }, }) if err != nil { err = database.ParseError(err) return } coll = db.Nodes() _, err = coll.UpdateMany(db, bson.M{ "network_roles": bson.M{ "$exists": true, }, }, []bson.M{ bson.M{ "$set": bson.M{ "roles": "$network_roles", }, }, bson.M{ "$unset": "network_roles", }, }) if err != nil { err = database.ParseError(err) return } coll = db.Firewalls() _, err = coll.UpdateMany(db, bson.M{ "network_roles": bson.M{ "$exists": true, }, }, []bson.M{ bson.M{ "$set": bson.M{ "roles": "$network_roles", }, }, bson.M{ "$unset": "network_roles", }, }) if err != nil { err = database.ParseError(err) return } coll = db.Authorities() _, err = coll.UpdateMany(db, bson.M{ "network_roles": bson.M{ "$exists": true, }, }, []bson.M{ bson.M{ "$set": bson.M{ "roles": "$network_roles", }, }, bson.M{ "$unset": "network_roles", }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/state.go ================================================ package upgrade import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) func instStateUpgrade(db *database.Database) (err error) { coll := db.Instances() _, err = coll.UpdateMany(db, bson.M{ "virt_state": bson.M{ "$exists": true, }, }, []bson.M{ bson.M{ "$set": bson.M{ "state": "$virt_state", }, }, bson.M{ "$unset": "virt_state", }, }) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: upgrade/upgrade.go ================================================ package upgrade import ( "github.com/pritunl/pritunl-cloud/database" ) func Upgrade() (err error) { db := database.GetDatabase() defer db.Close() err = nodeUpgrade(db) if err != nil { return } err = rolesUpgrade(db) if err != nil { return } err = instanceUpgrade(db) if err != nil { return } err = createdUpgrade(db) if err != nil { return } err = zoneDatacenterUpgrade(db) if err != nil { return } err = instStateUpgrade(db) if err != nil { return } err = objectIdUpgrade(db) if err != nil { return } return } ================================================ FILE: upgrade/zone_datacenter.go ================================================ package upgrade import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) type zoneUgradeDoc struct { Id bson.ObjectID `bson:"_id"` Node bson.ObjectID `bson:"node"` Datacenter bson.ObjectID `bson:"datacenter"` Zone bson.ObjectID `bson:"zone"` } func zoneDatacenterUpgrade(db *database.Database) (err error) { zoneColl := db.Zones() zoneDatacenterMap := make(map[bson.ObjectID]bson.ObjectID) nodeMap := make(map[bson.ObjectID]*zoneUgradeDoc) getDatacenterForZone := func(zoneID bson.ObjectID) ( bson.ObjectID, error) { if datacenterID, ok := zoneDatacenterMap[zoneID]; ok { return datacenterID, nil } zne := &zoneUgradeDoc{} err := zoneColl.FindOne(db, bson.M{ "_id": zoneID, }).Decode(zne) if err != nil { return bson.NilObjectID, database.ParseError(err) } zoneDatacenterMap[zoneID] = zne.Datacenter return zne.Datacenter, nil } getNode := func(nodeId bson.ObjectID) ( *zoneUgradeDoc, error) { if nde, ok := nodeMap[nodeId]; ok { return nde, nil } coll := db.Nodes() nde := &zoneUgradeDoc{} err := coll.FindOne(db, bson.M{ "_id": nodeId, }).Decode(nde) if err != nil { return nil, database.ParseError(err) } nodeMap[nodeId] = nde return nde, nil } coll := db.Nodes() cursor, err := coll.Find( db, bson.M{ "zone": bson.M{"$exists": true}, "datacenter": bson.M{"$exists": false}, }, ) if err != nil { return database.ParseError(err) } defer cursor.Close(db) for cursor.Next(db) { doc := &zoneUgradeDoc{} err = cursor.Decode(doc) if err != nil { return database.ParseError(err) } datacenterID, err := getDatacenterForZone(doc.Zone) if err != nil { return err } _, err = coll.UpdateOne( db, bson.M{"_id": doc.Id}, bson.M{"$set": bson.M{"datacenter": datacenterID}}, ) if err != nil { return database.ParseError(err) } } err = cursor.Err() if err != nil { return database.ParseError(err) } coll = db.Deployments() cursor, err = coll.Find( db, bson.M{ "zone": bson.M{"$exists": true}, "datacenter": bson.M{"$exists": false}, }, ) if err != nil { return database.ParseError(err) } defer cursor.Close(db) for cursor.Next(db) { doc := &zoneUgradeDoc{} err = cursor.Decode(doc) if err != nil { return database.ParseError(err) } datacenterID, err := getDatacenterForZone(doc.Zone) if err != nil { return err } _, err = coll.UpdateOne( db, bson.M{"_id": doc.Id}, bson.M{"$set": bson.M{"datacenter": datacenterID}}, ) if err != nil { return database.ParseError(err) } } err = cursor.Err() if err != nil { return database.ParseError(err) } coll = db.Instances() cursor, err = coll.Find( db, bson.M{ "zone": bson.M{"$exists": true}, "datacenter": bson.M{"$exists": false}, }, ) if err != nil { return database.ParseError(err) } defer cursor.Close(db) for cursor.Next(db) { doc := &zoneUgradeDoc{} err = cursor.Decode(doc) if err != nil { return database.ParseError(err) } datacenterID, err := getDatacenterForZone(doc.Zone) if err != nil { return err } _, err = coll.UpdateOne( db, bson.M{"_id": doc.Id}, bson.M{"$set": bson.M{"datacenter": datacenterID}}, ) if err != nil { return database.ParseError(err) } } err = cursor.Err() if err != nil { return database.ParseError(err) } coll = db.Pools() cursor, err = coll.Find( db, bson.M{ "zone": bson.M{"$exists": true}, "datacenter": bson.M{"$exists": false}, }, ) if err != nil { return database.ParseError(err) } defer cursor.Close(db) for cursor.Next(db) { doc := &zoneUgradeDoc{} err = cursor.Decode(doc) if err != nil { return database.ParseError(err) } datacenterID, err := getDatacenterForZone(doc.Zone) if err != nil { return err } _, err = coll.UpdateOne( db, bson.M{"_id": doc.Id}, bson.M{"$set": bson.M{"datacenter": datacenterID}}, ) if err != nil { return database.ParseError(err) } } err = cursor.Err() if err != nil { return database.ParseError(err) } coll = db.Specs() cursor, err = coll.Find( db, bson.M{ "zone": bson.M{"$exists": true}, "datacenter": bson.M{"$exists": false}, }, ) if err != nil { return database.ParseError(err) } defer cursor.Close(db) for cursor.Next(db) { doc := &zoneUgradeDoc{} err = cursor.Decode(doc) if err != nil { return database.ParseError(err) } datacenterID, err := getDatacenterForZone(doc.Zone) if err != nil { return err } _, err = coll.UpdateOne( db, bson.M{"_id": doc.Id}, bson.M{"$set": bson.M{"datacenter": datacenterID}}, ) if err != nil { return database.ParseError(err) } } err = cursor.Err() if err != nil { return database.ParseError(err) } coll = db.Disks() cursor, err = coll.Find( db, bson.M{ "zone": bson.M{"$exists": false}, "datacenter": bson.M{"$exists": false}, }, ) if err != nil { return database.ParseError(err) } defer cursor.Close(db) for cursor.Next(db) { doc := &zoneUgradeDoc{} err = cursor.Decode(doc) if err != nil { return database.ParseError(err) } nde, err := getNode(doc.Node) if err != nil { return err } _, err = coll.UpdateOne( db, bson.M{"_id": doc.Id}, bson.M{"$set": bson.M{ "datacenter": nde.Datacenter, "zone": nde.Zone, }}, ) if err != nil { return database.ParseError(err) } } err = cursor.Err() if err != nil { return database.ParseError(err) } return nil } ================================================ FILE: usb/usb.go ================================================ package usb import ( "fmt" "io/ioutil" "path" "path/filepath" "strings" "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) const ( syncInterval = 6 * time.Second ) var ( syncLast time.Time syncLock sync.Mutex devicesCache []*Device devicesIdMapCache map[string]*Device devicesBusMapCache map[string]*Device devicesBusPathMapCache map[string]*Device ) type Device struct { Name string `bson:"name" json:"name"` Vendor string `bson:"vendor" json:"vendor"` Product string `bson:"product" json:"product"` Bus string `bson:"bus" json:"bus"` Address string `bson:"address" json:"address"` DeviceName string `bson:"-" json:"-"` DevicePath string `bson:"-" json:"-"` BusPath string `bson:"-" json:"-"` } func (d *Device) GetQemuId() string { return fmt.Sprintf("usb_%s_%s_%s_%s_%d", d.Bus, d.Address, d.Vendor, d.Product, utils.RandInt(1111, 9999), ) } func (d *Device) Unbind() (err error) { unbindPath := path.Join(d.DevicePath, "driver", "unbind") exists, err := utils.Exists(unbindPath) if err != nil { return } if exists { err = ioutil.WriteFile( unbindPath, []byte(d.DeviceName), 0644, ) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "usb: Failed to unbind '%s'", unbindPath), } return } } return } func syncDevices() (err error) { syncLock.Lock() defer syncLock.Unlock() devices := []*Device{} devicesIdMap := map[string]*Device{} devicesBusMap := map[string]*Device{} devicesBusPathMap := map[string]*Device{} basePath := "/sys/bus/usb/devices/" files, err := ioutil.ReadDir(basePath) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "usb: Failed to read dir '%s'", basePath), } return } for _, file := range files { devName := file.Name() if strings.Contains(devName, ":") || strings.HasPrefix(devName, "usb") { continue } devPath := filepath.Join(basePath, devName) vendor, e := utils.ReadExists(filepath.Join(devPath, "idVendor")) if e != nil { err = e return } if vendor == "" { continue } product, e := utils.ReadExists(filepath.Join(devPath, "idProduct")) if e != nil { err = e return } if product == "" { continue } busNum, e := utils.ReadExists(filepath.Join(devPath, "busnum")) if e != nil { err = e return } if busNum == "" { continue } devNum, e := utils.ReadExists(filepath.Join(devPath, "devnum")) if e != nil { err = e return } if devNum == "" { continue } manufacturerDesc, e := utils.ReadExists( filepath.Join(devPath, "manufacturer")) if e != nil { err = e return } productDesc, e := utils.ReadExists( filepath.Join(devPath, "product")) if e != nil { err = e return } if manufacturerDesc == "" { manufacturerDesc = "Unknown Manufacturer" } if productDesc == "" { productDesc = "Unknown Product" } name := utils.FilterStr(strings.TrimSpace(manufacturerDesc)+ " "+strings.TrimSpace(productDesc), 256) vendor = strings.TrimSpace(vendor) product = strings.TrimSpace(product) busNum = fmt.Sprintf("%03s", strings.TrimSpace(busNum)) devNum = fmt.Sprintf("%03s", strings.TrimSpace(devNum)) busPath := filepath.Join("/dev/bus/usb", busNum, devNum) device := &Device{ Name: name, Vendor: vendor, Product: product, Bus: busNum, Address: devNum, DeviceName: devName, DevicePath: devPath, BusPath: busPath, } devices = append(devices, device) devicesIdMap[device.Vendor+":"+device.Product] = device devicesBusMap[device.Bus+"-"+device.Address] = device devicesBusPathMap[device.BusPath] = device } devicesCache = devices devicesIdMapCache = devicesIdMap devicesBusMapCache = devicesBusMap devicesBusPathMapCache = devicesBusPathMap syncLast = time.Now() return } func GetDevices() (devices []*Device, err error) { if time.Since(syncLast) > syncInterval { err = syncDevices() if err != nil { return } } syncLock.Lock() devices = devicesCache syncLock.Unlock() return } func GetDevice(bus, address, vendor, product string) ( device *Device, err error) { if time.Since(syncLast) > syncInterval { err = syncDevices() if err != nil { return } } syncLock.Lock() if bus != "" && address != "" { device = devicesBusMapCache[bus+"-"+address] if device != nil && vendor != "" && product != "" { if device.Vendor != vendor || device.Product != product { device = nil } } } else { device = devicesIdMapCache[vendor+":"+product] if device != nil && bus != "" && address != "" { if device.Bus != bus || device.Address != address { device = nil } } } syncLock.Unlock() return } func GetDeviceId(vendor, product string) (device *Device, err error) { if time.Since(syncLast) > syncInterval { err = syncDevices() if err != nil { return } } syncLock.Lock() device = devicesIdMapCache[vendor+":"+product] syncLock.Unlock() return } func GetDeviceBus(bus, address string) (device *Device, err error) { if time.Since(syncLast) > syncInterval { err = syncDevices() if err != nil { return } } syncLock.Lock() device = devicesBusMapCache[bus+"-"+address] syncLock.Unlock() return } func GetDeviceBusPath(busPath string) (device *Device, err error) { if time.Since(syncLast) > syncInterval { err = syncDevices() if err != nil { return } } syncLock.Lock() device = devicesBusPathMapCache[busPath] syncLock.Unlock() return } ================================================ FILE: usb/utils.go ================================================ package usb import ( "regexp" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" ) var ( reg = regexp.MustCompile("[^a-z0-9]+") ) func Available(db *database.Database, instId, nodeId bson.ObjectID, device *Device) (available bool, err error) { coll := db.Instances() query := bson.M{ "node": nodeId, } if !instId.IsZero() { query["_id"] = &bson.M{ "$ne": instId, } } if device.Vendor != "" && device.Product != "" { query["usb_devices"] = bson.M{ "$elemMatch": bson.M{ "vendor": device.Vendor, "product": device.Product, }, } } else if device.Bus != "" && device.Address != "" { query["usb_devices"] = bson.M{ "$elemMatch": bson.M{ "bus": device.Bus, "address": device.Address, }, } } count, err := coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } if count == 0 { available = true } return } func FilterId(deviceId string) string { deviceId = strings.ToLower(deviceId) deviceId = reg.ReplaceAllString(deviceId, "") if len(deviceId) != 4 { return "" } return deviceId } func FilterAddr(addr string) string { addr = strings.ToLower(addr) addr = reg.ReplaceAllString(addr, "") if len(addr) != 3 { return "" } return addr } ================================================ FILE: user/constants.go ================================================ package user import ( "github.com/dropbox/godropbox/container/set" ) const ( Local = "local" Api = "api" Azure = "azure" AuthZero = "authzero" Google = "google" OneLogin = "onelogin" Okta = "okta" JumpCloud = "jumpcloud" ) var ( types = set.NewSet( Local, Api, Azure, AuthZero, Google, OneLogin, Okta, JumpCloud, ) ) ================================================ FILE: user/user.go ================================================ package user import ( "sort" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/go-webauthn/webauthn/webauthn" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/device" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/utils" "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" ) type User struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Type string `bson:"type" json:"type"` Provider bson.ObjectID `bson:"provider" json:"provider"` Username string `bson:"username" json:"username"` Password string `bson:"password" json:"-"` Comment string `bson:"comment" json:"comment"` DefaultPassword string `bson:"default_password" json:"-"` Token string `bson:"token" json:"token"` Secret string `bson:"secret" json:"secret"` Theme string `bson:"theme" json:"-"` EditorTheme string `bson:"editor_theme" json:"-"` LastActive time.Time `bson:"last_active" json:"last_active"` LastSync time.Time `bson:"last_sync" json:"last_sync"` Roles []string `bson:"roles" json:"roles"` Administrator string `bson:"administrator" json:"administrator"` Disabled bool `bson:"disabled" json:"disabled"` ActiveUntil time.Time `bson:"active_until" json:"active_until"` Permissions []string `bson:"permissions" json:"permissions"` OracleLicense bool `bson:"oracle_licese" json:"oracle_license"` WanCredentials []webauthn.Credential `bson:"-" json:"-"` } func (u *User) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { if u.Roles == nil { u.Roles = []string{} } if u.Permissions == nil { u.Permissions = []string{} } if !types.Contains(u.Type) { errData = &errortypes.ErrorData{ Error: "user_type_invalid", Message: "User type is not valid", } return } if u.Username == "" { errData = &errortypes.ErrorData{ Error: "user_username_invalid", Message: "User username is not valid", } return } if u.Type == Local && u.Password == "" { errData = &errortypes.ErrorData{ Error: "user_password_missing", Message: "User password is not set", } return } u.Format() return } func (u *User) Format() { if u.Type == Local { u.Username = strings.ToLower(u.Username) } roles := []string{} rolesSet := set.NewSet() for _, role := range u.Roles { rolesSet.Add(role) } for role := range rolesSet.Iter() { roles = append(roles, role.(string)) } sort.Strings(roles) u.Roles = roles } func (u *User) SuperExists(db *database.Database) ( errData *errortypes.ErrorData, err error) { if u.Administrator != "super" && !u.Id.IsZero() { exists, e := hasSuperSkip(db, u.Id) if e != nil { err = e return } if !exists { errData = &errortypes.ErrorData{ Error: "user_missing_super", Message: "Missing super administrator", } return } } return } func (u *User) Commit(db *database.Database) (err error) { coll := db.Users() err = coll.Commit(u.Id, u) if err != nil { return } return } func (u *User) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Users() err = coll.CommitFields(u.Id, u, fields) if err != nil { return } return } func (u *User) Insert(db *database.Database) (err error) { coll := db.Users() if !u.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("user: User already exists"), } return } _, err = coll.InsertOne(db, u) if err != nil { err = database.ParseError(err) return } return } func (u *User) Upsert(db *database.Database) (err error) { coll := db.Users() opts := options.FindOneAndUpdate(). SetUpsert(true). SetReturnDocument(options.After) err = coll.FindOneAndUpdate( db, &bson.M{ "type": u.Type, "username": u.Username, }, &bson.M{ "$setOnInsert": u, }, opts, ).Decode(u) if err != nil { err = database.ParseError(err) return } return } func (u *User) RolesMatch(roles []string) bool { usrRoles := set.NewSet() for _, role := range u.Roles { usrRoles.Add(role) } for _, role := range roles { if usrRoles.Contains(role) { return true } } return false } func (u *User) RolesMerge(roles []string) bool { newRoles := set.NewSet() curRoles := set.NewSet() for _, role := range roles { newRoles.Add(role) } for _, role := range u.Roles { newRoles.Add(role) curRoles.Add(role) } if !curRoles.IsEqual(newRoles) { rls := []string{} for role := range newRoles.Iter() { rls = append(rls, role.(string)) } u.Roles = rls return true } return false } func (u *User) RolesOverwrite(roles []string) bool { newRoles := set.NewSet() curRoles := set.NewSet() for _, role := range roles { newRoles.Add(role) } for _, role := range u.Roles { curRoles.Add(role) } if !curRoles.IsEqual(newRoles) { u.Roles = roles return true } return false } func (u *User) SetPassword(password string) (err error) { if u.Type != Local { err = &errortypes.UnknownError{ errors.New("user: User type cannot store password"), } return } hash, err := bcrypt.GenerateFromPassword([]byte(password), 10) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "user: Failed to hash password"), } return } u.Password = string(hash) u.DefaultPassword = "" return } func (u *User) GenerateDefaultPassword() (err error) { passwd, err := utils.RandStr(12) if err != nil { return } err = u.SetPassword(passwd) if err != nil { return } u.DefaultPassword = passwd return } func (u *User) CheckPassword(password string) bool { if u.Type != Local || u.Password == "" { return false } err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) if err != nil { return false } return true } func (u *User) GenerateToken() (err error) { u.Token, err = utils.RandStr(48) if err != nil { return } u.Secret, err = utils.RandStr(48) if err != nil { return } return } func (u *User) GetDevices(db *database.Database) ( devices []*device.Device, err error) { devices, err = device.GetAll(db, u.Id) if err != nil { return } return } func (u *User) LoadWebAuthnDevices(db *database.Database) ( devices []*device.Device, hasU2f bool, err error) { devices, err = device.GetAll(db, u.Id) if err != nil { return } wanCredentials := []webauthn.Credential{} for _, devc := range devices { switch devc.Type { case device.WebAuthn: break case device.U2f: hasU2f = true break default: continue } wanCred, e := devc.UnmarshalWebauthn() if e != nil { err = e return } wanCredentials = append(wanCredentials, wanCred) } u.WanCredentials = wanCredentials return } func (u *User) WebAuthnID() []byte { return u.Id[:] } func (u *User) WebAuthnName() string { return u.Username } func (u *User) WebAuthnDisplayName() string { return u.Username } func (u *User) WebAuthnIcon() string { return "" } func (u *User) WebAuthnCredentials() []webauthn.Credential { return u.WanCredentials } func init() { module := requires.New("user") module.After("settings") module.Handler = func() (err error) { db := database.GetDatabase() defer db.Close() coll := db.Users() cursor, err := coll.Find(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { usr := &User{} err = cursor.Decode(usr) if err != nil { err = database.ParseError(err) return } newUsername := strings.ToLower(usr.Username) if usr.Username != newUsername { err = coll.UpdateId(usr.Id, &bson.M{ "$set": &bson.M{ "username": newUsername, }, }) if err != nil { return } } } count, err := Count(db) if err != nil { return } if count == 0 { logrus.Info("user: Creating default super user") usr := User{ Type: Local, Username: "pritunl", Administrator: "super", Roles: []string{"org"}, } err = usr.GenerateDefaultPassword() if err != nil { return } _, err = usr.Validate(db) if err != nil { return } err = usr.Insert(db) if err != nil { return } } return } } ================================================ FILE: user/utils.go ================================================ package user import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) func Get(db *database.Database, userId bson.ObjectID) ( usr *User, err error) { coll := db.Users() usr = &User{} err = coll.FindOneId(userId, usr) if err != nil { return } return } func GetUpdate(db *database.Database, userId bson.ObjectID) ( usr *User, err error) { coll := db.Users() usr = &User{} timestamp := time.Now() err = coll.FindOneAndUpdate( db, &bson.M{ "_id": userId, }, &bson.M{ "$set": &bson.M{ "last_active": timestamp, }, }, ).Decode(usr) if err != nil { err = database.ParseError(err) return } usr.LastActive = timestamp return } func GetTokenUpdate(db *database.Database, token string) ( usr *User, err error) { coll := db.Users() usr = &User{} timestamp := time.Now() err = coll.FindOneAndUpdate( db, &bson.M{ "token": token, }, &bson.M{ "$set": &bson.M{ "last_active": timestamp, }, }, ).Decode(usr) if err != nil { err = database.ParseError(err) return } usr.LastActive = timestamp return } func GetUsername(db *database.Database, typ, username string) ( usr *User, err error) { coll := db.Users() usr = &User{} if username == "" { err = &errortypes.NotFoundError{ errors.New("user: Username empty"), } return } err = coll.FindOne(db, &bson.M{ "type": typ, "username": username, }).Decode(usr) if err != nil { err = database.ParseError(err) return } return } func GetAll(db *database.Database, query *bson.M, page, pageCount int64) ( users []*User, count int64, err error) { coll := db.Users() users = []*User{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } opts := options.Find(). SetSort(bson.D{{"username", 1}}) if pageCount != 0 { if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) opts.SetSkip(skip).SetLimit(pageCount) } cursor, err := coll.Find( db, query, opts, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { usr := &User{} err = cursor.Decode(usr) if err != nil { err = database.ParseError(err) return } users = append(users, usr) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func Remove(db *database.Database, userIds []bson.ObjectID) ( errData *errortypes.ErrorData, err error) { coll := db.Users() opts := options.Count(). SetLimit(1) count, err := coll.CountDocuments( db, &bson.M{ "_id": &bson.M{ "$nin": userIds, }, "administrator": "super", }, opts, ) if err != nil { err = database.ParseError(err) return } if count == 0 { errData = &errortypes.ErrorData{ Error: "user_remove_super", Message: "Cannot remove all super administrators", } return } coll = db.Sessions() _, err = coll.DeleteMany(db, &bson.M{ "user": &bson.M{ "$in": userIds, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Users() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": userIds, }, }) if err != nil { err = database.ParseError(err) return } return } func Count(db *database.Database) (count int64, err error) { coll := db.Users() count, err = coll.CountDocuments(db, &bson.M{}) if err != nil { err = database.ParseError(err) return } return } func hasSuperSkip(db *database.Database, skipId bson.ObjectID) ( exists bool, err error) { coll := db.Users() opts := options.Count(). SetLimit(1) count, err := coll.CountDocuments( db, &bson.M{ "_id": &bson.M{ "$ne": skipId, }, "administrator": "super", }, opts, ) if err != nil { err = database.ParseError(err) return } if count > 0 { exists = true } return } ================================================ FILE: useragent/useragent.go ================================================ package useragent import ( "net/http" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/geo" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/sirupsen/logrus" "github.com/ua-parser/uap-go/uaparser" ) var ( parser = uaparser.NewFromSaved() ) const ( Linux = "linux" // Linux = Debian + Linux + Ubuntu MacOs1010 = "macos_1010" // macOS 10.10 = Mac OS X (10/10) MacOs1011 = "macos_1011" // macOS 10.11 = Mac OS X (10/11) MacOs1012 = "macos_1012" // macOS 10.12 = Mac OS X (10/12) MacOs1013 = "macos_1013" // macOS 10.13 = Mac OS X (10/13) MacOs1014 = "macos_1014" // macOS 10.14 = Mac OS X (10/14) MacOs1015 = "macos_1015" // macOS 10.15 = Mac OS X (10/15) MacOs11 = "macos_11" // macOS 11 = Mac OS X (11) MacOs12 = "macos_12" // macOS 12 = Mac OS X (12) MacOs13 = "macos_13" // macOS 13 = Mac OS X (13) MacOs14 = "macos_14" // macOS 14 = Mac OS X (14) MacOs15 = "macos_15" // macOS 15 = Mac OS X (15) MacOs16 = "macos_16" // macOS 16 = Mac OS X (16) WindowsXp = "windows_xp" // Windows XP = Windows XP Windows7 = "windows_7" // Windows 7 = Windows 7 WindowsVista = "windows_vista" // Windows Vista = Windows Vista Windows8 = "windows_8" // Windows 8 = Windows 8 + Windows 8.1 + Windows RT 8.1 Windows10 = "windows_10" // Windows 10 = Windows 10 Windows11 = "windows_11" // Windows 11 = Windows 11 ChromeOs = "chrome_os" // Chrome OS = Chrome OS Ios8 = "ios_8" // iOS 8 = iOS (8/x) Ios9 = "ios_9" // iOS 9 = iOS (9/x) Ios10 = "ios_10" // iOS 10 = iOS (10/x) Ios11 = "ios_11" // iOS 11 = iOS (11/x) Ios12 = "ios_12" // iOS 12 = iOS (12/x) Ios13 = "ios_13" // iOS 13 = iOS (13/x) Ios14 = "ios_14" // iOS 14 = iOS (14/x) Ios15 = "ios_15" // iOS 15 = iOS (15/x) Ios16 = "ios_16" // iOS 16 = iOS (16/x) Ios17 = "ios_17" // iOS 17 = iOS (17/x) Ios18 = "ios_18" // iOS 18 = iOS (18/x) Ios19 = "ios_19" // iOS 19 = iOS (19/x) Ios20 = "ios_20" // iOS 20 = iOS (20/x) Android4 = "android_4" // Android KitKat 4.4 = Android (4/4) Android5 = "android_5" // Android Lollipop 5.0 = Android (5/x) Android6 = "android_6" // Android Marshmallow 6.0 = Android (6/x) Android7 = "android_7" // Android Nougat 7.0 = Android (7/x) Android8 = "android_8" // Android Oreo 8.0 = Android (8/x) Android9 = "android_9" // Android Pie 9.0 = Android (9/x) Android10 = "android_10" // Android 10.0 = Android (10/x) Android11 = "android_11" // Android 11.0 = Android (11/x) Android12 = "android_12" // Android 12.0 = Android (12/x) Android13 = "android_13" // Android 13.0 = Android (13/x) Android14 = "android_14" // Android 14.0 = Android (14/x) Android15 = "android_15" // Android 15.0 = Android (15/x) Android16 = "android_16" // Android 16.0 = Android (16/x) Blackberry10 = "blackberry_10" // Blackerry 10 = BlackBerry OS (10/x) WindowsPhone = "windows_phone" // Windows Phone = Windows Phone FirefoxOs = "firefox_os" // Firefox OS = Firefox OS Kindle = "kindle" // Kindle = Kindle ) const ( Chrome = "chrome" // Chrome = Chrome + Chromium ChromeMobile = "chrome_mobile" // Chrome Mobile = Chrome Mobile + Chrome Mobile iOS + Chrome Mobile WebView Safari = "safari" // Safari = Safari SafariMobile = "safari_mobile" // Safari Mobile = Mobile Safari + Mobile Safari UI/WKWebView Firefox = "firefox" // Firefox = Firefox + Firefox Beta FirefoxMobile = "firefox_mobile" // Firefox Mobile = Firefox Mobile + Firefox iOS Edge = "edge" // Microsoft Edge = Edge InternetExplorer = "internet_explorer" // Internet Explorer = IE InternetExplorerMobile = "internet_explorer_mobile" // Internet Explorer Mobile = IE Mobile Opera = "opera" // Opera = Opera OperaMobile = "opera_mobile" // Opera Mobile = Opera Mini + Opera Mobile + Opera Tablet + Opera Coast ) type Agent struct { OperatingSystem string `bson:"operating_system" json:"operating_system"` Browser string `bson:"browser" json:"browser"` Ip string `bson:"ip" json:"ip"` Isp string `bson:"isp" json:"isp"` Continent string `bson:"continent" json:"continent"` ContinentCode string `bson:"continent_code" json:"continent_code"` Country string `bson:"country" json:"country"` CountryCode string `bson:"country_code" json:"country_code"` Region string `bson:"region" json:"region"` RegionCode string `bson:"region_code" json:"region_code"` City string `bson:"city" json:"city"` Latitude float64 `bson:"latitude" json:"latitude"` Longitude float64 `bson:"longitude" json:"longitude"` } func Parse(db *database.Database, r *http.Request) (agnt *Agent, err error) { if settings.System.Demo { return } client := parser.Parse(r.UserAgent()) ip := node.Self.GetRemoteAddr(r) ge, err := geo.Get(db, ip) if err != nil { logrus.WithFields(logrus.Fields{ "error": err, }).Error("agent: Failed to get geo IP information") err = nil return } agnt = &Agent{ Ip: ip, Isp: ge.Isp, Continent: ge.Continent, ContinentCode: ge.ContinentCode, Country: ge.Country, CountryCode: ge.CountryCode, Region: ge.Region, RegionCode: ge.RegionCode, City: ge.City, Longitude: ge.Longitude, Latitude: ge.Latitude, } switch client.Os.Family { case "Android": switch client.Os.Major { case "4": if client.Os.Minor == "4" { agnt.OperatingSystem = Android4 break } break case "5": agnt.OperatingSystem = Android5 break case "6": agnt.OperatingSystem = Android6 break case "7": agnt.OperatingSystem = Android7 break case "8": agnt.OperatingSystem = Android8 break case "9": agnt.OperatingSystem = Android9 break case "10": agnt.OperatingSystem = Android10 break case "11": agnt.OperatingSystem = Android11 break case "12": agnt.OperatingSystem = Android12 break case "13": agnt.OperatingSystem = Android13 break case "14": agnt.OperatingSystem = Android14 break case "15": agnt.OperatingSystem = Android15 break case "16": agnt.OperatingSystem = Android16 break } break case "BlackBerry OS": if client.Os.Major == "10" { agnt.OperatingSystem = Blackberry10 break } break case "Firefox OS": agnt.OperatingSystem = FirefoxOs break case "iOS": switch client.Os.Major { case "8": agnt.OperatingSystem = Ios8 break case "9": agnt.OperatingSystem = Ios9 break case "10": agnt.OperatingSystem = Ios10 break case "11": agnt.OperatingSystem = Ios11 break case "12": agnt.OperatingSystem = Ios12 break case "13": agnt.OperatingSystem = Ios13 break case "14": agnt.OperatingSystem = Ios14 break case "15": agnt.OperatingSystem = Ios15 break case "16": agnt.OperatingSystem = Ios16 break case "17": agnt.OperatingSystem = Ios17 break case "18": agnt.OperatingSystem = Ios18 break case "19": agnt.OperatingSystem = Ios19 break case "20": agnt.OperatingSystem = Ios20 break } break case "Kindle": agnt.OperatingSystem = Kindle break case "Mac OS X": switch client.Os.Major { case "10": switch client.Os.Minor { case "10": agnt.OperatingSystem = MacOs1010 break case "11": agnt.OperatingSystem = MacOs1011 break case "12": agnt.OperatingSystem = MacOs1012 break case "13": agnt.OperatingSystem = MacOs1013 break case "14": agnt.OperatingSystem = MacOs1014 break case "15": agnt.OperatingSystem = MacOs1015 break } break case "11": agnt.OperatingSystem = MacOs11 break case "12": agnt.OperatingSystem = MacOs12 break case "13": agnt.OperatingSystem = MacOs13 break case "14": agnt.OperatingSystem = MacOs14 break case "15": agnt.OperatingSystem = MacOs15 break case "16": agnt.OperatingSystem = MacOs16 break } break case "Windows Phone": agnt.OperatingSystem = WindowsPhone break case "Windows XP": agnt.OperatingSystem = WindowsXp break case "Windows 7": agnt.OperatingSystem = Windows7 break case "Windows Vista": agnt.OperatingSystem = WindowsVista break case "Windows 8", "Windows 8.1", "Windows RT 8.1": agnt.OperatingSystem = Windows8 break case "Windows 10": agnt.OperatingSystem = Windows10 break case "Windows 11": agnt.OperatingSystem = Windows11 break case "Chrome OS": agnt.OperatingSystem = ChromeOs break case "Linux", "Debian", "Ubuntu": agnt.OperatingSystem = Linux break } switch client.UserAgent.Family { case "Chrome", "Chromium": agnt.Browser = Chrome break case "Chrome Mobile", "Chrome Mobile iOS", "Chrome Mobile WebView": agnt.Browser = ChromeMobile break case "Safari": agnt.Browser = Safari break case "Mobile Safari", "Mobile Safari UI/WKWebView": agnt.Browser = SafariMobile break case "Firefox", "Firefox Beta": agnt.Browser = Firefox break case "Firefox Mobile", "Firefox iOS": agnt.Browser = FirefoxMobile break case "Edge": agnt.Browser = Edge break case "IE": agnt.Browser = InternetExplorer break case "IE Mobile": agnt.Browser = InternetExplorerMobile break case "Opera": agnt.Browser = Opera break case "Opera Mini", "Opera Mobile", "Opera Tablet", "Opera Coast": agnt.Browser = OperaMobile break } return } func (a *Agent) Diff(agnt *Agent) bool { if a.OperatingSystem != agnt.OperatingSystem || a.Browser != agnt.Browser || a.Ip != agnt.Ip || a.Isp != agnt.Isp || a.Continent != agnt.Continent || a.ContinentCode != agnt.ContinentCode || a.Country != agnt.Country || a.CountryCode != agnt.CountryCode || a.Region != agnt.Region || a.RegionCode != agnt.RegionCode || a.City != agnt.City || a.Longitude != agnt.Longitude || a.Latitude != agnt.Latitude { return true } return false } ================================================ FILE: utils/crypto.go ================================================ package utils import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/gob" "encoding/pem" "fmt" "hash/crc32" "math" "math/big" mathrand "math/rand" "regexp" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" ) var ( randRe = regexp.MustCompile("[^a-zA-Z0-9]+") randPasswdRe = regexp.MustCompile( "[^23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ]+") ) func CrcHash(input interface{}) (sum uint32, err error) { hash := crc32.NewIEEE() enc := gob.NewEncoder(hash) err = enc.Encode(input) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Failed to encode crc input"), } return } sum = hash.Sum32() return } func RandStr(n int) (str string, err error) { for i := 0; i < 10; i++ { input, e := RandBytes(int(math.Ceil(float64(n) * 1.25))) if e != nil { err = e return } output := base64.RawStdEncoding.EncodeToString(input) output = randRe.ReplaceAllString(output, "") if len(output) < n { continue } str = output[:n] break } if str == "" { err = &errortypes.UnknownError{ errors.Wrap(err, "utils: Random generate error"), } return } return } func RandPasswd(n int) (str string, err error) { for i := 0; i < 10; i++ { input, e := RandBytes(n * 2) if e != nil { err = e return } output := base64.RawStdEncoding.EncodeToString(input) output = randPasswdRe.ReplaceAllString(output, "") if len(output) < n { continue } str = output[:n] break } if str == "" { err = &errortypes.UnknownError{ errors.Wrap(err, "utils: Random generate error"), } return } return } func RandBytes(size int) (bytes []byte, err error) { bytes = make([]byte, size) _, err = rand.Read(bytes) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "utils: Random read error"), } return } return } func RandMacAddr() (addr string, err error) { bytes := make([]byte, 6) _, err = rand.Read(bytes) if err != nil { err = &errortypes.UnknownError{ errors.Wrap(err, "utils: Random read error"), } return } addr = strings.ToUpper(fmt.Sprintf("%x", bytes)) return } func GenerateRsaKey() (encodedPriv, encodedPub []byte, err error) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to generate rsa key"), } return } blockPriv := &pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey), } encodedPriv = pem.EncodeToMemory(blockPriv) bytesPub, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Failed to marshal rsa public key"), } return } blockPub := &pem.Block{ Type: "PUBLIC KEY", Bytes: bytesPub, } encodedPub = pem.EncodeToMemory(blockPub) return } func RandObjectId() (oid bson.ObjectID, err error) { rid, err := RandBytes(12) if err != nil { return } copy(oid[:], rid) return } func RandInt(min, max int) int { return mathrand.Intn(max-min+1) + min } func init() { n, err := rand.Int(rand.Reader, big.NewInt(9223372036854775806)) if err != nil { panic(err) } mathrand.Seed(n.Int64()) } ================================================ FILE: utils/dns.go ================================================ package utils import ( "context" "fmt" "net" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) func DnsLookup(server, host string) (addrs []string, err error) { serverIp := net.ParseIP(server) if serverIp == nil { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Invalid DNS server address"), } return } if serverIp.To4() == nil { server = fmt.Sprintf("[%s]:53", serverIp.String()) } else { server = fmt.Sprintf("%s:53", serverIp.String()) } resolver := &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { dialer := net.Dialer{ Timeout: 3 * time.Second, } return dialer.DialContext(ctx, network, server) }, } addrs, err = resolver.LookupHost(context.Background(), host) if err != nil { err = &errortypes.RequestError{ errors.Wrap(err, "utils: DNS lookup failed"), } return } if addrs == nil { addrs = []string{} } return } ================================================ FILE: utils/files.go ================================================ package utils import ( "bufio" "crypto/sha256" "fmt" "io" "io/ioutil" "os" "path/filepath" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var invalidPaths = set.NewSet("/", "", ".", "./") const pathSafeLimit = 256 var pathSafeChars = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_', '.', '+', '=', '@', '/', ) func FilterPath(pth string) string { if len(pth) > pathSafeLimit { pth = pth[:pathSafeLimit] } cleaned := "" for _, c := range pth { if pathSafeChars.Contains(c) { cleaned += string(c) } } cleaned = filepath.Clean(cleaned) cleaned, err := filepath.Abs(cleaned) if err != nil { return "" } cleaned = filepath.FromSlash(cleaned) cleaned = strings.ReplaceAll(cleaned, "..", "") return cleaned } func FilterRelPath(pth string) string { if len(pth) > pathSafeLimit { pth = pth[:pathSafeLimit] } cleaned := "" for _, c := range pth { if pathSafeChars.Contains(c) { cleaned += string(c) } } cleaned = filepath.Clean(cleaned) cleaned = filepath.FromSlash(cleaned) cleaned = strings.ReplaceAll(cleaned, "..", "") return cleaned } func Chmod(pth string, mode os.FileMode) (err error) { err = os.Chmod(pth, mode) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to chmod %s", pth), } return } return } func Exists(pth string) (exists bool, err error) { _, err = os.Stat(pth) if err == nil { exists = true return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to stat %s", pth), } return } func ExistsDir(pth string) (exists bool, err error) { stat, err := os.Stat(pth) if err == nil { exists = stat.IsDir() return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to stat %s", pth), } return } func ExistsFile(pth string) (exists bool, err error) { stat, err := os.Stat(pth) if err == nil { exists = !stat.IsDir() return } if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to stat %s", pth), } return } func ExistsMkdir(pth string, perm os.FileMode) (err error) { exists, err := ExistsDir(pth) if err != nil { return } if !exists { err = os.MkdirAll(pth, perm) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to mkdir %s", pth), } return } } return } func ExistsRemove(pth string) (err error) { exists, err := Exists(pth) if err != nil { return } if exists { err = os.RemoveAll(pth) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to rm %s", pth), } return } } return } func Remove(path string) (err error) { if invalidPaths.Contains(path) { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Invalid remove path '%s'", path), } return } err = os.Remove(path) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to remove '%s'", path), } return } return } func RemoveAll(path string) (err error) { if invalidPaths.Contains(path) { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Invalid remove path '%s'", path), } return } err = os.RemoveAll(path) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to remove '%s'", path), } return } return } func RemoveWildcard(matchPath string) (n int, err error) { matches, err := filepath.Glob(matchPath) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Error matching path '%s'", matchPath), } return } if len(matches) == 0 { return } delErrors := []string{} for _, pth := range matches { fileInfo, err := os.Stat(pth) if err != nil { delErrors = append(delErrors, fmt.Sprintf("%s: %v", pth, err)) continue } if fileInfo.IsDir() { continue } err = os.Remove(pth) if err != nil { delErrors = append(delErrors, fmt.Sprintf("%s: %v", pth, err)) } else { n += 1 } } if len(delErrors) > 0 { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Delete errors '%s'", strings.Join(delErrors, ",")), } return } return } func ContainsDir(pth string) (hasDir bool, err error) { exists, err := ExistsDir(pth) if !exists { return } entries, err := ioutil.ReadDir(pth) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "queue: Failed to read dir %s", pth), } return } for _, entry := range entries { if entry.IsDir() { hasDir = true return } } return } func Open(path string, perm os.FileMode) (file *os.File, err error) { file, err = os.OpenFile(path, os.O_RDWR|os.O_TRUNC, perm) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to open '%s'", path), } return } return } func Read(path string) (data string, err error) { dataByt, err := ioutil.ReadFile(path) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", path), } return } data = string(dataByt) return } func ReadExists(path string) (data string, err error) { dataByt, err := ioutil.ReadFile(path) if err != nil { if os.IsNotExist(err) { err = nil return } err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", path), } return } data = string(dataByt) return } func ReadLines(path string) (lines []string, err error) { file, err := os.Open(path) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to open '%s'", path), } return } defer func() { err = file.Close() if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", path), } return } }() lines = []string{} reader := bufio.NewReader(file) for { line, e := reader.ReadString('\n') if e != nil { break } lines = append(lines, strings.Trim(line, "\n")) } return } func Write(path string, data string, perm os.FileMode) (err error) { file, err := Open(path, perm) if err != nil { return } defer func() { err = file.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write '%s'", path), } return } }() _, err = file.WriteString(data) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write to file '%s'", path), } return } return } func Create(path string, perm os.FileMode) (file *os.File, err error) { file, err = os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to create '%s'", path), } return } return } func CreateWrite(path string, data string, perm os.FileMode) (err error) { file, err := Create(path, perm) if err != nil { return } defer func() { err = file.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write '%s'", path), } return } }() _, err = file.WriteString(data) if err != nil { err = &errortypes.WriteError{ errors.Wrapf(err, "utils: Failed to write to file '%s'", path), } return } return } func FileSha256(pth string) (hash string, err error) { file, err := os.Open(pth) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", pth), } return } defer file.Close() hasher := sha256.New() _, err = io.Copy(hasher, file) if err != nil { err = &errortypes.ReadError{ errors.Wrapf(err, "utils: Failed to read '%s'", pth), } return } hash = fmt.Sprintf("%x", hasher.Sum(nil)) return } ================================================ FILE: utils/filter.go ================================================ package utils import ( "strings" "github.com/dropbox/godropbox/container/set" ) const nameSafeLimit = 128 var nameSafeChar = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', ) var nameCmdSafeChar = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '.', ) var unitSafeChar = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '-', '_', '.', '\\', '@', ) func FilterName(s string) string { if len(s) == 0 { return "" } if s == "self" { s = "invalid-name" } if len(s) > nameSafeLimit { s = s[:nameSafeLimit] } var ns strings.Builder for _, c := range s { if nameSafeChar.Contains(c) { ns.WriteString(string(c)) } } return ns.String() } func FilterNameCmd(s string) string { if len(s) == 0 { return "" } if s == "self" { s = "invalid-name" } if len(s) > nameSafeLimit { s = s[:nameSafeLimit] } var ns strings.Builder for _, c := range s { if nameCmdSafeChar.Contains(c) { ns.WriteString(string(c)) } } return strings.ToLower(ns.String()) } func FilterUnit(s string) string { if len(s) == 0 { return "" } if len(s) > nameSafeLimit { s = s[:nameSafeLimit] } var ns strings.Builder for _, c := range s { if unitSafeChar.Contains(c) { ns.WriteString(string(c)) } } return ns.String() } func FilterDomain(s string) string { s = FilterName(strings.ToLower(s)) s = strings.TrimPrefix(s, ".") s = strings.TrimSuffix(s, ".") return s } ================================================ FILE: utils/limiter.go ================================================ package utils import ( "sync" "time" ) type Limiter struct { counter int limit int lock sync.Mutex } func (l *Limiter) Acquire() (acquired bool) { l.lock.Lock() if l.counter < l.limit { l.counter += 1 acquired = true } l.lock.Unlock() return } func (l *Limiter) Release() { l.lock.Lock() l.counter -= 1 if l.counter < 0 { panic("limiter: Counter below zero") } l.lock.Unlock() } func NewLimiter(limit int) *Limiter { return &Limiter{ counter: 0, limit: limit, lock: sync.Mutex{}, } } type TimeLimiter struct { lastRelease time.Time duration time.Duration acquired bool lock sync.Mutex } func (l *TimeLimiter) SetDuration(duration time.Duration) { l.lock.Lock() l.duration = duration l.lock.Unlock() } func (l *TimeLimiter) Acquire() (acquired bool) { l.lock.Lock() defer l.lock.Unlock() if l.acquired { return false } if time.Since(l.lastRelease) >= l.duration { l.acquired = true acquired = true } return } func (l *TimeLimiter) Release() { l.lock.Lock() defer l.lock.Unlock() if !l.acquired { panic("limiter: Release called without acquire") } l.lastRelease = time.Now() l.acquired = false } func NewTimeLimiter(duration time.Duration) *TimeLimiter { return &TimeLimiter{ lastRelease: time.Time{}, duration: duration, acquired: false, lock: sync.Mutex{}, } } ================================================ FILE: utils/math.go ================================================ package utils import ( "math" ) func Max(x, y int) int { if x > y { return x } return y } func Min(x, y int) int { if x < y { return x } return y } func Max64(x, y int64) int64 { if x > y { return x } return y } func Min64(x, y int64) int64 { if x < y { return x } return y } func ToFixed(x float64, p int) float64 { y := math.Pow(10, float64(p)) return float64(int(x*y+math.Copysign(0.5, x*y))) / y } ================================================ FILE: utils/misc.go ================================================ package utils import ( "container/list" "io/ioutil" "os/exec" "regexp" "runtime/debug" "strconv" "strings" "time" "unicode" "github.com/dropbox/godropbox/container/set" "github.com/sirupsen/logrus" ) var isSystemd *bool var safeChars = set.NewSet( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '=', '_', '/', ',', '.', '~', '@', '#', '!', '&', ' ', ) func FilterStr(s string, n int) string { if len(s) == 0 { return "" } if len(s) > n { s = s[:n] } var ns strings.Builder for _, c := range s { if safeChars.Contains(c) { ns.WriteString(string(c)) } } return ns.String() } func SinceAbs(t time.Time) (s time.Duration) { s = time.Since(t) if s < 0 { s = s * -1 } return } func PointerBool(x bool) *bool { return &x } func PointerInt(x int) *int { return &x } func PointerString(x string) *string { return &x } func Int8Str(arr []int8) string { b := make([]byte, 0, len(arr)) for _, v := range arr { if v == 0x00 { break } b = append(b, byte(v)) } return string(b) } func HasPreSuf(src, pre, suf string) bool { return strings.HasPrefix(src, pre) && strings.HasSuffix(src, suf) } func IsSystemd() bool { if isSystemd != nil { return *isSystemd } data, err := ioutil.ReadFile("/proc/1/cmdline") if err == nil { parts := strings.Split(string(data), "\x00") if len(parts) > 0 && strings.Contains( strings.ToLower(parts[0]), "systemd") { isSysd := true isSystemd = &isSysd return true } } data, err = ioutil.ReadFile("/proc/1/comm") if err == nil { if strings.Contains(strings.ToLower(string(data)), "systemd") { isSysd := true isSystemd = &isSysd return true } } cmd := exec.Command("ps", "-p", "1", "-o", "comm=") output, err := cmd.Output() if err == nil { if strings.Contains(strings.ToLower(string(output)), "systemd") { isSysd := true isSystemd = &isSysd return true } } isSysd := false isSystemd = &isSysd return false } func CompareStringSlices(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func CompareStringSlicesUnsorted(a, b []string) bool { aSet := set.NewSet() for _, val := range a { aSet.Add(val) } bSet := set.NewSet() for _, val := range b { bSet.Add(val) } return aSet.IsEqual(bSet) } func HasMatchingItem(s1, s2 []string) bool { roleMap := make(map[string]struct{}) for _, role := range s2 { roleMap[role] = struct{}{} } for _, role := range s1 { _, exists := roleMap[role] if exists { return true } } return false } func RecoverLog(msg string) { panc := recover() if panc != nil { logrus.WithFields(logrus.Fields{ "trace": string(debug.Stack()), "panic": panc, }).Error("sync: Panic in goroutine") } } func CopyList(src *list.List) *list.List { dst := list.New() for x := src.Front(); x != nil; x = x.Next() { dst.PushBack(x.Value) } return dst } func ToSnakeCase(s string) string { var result []rune for i, r := range s { if i > 0 && unicode.IsUpper(r) { result = append(result, '_') } result = append(result, unicode.ToLower(r)) } return string(result) } func GetIntVer(version string) int { re := regexp.MustCompile(`\d+`) ver := re.FindAllString(version, -1) if len(ver) == 0 { return 0 } lastNum, err := strconv.Atoi(ver[len(ver)-1]) if err != nil { return 0 } ver[len(ver)-1] = strconv.Itoa(lastNum + 4000) var builder strings.Builder for _, v := range ver { num, err := strconv.Atoi(v) if err != nil { return 0 } builder.WriteString(strings.Repeat( "0", 4-len(strconv.Itoa(num))) + strconv.Itoa(num)) } result, err := strconv.Atoi(builder.String()) if err != nil { return 0 } return result } ================================================ FILE: utils/multilock.go ================================================ package utils import ( "sync" ) type MultiLock struct { counts map[string]int locks map[string]*sync.Mutex lock sync.Mutex } func (m *MultiLock) Lock(id string) { m.lock.Lock() val := m.counts[id] lock, ok := m.locks[id] if !ok { lock = &sync.Mutex{} m.locks[id] = lock } m.counts[id] = val + 1 m.lock.Unlock() lock.Lock() } func (m *MultiLock) Unlock(id string) { m.lock.Lock() val := m.counts[id] lock := m.locks[id] if val <= 1 { delete(m.counts, id) delete(m.locks, id) } else { m.counts[id] = val - 1 lock.Unlock() } m.lock.Unlock() } func (m *MultiLock) Locked(id string) bool { m.lock.Lock() _, ok := m.locks[id] m.lock.Unlock() return ok } func NewMultiLock() *MultiLock { return &MultiLock{ counts: map[string]int{}, locks: map[string]*sync.Mutex{}, lock: sync.Mutex{}, } } ================================================ FILE: utils/multitimeoutlock.go ================================================ package utils import ( "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type MultiTimeoutLock struct { counts map[string]int locks map[string]*sync.Mutex lock sync.Mutex state map[bson.ObjectID]bool stateLock sync.Mutex timeout time.Duration } func (m *MultiTimeoutLock) Lock(id string) (lockId bson.ObjectID) { m.lock.Lock() val := m.counts[id] lock, ok := m.locks[id] if !ok { lock = &sync.Mutex{} m.locks[id] = lock } m.counts[id] = val + 1 m.lock.Unlock() lock.Lock() lockId = bson.NewObjectID() m.stateLock.Lock() m.state[lockId] = true m.stateLock.Unlock() if !constants.LockDebug { return } start := time.Now() go func() { for { time.Sleep(1 * time.Second) m.stateLock.Lock() state := m.state[lockId] m.stateLock.Unlock() if !state { return } if time.Since(start) > m.timeout { err := &errortypes.TimeoutError{ errors.New("utils: Multi lock timeout"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("utils: Multi lock timed out") return } } }() return } func (m *MultiTimeoutLock) LockOpen(id string) ( acquired bool, lockId bson.ObjectID) { m.lock.Lock() val := m.counts[id] lock, ok := m.locks[id] if ok { m.lock.Unlock() return } lock = &sync.Mutex{} m.locks[id] = lock m.counts[id] = val + 1 m.lock.Unlock() acquired = true lock.Lock() lockId = bson.NewObjectID() m.stateLock.Lock() m.state[lockId] = true m.stateLock.Unlock() if !constants.LockDebug { return } start := time.Now() go func() { for { time.Sleep(1 * time.Second) m.stateLock.Lock() state := m.state[lockId] m.stateLock.Unlock() if !state { return } if time.Since(start) > m.timeout { err := &errortypes.TimeoutError{ errors.New("utils: Multi lock timeout"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("utils: Multi lock timed out") return } } }() return } func (m *MultiTimeoutLock) LockTimeout(id string, timeout time.Duration) (lockId bson.ObjectID) { m.lock.Lock() val := m.counts[id] lock, ok := m.locks[id] if !ok { lock = &sync.Mutex{} m.locks[id] = lock } m.counts[id] = val + 1 m.lock.Unlock() lock.Lock() lockId = bson.NewObjectID() m.stateLock.Lock() m.state[lockId] = true m.stateLock.Unlock() if !constants.LockDebug { return } start := time.Now() go func() { for { time.Sleep(1 * time.Second) m.stateLock.Lock() state := m.state[lockId] m.stateLock.Unlock() if !state { return } if time.Since(start) > timeout { err := &errortypes.TimeoutError{ errors.New("utils: Multi lock timeout"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("utils: Multi lock timed out") return } } }() return } func (m *MultiTimeoutLock) LockOpenTimeout(id string, timeout time.Duration) (acquired bool, lockId bson.ObjectID) { m.lock.Lock() val := m.counts[id] lock, ok := m.locks[id] if ok { m.lock.Unlock() return } lock = &sync.Mutex{} m.locks[id] = lock m.counts[id] = val + 1 m.lock.Unlock() acquired = true lock.Lock() lockId = bson.NewObjectID() m.stateLock.Lock() m.state[lockId] = true m.stateLock.Unlock() if !constants.LockDebug { return } start := time.Now() go func() { for { time.Sleep(1 * time.Second) m.stateLock.Lock() state := m.state[lockId] m.stateLock.Unlock() if !state { return } if time.Since(start) > timeout { err := &errortypes.TimeoutError{ errors.New("utils: Multi lock timeout"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("utils: Multi lock timed out") return } } }() return } func (m *MultiTimeoutLock) Unlock(id string, lockId bson.ObjectID) { m.lock.Lock() val := m.counts[id] lock := m.locks[id] if val <= 1 { delete(m.counts, id) delete(m.locks, id) } else { m.counts[id] = val - 1 lock.Unlock() } m.lock.Unlock() m.stateLock.Lock() delete(m.state, lockId) m.stateLock.Unlock() } func (m *MultiTimeoutLock) DelayUnlock(id string, lockId bson.ObjectID, dur time.Duration) { go func() { time.Sleep(dur) m.Unlock(id, lockId) }() } func (m *MultiTimeoutLock) Locked(id string) bool { m.lock.Lock() _, ok := m.locks[id] m.lock.Unlock() return ok } func NewMultiTimeoutLock(timeout time.Duration) *MultiTimeoutLock { return &MultiTimeoutLock{ counts: map[string]int{}, locks: map[string]*sync.Mutex{}, lock: sync.Mutex{}, state: map[bson.ObjectID]bool{}, stateLock: sync.Mutex{}, timeout: timeout, } } ================================================ FILE: utils/network.go ================================================ package utils import ( "encoding/binary" "io/ioutil" "math/big" "net" "os" "path" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var ( private10 = net.IPNet{ IP: net.IPv4(10, 0, 0, 0), Mask: net.CIDRMask(8, 32), } private100 = net.IPNet{ IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32), } private172 = net.IPNet{ IP: net.IPv4(172, 16, 0, 0), Mask: net.CIDRMask(12, 32), } private192 = net.IPNet{ IP: net.IPv4(192, 168, 0, 0), Mask: net.CIDRMask(16, 32), } private198 = net.IPNet{ IP: net.IPv4(198, 18, 0, 0), Mask: net.CIDRMask(15, 32), } reserved6 = net.IPNet{ IP: net.IPv4(6, 0, 0, 0), Mask: net.CIDRMask(8, 32), } reserved11 = net.IPNet{ IP: net.IPv4(11, 0, 0, 0), Mask: net.CIDRMask(8, 32), } reserved21 = net.IPNet{ IP: net.IPv4(21, 0, 0, 0), Mask: net.CIDRMask(8, 32), } reserved25 = net.IPNet{ IP: net.IPv4(25, 0, 0, 0), Mask: net.CIDRMask(8, 32), } reserved26 = net.IPNet{ IP: net.IPv4(26, 0, 0, 0), Mask: net.CIDRMask(8, 32), } reserved53 = net.IPNet{ IP: net.IPv4(53, 0, 0, 0), Mask: net.CIDRMask(8, 32), } reserved57 = net.IPNet{ IP: net.IPv4(57, 0, 0, 0), Mask: net.CIDRMask(8, 32), } loopback127 = net.IPNet{ IP: net.IPv4(127, 0, 0, 0), Mask: net.CIDRMask(8, 32), } linkLocal169 = net.IPNet{ IP: net.IPv4(169, 254, 0, 0), Mask: net.CIDRMask(16, 32), } multicast224 = net.IPNet{ IP: net.IPv4(224, 0, 0, 0), Mask: net.CIDRMask(4, 32), } broadcast255 = net.IPNet{ IP: net.IPv4(255, 255, 255, 255), Mask: net.CIDRMask(32, 32), } zeroconf0 = net.IPNet{ IP: net.IPv4(0, 0, 0, 0), Mask: net.CIDRMask(8, 32), } ) func IsPrivateIp(ip net.IP) bool { if ip == nil { return false } if ip.To4() == nil { return (ip[0] & 0xfe) == 0xfc } if private10.Contains(ip) || private100.Contains(ip) || private172.Contains(ip) || private192.Contains(ip) || private198.Contains(ip) || reserved6.Contains(ip) || reserved11.Contains(ip) || reserved21.Contains(ip) || reserved25.Contains(ip) || reserved26.Contains(ip) || reserved53.Contains(ip) || reserved57.Contains(ip) { return true } return false } func IsPublicIp(ip net.IP) bool { if ip == nil { return false } if ip.To4() == nil { if (ip[0] & 0xfe) == 0xfc { return false } if ip[0] == 0xfe && (ip[1]&0xc0) == 0x80 { return false } if ip.Equal(net.IPv6loopback) { return false } if ip.Equal(net.IPv6unspecified) { return false } if ip[0] == 0xff { return false } return true } if IsPrivateIp(ip) || loopback127.Contains(ip) || linkLocal169.Contains(ip) || multicast224.Contains(ip) || broadcast255.Contains(ip) || zeroconf0.Contains(ip) { return false } return true } type Address struct { Address net.IP Network *net.IPNet Ip6 bool Private bool Public bool } func ParseAddress(addrStr string) (addr *Address) { addrStr = strings.TrimSpace(addrStr) if addrStr == "" { return } if strings.Contains(addrStr, "/") { ip, network, err := net.ParseCIDR(addrStr) if err != nil { return } if ip == nil { return } addr = &Address{ Address: ip, Network: network, Ip6: ip.To4() == nil, Private: IsPrivateIp(ip), Public: IsPublicIp(ip), } return } ip := net.ParseIP(addrStr) if ip == nil { return } addr = &Address{ Address: ip, Ip6: ip.To4() == nil, Private: IsPrivateIp(ip), Public: IsPublicIp(ip), } return } func IncIpAddress(ip net.IP) { for j := len(ip) - 1; j >= 0; j-- { ip[j]++ if ip[j] > 0 { break } } } func DecIpAddress(ip net.IP) { for j := len(ip) - 1; j >= 0; j-- { ip[j]-- if ip[j] < 255 { break } } } func CopyIpAddress(src net.IP) net.IP { dst := make(net.IP, len(src)) copy(dst, src) return dst } func IpAddress2BigInt(ip net.IP) (n *big.Int, bits int) { n = &big.Int{} n.SetBytes(ip) if len(ip) == net.IPv4len { bits = 32 } else { bits = 128 } return } func BigInt2IpAddress(n *big.Int, bits int) net.IP { byt := n.Bytes() ip := make([]byte, bits/8) for i := 1; i <= len(byt); i++ { ip[len(ip)-i] = byt[len(byt)-i] } return ip } func IpAddress2Int(ip net.IP) int64 { if len(ip) == 16 { return int64(binary.BigEndian.Uint32(ip[12:16])) } return int64(binary.BigEndian.Uint32(ip)) } func Int2IpAddress(n int64) net.IP { ip := make(net.IP, 4) binary.BigEndian.PutUint32(ip, uint32(n)) return ip } func Int2IpIndex(n int64) (x int64, err error) { if n%2 != 0 { err = errortypes.ParseError{ errors.Newf("utils: Odd network int divide %d", n), } return } x = n / 2 return } func GetFirstIpIndex(network *net.IPNet) (n int64, err error) { startIp := CopyIpAddress(network.IP) startInt := IpAddress2Int(startIp) startIndex, err := Int2IpIndex(startInt) if err != nil { return } n = startIndex + 1 return } func GetLastIpIndex(network *net.IPNet) (n int64, err error) { endIp := GetLastIpAddress(network) endInt := IpAddress2Int(endIp) - 1 endIndex, err := Int2IpIndex(endInt) if err != nil { return } n = endIndex - 1 return } func IpIndex2Ip(index int64) (x, y net.IP) { x = Int2IpAddress(index * 2) y = CopyIpAddress(x) IncIpAddress(y) return } func GetLastIpAddress(network *net.IPNet) net.IP { prefixLen, bits := network.Mask.Size() if prefixLen == bits { return CopyIpAddress(network.IP) } start, bits := IpAddress2BigInt(network.IP) n := uint(bits) - uint(prefixLen) end := big.NewInt(1) end.Lsh(end, n) end.Sub(end, big.NewInt(1)) end.Or(end, start) return BigInt2IpAddress(end, bits) } func NetworkContains(x, y *net.IPNet) bool { return x.Contains(y.IP) && x.Contains(GetLastIpAddress(y)) } func ParseIpMask(mask string) net.IPMask { maskIp := net.ParseIP(mask) if maskIp == nil { return nil } return net.IPv4Mask(maskIp[12], maskIp[13], maskIp[14], maskIp[15]) } func GetNamespaces() (namespaces []string, err error) { items, err := ioutil.ReadDir("/var/run/netns") if err != nil { if os.IsNotExist(os.ErrNotExist) { namespaces = []string{} err = nil } else { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read network namespaces"), } } return } namespaces = []string{} for _, item := range items { namespaces = append(namespaces, item.Name()) } return } func GetInterfaces() (ifaces []string, err error) { ifaces, _, err = GetInterfacesSet() if err != nil { return } return } func GetInterfacesSet() (ifaces []string, ifacesSet set.Set, err error) { items, err := ioutil.ReadDir("/sys/class/net") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read network interfaces"), } return } ifaces = []string{} ifacesSet = set.NewSet() for _, item := range items { name := item.Name() if name == "" { continue } ifaces = append(ifaces, name) ifacesSet.Add(name) } exists, err := ExistsDir("/etc/sysconfig/network-scripts") if err != nil { return } if exists { items, err = ioutil.ReadDir("/etc/sysconfig/network-scripts") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read network scripts"), } return } for _, item := range items { name := item.Name() if !strings.HasPrefix(name, "ifcfg-") || !strings.Contains(name, ":") { continue } name = name[6:] names := strings.Split(name, ":") if len(names) != 2 { continue } if name == "" { continue } if ifacesSet.Contains(names[0]) && !ifacesSet.Contains(name) { ifaces = append(ifaces, name) ifacesSet.Add(name) } } } return } func GetInterfaceUpper(iface string) (upper string, err error) { iface = strings.Split(iface, ":")[0] items, err := ioutil.ReadDir("/sys/class/net/" + iface) if err != nil { if os.IsNotExist(os.ErrNotExist) { err = nil } else { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read network interface"), } } return } for _, item := range items { name := item.Name() if strings.HasPrefix(name, "upper_") { upper = name[6:] } } return } func IsInterfaceBridge(iface string) (bridge bool, err error) { bridge, err = ExistsDir( path.Join("/", "sys", "class", "net", iface, "bridge")) if err != nil { return } return } func FilterIp(input string) string { input = strings.TrimSpace(input) if input == "" { return "" } ip := net.ParseIP(input) if ip == nil { return "" } return ip.String() } ================================================ FILE: utils/proc.go ================================================ package utils import ( "bytes" "io" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/tools/commander" "github.com/sirupsen/logrus" ) var ( clockTicks = 0 ) func Exec(dir, name string, arg ...string) (err error) { cmd := exec.Command(name, arg...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if dir != "" { cmd.Dir = dir } err = cmd.Run() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } return } func ExecInput(dir, input, name string, arg ...string) (err error) { cmd := exec.Command(name, arg...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr stdin, err := cmd.StdinPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to get stdin in exec '%s'", name), } return } if dir != "" { cmd.Dir = dir } err = cmd.Start() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } var wrErr error go func() { defer func() { wrErr = stdin.Close() if wrErr != nil { wrErr = &errortypes.ExecError{ errors.Wrapf( wrErr, "utils: Failed to close stdin in exec '%s'", name, ), } } }() _, wrErr = io.WriteString(stdin, input) if wrErr != nil { wrErr = &errortypes.ExecError{ errors.Wrapf( wrErr, "utils: Failed to write stdin in exec '%s'", name, ), } return } }() err = cmd.Wait() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } if wrErr != nil { err = wrErr return } return } func ExecInputOutput(input, name string, arg ...string) ( output string, err error) { cmd := exec.Command(name, arg...) stdout := &bytes.Buffer{} cmd.Stdout = stdout cmd.Stderr = os.Stderr stdin, err := cmd.StdinPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to get stdin in exec '%s'", name), } return } err = cmd.Start() if err != nil { stdin.Close() err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } var wrErr error go func() { defer func() { wrErr = stdin.Close() if wrErr != nil { wrErr = &errortypes.ExecError{ errors.Wrapf( wrErr, "utils: Failed to close stdin in exec '%s'", name, ), } } }() _, wrErr = io.WriteString(stdin, input) if wrErr != nil { wrErr = &errortypes.ExecError{ errors.Wrapf( wrErr, "utils: Failed to write stdin in exec '%s'", name, ), } return } }() err = cmd.Wait() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } if wrErr != nil { err = wrErr return } output = string(stdout.Bytes()) return } func ExecInputOutputCombindLogged(input, name string, arg ...string) ( output string, err error) { cmd := exec.Command(name, arg...) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} cmd.Stdout = stdout cmd.Stderr = stderr stdin, err := cmd.StdinPipe() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to get stdin in exec '%s'", name), } return } err = cmd.Start() if err != nil { stdin.Close() err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } var wrErr error go func() { defer func() { wrErr = stdin.Close() if wrErr != nil { wrErr = &errortypes.ExecError{ errors.Wrapf( wrErr, "utils: Failed to close stdin in exec '%s'", name, ), } } }() _, wrErr = io.WriteString(stdin, input) if wrErr != nil { wrErr = &errortypes.ExecError{ errors.Wrapf( wrErr, "utils: Failed to write stdin in exec '%s'", name, ), } return } }() err = cmd.Wait() output = stdout.String() errOutput := stderr.String() if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } logrus.WithFields(logrus.Fields{ "output": output, "error_output": errOutput, "cmd": name, "arg": arg, "error": err, }).Error("utils: Process exec error") return } if wrErr != nil { logrus.WithFields(logrus.Fields{ "output": output, "error_output": errOutput, "cmd": name, "arg": arg, "error": wrErr, }).Error("utils: Process exec error") return } output = string(stdout.Bytes()) return } func ExecOutput(dir, name string, arg ...string) (output string, err error) { cmd := exec.Command(name, arg...) cmd.Stderr = os.Stderr if dir != "" { cmd.Dir = dir } outputByt, err := cmd.Output() if outputByt != nil { output = string(outputByt) } if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } return } func ExecCombinedOutput(dir, name string, arg ...string) ( output string, err error) { cmd := exec.Command(name, arg...) if dir != "" { cmd.Dir = dir } outputByt, err := cmd.CombinedOutput() if outputByt != nil { output = string(outputByt) } if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } return } return } func ExecCombinedOutputLogged(ignores []string, name string, arg ...string) ( output string, err error) { cmd := exec.Command(name, arg...) outputByt, err := cmd.CombinedOutput() if outputByt != nil { output = string(outputByt) } if err != nil && ignores != nil { for _, ignore := range ignores { if strings.Contains(output, ignore) { err = nil output = "" break } } } if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } logrus.WithFields(logrus.Fields{ "output": output, "cmd": name, "arg": arg, "error": err, }).Error("utils: Process exec error") return } return } func ExecCombinedOutputLoggedDir(ignores []string, dir, name string, arg ...string) ( output string, err error) { cmd := exec.Command(name, arg...) if dir != "" { cmd.Dir = dir } outputByt, err := cmd.CombinedOutput() if outputByt != nil { output = string(outputByt) } if err != nil && ignores != nil { for _, ignore := range ignores { if strings.Contains(output, ignore) { err = nil output = "" break } } } if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } logrus.WithFields(logrus.Fields{ "output": output, "cmd": name, "arg": arg, "error": err, }).Error("utils: Process exec error") return } return } func ExecOutputLogged(ignores []string, name string, arg ...string) ( output string, err error) { cmd := exec.Command(name, arg...) stdout := &bytes.Buffer{} stderr := &bytes.Buffer{} cmd.Stdout = stdout cmd.Stderr = stderr err = cmd.Run() output = stdout.String() errOutput := stderr.String() if err != nil && ignores != nil { for _, ignore := range ignores { if strings.Contains(output, ignore) || strings.Contains(errOutput, ignore) { err = nil output = "" break } } } if err != nil { err = &errortypes.ExecError{ errors.Wrapf(err, "utils: Failed to exec '%s'", name), } logrus.WithFields(logrus.Fields{ "output": output, "error_output": errOutput, "cmd": name, "arg": arg, "error": err, }).Error("utils: Process exec error") return } return } func getClockTicks() (ticks int) { if clockTicks != 0 { ticks = clockTicks return } resp, err := commander.Exec(&commander.Opt{ Name: "getconf", Args: []string{"CLK_TCK"}, PipeOut: true, PipeErr: true, }) if err != nil { ticks = 100 clockTicks = 100 return } if resp.Output != nil { ticks, _ = strconv.Atoi(strings.TrimSpace(string(resp.Output))) } if ticks == 0 { ticks = 100 clockTicks = 100 return } clockTicks = ticks return } func GetProcessTimestamp(pid int) (timestamp time.Time, err error) { procPath := filepath.Join("/proc", strconv.Itoa(pid)) _, err = os.Stat(procPath) if os.IsNotExist(err) { err = nil return } statPath := filepath.Join(procPath, "stat") statData, err := os.ReadFile(statPath) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read process stat"), } return } statFields := strings.Fields(string(statData)) if len(statFields) < 22 { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Invalid process state format"), } return } startTimeTicks, err := strconv.ParseInt(statFields[21], 10, 64) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to process stat"), } return } uptimeData, err := os.ReadFile("/proc/uptime") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read uptime"), } return } uptimeFields := strings.Fields(string(uptimeData)) if len(uptimeFields) < 1 { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Invalid uptime format"), } return } systemUptimeSec, err := strconv.ParseFloat(uptimeFields[0], 64) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to process uptime"), } return } processStartTimeSec := float64(startTimeTicks) / float64(getClockTicks()) processUptimeSec := systemUptimeSec - processStartTimeSec timestamp = time.Now().Add( time.Duration(processUptimeSec * -float64(time.Second))) return } ================================================ FILE: utils/prompt.go ================================================ package utils import ( "bufio" "fmt" "os" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) var ( AssumeYes = false ) func parseYesNo(input string) (val bool, err error) { input = strings.ToLower(input) if input == "y" || input == "yes" { val = true return } else if input == "n" || input == "no" { val = false return } err = &errortypes.ParseError{ errors.New("prompt: Invalid confirm input"), } return } func ConfirmDefault(label string, def bool) (resp bool, err error) { if AssumeYes { resp = true return } var prompt string if def { prompt = fmt.Sprintf("%s [Y/n]: ", label) } else { prompt = fmt.Sprintf("%s [y/N]: ", label) } fmt.Print(prompt) reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { return } input = strings.TrimSpace(input) if input == "" { resp = def return } resp, err = parseYesNo(input) if err != nil { return } return } ================================================ FILE: utils/psutil_freebsd.go ================================================ package utils import ( "encoding/binary" "runtime" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "golang.org/x/sys/unix" ) type MemInfo struct { Total uint64 Free uint64 Available uint64 Buffers uint64 Cached uint64 Used uint64 UsedPercent float64 Dirty uint64 SwapTotal uint64 SwapFree uint64 SwapUsed uint64 SwapUsedPercent float64 HugePagesTotal uint64 HugePagesFree uint64 HugePagesReserved uint64 HugePagesUsed uint64 HugePagesUsedPercent float64 HugePageSize uint64 } func getSysctlUint64(name string) (uint64, error) { value32, err := unix.SysctlUint32(name) if err == nil { return uint64(value32), nil } return unix.SysctlUint64(name) } func GetMemInfo() (info *MemInfo, err error) { info = &MemInfo{} totalMem, err := getSysctlUint64("hw.physmem") if err != nil { return nil, &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read physmem"), } } info.Total = totalMem / 1024 pageSize, err := getSysctlUint64("hw.pagesize") if err != nil { return nil, &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read pagesize"), } } freePages, err := getSysctlUint64("vm.stats.vm.v_free_count") if err != nil { return nil, &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read freecount"), } } info.Free = (uint64(freePages) * uint64(pageSize)) / 1024 info.Available = info.Free info.Used = info.Total - info.Free if info.Total > 0 { info.UsedPercent = float64(info.Used) / float64(info.Total) * 100.0 } return info, nil } type LoadStat struct { CpuUnits int Load1 float64 Load5 float64 Load15 float64 } func LoadAverage() (ld *LoadStat, err error) { count := runtime.NumCPU() countFloat := float64(count) loadavgRaw, err := unix.SysctlRaw("vm.loadavg") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read loadavg"), } return } if len(loadavgRaw) < 12 { err = &errortypes.ReadError{ errors.New("utils: Invalid loadavg size"), } return } fscale := float64(1 << 11) if len(loadavgRaw) >= 20 { readFscale := float64(binary.LittleEndian.Uint32(loadavgRaw[16:20])) if readFscale > 0 { fscale = readFscale } } load1 := float64(binary.LittleEndian.Uint32(loadavgRaw[0:4])) / fscale load5 := float64(binary.LittleEndian.Uint32(loadavgRaw[4:8])) / fscale load15 := float64(binary.LittleEndian.Uint32(loadavgRaw[8:12])) / fscale ld = &LoadStat{ CpuUnits: count, Load1: ToFixed(load1/countFloat*100, 2), Load5: ToFixed(load5/countFloat*100, 2), Load15: ToFixed(load15/countFloat*100, 2), } return } ================================================ FILE: utils/psutil_linux.go ================================================ package utils import ( "io/ioutil" "runtime" "strconv" "strings" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" ) type MemInfo struct { Total uint64 Free uint64 Available uint64 Buffers uint64 Cached uint64 Used uint64 UsedPercent float64 Dirty uint64 SwapTotal uint64 SwapFree uint64 SwapUsed uint64 SwapUsedPercent float64 HugePagesTotal uint64 HugePagesFree uint64 HugePagesReserved uint64 HugePagesUsed uint64 HugePagesUsedPercent float64 HugePageSize uint64 } func GetMemInfo() (info *MemInfo, err error) { info = &MemInfo{} lines, err := ReadLines("/proc/meminfo") if err != nil { return } for _, line := range lines { fields := strings.Split(line, ":") if len(fields) != 2 { continue } key := strings.TrimSpace(fields[0]) value := strings.TrimSpace(fields[1]) value = strings.Replace(value, " kB", "", -1) switch key { case "MemTotal": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse mem total"), } return } info.Total = valueInt case "MemFree": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse mem free"), } return } info.Free = valueInt case "MemAvailable": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse mem available"), } return } info.Available = valueInt case "Buffers": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse buffers"), } return } info.Buffers = valueInt case "Cached": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse cached"), } return } info.Cached = valueInt case "Dirty": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse dirty"), } return } info.Dirty = valueInt case "SwapTotal": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse swap total"), } return } info.SwapTotal = valueInt case "SwapFree": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse swap free"), } return } info.SwapFree = valueInt case "HugePages_Total": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse hugepages total"), } return } info.HugePagesTotal = valueInt case "HugePages_Free": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse hugepages total"), } return } info.HugePagesFree = valueInt case "HugePages_Rsvd": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse hugepages reserved"), } return } info.HugePagesReserved = valueInt case "Hugepagesize": valueInt, e := strconv.ParseUint(value, 10, 64) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "utils: Failed to parse hugepages size"), } return } info.HugePageSize = valueInt } } info.Used = info.Total - info.Free - info.Buffers - info.Cached info.UsedPercent = float64(info.Used) / float64(info.Total) * 100.0 info.SwapUsed = info.SwapTotal - info.SwapFree if info.SwapUsed != 0 { info.SwapUsedPercent = float64( info.SwapUsed) / float64(info.SwapTotal) * 100.0 } info.HugePagesUsed = (info.HugePagesTotal - info.HugePagesFree) + info.HugePagesReserved if info.HugePagesUsed != 0 { info.HugePagesUsedPercent = float64( info.HugePagesUsed) / float64(info.HugePagesTotal) * 100.0 } return } type LoadStat struct { CpuUnits int Load1 float64 Load5 float64 Load15 float64 } func LoadAverage() (ld *LoadStat, err error) { count := runtime.NumCPU() countFloat := float64(count) _ = countFloat ld = &LoadStat{} line, err := ioutil.ReadFile("/proc/loadavg") if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "utils: Failed to read loadavg"), } return } values := strings.Fields(string(line)) if len(values) < 3 { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Invalid loadavg data"), } return } load1, err := strconv.ParseFloat(values[0], 64) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Invalid load1 data"), } return } load5, err := strconv.ParseFloat(values[1], 64) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Invalid load5 data"), } return } load15, err := strconv.ParseFloat(values[2], 64) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "utils: Invalid load15 data"), } return } ld = &LoadStat{ CpuUnits: count, Load1: ToFixed(load1/countFloat*100, 2), Load5: ToFixed(load5/countFloat*100, 2), Load15: ToFixed(load15/countFloat*100, 2), } return } ================================================ FILE: utils/randomname.go ================================================ package utils import ( "bytes" "fmt" "math/rand" ) var ( randElm = []string{ "copper", "argon", "xenon", "radon", "cobalt", "nickel", "carbon", "helium", "nitrogen", "radium", "lithium", "silicon", } ) func RandName() (name string) { name = fmt.Sprintf("%s-%d", randElm[rand.Intn(len(randElm))], rand.Intn(8999)+1000) return } func RandIp() string { return fmt.Sprintf("26.197.%d.%d", rand.Intn(250)+4, rand.Intn(250)+4) } func RandIp6() (addr string) { addr = "2604:4080" randByt, _ := RandBytes(12) randHex := fmt.Sprintf("%x", randByt) buf := bytes.Buffer{} for i, run := range randHex { if i%4 == 0 && i != len(randHex)-1 { buf.WriteRune(':') } buf.WriteRune(run) } addr += buf.String() return } func RandPrivateIp() string { return fmt.Sprintf("10.232.%d.%d", rand.Intn(250)+4, rand.Intn(250)+4) } func RandPrivateIp6() (addr string) { addr = "fd97:7d1d" randByt, _ := RandBytes(12) randHex := fmt.Sprintf("%x", randByt) buf := bytes.Buffer{} for i, run := range randHex { if i%4 == 0 && i != len(randHex)-1 { buf.WriteRune(':') } buf.WriteRune(run) } addr += buf.String() return } ================================================ FILE: utils/request.go ================================================ package utils import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "strings" "github.com/dropbox/godropbox/errors" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/render" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type NopCloser struct { io.Reader } func (NopCloser) Close() error { return nil } var httpErrCodes = map[int]string{ 400: "Bad Request", 401: "Unauthorized", 402: "Payment Required", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 406: "Not Acceptable", 407: "Proxy Authentication Required", 408: "Request Timeout", 409: "Conflict", 410: "Gone", 411: "Length Required", 412: "Precondition Failed", 413: "Payload Too Large", 414: "URI Too Long", 415: "Unsupported Media Type", 416: "Range Not Satisfiable", 417: "Expectation Failed", 421: "Misdirected Request", 422: "Unprocessable Entity", 423: "Locked", 424: "Failed Dependency", 426: "Upgrade Required", 428: "Precondition Required", 429: "Too Many Requests", 431: "Request Header Fields Too Large", 451: "Unavailable For Legal Reasons", 500: "Internal Server Error", 501: "Not Implemented", 502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout", 505: "HTTP Version Not Supported", 506: "Variant Also Negotiates", 507: "Insufficient Storage", 508: "Loop Detected", 510: "Not Extended", 511: "Network Authentication Required", } func CopyBody(r *http.Request) (buffer *bytes.Buffer, err error) { body, err := ioutil.ReadAll(r.Body) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "handler: Request read error"), } return } _ = r.Body.Close() r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) buffer = bytes.NewBuffer(body) return } func StripPort(hostport string) string { colon := strings.IndexByte(hostport, ':') if colon == -1 { return hostport } n := strings.Count(hostport, ":") if n > 1 { if i := strings.IndexByte(hostport, ']'); i != -1 { return strings.TrimPrefix(hostport[:i], "[") } return hostport } return hostport[:colon] } func FormatHostPort(hostname string, port int) string { if strings.Contains(hostname, ":") { hostname = "[" + hostname + "]" } return fmt.Sprintf("%s:%d", hostname, port) } func ParseObjectId(strId string) (objId bson.ObjectID, ok bool) { if strId == "" { objId = bson.NilObjectID return } objectId, err := bson.ObjectIDFromHex(strId) if err != nil { objId = bson.NilObjectID return } objId = objectId ok = true return } func ObjectIdHex(strId string) (objId bson.ObjectID) { if strId == "" { objId = bson.NilObjectID return } objectId, err := bson.ObjectIDFromHex(strId) if err != nil { objId = bson.NilObjectID return } objId = objectId return } func GetStatusMessage(code int) string { return fmt.Sprintf("%d %s", code, http.StatusText(code)) } func AbortWithStatus(c *gin.Context, code int) { r := render.String{ Format: GetStatusMessage(code), } c.Status(code) r.WriteContentType(c.Writer) c.Writer.WriteHeaderNow() r.Render(c.Writer) c.Abort() } func AbortWithError(c *gin.Context, code int, err error) { AbortWithStatus(c, code) c.Error(err) } func WriteStatus(w http.ResponseWriter, code int) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(code) fmt.Fprintln(w, GetStatusMessage(code)) } func WriteText(w http.ResponseWriter, code int, text string) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(code) fmt.Fprintln(w, text) } func WriteUnauthorized(w http.ResponseWriter, msg string) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(401) fmt.Fprintln(w, "401 "+msg) } func CloneHeader(src http.Header) (dst http.Header) { dst = make(http.Header, len(src)) for k, vv := range src { vv2 := make([]string, len(vv)) copy(vv2, vv) dst[k] = vv2 } return dst } func GetLocation(r *http.Request, domains []string) string { host := "" if r.Host != "" { host = r.Host } else if r.URL.Host != "" { host = r.URL.Host } if host != "" { for _, domain := range domains { if domain == host { return "https://" + domain } } } for _, domain := range domains { if domain != "" { return "https://" + domain } } return "" } func GetOrigin(r *http.Request) string { origin := r.Header.Get("Origin") if origin == "" { host := "" switch { case r.Host != "": host = r.Host break case r.URL.Host != "": host = r.URL.Host break } origin = "https://" + host } return origin } func CheckRequestN(resp *http.Response, msg string, codes []int) (err error) { for _, code := range codes { if resp.StatusCode == code { return } } bodyStr := "" bodyBytes, readErr := io.ReadAll(io.LimitReader(resp.Body, 10*1024)) if readErr != nil { bodyStr = fmt.Sprintf("[%v]", readErr) } else { bodyStr = string(bodyBytes) } logrus.WithFields(logrus.Fields{ "body": bodyStr, "status_code": resp.StatusCode, "message": msg, }).Error(msg) err = &errortypes.RequestError{ errors.Newf("request: Response status error %d", resp.StatusCode), } return } func CheckRequest(resp *http.Response, msg string) (err error) { if resp.StatusCode == 200 { return } bodyStr := "" bodyBytes, readErr := io.ReadAll(io.LimitReader(resp.Body, 10*1024)) if readErr != nil { bodyStr = fmt.Sprintf("[%v]", readErr) } else { bodyStr = string(bodyBytes) } logrus.WithFields(logrus.Fields{ "body": bodyStr, "status_code": resp.StatusCode, "message": msg, }).Error(msg) err = &errortypes.RequestError{ errors.Newf("request: Response status error %d", resp.StatusCode), } return } ================================================ FILE: utils/sort.go ================================================ package utils import ( "regexp" "sort" "strconv" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" ) var ( numRe = regexp.MustCompile(`(\d+|\D+)`) ) type ObjectIdSlice []bson.ObjectID func (o ObjectIdSlice) Len() int { return len(o) } func (o ObjectIdSlice) Less(i, j int) bool { return o[i].Hex() < o[j].Hex() } func (o ObjectIdSlice) Swap(i, j int) { o[i], o[j] = o[j], o[i] } func SortObjectIds(x []bson.ObjectID) { sort.Sort(ObjectIdSlice(x)) } func NaturalCompare(a, b string) int { aParts := numRe.FindAllString(a, -1) bParts := numRe.FindAllString(b, -1) minLen := len(aParts) if len(bParts) < minLen { minLen = len(bParts) } for i := 0; i < minLen; i++ { aPart := aParts[i] bPart := bParts[i] aNum, aErr := strconv.Atoi(aPart) bNum, bErr := strconv.Atoi(bPart) if aErr == nil && bErr == nil { if aNum != bNum { return aNum - bNum } } else if (aErr == nil) != (bErr == nil) { if aErr == nil { return -1 } return 1 } else { if aPart != bPart { return strings.Compare(aPart, bPart) } } } return len(aParts) - len(bParts) } ================================================ FILE: utils/timeoutlock.go ================================================ package utils import ( "sync" "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/constants" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/sirupsen/logrus" ) type TimeoutLock struct { lock sync.Mutex state map[bson.ObjectID]bool stateLock sync.Mutex timeout time.Duration } func (l *TimeoutLock) Lock() (id bson.ObjectID) { id = bson.NewObjectID() l.lock.Lock() l.stateLock.Lock() l.state[id] = true l.stateLock.Unlock() if !constants.LockDebug { return } start := time.Now() go func() { for { time.Sleep(1 * time.Second) l.stateLock.Lock() state := l.state[id] l.stateLock.Unlock() if !state { return } if time.Since(start) > l.timeout { err := &errortypes.TimeoutError{ errors.New("utils: Multi lock timeout"), } logrus.WithFields(logrus.Fields{ "error": err, }).Error("utils: Lock timed out") return } } }() return } func (l *TimeoutLock) Unlock(id bson.ObjectID) { l.lock.Unlock() l.stateLock.Lock() delete(l.state, id) l.stateLock.Unlock() } func NewTimeoutLock(timeout time.Duration) *TimeoutLock { return &TimeoutLock{ lock: sync.Mutex{}, state: map[bson.ObjectID]bool{}, stateLock: sync.Mutex{}, timeout: timeout, } } ================================================ FILE: utils/unix.go ================================================ package utils import ( "crypto/sha512" "strconv" ) const b64x24Chars = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" func Base64x24(src []byte) (hash []byte) { if len(src) == 0 { return []byte{} } hashSize := (len(src) * 8) / 6 if (len(src) % 6) != 0 { hashSize += 1 } hash = make([]byte, hashSize) dst := hash for len(src) > 0 { switch len(src) { default: dst[0] = b64x24Chars[src[0]&0x3f] dst[1] = b64x24Chars[((src[0]>>6)|(src[1]<<2))&0x3f] dst[2] = b64x24Chars[((src[1]>>4)|(src[2]<<4))&0x3f] dst[3] = b64x24Chars[(src[2]>>2)&0x3f] src = src[3:] dst = dst[4:] case 2: dst[0] = b64x24Chars[src[0]&0x3f] dst[1] = b64x24Chars[((src[0]>>6)|(src[1]<<2))&0x3f] dst[2] = b64x24Chars[(src[1]>>4)&0x3f] src = src[2:] dst = dst[3:] case 1: dst[0] = b64x24Chars[src[0]&0x3f] dst[1] = b64x24Chars[(src[0]>>6)&0x3f] src = src[1:] dst = dst[2:] } } return } func GenerateShadow(passwd string) (output string, err error) { var i int rounds := 4096 saltStr, err := RandStr(8) if err != nil { return } salt := []byte(saltStr) passwdByt := []byte(passwd) alternateHash := sha512.New() alternateHash.Write(passwdByt) alternateHash.Write(salt) alternateHash.Write(passwdByt) alernateSum := alternateHash.Sum(nil) aSeqHash := sha512.New() aSeqHash.Write(passwdByt) aSeqHash.Write(salt) for i = len(passwdByt); i > 64; i -= 64 { aSeqHash.Write(alernateSum) } aSeqHash.Write(alernateSum[0:i]) for i = len(passwdByt); i > 0; i >>= 1 { if (i & 1) != 0 { aSeqHash.Write(alernateSum) } else { aSeqHash.Write(passwdByt) } } aSeqSum := aSeqHash.Sum(nil) pSeqHash := sha512.New() for i = 0; i < len(passwdByt); i++ { pSeqHash.Write(passwdByt) } pSeqSum := pSeqHash.Sum(nil) pSeq := make([]byte, 0, len(passwdByt)) for i = len(passwdByt); i > 64; i -= 64 { pSeq = append(pSeq, pSeqSum...) } pSeq = append(pSeq, pSeqSum[0:i]...) sSeqHash := sha512.New() for i = 0; i < (16 + int(aSeqSum[0])); i++ { sSeqHash.Write(salt) } sSeqSum := sSeqHash.Sum(nil) sSeq := make([]byte, 0, len(salt)) for i = len(salt); i > 64; i -= 64 { sSeq = append(sSeq, sSeqSum...) } sSeq = append(sSeq, sSeqSum[0:i]...) cSum := aSeqSum for i = 0; i < rounds; i++ { C := sha512.New() if (i & 1) != 0 { C.Write(pSeq) } else { C.Write(cSum) } if (i % 3) != 0 { C.Write(sSeq) } if (i % 7) != 0 { C.Write(pSeq) } if (i & 1) != 0 { C.Write(cSum) } else { C.Write(pSeq) } cSum = C.Sum(nil) } out := make([]byte, 0, 123) out = append(out, "$6$"...) out = append(out, []byte("rounds="+strconv.Itoa(rounds)+"$")...) out = append(out, salt...) out = append(out, '$') out = append(out, Base64x24([]byte{ cSum[42], cSum[21], cSum[0], cSum[1], cSum[43], cSum[22], cSum[23], cSum[2], cSum[44], cSum[45], cSum[24], cSum[3], cSum[4], cSum[46], cSum[25], cSum[26], cSum[5], cSum[47], cSum[48], cSum[27], cSum[6], cSum[7], cSum[49], cSum[28], cSum[29], cSum[8], cSum[50], cSum[51], cSum[30], cSum[9], cSum[10], cSum[52], cSum[31], cSum[32], cSum[11], cSum[53], cSum[54], cSum[33], cSum[12], cSum[13], cSum[55], cSum[34], cSum[35], cSum[14], cSum[56], cSum[57], cSum[36], cSum[15], cSum[16], cSum[58], cSum[37], cSum[38], cSum[17], cSum[59], cSum[60], cSum[39], cSum[18], cSum[19], cSum[61], cSum[40], cSum[41], cSum[20], cSum[62], cSum[63], })...) aSeqHash.Reset() alternateHash.Reset() pSeqHash.Reset() for i = 0; i < len(aSeqSum); i++ { aSeqSum[i] = 0 } for i = 0; i < len(alernateSum); i++ { alernateSum[i] = 0 } for i = 0; i < len(pSeq); i++ { pSeq[i] = 0 } output = string(out) return } ================================================ FILE: utils/webauthn.go ================================================ package utils import ( "fmt" "github.com/dropbox/godropbox/errors" "github.com/go-webauthn/webauthn/protocol" "github.com/pritunl/pritunl-cloud/errortypes" ) func ParseWebauthnError(err error) (newErr error) { if e, ok := err.(*protocol.Error); ok { newErr = &errortypes.AuthenticationError{ errors.Wrapf( err, "secondary: Webauthn error %s - %s - %s", e.Type, e.DevInfo, e.Details, ), } } else { newErr = &errortypes.AuthenticationError{ errors.Wrap(err, fmt.Sprintf( "secondary: Webauthn unknown error")), } } return } ================================================ FILE: validator/validator.go ================================================ package validator import ( "net/http" "time" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/audit" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/event" "github.com/pritunl/pritunl-cloud/policy" "github.com/pritunl/pritunl-cloud/user" ) func ValidateAdmin(db *database.Database, usr *user.User, isApi bool, r *http.Request) (deviceAuth bool, secProvider bson.ObjectID, errAudit audit.Fields, errData *errortypes.ErrorData, err error) { if !usr.ActiveUntil.IsZero() && usr.ActiveUntil.Before(time.Now()) { usr.ActiveUntil = time.Time{} usr.Disabled = true err = usr.CommitFields(db, set.NewSet("active_until", "disabled")) if err != nil { return } event.PublishDispatch(db, "user.change") errAudit = audit.Fields{ "error": "user_disabled", "message": "User is disabled from expired active time", } errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } return } if usr.Disabled { errAudit = audit.Fields{ "error": "user_disabled", "message": "User is disabled", } errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } return } if usr.Administrator != "super" { errAudit = audit.Fields{ "error": "user_not_super", "message": "User is not super user", } errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } return } if !isApi { policies, e := policy.GetRoles(db, usr.Roles) if e != nil { err = e return } for _, polcy := range policies { errData, err = polcy.ValidateUser(db, usr, r) if err != nil || errData != nil { return } } for _, polcy := range policies { if polcy.Disabled { continue } if polcy.AdminDeviceSecondary { deviceAuth = true } if !polcy.AdminSecondary.IsZero() && secProvider.IsZero() { secProvider = polcy.AdminSecondary } } } return } func ValidateUser(db *database.Database, usr *user.User, isApi bool, r *http.Request) (deviceAuth bool, secProvider bson.ObjectID, errAudit audit.Fields, errData *errortypes.ErrorData, err error) { if !usr.ActiveUntil.IsZero() && usr.ActiveUntil.Before(time.Now()) { usr.ActiveUntil = time.Time{} usr.Disabled = true err = usr.CommitFields(db, set.NewSet("active_until", "disabled")) if err != nil { return } event.PublishDispatch(db, "user.change") errAudit = audit.Fields{ "error": "user_disabled", "message": "User is disabled from expired active time", } errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } return } if usr.Disabled { errAudit = audit.Fields{ "error": "user_disabled", "message": "User is disabled", } errData = &errortypes.ErrorData{ Error: "unauthorized", Message: "Not authorized", } return } if !isApi { policies, e := policy.GetRoles(db, usr.Roles) if e != nil { err = e return } for _, polcy := range policies { errData, err = polcy.ValidateUser(db, usr, r) if err != nil || errData != nil { return } } for _, polcy := range policies { if polcy.Disabled { continue } if polcy.UserDeviceSecondary { deviceAuth = true } if !polcy.UserSecondary.IsZero() && secProvider.IsZero() { secProvider = polcy.UserSecondary } } } return } ================================================ FILE: version/cache.go ================================================ package version import ( "sync" "time" ) var ( cacheStore = map[string]*cache{} cacheLock = sync.Mutex{} ) const ( cacheTtl = 5 * time.Minute ) type cache struct { Version int Timestamp time.Time } func cacheCheck(module string, ver int) (supported bool) { cacheLock.Lock() defer cacheLock.Unlock() cach, ok := cacheStore[module] if !ok { return true } if time.Since(cach.Timestamp) > cacheTtl { delete(cacheStore, module) return true } return ver >= cach.Version } func cacheSet(module string, ver int) { cacheLock.Lock() defer cacheLock.Unlock() cacheStore[module] = &cache{ Version: ver, Timestamp: time.Now(), } existing, ok := cacheStore[module] if !ok || ver > existing.Version { cacheStore[module] = &cache{ Version: ver, Timestamp: time.Now(), } } } ================================================ FILE: version/utils.go ================================================ package version import ( "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" ) func Check(db *database.Database, module string, ver int) ( supported bool, err error) { if !cacheCheck(module, ver) { return false, nil } coll := db.Versions() vr := &Version{} err = coll.FindOneId(module, vr) if err != nil { if _, ok := err.(*database.NotFoundError); ok { vr = nil err = nil } else { return } } if vr == nil || ver >= vr.Version { supported = true return } cacheSet(module, vr.Version) return } func Set(db *database.Database, module string, ver int) (err error) { coll := db.Versions() opts := options.UpdateOne(). SetUpsert(true) _, err = coll.UpdateOne( db, &bson.M{ "_id": module, }, &bson.M{ "$max": &bson.M{ "version": ver, }, "$setOnInsert": &bson.M{ "_id": module, }, }, opts, ) if err != nil { err = database.ParseError(err) return } return } ================================================ FILE: version/version.go ================================================ package version type Version struct { Module string `bson:"_id,omitempty" json:"id"` Version int `bson:"version,omitempty" json:"version"` } ================================================ FILE: virtiofs/systemd.go ================================================ package virtiofs import ( "fmt" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) const systemdTemplate = `[Unit] Description=Pritunl Cloud VirtIO-FS Daemon After=network.target [Service] Type=simple User=root ExecStart=%s --socket-path="%s" --shared-dir="%s" --sandbox=namespace TimeoutStopSec=5 PrivateTmp=true ProtectSystem=full ProtectHostname=true ProtectKernelTunables=true ` func WriteService(vmId bson.ObjectID, shareId, sharePath string) (err error) { unitPath := paths.GetUnitPathShare(vmId, shareId) sockPath := paths.GetShareSockPath(vmId, shareId) output := fmt.Sprintf( systemdTemplate, GetVirtioFsdPath(), sockPath, sharePath, ) err = utils.CreateWrite(unitPath, output, 0600) if err != nil { return } return } func Start(db *database.Database, virt *vm.VirtualMachine, shareId, sharePath string) (err error) { unit := paths.GetUnitNameShare(virt.Id, shareId) logrus.WithFields(logrus.Fields{ "id": virt.Id.Hex(), "systemd_unit": unit, }).Info("virtiofs: Starting virtual machine virtiofsd") _ = systemd.Stop(unit) err = WriteService(virt.Id, shareId, sharePath) if err != nil { return } err = systemd.Reload() if err != nil { return } err = systemd.Start(unit) if err != nil { return } return } func Stop(virt *vm.VirtualMachine, shareId string) (err error) { unit := paths.GetUnitNameShare(virt.Id, shareId) _ = systemd.Stop(unit) return } ================================================ FILE: virtiofs/utils.go ================================================ package virtiofs import ( "github.com/pritunl/pritunl-cloud/utils" ) const ( Libexec = "/usr/libexec/virtiofsd" System = "/usr/bin/virtiofsd" ) func GetVirtioFsdPath() string { exists, _ := utils.Exists(System) if exists { return System } return Libexec } ================================================ FILE: virtiofs/virtiofs.go ================================================ package virtiofs import ( "time" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/organization" "github.com/pritunl/pritunl-cloud/paths" "github.com/pritunl/pritunl-cloud/systemd" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" ) func StartAll(db *database.Database, virt *vm.VirtualMachine) (err error) { mounts := []*vm.Mount{} unitPathShares := paths.GetUnitPathShares(virt.Id) _, err = utils.RemoveWildcard(unitPathShares) if err != nil { return } if len(virt.Mounts) == 0 { return } org, err := organization.Get(db, virt.Organization) if err != nil { return } if org == nil { err = &errortypes.ParseError{ errors.New("virtiofs: Failed to get org"), } return } for _, mount := range virt.Mounts { matchPath := false matchRoles := false for _, share := range node.Self.Shares { if share.MatchPath(mount.HostPath) { matchPath = true if utils.HasMatchingItem(share.Roles, org.Roles) { matchRoles = true break } } } if !matchPath && !matchRoles { err = &errortypes.ParseError{ errors.Newf("virtiofs: Failed to find matching "+ "share path for mount '%s'", mount.HostPath), } return } if !matchPath || !matchRoles { err = &errortypes.ParseError{ errors.Newf("virtiofs: Failed to find matching "+ "role for mount '%s'", mount.HostPath), } return } mounts = append(mounts, mount) } for _, mount := range mounts { shareId := paths.GetShareId(virt.Id, mount.Name) err = Start(db, virt, shareId, mount.HostPath) if err != nil { return } } time.Sleep(1 * time.Second) return } func StopAll(virt *vm.VirtualMachine) (err error) { unit := paths.GetUnitNameShares(virt.Id) _ = systemd.Stop(unit) return } ================================================ FILE: vm/constants.go ================================================ package vm const ( Starting = "starting" Running = "running" Stopped = "stopped" Failed = "failed" Updating = "updating" Provisioning = "provisioning" Bridge = "bridge" Vxlan = "vxlan" Physical = "physical" Lvm = "lvm" ) ================================================ FILE: vm/sort.go ================================================ package vm type SortDisks []*Disk func (d SortDisks) Len() int { return len(d) } func (d SortDisks) Swap(i, j int) { d[i], d[j] = d[j], d[i] } func (d SortDisks) Less(i, j int) bool { return d[i].Index < d[j].Index } ================================================ FILE: vm/utils.go ================================================ package vm import ( "bytes" "crypto/md5" "encoding/base32" "fmt" "strings" "github.com/pritunl/mongo-go-driver/v2/bson" ) func GetMacAddr(id bson.ObjectID, secondId bson.ObjectID) string { hash := md5.New() hash.Write([]byte(id.Hex())) hash.Write([]byte(secondId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:10] macBuf := bytes.Buffer{} for i, run := range macHash { macBuf.WriteRune(run) if i%2 == 1 && i != len(macHash)-1 { macBuf.WriteRune(':') } } return "00:" + macBuf.String() } func GetMacAddrExternal(id bson.ObjectID, secondId bson.ObjectID) string { hash := md5.New() hash.Write([]byte(id.Hex())) hash.Write([]byte(secondId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:10] macBuf := bytes.Buffer{} for i, run := range macHash { macBuf.WriteRune(run) if i%2 == 1 && i != len(macHash)-1 { macBuf.WriteRune(':') } } return "02:" + macBuf.String() } func GetMacAddrExternal6(id bson.ObjectID, secondId bson.ObjectID) string { hash := md5.New() hash.Write([]byte(id.Hex())) hash.Write([]byte(secondId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:10] macBuf := bytes.Buffer{} for i, run := range macHash { macBuf.WriteRune(run) if i%2 == 1 && i != len(macHash)-1 { macBuf.WriteRune(':') } } return "08:" + macBuf.String() } func GetMacAddrInternal(id bson.ObjectID, secondId bson.ObjectID) string { hash := md5.New() hash.Write([]byte(id.Hex())) hash.Write([]byte(secondId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:10] macBuf := bytes.Buffer{} for i, run := range macHash { macBuf.WriteRune(run) if i%2 == 1 && i != len(macHash)-1 { macBuf.WriteRune(':') } } return "04:" + macBuf.String() } func GetMacAddrHost(id bson.ObjectID, secondId bson.ObjectID) string { hash := md5.New() hash.Write([]byte(id.Hex())) hash.Write([]byte(secondId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:10] macBuf := bytes.Buffer{} for i, run := range macHash { macBuf.WriteRune(run) if i%2 == 1 && i != len(macHash)-1 { macBuf.WriteRune(':') } } return "06:" + macBuf.String() } func GetMacAddrNodePort(id bson.ObjectID, secondId bson.ObjectID) string { hash := md5.New() hash.Write([]byte(id.Hex())) hash.Write([]byte(secondId.Hex())) macHash := fmt.Sprintf("%x", hash.Sum(nil)) macHash = macHash[:10] macBuf := bytes.Buffer{} for i, run := range macHash { macBuf.WriteRune(run) if i%2 == 1 && i != len(macHash)-1 { macBuf.WriteRune(':') } } return "0a:" + macBuf.String() } func GetIface(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("p%s%d", strings.ToLower(hashSum), n) } func GetIfaceVirt(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("v%s%d", strings.ToLower(hashSum), n) } func GetIfaceExternal(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("e%s%d", strings.ToLower(hashSum), n) } func GetIfaceNodeExternal(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("r%s%d", strings.ToLower(hashSum), n) } func GetIfaceInternal(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("i%s%d", strings.ToLower(hashSum), n) } func GetIfaceNodeInternal(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("j%s%d", strings.ToLower(hashSum), n) } func GetIfaceHost(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("h%s%d", strings.ToLower(hashSum), n) } func GetIfaceNodePort(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("m%s%d", strings.ToLower(hashSum), n) } func GetIfaceCloud(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("o%s%d", strings.ToLower(hashSum), n) } func GetIfaceCloudVirt(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("t%s%d", strings.ToLower(hashSum), n) } func GetIfaceVlan(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("x%s%d", strings.ToLower(hashSum), n) } func GetNamespace(id bson.ObjectID, n int) string { hash := md5.New() hash.Write([]byte(id.Hex())) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("n%s%d", strings.ToLower(hashSum), n) } func GetHostVxlanIface(parentIface string) string { hash := md5.New() hash.Write([]byte(parentIface)) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("k%s0", strings.ToLower(hashSum)) } func GetHostBridgeIface(parentIface string) string { hash := md5.New() hash.Write([]byte(parentIface)) hashSum := base32.StdEncoding.EncodeToString(hash.Sum(nil))[:12] return fmt.Sprintf("b%s0", strings.ToLower(hashSum)) } ================================================ FILE: vm/vm.go ================================================ package vm import ( "fmt" "path" "strings" "time" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/usb" "github.com/pritunl/pritunl-cloud/utils" ) type VirtualMachine struct { Id bson.ObjectID `json:"id"` Organization bson.ObjectID `json:"organization"` UnixId int `json:"unix_id"` State string `json:"state"` Timestamp time.Time `json:"timestamp"` QemuVersion string `json:"qemu_version"` DiskType string `json:"disk_type"` DiskPool bson.ObjectID `json:"disk_pool"` Image bson.ObjectID `json:"image"` Processors int `json:"processors"` Memory int `json:"memory"` Hugepages bool `json:"hugepages"` Vnc bool `json:"vnc"` VncDisplay int `json:"vnc_display"` Spice bool `json:"spice"` SpicePort int `json:"spice_port"` Gui bool `json:"gui"` Disks []*Disk `json:"disks"` DisksAvailable bool `json:"-"` NetworkAdapters []*NetworkAdapter `json:"network_adapters"` CloudSubnet string `json:"cloud_subnet"` CloudVnic string `json:"cloud_vnic"` CloudVnicAttach string `json:"cloud_vnic_attach"` CloudPrivateIp string `json:"cloud_private_ip"` CloudPublicIp string `json:"cloud_public_ip"` CloudPublicIp6 string `json:"cloud_public_ip6"` DhcpIp string `json:"dhcp_ip"` DhcpIp6 string `json:"dhcp_ip6"` Uefi bool `json:"uefi"` SecureBoot bool `json:"secure_boot"` Tpm bool `json:"tpm"` DhcpServer bool `json:"dhcp_server"` Deployment bson.ObjectID `json:"deployment"` CloudType string `json:"cloud_type"` SystemKind string `json:"system_kind"` NoPublicAddress bool `json:"no_public_address"` NoPublicAddress6 bool `json:"no_public_address6"` NoHostAddress bool `json:"no_host_address"` Isos []*Iso `json:"isos"` UsbDevices []*UsbDevice `json:"usb_devices"` UsbDevicesAvailable bool `json:"-"` PciDevices []*PciDevice `json:"pci_devices"` DriveDevices []*DriveDevice `json:"drive_devices"` IscsiDevices []*IscsiDevice `json:"iscsi_devices"` Mounts []*Mount `json:"mounts"` ImdsVersion int `json:"imds_version"` ImdsClientSecret string `json:"-"` ImdsDhcpSecret string `json:"imds_dhcp_secret"` ImdsHostSecret string `json:"imds_host_secret"` } func (v *VirtualMachine) HasExternalNetwork() bool { return v.Vnc || v.Spice || (v.IscsiDevices != nil && len(v.IscsiDevices) > 0) } func (v *VirtualMachine) ProtectHome() bool { return !v.Gui } func (v *VirtualMachine) ProtectTmp() bool { return !v.Gui } func (v *VirtualMachine) Running() bool { return v.State == Starting || v.State == Running } func (v *VirtualMachine) GenerateImdsSecret() (err error) { v.ImdsVersion = 1 v.ImdsClientSecret, err = utils.RandStr(32) if err != nil { return } v.ImdsDhcpSecret, err = utils.RandStr(32) if err != nil { return } v.ImdsHostSecret, err = utils.RandStr(32) if err != nil { return } return } type Disk struct { Id bson.ObjectID `json:"id"` Index int `json:"index"` Path string `json:"path"` } type Iso struct { Name string `json:"name"` } type UsbDevice struct { Id string `json:"id"` Vendor string `json:"vendor"` Product string `json:"product"` Bus string `json:"bus"` Address string `json:"address"` } func (u *UsbDevice) Key() string { return fmt.Sprintf("%s_%s_%s_%s", u.Bus, u.Address, u.Vendor, u.Product, ) } func (u *UsbDevice) Copy() (device *UsbDevice) { device = &UsbDevice{ Id: u.Id, Vendor: u.Vendor, Product: u.Product, Bus: u.Bus, Address: u.Address, } return } func (u *UsbDevice) GetQemuId() string { return fmt.Sprintf("usb_%s_%s_%s_%s_%d", u.Bus, u.Address, u.Vendor, u.Product, utils.RandInt(1111, 9999), ) } func (u *UsbDevice) GetDevice() (device *usb.Device, err error) { device, err = usb.GetDevice(u.Bus, u.Address, u.Vendor, u.Product) if err != nil { return } return } type PciDevice struct { Slot string `json:"slot"` } type DriveDevice struct { Id string `json:"id"` Type string `json:"type"` VgName string `json:"vg_name"` LvName string `json:"lv_name"` } type IscsiDevice struct { Uri string `json:"iscsi"` } type Mount struct { Name string `json:"name"` Type string `json:"type"` Path string `json:"path"` HostPath string `json:"host_path"` } func (d *Disk) GetId() bson.ObjectID { idStr := strings.Split(path.Base(d.Path), ".")[0] objId, err := bson.ObjectIDFromHex(idStr) if err != nil { return bson.NilObjectID } return objId } func (d *Disk) Copy() (dsk *Disk) { dsk = &Disk{ Id: d.Id, Index: d.Index, Path: d.Path, } return } type NetworkAdapter struct { Type string `json:"type"` MacAddress string `json:"mac_address"` Vpc bson.ObjectID `json:"vpc"` Subnet bson.ObjectID `json:"subnet"` IpAddress string `json:"ip_address,omitempty"` IpAddress6 string `json:"ip_address6,omitempty"` } func (v *VirtualMachine) Commit(db *database.Database) (err error) { coll := db.Instances() addrs := []string{} addrs6 := []string{} for _, adapter := range v.NetworkAdapters { if adapter.IpAddress != "" { addrs = append(addrs, adapter.IpAddress) } if adapter.IpAddress6 != "" { addrs6 = append(addrs6, adapter.IpAddress6) } } data := bson.M{ "state": v.State, "timestamp": v.Timestamp, "public_ips": addrs, } if v.State == Running || len(addrs6) > 0 { data["public_ips6"] = addrs6 } if v.QemuVersion != "" { data["qemu_version"] = v.QemuVersion } err = coll.UpdateId(v.Id, &bson.M{ "$set": data, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if !v.Deployment.IsZero() { coll = db.Deployments() fields := bson.M{ "instance_data.public_ips": addrs, } if v.State == Running || len(addrs6) > 0 { fields["instance_data.public_ips6"] = addrs6 } err = coll.UpdateId(v.Deployment, &bson.M{ "$set": fields, }) if err != nil { err = database.ParseError(err) return } } return } func (v *VirtualMachine) CommitCloudVnic(db *database.Database) (err error) { coll := db.Instances() err = coll.UpdateId(v.Id, &bson.M{ "$set": &bson.M{ "cloud_vnic": v.CloudVnic, "cloud_vnic_attach": v.CloudVnicAttach, }, }) if err != nil { err = database.ParseError(err) return } return } func (v *VirtualMachine) CommitCloudIps(db *database.Database) (err error) { coll := db.Instances() cloudPivateAddrs := []string{} if v.CloudPrivateIp != "" { cloudPivateAddrs = append(cloudPivateAddrs, v.CloudPrivateIp) } cloudPublicAddrs := []string{} if v.CloudPublicIp != "" { cloudPublicAddrs = append(cloudPublicAddrs, v.CloudPublicIp) } cloudPublicAddrs6 := []string{} if v.CloudPublicIp6 != "" { cloudPublicAddrs6 = append(cloudPublicAddrs6, v.CloudPublicIp6) } err = coll.UpdateId(v.Id, &bson.M{ "$set": &bson.M{ "cloud_private_ips": cloudPivateAddrs, "cloud_public_ips": cloudPublicAddrs, "cloud_public_ips6": cloudPublicAddrs6, }, }) if err != nil { err = database.ParseError(err) return } if !v.Deployment.IsZero() { coll = db.Deployments() err = coll.UpdateId(v.Deployment, &bson.M{ "$set": &bson.M{ "instance_data.cloud_private_ips": cloudPivateAddrs, "instance_data.cloud_public_ips": cloudPublicAddrs, "instance_data.cloud_public_ips6": cloudPublicAddrs6, }, }) if err != nil { err = database.ParseError(err) return } } return } func (v *VirtualMachine) CommitState(db *database.Database, action string) ( err error) { coll := db.Instances() addrs := []string{} addrs6 := []string{} for _, adapter := range v.NetworkAdapters { if adapter.IpAddress != "" { addrs = append(addrs, adapter.IpAddress) } if adapter.IpAddress6 != "" { addrs6 = append(addrs6, adapter.IpAddress6) } } data := bson.M{ "action": action, "state": v.State, "timestamp": v.Timestamp, "public_ips": addrs, "public_ips6": addrs6, } if v.QemuVersion != "" { data["qemu_version"] = v.QemuVersion } err = coll.UpdateId(v.Id, &bson.M{ "$set": data, }) if err != nil { err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } return } ================================================ FILE: vmdk/utils.go ================================================ package vmdk import ( "bytes" "os" "github.com/dropbox/godropbox/errors" "github.com/google/uuid" "github.com/pritunl/pritunl-cloud/errortypes" ) func SetRandUuid(diskPath string) (err error) { diskUuid := uuid.New() diskFile, err := os.OpenFile(diskPath, os.O_RDWR, 0) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "vmdk: Failed to open file"), } return } defer func() { err = diskFile.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to write file"), } return } }() buffer := make([]byte, 10000) nRead, err := diskFile.Read(buffer) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "vmdk: Failed to read file"), } return } i := bytes.Index(buffer, []byte("ddb.uuid.image=")) newBuffer := append(buffer[:i+16], []byte(diskUuid.String())...) newBuffer = append(newBuffer, buffer[i+52:]...) nWrite, err := diskFile.WriteAt(newBuffer, 0) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to write file"), } return } if nRead != nWrite { err = &errortypes.WriteError{ errors.New("vmdk: Write count mismatch"), } return } err = diskFile.Sync() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to sync file"), } return } return } func SetUuid(diskPath string, diskUuid string) (err error) { diskFile, err := os.OpenFile(diskPath, os.O_RDWR, 0) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "vmdk: Failed to open file"), } return } defer func() { err = diskFile.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to write file"), } return } }() buffer := make([]byte, 10000) nRead, err := diskFile.Read(buffer) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "vmdk: Failed to read file"), } return } i := bytes.Index(buffer, []byte("ddb.uuid.image=")) newBuffer := append(buffer[:i+16], []byte(diskUuid)...) newBuffer = append(newBuffer, buffer[i+52:]...) nWrite, err := diskFile.WriteAt(newBuffer, 0) if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to write file"), } return } if nRead != nWrite { err = &errortypes.WriteError{ errors.New("vmdk: Write count mismatch"), } return } err = diskFile.Sync() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to sync file"), } return } return } func GetUuid(diskPath string) (diskUuid string, err error) { diskFile, err := os.Open(diskPath) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "vmdk: Failed to open file"), } return } defer func() { err = diskFile.Close() if err != nil { err = &errortypes.WriteError{ errors.Wrap(err, "vmdk: Failed to write file"), } return } }() buffer := make([]byte, 10000) _, err = diskFile.Read(buffer) if err != nil { err = &errortypes.ReadError{ errors.Wrap(err, "vmdk: Failed to read file"), } return } i := bytes.Index(buffer, []byte("ddb.uuid.image=")) diskUuid = string(buffer[i+16 : i+52]) return } ================================================ FILE: vpc/constants.go ================================================ package vpc const ( Destination = "destination" ) ================================================ FILE: vpc/ip.go ================================================ package vpc import ( "net" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/utils" ) type VpcIp struct { Id bson.ObjectID `bson:"_id,omitempty"` Vpc bson.ObjectID `bson:"vpc"` Subnet bson.ObjectID `bson:"subnet"` Ip int64 `bson:"ip"` Instance bson.ObjectID `bson:"instance"` } func (i *VpcIp) GetIp() net.IP { return utils.Int2IpAddress(i.Ip * 2) } func (i *VpcIp) GetIps() (net.IP, net.IP) { return utils.IpIndex2Ip(i.Ip) } ================================================ FILE: vpc/subnet.go ================================================ package vpc import ( "net" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/utils" ) type Subnet struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Network string `bson:"network" json:"network"` } func (s *Subnet) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { s.Name = utils.FilterName(s.Name) return } func (s *Subnet) GetNetwork() (network *net.IPNet, err error) { _, network, err = net.ParseCIDR(s.Network) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "vpc: Failed to parse subnet"), } return } return } func (s *Subnet) GetIndexRange() (start, stop int64, err error) { network, err := s.GetNetwork() if err != nil { return } start, err = utils.GetFirstIpIndex(network) if err != nil { return } stop, err = utils.GetLastIpIndex(network) if err != nil { return } return } ================================================ FILE: vpc/utils.go ================================================ package vpc import ( "bytes" "crypto/md5" "fmt" "net" "github.com/dropbox/godropbox/container/set" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/utils" ) func GetIp6(vpcId, instId bson.ObjectID) net.IP { netHash := md5.New() netHash.Write(vpcId[:]) netHashSum := fmt.Sprintf("%x", netHash.Sum(nil))[:12] instHash := md5.New() instHash.Write(instId[:]) instHashSum := "0" + fmt.Sprintf("%x", instHash.Sum(nil))[:15] ip := fmt.Sprintf("fd97%s%s", netHashSum, instHashSum) ipBuf := bytes.Buffer{} for i, run := range ip { if i%4 == 0 && i != 0 && i != len(ip)-1 { ipBuf.WriteRune(':') } ipBuf.WriteRune(run) } return net.ParseIP(ipBuf.String()) } func GetGatewayIp6(vpcId, instId bson.ObjectID) net.IP { netHash := md5.New() netHash.Write(vpcId[:]) netHashSum := fmt.Sprintf("%x", netHash.Sum(nil))[:12] instHash := md5.New() instHash.Write([]byte("gateway")) instHash.Write(instId[:]) instHashSum := "0" + fmt.Sprintf("%x", instHash.Sum(nil))[:15] ip := fmt.Sprintf("fd97%s%s", netHashSum, instHashSum) ipBuf := bytes.Buffer{} for i, run := range ip { if i%4 == 0 && i != 0 && i != len(ip)-1 { ipBuf.WriteRune(':') } ipBuf.WriteRune(run) } return net.ParseIP(ipBuf.String()) } func GetLinkIp6(vpcId, instId bson.ObjectID) net.IP { netHash := md5.New() netHash.Write(vpcId[:]) netHashSum := fmt.Sprintf("%x", netHash.Sum(nil))[:12] instHash := md5.New() instHash.Write(instId[:]) instHashSum := "0" + fmt.Sprintf("%x", instHash.Sum(nil))[:15] ip := fmt.Sprintf("fd97%s%s", netHashSum, instHashSum) ipBuf := bytes.Buffer{} for i, run := range ip { if i%4 == 0 && i != 0 && i != len(ip)-1 { ipBuf.WriteRune(':') } ipBuf.WriteRune(run) } return net.ParseIP(ipBuf.String()) } func GetGatewayLinkIp6(vpcId, instId bson.ObjectID) net.IP { netHash := md5.New() netHash.Write(vpcId[:]) netHashSum := fmt.Sprintf("%x", netHash.Sum(nil))[:12] instHash := md5.New() instHash.Write([]byte("gateway")) instHash.Write(instId[:]) instHashSum := "0" + fmt.Sprintf("%x", instHash.Sum(nil))[:15] ip := fmt.Sprintf("fd97%s%s", netHashSum, instHashSum) ipBuf := bytes.Buffer{} for i, run := range ip { if i%4 == 0 && i != 0 && i != len(ip)-1 { ipBuf.WriteRune(':') } ipBuf.WriteRune(run) } return net.ParseIP(ipBuf.String()) } func Get(db *database.Database, vcId bson.ObjectID) ( vc *Vpc, err error) { coll := db.Vpcs() vc = &Vpc{} err = coll.FindOneId(vcId, vc) if err != nil { return } return } func GetOrg(db *database.Database, orgId, vcId bson.ObjectID) ( vc *Vpc, err error) { coll := db.Vpcs() vc = &Vpc{} err = coll.FindOne(db, &bson.M{ "_id": vcId, "organization": orgId, }).Decode(vc) if err != nil { err = database.ParseError(err) return } return } func ExistsOrg(db *database.Database, orgId, vcId bson.ObjectID) ( exists bool, err error) { coll := db.Vpcs() n, err := coll.CountDocuments( db, &bson.M{ "_id": vcId, "organization": orgId, }, ) if err != nil { return } if n > 0 { exists = true } return } func GetAll(db *database.Database, query *bson.M) ( vcs []*Vpc, err error) { coll := db.Vpcs() vcs = []*Vpc{} cursor, err := coll.Find( db, query, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { vc := &Vpc{} err = cursor.Decode(vc) if err != nil { err = database.ParseError(err) return } vcs = append(vcs, vc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetOne(db *database.Database, query *bson.M) (vc *Vpc, err error) { coll := db.Vpcs() vc = &Vpc{} err = coll.FindOne(db, query).Decode(vc) if err != nil { err = database.ParseError(err) return } return } func GetAllNames(db *database.Database, query *bson.M) ( vpcs []*Vpc, err error) { coll := db.Vpcs() vpcs = []*Vpc{} cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetProjection(bson.D{ {"name", 1}, {"organization", 1}, {"datacenter", 1}, {"type", 1}, {"subnets", 1}, }), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { vc := &Vpc{} err = cursor.Decode(vc) if err != nil { err = database.ParseError(err) return } vpcs = append(vpcs, vc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetAllPaged(db *database.Database, query *bson.M, page, pageCount int64) (vcs []*Vpc, count int64, err error) { coll := db.Vpcs() vcs = []*Vpc{} if len(*query) == 0 { count, err = coll.EstimatedDocumentCount(db) if err != nil { err = database.ParseError(err) return } } else { count, err = coll.CountDocuments(db, query) if err != nil { err = database.ParseError(err) return } } if pageCount == 0 { pageCount = 20 } maxPage := count / pageCount if count == pageCount { maxPage = 0 } page = utils.Min64(page, maxPage) skip := utils.Min64(page*pageCount, count) cursor, err := coll.Find( db, query, options.Find(). SetSort(bson.D{{"name", 1}}). SetSkip(skip). SetLimit(pageCount), ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { vc := &Vpc{} err = cursor.Decode(vc) if err != nil { err = database.ParseError(err) return } vcs = append(vcs, vc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetIds(db *database.Database, ids []bson.ObjectID) ( vcs []*Vpc, err error) { coll := db.Vpcs() vcs = []*Vpc{} cursor, err := coll.Find( db, &bson.M{ "_id": &bson.M{ "$in": ids, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { vc := &Vpc{} err = cursor.Decode(vc) if err != nil { err = database.ParseError(err) return } vcs = append(vcs, vc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func GetDatacenter(db *database.Database, dcId bson.ObjectID) ( vcs []*Vpc, err error) { coll := db.Vpcs() vcs = []*Vpc{} cursor, err := coll.Find( db, &bson.M{ "datacenter": dcId, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { vc := &Vpc{} err = cursor.Decode(vc) if err != nil { err = database.ParseError(err) return } vcs = append(vcs, vc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func DistinctIds(db *database.Database, matchIds []bson.ObjectID) ( idsSet set.Set, err error) { coll := db.Images() ids := []bson.ObjectID{} idsSet = set.NewSet() err = coll.Distinct( db, "_id", &bson.M{ "_id": &bson.M{ "$in": matchIds, }, }, ).Decode(&ids) if err != nil { err = database.ParseError(err) return } for _, id := range ids { idsSet.Add(id) } return } func Remove(db *database.Database, vcId bson.ObjectID) (err error) { coll := db.VpcsIp() _, err = coll.DeleteMany(db, &bson.M{ "vpc": vcId, }) if err != nil { err = database.ParseError(err) return } coll = db.Vpcs() _, err = coll.DeleteOne(db, &bson.M{ "_id": vcId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveOrg(db *database.Database, orgId, vcId bson.ObjectID) ( err error) { coll := db.VpcsIp() _, err = coll.DeleteMany(db, &bson.M{ "vpc": vcId, }) if err != nil { err = database.ParseError(err) return } coll = db.Vpcs() _, err = coll.DeleteOne(db, &bson.M{ "organization": orgId, "_id": vcId, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveMulti(db *database.Database, vcIds []bson.ObjectID) (err error) { coll := db.VpcsIp() _, err = coll.DeleteMany(db, &bson.M{ "vpc": &bson.M{ "$in": vcIds, }, }) if err != nil { err = database.ParseError(err) return } coll = db.Vpcs() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": vcIds, }, }) if err != nil { err = database.ParseError(err) return } return } func RemoveMultiOrg(db *database.Database, orgId bson.ObjectID, vcIds []bson.ObjectID) (err error) { coll := db.VpcsIp() _, err = coll.DeleteMany(db, &bson.M{ "vpc": &bson.M{ "$in": vcIds, }, "organization": orgId, }) if err != nil { err = database.ParseError(err) return } coll = db.Vpcs() _, err = coll.DeleteMany(db, &bson.M{ "_id": &bson.M{ "$in": vcIds, }, }) if err != nil { err = database.ParseError(err) return } return } func GetIpsMapped(db *database.Database, ids []bson.ObjectID) ( vpcsMap map[bson.ObjectID][]*VpcIp, err error) { coll := db.VpcsIp() vpcsMap = map[bson.ObjectID][]*VpcIp{} cursor, err := coll.Find( db, &bson.M{ "vpc": &bson.M{ "$in": ids, }, }, ) if err != nil { err = database.ParseError(err) return } defer cursor.Close(db) for cursor.Next(db) { vc := &VpcIp{} err = cursor.Decode(vc) if err != nil { err = database.ParseError(err) return } vpcsMap[vc.Vpc] = append(vpcsMap[vc.Vpc], vc) } err = cursor.Err() if err != nil { err = database.ParseError(err) return } return } func RemoveInstanceIps(db *database.Database, instId bson.ObjectID) ( err error) { coll := db.VpcsIp() _, err = coll.UpdateMany(db, &bson.M{ "instance": instId, }, &bson.M{ "$set": &bson.M{ "instance": nil, }, }) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } func RemoveInstanceIp(db *database.Database, instId, vpcId bson.ObjectID) (err error) { coll := db.VpcsIp() _, err = coll.UpdateOne( db, &bson.M{ "vpc": vpcId, "instance": instId, }, &bson.M{ "$set": &bson.M{ "instance": nil, }, }, ) if err != nil { err = database.ParseError(err) switch err.(type) { case *database.NotFoundError: err = nil default: return } } return } ================================================ FILE: vpc/vpc.go ================================================ package vpc import ( "bytes" "crypto/md5" "fmt" "math/rand/v2" "net" "strconv" "strings" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/mongo-go-driver/v2/bson" "github.com/pritunl/mongo-go-driver/v2/mongo/options" "github.com/pritunl/pritunl-cloud/database" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/requires" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/utils" ) type Route struct { Destination string `bson:"destination" json:"destination"` Target string `bson:"target" json:"target"` } type Map struct { Type string `bson:"type" json:"type"` Destination string `bson:"destination" json:"destination"` Target string `bson:"target" json:"target"` } type Arp struct { Ip string `bson:"ip" json:"ip"` Mac string `bson:"mac" json:"mac"` } type Vpc struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Comment string `bson:"comment" json:"comment"` VpcId int `bson:"vpc_id" json:"vpc_id"` Network string `bson:"network" json:"network"` Network6 string `bson:"-" json:"network6"` Subnets []*Subnet `bson:"subnets" json:"subnets"` Organization bson.ObjectID `bson:"organization" json:"organization"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` IcmpRedirects bool `bson:"icmp_redirects" json:"icmp_redirects"` Routes []*Route `bson:"routes" json:"routes"` Maps []*Map `bson:"maps" json:"maps"` Arps []*Arp `bson:"arps" json:"arps"` DeleteProtection bool `bson:"delete_protection" json:"delete_protection"` curSubnets []*Subnet `bson:"-" json:"-"` } type Completion struct { Id bson.ObjectID `bson:"_id,omitempty" json:"id"` Name string `bson:"name" json:"name"` Organization bson.ObjectID `bson:"organization" json:"organization"` VpcId int `bson:"vpc_id" json:"vpc_id"` Network string `bson:"network" json:"network"` Subnets []*Subnet `bson:"subnets" json:"subnets"` Datacenter bson.ObjectID `bson:"datacenter" json:"datacenter"` } func (v *Vpc) Validate(db *database.Database) ( errData *errortypes.ErrorData, err error) { v.Name = utils.FilterName(v.Name) if v.Organization.IsZero() { errData = &errortypes.ErrorData{ Error: "organization_required", Message: "Missing required organization", } return } if v.Datacenter.IsZero() { errData = &errortypes.ErrorData{ Error: "datacenter_required", Message: "Missing required datacenter", } return } network, e := v.GetNetwork() if e != nil { errData = &errortypes.ErrorData{ Error: "network_invalid", Message: "Network address invalid", } return } network6, e := v.GetNetwork6() if e != nil { errData = &errortypes.ErrorData{ Error: "network_invalid6", Message: "IPv6 network address invalid", } return } v.Network = network.String() if v.Subnets == nil { v.Subnets = []*Subnet{} } subnetRanges := []struct { Id bson.ObjectID Start int64 Stop int64 }{} subs := []*Subnet{} for _, sub := range v.Subnets { errData, err = sub.Validate(db) if err != nil { return } if errData != nil { return } if sub.Network == "" { continue } subNetwork, e := sub.GetNetwork() if e != nil { errData = &errortypes.ErrorData{ Error: "subnet_network_invalid", Message: "Subnet network address invalid", } return } cidr, _ := subNetwork.Mask.Size() if cidr < 8 { errData = &errortypes.ErrorData{ Error: "subnet_network_size_invalid", Message: "Subnet network size too big", } return } if cidr > 28 { errData = &errortypes.ErrorData{ Error: "subnet_network_size_invalid", Message: "Subnet network size too small", } return } sub.Network = subNetwork.String() if !utils.NetworkContains(network, subNetwork) { errData = &errortypes.ErrorData{ Error: "subnet_network_range_invalid", Message: "Subnet network outside of VPC network", } return } subStart, subStop, e := sub.GetIndexRange() if e != nil { err = e return } subnetRanges = append(subnetRanges, struct { Id bson.ObjectID Start int64 Stop int64 }{ Id: sub.Id, Start: subStart, Stop: subStop, }) subs = append(subs, sub) } v.Subnets = subs for _, sub := range v.Subnets { subStart, subStop, e := sub.GetIndexRange() if e != nil { err = e return } for _, s := range subnetRanges { if s.Id == sub.Id { continue } if (subStart >= s.Start && subStart <= s.Stop) || (subStop >= s.Start && subStop <= s.Stop) { errData = &errortypes.ErrorData{ Error: "subnet_network_range_overlap", Message: "VPC cannot have overlapping subnets", } return } } } if v.Routes == nil { v.Routes = []*Route{} } destinations := set.NewSet() for _, route := range v.Routes { if destinations.Contains(route.Destination) { errData = &errortypes.ErrorData{ Error: "route_duplicate_destination", Message: "Duplicate mp destinations", } return } destinations.Add(route.Destination) if strings.Contains(route.Destination, ":") != strings.Contains(route.Target, ":") { errData = &errortypes.ErrorData{ Error: "route_target_destination_invalid", Message: "Route target/destination invalid", } return } _, destination, e := net.ParseCIDR(route.Destination) if e != nil { errData = &errortypes.ErrorData{ Error: "route_destination_invalid", Message: "Route destination invalid", } return } route.Destination = destination.String() if route.Destination == "0.0.0.0/0" || route.Destination == "::/0" { errData = &errortypes.ErrorData{ Error: "route_destination_invalid", Message: "Route destination invalid", } return } target := net.ParseIP(route.Target) if target == nil { errData = &errortypes.ErrorData{ Error: "route_target_invalid", Message: "Route target invalid", } return } route.Target = target.String() if route.Target == "0.0.0.0" { errData = &errortypes.ErrorData{ Error: "route_target_invalid", Message: "Route target invalid", } return } if !strings.Contains(route.Target, ":") { if !network.Contains(target) { errData = &errortypes.ErrorData{ Error: "route_target_invalid_network", Message: "Route target not in VPC network", } return } } else { if !network6.Contains(target) { errData = &errortypes.ErrorData{ Error: "route_target_invalid_network6", Message: "Route target not in VPC IPv6 network", } return } } } maps := []*Map{} destinations = set.NewSet() for _, mp := range v.Maps { if mp.Target == "" && mp.Destination == "" { continue } if mp.Type == "" { mp.Type = Destination } if mp.Type != Destination { errData = &errortypes.ErrorData{ Error: "map_invalid_type", Message: "Map type invalid", } return } _, destination, e := net.ParseCIDR(mp.Destination) if e != nil { errData = &errortypes.ErrorData{ Error: "map_destination_invalid", Message: "Map destination invalid", } return } mp.Destination = destination.String() target := net.ParseIP(mp.Target) if target == nil { errData = &errortypes.ErrorData{ Error: "map_target_invalid", Message: "Map target invalid", } return } mp.Target = target.String() if destinations.Contains(mp.Destination) { errData = &errortypes.ErrorData{ Error: "map_duplicate_destination", Message: "Duplicate map destinations", } return } destinations.Add(mp.Destination) if strings.Contains(mp.Destination, ":") != strings.Contains(mp.Target, ":") { errData = &errortypes.ErrorData{ Error: "map_target_destination_invalid", Message: "Map target/destination invalid", } return } if mp.Destination == "0.0.0.0/0" || mp.Destination == "::/0" { errData = &errortypes.ErrorData{ Error: "map_destination_invalid", Message: "Map destination invalid", } return } if mp.Target == "0.0.0.0" { errData = &errortypes.ErrorData{ Error: "map_target_invalid", Message: "Map target invalid", } return } if !strings.Contains(mp.Target, ":") { if !network.Contains(target) { errData = &errortypes.ErrorData{ Error: "map_target_invalid_network", Message: "Map target not in VPC network", } return } } else { if !network6.Contains(target) { errData = &errortypes.ErrorData{ Error: "map_target_invalid_network6", Message: "Map target not in VPC IPv6 network", } return } } maps = append(maps, mp) } v.Maps = maps arps := []*Arp{} ips := set.NewSet() for _, ap := range v.Arps { if ap.Ip == "" && ap.Mac == "" { continue } arpIp := net.ParseIP(ap.Ip) if arpIp == nil { errData = &errortypes.ErrorData{ Error: "arp_ip_invalid", Message: "Arp IP invalid", } return } ap.Ip = arpIp.String() if ips.Contains(ap.Ip) { errData = &errortypes.ErrorData{ Error: "arp_duplicate_destination", Message: "Duplicate arp destinations", } return } ips.Add(ap.Ip) arpMac, e := net.ParseMAC(ap.Mac) if e != nil { errData = &errortypes.ErrorData{ Error: "arp_mac_invalid", Message: "Arp mac invalid", } return } ap.Mac = arpMac.String() if !strings.Contains(ap.Ip, ":") { if !network.Contains(arpIp) { errData = &errortypes.ErrorData{ Error: "arp_ip_subnet_invalid", Message: "ARP IP outside of VPC network", } return } } else { if !network6.Contains(arpIp) { errData = &errortypes.ErrorData{ Error: "arp_ip6_subnet_invalid", Message: "ARP IP outside of VPC network", } return } } arps = append(arps, ap) } v.Arps = arps return } func (v *Vpc) PreCommit() { if v.Subnets == nil { v.curSubnets = []*Subnet{} } else { v.curSubnets = v.Subnets } } func (v *Vpc) PostCommit(db *database.Database) ( errData *errortypes.ErrorData, err error) { curSubnets := map[bson.ObjectID]*Subnet{} for _, sub := range v.curSubnets { curSubnets[sub.Id] = sub } newIds := set.NewSet() for _, sub := range v.Subnets { newIds.Add(sub.Id) curSub := curSubnets[sub.Id] if !sub.Id.IsZero() && curSub != nil { if curSub.Network != sub.Network { errData = &errortypes.ErrorData{ Error: "subnet_network_modified", Message: "Cannot modify VPC subnet", } return } } else { sub.Id = bson.NewObjectID() for _, s := range v.curSubnets { if s.Network == sub.Network { sub.Id = s.Id } } } } for _, sub := range v.curSubnets { if !newIds.Contains(sub.Id) { err = v.RemoveSubnet(db, sub.Id) if err != nil { return } } } return } func (v *Vpc) Json() { netHash := md5.New() netHash.Write(v.Id[:]) netHashSum := fmt.Sprintf("%x", netHash.Sum(nil))[:12] ip := fmt.Sprintf("fd97%s", netHashSum) ipBuf := bytes.Buffer{} for i, run := range ip { if i%4 == 0 && i != 0 && i != len(ip)-1 { ipBuf.WriteRune(':') } ipBuf.WriteRune(run) } v.Network6 = ipBuf.String() + "::/64" } func (v *Vpc) GetSubnet(id bson.ObjectID) (sub *Subnet) { if v.Subnets == nil || id.IsZero() { return } for _, s := range v.Subnets { if s.Id == id { sub = s return } } return } func (v *Vpc) GetSubnetName(name string) (sub *Subnet) { if v.Subnets == nil || name == "" { return } for _, s := range v.Subnets { if s.Name == name { sub = s return } } return } func (v *Vpc) GetNetwork() (network *net.IPNet, err error) { _, network, err = net.ParseCIDR(v.Network) if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "vpc: Failed to parse network"), } return } return } func (v *Vpc) GetNetwork6() (network *net.IPNet, err error) { netHash := md5.New() netHash.Write(v.Id[:]) netHashSum := fmt.Sprintf("%x", netHash.Sum(nil))[:12] ip := fmt.Sprintf("fd97%s", netHashSum) ipBuf := bytes.Buffer{} for i, run := range ip { if i%4 == 0 && i != 0 && i != len(ip)-1 { ipBuf.WriteRune(':') } ipBuf.WriteRune(run) } _, network, err = net.ParseCIDR(ipBuf.String() + "::/64") if err != nil { err = &errortypes.ParseError{ errors.Wrap(err, "vpc: Failed to parse network"), } return } return } func (v *Vpc) InitVpc() { if v.Subnets != nil { for _, sub := range v.Subnets { sub.Id = bson.NewObjectID() } } } func (v *Vpc) GetGateway() (ip net.IP, err error) { network, err := v.GetNetwork() if err != nil { return } ip = network.IP utils.IncIpAddress(ip) return } func (v *Vpc) GetGateway6() (ip net.IP, err error) { network, err := v.GetNetwork6() if err != nil { return } ip = network.IP utils.IncIpAddress(ip) return } func (v *Vpc) GetIp(db *database.Database, subId, instId bson.ObjectID) (instIp, gateIp net.IP, err error) { subnet := v.GetSubnet(subId) if subnet == nil { err = &errortypes.ReadError{ errors.New("vpc: Subnet does not exist"), } return } coll := db.VpcsIp() vpcIp := &VpcIp{} err = coll.FindOne(db, &bson.M{ "vpc": v.Id, "instance": instId, }).Decode(vpcIp) if err != nil { err = database.ParseError(err) vpcIp = nil if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } if vpcIp == nil { vpcIp = &VpcIp{} opts := options.FindOneAndUpdate(). SetReturnDocument(options.After) err = coll.FindOneAndUpdate( db, &bson.M{ "vpc": v.Id, "subnet": subId, "instance": nil, }, &bson.M{ "$set": &bson.M{ "instance": instId, }, }, opts, ).Decode(vpcIp) if err != nil { err = database.ParseError(err) vpcIp = nil if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } } if vpcIp == nil { vpcIp = &VpcIp{} err = coll.FindOne( db, &bson.M{ "vpc": v.Id, "subnet": subId, }, options.FindOne(). SetSort(bson.D{{"ip", -1}}), ).Decode(vpcIp) if err != nil { vpcIp = nil err = database.ParseError(err) if _, ok := err.(*database.NotFoundError); ok { err = nil } else { return } } start, stop, e := subnet.GetIndexRange() if e != nil { err = e return } curIp := start if vpcIp != nil { start = vpcIp.Ip + 1 } for { if curIp > stop { err = &errortypes.NotFoundError{ errors.New("vpc: Address pool full"), } return } vpcIp = &VpcIp{ Vpc: v.Id, Subnet: subId, Ip: curIp, Instance: instId, } _, err = coll.InsertOne(db, vpcIp) if err != nil { vpcIp = nil err = database.ParseError(err) if _, ok := err.(*database.DuplicateKeyError); ok { err = nil curIp += 1 continue } return } break } } instIp, gateIp = vpcIp.GetIps() gateIp, err = v.GetGateway() if err != nil { return } return } func (v *Vpc) GetIp6(instId bson.ObjectID) net.IP { return GetIp6(v.Id, instId) } func (v *Vpc) GetLinkIp6(instId bson.ObjectID) net.IP { return GetLinkIp6(v.Id, instId) } func (v *Vpc) GetGatewayIp6(instId bson.ObjectID) net.IP { return GetGatewayIp6(v.Id, instId) } func (v *Vpc) GetGatewayLinkIp6(instId bson.ObjectID) net.IP { return GetGatewayLinkIp6(v.Id, instId) } func (v *Vpc) RemoveSubnet(db *database.Database, subId bson.ObjectID) ( err error) { coll := db.VpcsIp() _, err = coll.DeleteMany(db, &bson.M{ "vpc": v.Id, "subnet": subId, }) if err != nil { err = database.ParseError(err) return } return } func (v *Vpc) Commit(db *database.Database) (err error) { coll := db.Vpcs() err = coll.Commit(v.Id, v) if err != nil { return } return } func (v *Vpc) CommitFields(db *database.Database, fields set.Set) ( err error) { coll := db.Vpcs() err = coll.CommitFields(v.Id, v, fields) if err != nil { return } return } func (v *Vpc) Insert(db *database.Database) (err error) { coll := db.Vpcs() if !v.Id.IsZero() { err = &errortypes.DatabaseError{ errors.New("vpc: Vpc already exists"), } return } vpcIds := []int{} parts := strings.Split(settings.Hypervisor.VlanRanges, ",") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } bounds := strings.Split(part, "-") if len(bounds) != 2 { err = &errortypes.ParseError{ errors.New("vpc: Invalid vlan range format"), } return } start, e := strconv.Atoi(strings.TrimSpace(bounds[0])) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "vpc: Invalid start vlan"), } return } end, e := strconv.Atoi(strings.TrimSpace(bounds[1])) if e != nil { err = &errortypes.ParseError{ errors.Wrap(e, "vpc: Invalid end vlan"), } return } if start >= end { err = &errortypes.ParseError{ errors.New("vpc: Start vlan larger than end vlan"), } return } for i := start; i <= end; i++ { vpcIds = append(vpcIds, i) } } rand.Shuffle(len(vpcIds), func(i, j int) { vpcIds[i], vpcIds[j] = vpcIds[j], vpcIds[i] }) for _, vpcId := range vpcIds { v.VpcId = vpcId resp, e := coll.InsertOne(db, v) if e != nil { err = database.ParseError(e) if _, ok := err.(*database.DuplicateKeyError); ok { err = nil continue } return } v.Id = resp.InsertedID.(bson.ObjectID) return } err = &errortypes.DatabaseError{ errors.New("vpc: No available vlan IDs"), } return } func init() { module := requires.New("vpc") module.After("settings") module.Handler = func() (err error) { db := database.GetDatabase() defer db.Close() coll := db.VpcsIp() _, err = coll.DeleteMany(db, &bson.M{ "subnet": nil, }) if err != nil { err = database.ParseError(err) return } return } } ================================================ FILE: vxlan/vxlan.go ================================================ package vxlan import ( "strconv" "strings" "time" "github.com/dropbox/godropbox/container/set" "github.com/dropbox/godropbox/errors" "github.com/pritunl/pritunl-cloud/errortypes" "github.com/pritunl/pritunl-cloud/ip" "github.com/pritunl/pritunl-cloud/iproute" "github.com/pritunl/pritunl-cloud/node" "github.com/pritunl/pritunl-cloud/settings" "github.com/pritunl/pritunl-cloud/state" "github.com/pritunl/pritunl-cloud/utils" "github.com/pritunl/pritunl-cloud/vm" "github.com/sirupsen/logrus" ) var ( curIfaces set.Set curDatabase set.Set curDatabaseIfaces set.Set ) func initIfaces(stat *state.State, internaIfaces []string) (err error) { ifaces := set.NewSet() newCurIfaces := set.NewSet() for _, iface := range stat.Interfaces() { ifaces.Add(iface) if len(iface) == 14 && (strings.HasPrefix(iface, "k") || strings.HasPrefix(iface, "b")) { newCurIfaces.Add(iface) } } parentVxIfaces := map[string]string{} parentBrIfaces := map[string]string{} newIfaces := set.NewSet() for _, iface := range internaIfaces { vxIface := vm.GetHostVxlanIface(iface) brIface := vm.GetHostBridgeIface(iface) parentVxIfaces[vxIface] = iface parentBrIfaces[brIface] = iface newIfaces.Add(vxIface) newIfaces.Add(brIface) } remIfaces := newCurIfaces.Copy() remIfaces.Subtract(newIfaces) for ifaceInf := range remIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "b") { logrus.WithFields(logrus.Fields{ "bridge": iface, }).Info("vxlan: Removing bridge") _, _ = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "set", "dev", iface, "down", ) _ = iproute.BridgeDelete("", iface) } } for ifaceInf := range remIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "k") { logrus.WithFields(logrus.Fields{ "vxlan": iface, }).Info("vxlan: Removing vxlan") _, _ = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "del", iface, ) } } time.Sleep(200 * time.Millisecond) newCurIfaces.Intersect(newIfaces) for ifaceInf := range newCurIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "b") { parentIface := parentBrIfaces[iface] if parentIface == "" { continue } vxIface := vm.GetHostVxlanIface(parentIface) if ifaces.Contains(vxIface) { _, err = utils.ExecCombinedOutputLogged( []string{"does not exist"}, "ip", "link", "set", vxIface, "master", iface, ) if err != nil { return } } } } time.Sleep(300 * time.Millisecond) for ifaceInf := range newCurIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "b") { parentIface := parentBrIfaces[iface] if parentIface == "" { continue } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", iface, "up", ) if err != nil { return } } } time.Sleep(500 * time.Millisecond) for ifaceInf := range newCurIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "k") { _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", iface, "up", ) if err != nil { return } } } ip.ClearIfacesCache("") curIfaces = newCurIfaces return } func initDatabase(stat *state.State, internaIfaces []string) (err error) { output, err := utils.ExecOutput("", "bridge", "fdb") if err != nil { return } nodeSelf := stat.Node() nodeDc := stat.NodeDatacenter() if nodeDc == nil { return } nodes := stat.Nodes() if nodes == nil { nodes = []*node.Node{} } newDb := set.NewSet() for _, nde := range nodes { if nde.Id == nodeSelf.Id || nde.Datacenter != nodeDc.Id || nde.PrivateIps == nil || !nodeDc.Vxlan() { continue } for _, privateIp := range nde.PrivateIps { newDb.Add(privateIp) } } newCurDb := set.NewSet() newCurIfaces := set.NewSet() ifaceBridgeDb := map[string]set.Set{} for _, line := range strings.Split(output, "\n") { fields := strings.Fields(line) if len(fields) != 7 || fields[0] != "00:00:00:00:00:00" { continue } iface := fields[2] if len(iface) != 14 || !strings.HasPrefix(iface, "k") { continue } dest := fields[4] bridgeSet := ifaceBridgeDb[iface] if bridgeSet == nil { bridgeSet = set.NewSet() ifaceBridgeDb[iface] = bridgeSet } newCurIfaces.Add(iface) bridgeSet.Add(dest) newCurDb.Add(dest) } for ifaceInf := range newCurIfaces.Iter() { iface := ifaceInf.(string) ifaceDb := ifaceBridgeDb[iface] addDb := newDb.Copy() addDb.Subtract(ifaceDb) for destInf := range addDb.Iter() { dest := destInf.(string) if dest == "" { logrus.Warning("vxlan: Empty destination") continue } _, err = utils.ExecCombinedOutputLogged( nil, "bridge", "fdb", "append", "00:00:00:00:00:00", "dev", iface, "dst", dest, ) if err != nil { return } } remDb := ifaceDb.Copy() remDb.Subtract(newDb) for destInf := range remDb.Iter() { dest := destInf.(string) if dest == "" { logrus.Warning("vxlan: Empty destination") continue } _, err = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", "No such file", }, "bridge", "fdb", "del", "00:00:00:00:00:00", "dev", iface, "dst", dest, ) if err != nil { return } } } curDatabase = newCurDb curDatabaseIfaces = newCurIfaces return } func syncIfaces(stat *state.State, internaIfaces []string, ifacesData map[string]*ip.Iface, retry bool) (err error) { cIfaces := curIfaces nodeSelf := stat.Node() clearCache := false lostIfaces := set.NewSet() for ifaceInf := range cIfaces.Iter() { iface := ifaceInf.(string) if ifacesData[iface] == nil { logrus.WithFields(logrus.Fields{ "iface": iface, }).Error("vxlan: Lost vxlan interface") lostIfaces.Add(iface) } } cIfaces.Subtract(lostIfaces) parentVxIfaces := map[string]string{} parentBrIfaces := map[string]string{} vxBrIfaces := map[string]string{} newIfaces := set.NewSet() if internaIfaces != nil && stat.VxLan() { for _, iface := range internaIfaces { vxIface := vm.GetHostVxlanIface(iface) brIface := vm.GetHostBridgeIface(iface) parentVxIfaces[vxIface] = iface parentBrIfaces[brIface] = iface vxBrIfaces[vxIface] = brIface newIfaces.Add(vxIface) newIfaces.Add(brIface) } } remIfaces := cIfaces.Copy() remIfaces.Subtract(newIfaces) for ifaceInf := range remIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "b") { logrus.WithFields(logrus.Fields{ "bridge": iface, }).Info("vxlan: Removing bridge") _, _ = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "set", "dev", iface, "down", ) _ = iproute.BridgeDelete("", iface) clearCache = true } } for ifaceInf := range remIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "k") { logrus.WithFields(logrus.Fields{ "vxlan": iface, }).Info("vxlan: Removing vxlan") _, _ = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", }, "ip", "link", "del", iface, ) clearCache = true } } addIfaces := newIfaces.Copy() addIfaces.Subtract(cIfaces) for ifaceInf := range addIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "k") { vxId := settings.Hypervisor.VxlanId destPort := settings.Hypervisor.VxlanDestPort parentIface := parentVxIfaces[iface] localIp := "" if nodeSelf.PrivateIps != nil { localIp = nodeSelf.PrivateIps[parentIface] } if localIp == "" { if !retry { nodeSelf.SyncNetwork(true) err = syncIfaces(stat, internaIfaces, ifacesData, true) return } err = &errortypes.NotFoundError{ errors.New("vxlan: Missing private IP for " + "internal interface"), } return } logrus.WithFields(logrus.Fields{ "vxlan": iface, }).Info("vxlan: Adding vxlan") _, err = utils.ExecCombinedOutputLogged( []string{ "File exists", }, "ip", "link", "add", iface, "type", "vxlan", "id", strconv.Itoa(vxId), "local", localIp, "dstport", strconv.Itoa(destPort), "dev", parentIface, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", iface, "up", ) if err != nil { return } clearCache = true } } for ifaceInf := range addIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "b") { parentIface := parentBrIfaces[iface] logrus.WithFields(logrus.Fields{ "bridge": iface, }).Info("vxlan: Adding bridge") err = iproute.BridgeAdd("", iface) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", vm.GetHostVxlanIface(parentIface), "master", iface, ) if err != nil { return } _, err = utils.ExecCombinedOutputLogged( nil, "ip", "link", "set", "dev", iface, "up", ) if err != nil { return } clearCache = true } } existIfaces := cIfaces.Copy() existIfaces.Subtract(remIfaces) for ifaceInf := range existIfaces.Iter() { iface := ifaceInf.(string) if strings.HasPrefix(iface, "k") { brIface := vxBrIfaces[iface] ifaceData := ifacesData[iface] if ifaceData != nil && ifaceData.Master != brIface { logrus.WithFields(logrus.Fields{ "vxlan": iface, "bridge": brIface, }).Warn("vxlan: Correct vxlan master") _, err = utils.ExecCombinedOutputLogged( []string{"does not exist"}, "ip", "link", "set", iface, "master", brIface, ) if err != nil { return } clearCache = true } } } if clearCache { ip.ClearIfacesCache("") } curIfaces = newIfaces return } func syncDatabase(stat *state.State, internaIfaces []string) (err error) { nodeSelf := stat.Node() cDatabase := curDatabase cIfaces := curDatabaseIfaces nodes := stat.Nodes() if nodes == nil { nodes = []*node.Node{} } nodeDc := stat.NodeDatacenter() if nodeDc == nil { return } newIfaces := set.NewSet() for _, iface := range internaIfaces { newIfaces.Add(vm.GetHostVxlanIface(iface)) } newDb := set.NewSet() for _, nde := range nodes { if nde.Id == nodeSelf.Id || nde.Datacenter != nodeDc.Id || nde.PrivateIps == nil || !nodeDc.Vxlan() { continue } for _, privateIp := range nde.PrivateIps { newDb.Add(privateIp) } } addDb := newDb.Copy() addDb.Subtract(cDatabase) for destInf := range addDb.Iter() { dest := destInf.(string) if dest == "" { logrus.Warning("vxlan: Empty destination") continue } for ifaceInf := range newIfaces.Iter() { iface := ifaceInf.(string) _, err = utils.ExecCombinedOutputLogged( nil, "bridge", "fdb", "append", "00:00:00:00:00:00", "dev", iface, "dst", dest, ) if err != nil { return } } } remDb := cDatabase.Copy() remDb.Subtract(newDb) for destInf := range remDb.Iter() { dest := destInf.(string) if dest == "" { logrus.Warning("vxlan: Empty destination") continue } for ifaceInf := range newIfaces.Iter() { iface := ifaceInf.(string) _, err = utils.ExecCombinedOutputLogged( []string{ "Cannot find device", "No such file", }, "bridge", "fdb", "del", "00:00:00:00:00:00", "dev", iface, "dst", dest, ) if err != nil { return } } } addIfaces := newIfaces.Copy() addIfaces.Subtract(cIfaces) for ifaceInf := range addIfaces.Iter() { iface := ifaceInf.(string) for destInf := range newDb.Iter() { dest := destInf.(string) if dest == "" { logrus.Warning("vxlan: Empty destination") continue } _, err = utils.ExecCombinedOutputLogged( nil, "bridge", "fdb", "append", "00:00:00:00:00:00", "dev", iface, "dst", dest, ) if err != nil { return } } } curDatabase = newDb curDatabaseIfaces = newIfaces return } func ApplyState(stat *state.State) (err error) { nodeSelf := stat.Node() internaIfaces := nodeSelf.InternalInterfaces if curIfaces == nil { err = initIfaces(stat, internaIfaces) if err != nil { return } } if curDatabase == nil { err = initDatabase(stat, internaIfaces) if err != nil { return } } ifacesData, err := ip.GetIfacesCached("") if err != nil { return } err = syncIfaces(stat, internaIfaces, ifacesData, false) if err != nil { return } err = syncDatabase(stat, internaIfaces) if err != nil { return } return } ================================================ FILE: www/.gitignore ================================================ *.js *.map node_modules/* jspm_packages/* !webpack.config.js !webpack.dev.config.js !static/* !dist/**/*.js !dist/**/*.map ================================================ FILE: www/README.md ================================================ ### pritunl-cloud-www ``` npm install cd ./node_modules/@github/webauthn-json/dist/ ln -sf ./esm/* ./ cd ../../../../ ``` ### development ``` ./node_modules/.bin/tsc --watch ./node_modules/.bin/webpack-cli --config webpack.dev.config --progress --color --watch ``` #### production ``` sh build.sh ``` ### clean ``` rm -rf app/*.js* rm -rf app/**/*.js* ``` ### internal ``` # desktop rsync --human-readable --archive --xattrs --progress --delete --exclude "/node_modules/*" --exclude "/jspm_packages/*" --exclude "app/*.js" --exclude "app/*.js.map" --exclude "app/**/*.js" --exclude "app/**/*.js.map" /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/ $NPM_SERVER:/home/cloud/pritunl-cloud-www/ # npm-server cd /home/cloud/pritunl-cloud-www/ rm package-lock.json rm -rf node_modules npm install rm ./node_modules/react-stripe-checkout/index.d.ts cd ./node_modules/@github/webauthn-json/dist/ ln -sf ./esm/* ./ cd ../../../../ # desktop scp $NPM_SERVER:/home/cloud/pritunl-cloud-www/package.json /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/package.json scp $NPM_SERVER:/home/cloud/pritunl-cloud-www/package-lock.json /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/package-lock.json rsync --human-readable --archive --xattrs --progress --delete $NPM_SERVER:/home/cloud/pritunl-cloud-www/node_modules/ /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/node_modules/ rsync --human-readable --archive --xattrs --progress --delete --exclude "/node_modules/*" --exclude "/jspm_packages/*" --exclude "app/*.js" --exclude "app/*.js.map" --exclude "app/**/*.js" --exclude "app/**/*.js.map" /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/ $NPM_SERVER:/home/cloud/pritunl-cloud-www/ # npm-server sh build.sh # desktop rsync --human-readable --archive --xattrs --progress --delete $NPM_SERVER:/home/cloud/pritunl-cloud-www/dist/ /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/dist/ rsync --human-readable --archive --xattrs --progress --delete $NPM_SERVER:/home/cloud/pritunl-cloud-www/dist-dev/ /home/cloud/go/src/github.com/pritunl/pritunl-cloud/www/dist-dev/ ``` ================================================ FILE: www/app/Alert.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Blueprint from '@blueprintjs/core'; let toaster: Blueprint.Toaster; export function success(message: string, timeout?: number): string { if (timeout === undefined) { timeout = 5000; } return toaster.show({ intent: Blueprint.Intent.SUCCESS, message: message, timeout: timeout, }); } export function info(message: string, timeout?: number): string { if (timeout === undefined) { timeout = 5000; } return toaster.show({ intent: Blueprint.Intent.PRIMARY, message: message, timeout: timeout, }); } export function warning(message: string, timeout?: number): string { if (timeout === undefined) { timeout = 5000; } return toaster.show({ intent: Blueprint.Intent.WARNING, message: message, timeout: timeout, }); } export function error(message: string, timeout?: number): string { if (timeout === undefined) { timeout = 5000; } return toaster.show({ intent: Blueprint.Intent.DANGER, message: message, timeout: timeout, }); } export function errorRes(res: SuperAgent.Response, message: string, timeout?: number): string { if (timeout === undefined) { timeout = 5000; } try { message = res.body.error_msg || message; } catch(err) { } return toaster.show({ intent: Blueprint.Intent.DANGER, message: message, timeout: timeout, }); } export function dismiss(key: string) { toaster.dismiss(key); } export function init() { if (toaster) { return; } if (Blueprint.OverlayToaster) { Blueprint.OverlayToaster.createAsync({ position: Blueprint.Position.BOTTOM, }).then((toastr) => { toaster = toastr }); } else { console.error('Failed to load toaster') } } ================================================ FILE: www/app/App.tsx ================================================ /// import * as Monaco from "monaco-editor"; import * as MonacoEditor from "@monaco-editor/react"; MonacoEditor.loader.config({ monaco: Monaco }) import * as React from 'react'; import * as ReactDOM from 'react-dom'; import * as Blueprint from '@blueprintjs/core'; import Main from './components/Main'; import * as Alert from './Alert'; import * as Event from './Event'; import * as Csrf from './Csrf'; import * as MiscUtils from './utils/MiscUtils'; import * as CompletionActions from './actions/CompletionActions'; import hljs from 'highlight.js/lib/core'; import plaintext from 'highlight.js/lib/languages/plaintext'; import bash from 'highlight.js/lib/languages/bash'; import python from 'highlight.js/lib/languages/python'; import yaml from 'highlight.js/lib/languages/yaml'; Csrf.load().then((): void => { Blueprint.FocusStyleManager.onlyShowFocusOnTabs(); Alert.init(); Event.init(); new MiscUtils.SyncInterval( async () => { let lastSync = CompletionActions.lastSync() if (lastSync && (Date.now() - lastSync) > 5000) { CompletionActions.sync(); } }, 1000, ) hljs.registerLanguage('plaintext', plaintext) hljs.registerLanguage('shell', bash) hljs.registerLanguage('python', python) hljs.registerLanguage('yaml', yaml) ReactDOM.render(
, document.getElementById('app'), ); }); ================================================ FILE: www/app/Constants.ts ================================================ /// import * as MobileDetect from 'mobile-detect'; let md = new MobileDetect(window.navigator.userAgent); export const user: boolean = !!(window as any).user; export const mobile = !!md.mobile(); export const mobileOs = md.os(); export const loadDelay = 700; export const u2fErrorCodes: {[index: number]: string} = { 0: 'ok', 1: 'other', 2: 'bad request', 3: 'configuration unsupported', 4: 'device ineligible', 5: 'timed out', }; export const sessionTypes: {[key: string]: string} = { admin: 'Admin', user: 'User', }; export const operatingSystems: {[key: string]: string} = { linux: 'Linux', macos_1010: 'macOS 10.10', macos_1011: 'macOS 10.11', macos_1012: 'macOS 10.12', macos_1013: 'macOS 10.13', macos_1014: 'macOS 10.14', macos_1015: 'macOS 10.15', macos11: 'macOS 11', macos12: 'macOS 12', macos13: 'macOS 13', macos14: 'macOS 14', macos15: 'macOS 15', macos16: 'macOS 16', windows_xp: 'Windows XP', windows_7: 'Windows 7', windows_vista: 'Windows Vista', windows_8: 'Windows 8', windows_10: 'Windows 10', windows_11: 'Windows 11', chrome_os: 'Chrome OS', ios_8: 'iOS 8', ios_9: 'iOS 9', ios_10: 'iOS 10', ios_11: 'iOS 11', ios_12: 'iOS 12', ios_13: 'iOS 13', ios_14: 'iOS 14', ios_15: 'iOS 15', ios_16: 'iOS 16', ios_17: 'iOS 17', ios_18: 'iOS 18', ios_19: 'iOS 19', ios_20: 'iOS 20', android_4: 'Android KitKat 4.4', android_5: 'Android Lollipop 5', android_6: 'Android Marshmallow 6', android_7: 'Android Nougat 7', android_8: 'Android Oreo 8', android_9: 'Android Pie 9', android_10: 'Android 10', android_11: 'Android 11', android_12: 'Android 12', android_13: 'Android 13', android_14: 'Android 14', android_15: 'Android 15', android_16: 'Android 16', blackberry_10: 'Blackerry 10', windows_phone: 'Windows Phone', firefox_os: 'Firefox OS', kindle: 'Kindle', }; export const browsers: {[key: string]: string} = { chrome: 'Chrome', chrome_mobile: 'Chrome Mobile', safari: 'Safari', safari_mobile: 'Safari Mobile', firefox: 'Firefox', firefox_mobile: 'Firefox Mobile', edge: 'Microsoft Edge', internet_explorer: 'Internet Explorer', internet_explorer_mobile: 'Internet Explorer Mobile', opera: 'Opera', opera_mobile: 'Opera Mobile', }; export const locations: {[key: string]: string} = { US: 'United States', US_AL: 'Alabama, US', US_AK: 'Alaska, US', US_AZ: 'Arizona, US', US_AR: 'Arkansas, US', US_CA: 'California, US', US_CO: 'Colorado, US', US_CT: 'Connecticut, US', US_DE: 'Delaware, US', US_FL: 'Florida, US', US_GA: 'Georgia, US', US_HI: 'Hawaii, US', US_ID: 'Idaho, US', US_IL: 'Illinois, US', US_IN: 'Indiana, US', US_IA: 'Iowa, US', US_KS: 'Kansas, US', US_KY: 'Kentucky, US', US_LA: 'Louisiana, US', US_ME: 'Maine, US', US_MD: 'Maryland, US', US_MA: 'Massachusetts, US', US_MI: 'Michigan, US', US_MN: 'Minnesota, US', US_MS: 'Mississippi, US', US_MO: 'Missouri, US', US_MT: 'Montana, US', US_NE: 'Nebraska, US', US_NV: 'Nevada, US', US_NH: 'New Hampshire, US', US_NJ: 'New Jersey, US', US_NM: 'New Mexico, US', US_NY: 'New York, US', US_NC: 'North Carolina, US', US_ND: 'North Dakota, US', US_OH: 'Ohio, US', US_OK: 'Oklahoma, US', US_OR: 'Oregon, US', US_PA: 'Pennsylvania, US', US_RI: 'Rhode Island, US', US_SC: 'South Carolina, US', US_SD: 'South Dakota, US', US_TN: 'Tennessee, US', US_TX: 'Texas, US', US_UT: 'Utah, US', US_VT: 'Vermont, US', US_VA: 'Virginia, US', US_WA: 'Washington, US', US_DC: 'Washington DC, US', US_WV: 'West Virginia, US', US_WI: 'Wisconsin, US', US_WY: 'Wyoming, US', AF: 'Afghanistan', AX: 'Åland Islands', AL: 'Albania', DZ: 'Algeria', AS: 'American Samoa', AD: 'Andorra', AO: 'Angola', AI: 'Anguilla', AQ: 'Antarctica', AG: 'Antigua and Barbuda', AR: 'Argentina', AM: 'Armenia', AW: 'Aruba', AU: 'Australia', AT: 'Austria', AZ: 'Azerbaijan', BS: 'Bahamas', BH: 'Bahrain', BD: 'Bangladesh', BB: 'Barbados', BY: 'Belarus', BE: 'Belgium', BZ: 'Belize', BJ: 'Benin', BM: 'Bermuda', BT: 'Bhutan', BO: 'Bolivia', BQ: 'Bonaire', BA: 'Bosnia and Herzegovina', BW: 'Botswana', BV: 'Bouvet Island', BR: 'Brazil', IO: 'British Indian Ocean Territory', BN: 'Brunei Darussalam', BG: 'Bulgaria', BF: 'Burkina Faso', BI: 'Burundi', CV: 'Cabo Verde', KH: 'Cambodia', CM: 'Cameroon', CA: 'Canada', KY: 'Cayman Islands', CF: 'Central African Republic', TD: 'Chad', CL: 'Chile', CN: 'China', CX: 'Christmas Island', CC: 'Cocos Islands', CO: 'Colombia', KM: 'Comoros', CG: 'Congo', CD: 'Congo Democratic Republic', CK: 'Cook Islands', CR: 'Costa Rica', CI: 'Côte dIvoire', HR: 'Croatia', CU: 'Cuba', CW: 'Curaçao', CY: 'Cyprus', CZ: 'Czechia', DK: 'Denmark', DJ: 'Djibouti', DM: 'Dominica', DO: 'Dominican Republic', EC: 'Ecuador', EG: 'Egypt', SV: 'El Salvador', GQ: 'Equatorial Guinea', ER: 'Eritrea', EE: 'Estonia', ET: 'Ethiopia', FK: 'Falkland Islands', FO: 'Faroe Islands', FJ: 'Fiji', FI: 'Finland', FR: 'France', GF: 'French Guiana', PF: 'French Polynesia', TF: 'French Southern Territories', GA: 'Gabon', GM: 'Gambia', GE: 'Georgia', DE: 'Germany', GH: 'Ghana', GI: 'Gibraltar', GR: 'Greece', GL: 'Greenland', GD: 'Grenada', GP: 'Guadeloupe', GU: 'Guam', GT: 'Guatemala', GG: 'Guernsey', GN: 'Guinea', GW: 'Guinea-Bissau', GY: 'Guyana', HT: 'Haiti', HM: 'Heard Island and McDonald Islands', VA: 'Holy See', HN: 'Honduras', HK: 'Hong Kong', HU: 'Hungary', IS: 'Iceland', IN: 'India', ID: 'Indonesia', IR: 'Iran', IQ: 'Iraq', IE: 'Ireland', IM: 'Isle of Man', IL: 'Israel', IT: 'Italy', JM: 'Jamaica', JP: 'Japan', JE: 'Jersey', JO: 'Jordan', KZ: 'Kazakhstan', KE: 'Kenya', KI: 'Kiribati', KP: 'North Korea', KR: 'South Korea', KW: 'Kuwait', KG: 'Kyrgyzstan', LA: 'Lao Peoples', LV: 'Latvia', LB: 'Lebanon', LS: 'Lesotho', LR: 'Liberia', LY: 'Libya', LI: 'Liechtenstein', LT: 'Lithuania', LU: 'Luxembourg', MO: 'Macao', MK: 'Macedonia', MG: 'Madagascar', MW: 'Malawi', MY: 'Malaysia', MV: 'Maldives', ML: 'Mali', MT: 'Malta', MH: 'Marshall Islands', MQ: 'Martinique', MR: 'Mauritania', MU: 'Mauritius', YT: 'Mayotte', MX: 'Mexico', FM: 'Micronesia', MD: 'Moldova', MC: 'Monaco', MN: 'Mongolia', ME: 'Montenegro', MS: 'Montserrat', MA: 'Morocco', MZ: 'Mozambique', MM: 'Myanmar', NA: 'Namibia', NR: 'Nauru', NP: 'Nepal', NL: 'Netherlands', NC: 'New Caledonia', NZ: 'New Zealand', NI: 'Nicaragua', NE: 'Niger', NG: 'Nigeria', NU: 'Niue', NF: 'Norfolk Island', MP: 'Northern Mariana Islands', NO: 'Norway', OM: 'Oman', PK: 'Pakistan', PW: 'Palau', PS: 'Palestine, State of', PA: 'Panama', PG: 'Papua New Guinea', PY: 'Paraguay', PE: 'Peru', PH: 'Philippines', PN: 'Pitcairn', PL: 'Poland', PT: 'Portugal', PR: 'Puerto Rico', QA: 'Qatar', RE: 'Réunion', RO: 'Romania', RU: 'Russian Federation', RW: 'Rwanda', BL: 'Saint Barthélemy', SH: 'Saint Helena', KN: 'Saint Kitts and Nevis', LC: 'Saint Lucia', MF: 'Saint Martin', PM: 'Saint Pierre and Miquelon', VC: 'Saint Vincent and the Grenadines', WS: 'Samoa', SM: 'San Marino', ST: 'Sao Tome and Principe', SA: 'Saudi Arabia', SN: 'Senegal', RS: 'Serbia', SC: 'Seychelles', SL: 'Sierra Leone', SG: 'Singapore', SX: 'Sint Maarten', SK: 'Slovakia', SI: 'Slovenia', SB: 'Solomon Islands', SO: 'Somalia', ZA: 'South Africa', GS: 'South Georgia and the South Sandwich Islands', SS: 'South Sudan', ES: 'Spain', LK: 'Sri Lanka', SD: 'Sudan', SR: 'Suriname', SJ: 'Svalbard and Jan Mayen', SZ: 'Swaziland', SE: 'Sweden', CH: 'Switzerland', SY: 'Syrian Arab Republic', TW: 'Taiwan', TJ: 'Tajikistan', TZ: 'Tanzania', TH: 'Thailand', TL: 'Timor-Leste', TG: 'Togo', TK: 'Tokelau', TO: 'Tonga', TT: 'Trinidad and Tobago', TN: 'Tunisia', TR: 'Turkey', TM: 'Turkmenistan', TC: 'Turks and Caicos Islands', TV: 'Tuvalu', UG: 'Uganda', UA: 'Ukraine', AE: 'United Arab Emirates', GB: 'United Kingdom', UM: 'United States Minor Outlying Islands', UY: 'Uruguay', UZ: 'Uzbekistan', VU: 'Vanuatu', VE: 'Venezuela', VN: 'Viet Nam', VG: 'British Virgin Islands', VI: 'US Virgin Islands', WF: 'Wallis and Futuna', EH: 'Western Sahara', YE: 'Yemen', ZM: 'Zambia', ZW: 'Zimbabwe', }; ================================================ FILE: www/app/Csrf.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as License from './License'; import * as Theme from './Theme'; export let token = ''; export function load(): Promise { return new Promise((resolve, reject): void => { SuperAgent .get('/csrf') .set('Accept', 'application/json') .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { reject(err); return; } token = res.body.token; License.setOracle(!!res.body.oracle_license); let theme = res.body.theme if (theme) { let themeParts = theme.split("-") if (themeParts[1] === "3") { Theme.themeVer3() } else { Theme.themeVer5() } if (themeParts[0] === "light") { Theme.light(); } else { Theme.dark(); } } else { Theme.dark(); } if (res.body.editor_theme) { Theme.setEditorTheme(res.body.editor_theme); } resolve(); }); }); } ================================================ FILE: www/app/EditorThemes.ts ================================================ // The MIT License (MIT) // Copyright (c) Brijesh Bittu // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import * as Monaco from "monaco-editor" let allHallowsEve = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "000000","token": ""}, {"foreground": "ffffff","background": "434242","token": "text"}, {"foreground": "ffffff","background": "000000","token": "source"}, {"foreground": "9933cc","token": "comment"}, {"foreground": "3387cc","token": "constant"}, {"foreground": "cc7833","token": "keyword"}, {"foreground": "d0d0ff","token": "meta.preprocessor.c"}, {"fontStyle": "italic","token": "variable.parameter"}, {"foreground": "ffffff","background": "9b9b9b","token": "source comment.block"}, {"foreground": "66cc33","token": "string"}, {"foreground": "aaaaaa","token": "string constant.character.escape"}, {"foreground": "000000","background": "cccc33","token": "string.interpolated"}, {"foreground": "cccc33","token": "string.regexp"}, {"foreground": "cccc33","token": "string.literal"}, {"foreground": "555555","token": "string.interpolated constant.character.escape"}, {"fontStyle": "underline","token": "entity.name.type"}, {"fontStyle": "italic underline","token": "entity.other.inherited-class"}, {"fontStyle": "underline","token": "entity.name.tag"}, {"foreground": "c83730","token": "support.function"} ], "colors": { "editor.foreground": "#FFFFFF", "editor.background": "#000000", "editor.selectionBackground": "#73597EE0", "editor.lineHighlightBackground": "#333300", "editorCursor.foreground": "#FFFFFF", "editorWhitespace.foreground": "#404040", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let amy = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "200020","token": ""}, {"foreground": "404080","background": "200020","fontStyle": "italic","token": "comment.block"}, {"foreground": "999999","token": "string"}, {"foreground": "707090","token": "constant.language"}, {"foreground": "7090b0","token": "constant.numeric"}, {"fontStyle": "bold","token": "constant.numeric.integer.int32"}, {"fontStyle": "italic","token": "constant.numeric.integer.int64"}, {"fontStyle": "bold italic","token": "constant.numeric.integer.nativeint"}, {"fontStyle": "underline","token": "constant.numeric.floating-point.ocaml"}, {"foreground": "666666","token": "constant.character"}, {"foreground": "8080a0","token": "constant.language.boolean"}, {"foreground": "008080","token": "variable.language"}, {"foreground": "008080","token": "variable.other"}, {"foreground": "a080ff","token": "keyword"}, {"foreground": "a0a0ff","token": "keyword.operator"}, {"foreground": "d0d0ff","token": "keyword.other.decorator"}, {"fontStyle": "underline","token": "keyword.operator.infix.floating-point.ocaml"}, {"fontStyle": "underline","token": "keyword.operator.prefix.floating-point.ocaml"}, {"foreground": "c080c0","token": "keyword.other.directive"}, {"foreground": "c080c0","fontStyle": "underline","token": "keyword.other.directive.line-number"}, {"foreground": "80a0ff","token": "keyword.control"}, {"foreground": "b0fff0","token": "storage"}, {"foreground": "60b0ff","token": "entity.name.type.variant"}, {"foreground": "60b0ff","fontStyle": "italic","token": "storage.type.variant.polymorphic"}, {"foreground": "60b0ff","fontStyle": "italic","token": "entity.name.type.variant.polymorphic"}, {"foreground": "b000b0","token": "entity.name.type.module"}, {"foreground": "b000b0","fontStyle": "underline","token": "entity.name.type.module-type.ocaml"}, {"foreground": "a00050","token": "support.other"}, {"foreground": "70e080","token": "entity.name.type.class"}, {"foreground": "70e0a0","token": "entity.name.type.class-type"}, {"foreground": "50a0a0","token": "entity.name.function"}, {"foreground": "80b0b0","token": "variable.parameter"}, {"foreground": "3080a0","token": "entity.name.type.token"}, {"foreground": "3cb0d0","token": "entity.name.type.token.reference"}, {"foreground": "90e0e0","token": "entity.name.function.non-terminal"}, {"foreground": "c0f0f0","token": "entity.name.function.non-terminal.reference"}, {"foreground": "009090","token": "entity.name.tag"}, {"background": "200020","token": "support.constant"}, {"foreground": "400080","background": "ffff00","fontStyle": "bold","token": "invalid.illegal"}, {"foreground": "200020","background": "cc66ff","token": "invalid.deprecated"}, {"background": "40008054","token": "source.camlp4.embedded"}, {"foreground": "805080","token": "punctuation"} ], "colors": { "editor.foreground": "#D0D0FF", "editor.background": "#200020", "editor.selectionBackground": "#80000080", "editor.lineHighlightBackground": "#80000040", "editorCursor.foreground": "#7070FF", "editorWhitespace.foreground": "#BFBFBF", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let birdsOfParadise = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "372725","token": ""}, {"foreground": "e6e1c4","background": "322323","token": "source"}, {"foreground": "6b4e32","fontStyle": "italic","token": "comment"}, {"foreground": "ef5d32","token": "keyword"}, {"foreground": "ef5d32","token": "storage"}, {"foreground": "efac32","token": "entity.name.function"}, {"foreground": "efac32","token": "keyword.other.name-of-parameter.objc"}, {"foreground": "efac32","fontStyle": "bold","token": "entity.name"}, {"foreground": "6c99bb","token": "constant.numeric"}, {"foreground": "7daf9c","token": "variable.language"}, {"foreground": "7daf9c","token": "variable.other"}, {"foreground": "6c99bb","token": "constant"}, {"foreground": "efac32","token": "variable.other.constant"}, {"foreground": "6c99bb","token": "constant.language"}, {"foreground": "d9d762","token": "string"}, {"foreground": "efac32","token": "support.function"}, {"foreground": "efac32","token": "support.type"}, {"foreground": "6c99bb","token": "support.constant"}, {"foreground": "efcb43","token": "meta.tag"}, {"foreground": "efcb43","token": "declaration.tag"}, {"foreground": "efcb43","token": "entity.name.tag"}, {"foreground": "efcb43","token": "entity.other.attribute-name"}, {"foreground": "ffffff","background": "990000","token": "invalid"}, {"foreground": "7daf9c","token": "constant.character.escaped"}, {"foreground": "7daf9c","token": "constant.character.escape"}, {"foreground": "7daf9c","token": "string source"}, {"foreground": "7daf9c","token": "string source.ruby"}, {"foreground": "e6e1dc","background": "144212","token": "markup.inserted"}, {"foreground": "e6e1dc","background": "660000","token": "markup.deleted"}, {"background": "2f33ab","token": "meta.diff.header"}, {"background": "2f33ab","token": "meta.separator.diff"}, {"background": "2f33ab","token": "meta.diff.index"}, {"background": "2f33ab","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#E6E1C4", "editor.background": "#372725", "editor.selectionBackground": "#16120E", "editor.lineHighlightBackground": "#1F1611", "editorCursor.foreground": "#E6E1C4", "editorWhitespace.foreground": "#42302D", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let blackboard = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "0C1021","token": ""}, {"foreground": "aeaeae","token": "comment"}, {"foreground": "d8fa3c","token": "constant"}, {"foreground": "ff6400","token": "entity"}, {"foreground": "fbde2d","token": "keyword"}, {"foreground": "fbde2d","token": "storage"}, {"foreground": "61ce3c","token": "string"}, {"foreground": "61ce3c","token": "meta.verbatim"}, {"foreground": "8da6ce","token": "support"}, {"foreground": "ab2a1d","fontStyle": "italic","token": "invalid.deprecated"}, {"foreground": "f8f8f8","background": "9d1e15","token": "invalid.illegal"}, {"foreground": "ff6400","fontStyle": "italic","token": "entity.other.inherited-class"}, {"foreground": "ff6400","token": "string constant.other.placeholder"}, {"foreground": "becde6","token": "meta.function-call.py"}, {"foreground": "7f90aa","token": "meta.tag"}, {"foreground": "7f90aa","token": "meta.tag entity"}, {"foreground": "ffffff","token": "entity.name.section"}, {"foreground": "d5e0f3","token": "keyword.type.variant"}, {"foreground": "f8f8f8","token": "source.ocaml keyword.operator.symbol"}, {"foreground": "8da6ce","token": "source.ocaml keyword.operator.symbol.infix"}, {"foreground": "8da6ce","token": "source.ocaml keyword.operator.symbol.prefix"}, {"fontStyle": "underline","token": "source.ocaml keyword.operator.symbol.infix.floating-point"}, {"fontStyle": "underline","token": "source.ocaml keyword.operator.symbol.prefix.floating-point"}, {"fontStyle": "underline","token": "source.ocaml constant.numeric.floating-point"}, {"background": "ffffff08","token": "text.tex.latex meta.function.environment"}, {"background": "7a96fa08","token": "text.tex.latex meta.function.environment meta.function.environment"}, {"foreground": "fbde2d","token": "text.tex.latex support.function"}, {"foreground": "ffffff","token": "source.plist string.unquoted"}, {"foreground": "ffffff","token": "source.plist keyword.operator"} ], "colors": { "editor.foreground": "#F8F8F8", "editor.background": "#0C1021", "editor.selectionBackground": "#253B76", "editor.lineHighlightBackground": "#FFFFFF0F", "editorCursor.foreground": "#FFFFFFA6", "editorWhitespace.foreground": "#FFFFFF40", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let brillianceBlack = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "0D0D0DFA","token": ""}, {"foreground": "000000","background": "ffffff","fontStyle": "bold","token": "meta.thomas_aylott"}, {"foreground": "555555","background": "ffffff","fontStyle": "underline","token": "meta.subtlegradient"}, {"foreground": "fffc80","background": "803d0033","token": "string -meta.tag -meta.doctype -string.regexp -string.literal -string.interpolated -string.quoted.literal -string.unquoted"}, {"foreground": "fffc80","background": "803d0033","token": "variable.parameter.misc.css"}, {"foreground": "fffc80","background": "803d0033","token": "text string source string"}, {"foreground": "fffc80","background": "803d0033","token": "string.unquoted string"}, {"foreground": "fffc80","background": "803d0033","token": "string.regexp string"}, {"foreground": "fffc80","background": "803d0033","token": "string.interpolated string"}, {"foreground": "fffc80","background": "803d0033","token": "meta.tag source string"}, {"foreground": "803d00","token": "punctuation.definition.string -meta.tag"}, {"foreground": "fff80033","token": "string.regexp punctuation.definition.string"}, {"foreground": "fff80033","token": "string.quoted.literal punctuation.definition.string"}, {"foreground": "fff80033","token": "string.quoted.double.ruby.mod punctuation.definition.string"}, {"foreground": "fff800","background": "43800033","token": "string.quoted.literal"}, {"foreground": "fff800","background": "43800033","token": "string.quoted.double.ruby.mod"}, {"foreground": "ffbc80","token": "string.unquoted -string.unquoted.embedded"}, {"foreground": "ffbc80","token": "string.quoted.double.multiline"}, {"foreground": "ffbc80","token": "meta.scope.heredoc"}, {"foreground": "fffc80","background": "1a1a1a","token": "string.interpolated"}, {"foreground": "fff800","background": "43800033","token": "string.regexp"}, {"background": "43800033","token": "string.regexp.group"}, {"foreground": "ffffff66","background": "43800033","token": "string.regexp.group string.regexp.group"}, {"foreground": "ffffff66","background": "43800033","token": "string.regexp.group string.regexp.group string.regexp.group"}, {"foreground": "ffffff66","background": "43800033","token": "string.regexp.group string.regexp.group string.regexp.group string.regexp.group"}, {"foreground": "86ff00","background": "43800033","token": "string.regexp.character-class"}, {"foreground": "00fff8","background": "43800033","token": "string.regexp.arbitrary-repitition"}, {"foreground": "803d00","token": "string.regexp punctuation.definition.string keyword.other"}, {"background": "0086ff33","token": "meta.group.assertion.regexp"}, {"foreground": "0086ff","token": "meta.assertion"}, {"foreground": "0086ff","token": "meta.group.assertion keyword.control.group.regexp"}, {"foreground": "0086ff","token": "meta.group.assertion punctuation.definition.group"}, {"foreground": "c6ff00","token": "constant.numeric"}, {"foreground": "86ff00","token": "constant.character"}, {"foreground": "07ff00","token": "constant.language"}, {"foreground": "07ff00","token": "keyword.other.unit"}, {"foreground": "07ff00","token": "constant.other.java"}, {"foreground": "07ff00","token": "constant.other.unit"}, {"foreground": "07ff00","background": "04800033","token": "constant.language.pseudo-variable"}, {"foreground": "00ff79","token": "constant.other"}, {"foreground": "00ff79","token": "constant.block"}, {"foreground": "00fff8","token": "support.constant"}, {"foreground": "00fff8","token": "constant.name"}, {"foreground": "00ff79","background": "00807c33","token": "variable.other.readwrite.global.pre-defined"}, {"foreground": "00ff79","background": "00807c33","token": "variable.language"}, {"foreground": "00fff8","token": "variable.other.constant"}, {"foreground": "00fff8","background": "00807c33","token": "support.variable"}, {"foreground": "00807c","background": "00438033","token": "variable.other.readwrite.global"}, {"foreground": "31a6ff","token": "variable.other"}, {"foreground": "31a6ff","token": "variable.js"}, {"foreground": "31a6ff","token": "punctuation.separator.variable"}, {"foreground": "0086ff","background": "0008ff33","token": "variable.other.readwrite.class"}, {"foreground": "406180","token": "variable.other.readwrite.instance"}, {"foreground": "406180","token": "variable.other.php"}, {"foreground": "406180","token": "variable.other.normal"}, {"foreground": "00000080","token": "punctuation.definition"}, {"foreground": "00000080","token": "punctuation.separator.variable"}, {"foreground": "7e0080","token": "storage -storage.modifier"}, {"background": "803d0033","token": "other.preprocessor"}, {"background": "803d0033","token": "entity.name.preprocessor"}, {"foreground": "666666","token": "variable.language.this.js"}, {"foreground": "803d00","token": "storage.modifier"}, {"foreground": "ff0000","token": "entity.name.class"}, {"foreground": "ff0000","token": "entity.name.type.class"}, {"foreground": "ff0000","token": "entity.name.type.module"}, {"foreground": "870000","background": "ff000033","token": "meta.class -meta.class.instance"}, {"foreground": "870000","background": "ff000033","token": "declaration.class"}, {"foreground": "870000","background": "ff000033","token": "meta.definition.class"}, {"foreground": "870000","background": "ff000033","token": "declaration.module"}, {"foreground": "ff0000","background": "87000033","token": "support.type"}, {"foreground": "ff0000","background": "87000033","token": "support.class"}, {"foreground": "ff3d44","token": "entity.name.instance"}, {"foreground": "ff3d44","token": "entity.name.type.instance"}, {"background": "831e5133","token": "meta.class.instance.constructor"}, {"foreground": "ff0086","background": "80000433","token": "entity.other.inherited-class"}, {"foreground": "ff0086","background": "80000433","token": "entity.name.module"}, {"foreground": "ff0086","token": "meta.definition.method"}, {"foreground": "ff0086","token": "entity.name.function"}, {"foreground": "ff0086","token": "entity.name.preprocessor"}, {"foreground": "9799ff","token": "variable.parameter.function"}, {"foreground": "9799ff","token": "variable.parameter -variable.parameter.misc.css"}, {"foreground": "9799ff","token": "meta.definition.method meta.definition.param-list"}, {"foreground": "9799ff","token": "meta.function.method.with-arguments variable.parameter.function"}, {"foreground": "800004","token": "punctuation.definition.parameters"}, {"foreground": "800004","token": "variable.parameter.function punctuation.separator.object"}, {"foreground": "782ec1","token": "keyword.other.special-method"}, {"foreground": "782ec1","token": "meta.function-call entity.name.function -(meta.function-call meta.function)"}, {"foreground": "782ec1","token": "support.function - variable"}, {"foreground": "9d3eff","token": "meta.function-call support.function - variable"}, {"foreground": "603f80","background": "603f8033","token": "support.function"}, {"foreground": "bc80ff","token": "punctuation.section.function"}, {"foreground": "bc80ff","token": "meta.brace.curly.function"}, {"foreground": "bc80ff","token": "meta.function-call punctuation.section.scope.ruby"}, {"foreground": "bc80ff","token": "meta.function-call punctuation.separator.object"}, {"foreground": "bc80ff","fontStyle": "bold","token": "meta.group.braces.round punctuation.section.scope"}, {"foreground": "bc80ff","fontStyle": "bold","token": "meta.group.braces.round meta.delimiter.object.comma"}, {"foreground": "bc80ff","fontStyle": "bold","token": "meta.group.braces.curly.function meta.delimiter.object.comma"}, {"foreground": "bc80ff","fontStyle": "bold","token": "meta.brace.round"}, {"foreground": "a88fc0","token": "meta.function-call.method.without-arguments"}, {"foreground": "a88fc0","token": "meta.function-call.method.without-arguments entity.name.function"}, {"foreground": "f800ff","token": "keyword.control"}, {"foreground": "7900ff","token": "keyword.other"}, {"foreground": "0000ce","token": "keyword.operator"}, {"foreground": "0000ce","token": "declaration.function.operator"}, {"foreground": "0000ce","token": "meta.preprocessor.c.include"}, {"foreground": "0000ce","token": "punctuation.separator.operator"}, {"foreground": "0000ce","background": "00009a33","token": "keyword.operator.assignment"}, {"foreground": "2136ce","token": "keyword.operator.arithmetic"}, {"foreground": "3759ff","background": "00009a33","token": "keyword.operator.logical"}, {"foreground": "7c88ff","token": "keyword.operator.comparison"}, {"foreground": "800043","token": "meta.class.instance.constructor keyword.operator.new"}, {"foreground": "cccccc","background": "333333","token": "meta.doctype"}, {"foreground": "cccccc","background": "333333","token": "meta.tag.sgml-declaration.doctype"}, {"foreground": "cccccc","background": "333333","token": "meta.tag.sgml.doctype"}, {"foreground": "333333","token": "meta.tag"}, {"foreground": "666666","background": "333333bf","token": "meta.tag.structure"}, {"foreground": "666666","background": "333333bf","token": "meta.tag.segment"}, {"foreground": "4c4c4c","background": "4c4c4c33","token": "meta.tag.block"}, {"foreground": "4c4c4c","background": "4c4c4c33","token": "meta.tag.xml"}, {"foreground": "4c4c4c","background": "4c4c4c33","token": "meta.tag.key"}, {"foreground": "ff7900","background": "803d0033","token": "meta.tag.inline"}, {"background": "803d0033","token": "meta.tag.inline source"}, {"foreground": "ff0007","background": "80000433","token": "meta.tag.other"}, {"foreground": "ff0007","background": "80000433","token": "entity.name.tag.style"}, {"foreground": "ff0007","background": "80000433","token": "entity.name.tag.script"}, {"foreground": "ff0007","background": "80000433","token": "meta.tag.block.script"}, {"foreground": "ff0007","background": "80000433","token": "source.js.embedded punctuation.definition.tag.html"}, {"foreground": "ff0007","background": "80000433","token": "source.css.embedded punctuation.definition.tag.html"}, {"foreground": "0086ff","background": "00438033","token": "meta.tag.form"}, {"foreground": "0086ff","background": "00438033","token": "meta.tag.block.form"}, {"foreground": "f800ff","background": "3c008033","token": "meta.tag.meta"}, {"background": "121212","token": "meta.section.html.head"}, {"background": "0043801a","token": "meta.section.html.form"}, {"foreground": "666666","token": "meta.tag.xml"}, {"foreground": "ffffff4d","token": "entity.name.tag"}, {"foreground": "ffffff33","token": "entity.other.attribute-name"}, {"foreground": "ffffff33","token": "meta.tag punctuation.definition.string"}, {"foreground": "ffffff66","token": "meta.tag string -source -punctuation"}, {"foreground": "ffffff66","token": "text source text meta.tag string -punctuation"}, {"foreground": "999999","token": "text meta.paragraph"}, {"foreground": "fff800","background": "33333333","token": "markup markup -(markup meta.paragraph.list)"}, {"foreground": "000000","background": "ffffff","token": "markup.hr"}, {"foreground": "ffffff","token": "markup.heading"}, {"foreground": "95d4ff80","fontStyle": "bold","token": "markup.bold"}, {"fontStyle": "italic","token": "markup.italic"}, {"fontStyle": "underline","token": "markup.underline"}, {"foreground": "0086ff","token": "meta.reference"}, {"foreground": "0086ff","token": "markup.underline.link"}, {"foreground": "00fff8","background": "00438033","token": "entity.name.reference"}, {"foreground": "00fff8","fontStyle": "underline","token": "meta.reference.list markup.underline.link"}, {"foreground": "00fff8","fontStyle": "underline","token": "text.html.textile markup.underline.link"}, {"background": "80808040","token": "markup.raw.block"}, {"background": "ffffff1a","token": "markup.quote"}, {"foreground": "ffffff","token": "markup.list meta.paragraph"}, {"foreground": "000000","background": "ffffff","token": "text.html.markdown"}, {"foreground": "000000","token": "text.html.markdown meta.paragraph"}, {"foreground": "555555","token": "text.html.markdown markup.list meta.paragraph"}, {"foreground": "000000","fontStyle": "bold","token": "text.html.markdown markup.heading"}, {"foreground": "8a5420","token": "text.html.markdown string"}, {"foreground": "666666","token": "meta.selector"}, {"foreground": "006680","token": "source.css meta.scope.property-list meta.property-value punctuation.definition.arguments"}, {"foreground": "006680","token": "source.css meta.scope.property-list meta.property-value punctuation.separator.arguments"}, {"foreground": "4f00ff","token": "entity.other.attribute-name.pseudo-element"}, {"foreground": "7900ff","token": "entity.other.attribute-name.pseudo-class"}, {"foreground": "7900ff","token": "entity.other.attribute-name.tag.pseudo-class"}, {"foreground": "f800ff","token": "meta.selector entity.other.attribute-name.class"}, {"foreground": "ff0086","token": "meta.selector entity.other.attribute-name.id"}, {"foreground": "ff0007","token": "meta.selector entity.name.tag"}, {"foreground": "ff7900","fontStyle": "bold","token": "entity.name.tag.wildcard"}, {"foreground": "ff7900","fontStyle": "bold","token": "entity.other.attribute-name.universal"}, {"foreground": "c25a00","token": "source.css entity.other.attribute-name.attribute"}, {"foreground": "673000","token": "source.css meta.attribute-selector keyword.operator.comparison"}, {"foreground": "333333","fontStyle": "bold","token": "meta.scope.property-list"}, {"foreground": "999999","token": "meta.property-name"}, {"foreground": "ffffff","background": "0d0d0d","token": "support.type.property-name"}, {"foreground": "999999","background": "19191980","token": "meta.property-value"}, {"background": "000000","token": "text.latex markup.raw"}, {"foreground": "bc80ff","token": "text.latex support.function -support.function.textit -support.function.emph"}, {"foreground": "ffffffbf","token": "text.latex support.function.section"}, {"foreground": "000000","background": "ffffff","token": "text.latex entity.name.section -meta.group -keyword.operator.braces"}, {"background": "00000080","token": "text.latex keyword.operator.delimiter"}, {"foreground": "999999","token": "text.latex keyword.operator.brackets"}, {"foreground": "666666","token": "text.latex keyword.operator.braces"}, {"foreground": "0008ff4d","background": "00008033","token": "meta.footnote"}, {"background": "ffffff0d","token": "text.latex meta.label.reference"}, {"foreground": "ff0007","background": "260001","token": "text.latex keyword.control.ref"}, {"foreground": "ffbc80","background": "400002","token": "text.latex variable.parameter.label.reference"}, {"foreground": "ff0086","background": "260014","token": "text.latex keyword.control.cite"}, {"foreground": "ffbfe1","background": "400022","token": "variable.parameter.cite"}, {"foreground": "ffffff80","token": "text.latex variable.parameter.label"}, {"foreground": "cdcdcd","token": "meta.function markup"}, {"foreground": "33333333","token": "text.latex meta.group.braces"}, {"foreground": "33333333","background": "00000080","token": "text.latex meta.environment.list"}, {"foreground": "33333333","background": "00000080","token": "text.latex meta.environment.list meta.environment.list"}, {"foreground": "33333333","background": "000000","token": "text.latex meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "33333333","token": "text.latex meta.environment.list meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "33333333","token": "text.latex meta.environment.list meta.environment.list meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "33333333","token": "text.latex meta.environment.list meta.environment.list meta.environment.list meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "000000","background": "cccccc","token": "text.latex meta.end-document"}, {"foreground": "000000","background": "cccccc","token": "text.latex meta.begin-document"}, {"foreground": "000000","background": "cccccc","token": "meta.end-document.latex support.function"}, {"foreground": "000000","background": "cccccc","token": "meta.end-document.latex variable.parameter"}, {"foreground": "000000","background": "cccccc","token": "meta.begin-document.latex support.function"}, {"foreground": "000000","background": "cccccc","token": "meta.begin-document.latex variable.parameter"}, {"foreground": "00ffaa","background": "00805533","token": "meta.brace.erb.return-value"}, {"background": "8080801a","token": "source.ruby.rails.embedded.return-value.one-line"}, {"foreground": "00fff8","background": "00fff81a","token": "punctuation.section.embedded -(source string source punctuation.section.embedded)"}, {"foreground": "00fff8","background": "00fff81a","token": "meta.brace.erb.html"}, {"background": "00fff81a","token": "source.ruby.rails.embedded.one-line"}, {"foreground": "406180","token": "source string source punctuation.section.embedded"}, {"background": "0d0d0d","token": "source.js.embedded"}, {"background": "000000","token": "meta.brace.erb"}, {"foreground": "ffffff","background": "33333380","token": "source string source"}, {"foreground": "999999","background": "00000099","token": "source string.interpolated source"}, {"background": "3333331a","token": "source source"}, {"background": "3333331a","token": "source.java.embedded"}, {"foreground": "ffffff","token": "text -text.xml.strict"}, {"foreground": "cccccc","background": "000000","token": "text source"}, {"foreground": "cccccc","background": "000000","token": "meta.scope.django.template"}, {"foreground": "999999","token": "text string source"}, {"foreground": "330004","background": "ff0007","fontStyle": "bold","token": "invalid -invalid.SOMETHING"}, {"foreground": "ff3600","fontStyle": "underline","token": "invalid.SOMETHING"}, {"foreground": "333333","token": "meta.syntax"}, {"foreground": "4c4c4c","background": "33333333","token": "comment -comment.line"}, {"foreground": "4c4c4c","fontStyle": "italic","token": "comment.line"}, {"fontStyle": "italic","token": "text comment.block -source"}, {"foreground": "40ff9a","background": "00401e","token": "markup.inserted"}, {"foreground": "ff40a3","background": "400022","token": "markup.deleted"}, {"foreground": "ffff55","background": "803d00","token": "markup.changed"}, {"foreground": "ffffff","background": "000000","token": "text.subversion-commit meta.scope.changed-files"}, {"foreground": "ffffff","background": "000000","token": "text.subversion-commit meta.scope.changed-files.svn meta.diff.separator"}, {"foreground": "000000","background": "ffffff","token": "text.subversion-commit"}, {"foreground": "7f7f7f","background": "ffffff03","fontStyle": "bold","token": "punctuation.terminator"}, {"foreground": "7f7f7f","background": "ffffff03","fontStyle": "bold","token": "meta.delimiter"}, {"foreground": "7f7f7f","background": "ffffff03","fontStyle": "bold","token": "punctuation.separator.method"}, {"background": "00000080","token": "punctuation.terminator.statement"}, {"background": "00000080","token": "meta.delimiter.statement.js"}, {"background": "00000040","token": "meta.delimiter.object.js"}, {"foreground": "803d00","fontStyle": "bold","token": "string.quoted.single.brace"}, {"foreground": "803d00","fontStyle": "bold","token": "string.quoted.double.brace"}, {"foreground": "333333","background": "dcdcdc","token": "text.blog"}, {"foreground": "333333","background": "dcdcdc","token": "text.mail"}, {"foreground": "cccccc","background": "000000","token": "text.blog text"}, {"foreground": "cccccc","background": "000000","token": "text.mail text"}, {"foreground": "06403e","background": "00fff81a","token": "meta.header.blog keyword.other"}, {"foreground": "06403e","background": "00fff81a","token": "meta.header.mail keyword.other"}, {"foreground": "803d00","background": "ffff551a","token": "meta.header.blog string.unquoted.blog"}, {"foreground": "803d00","background": "ffff551a","token": "meta.header.mail string.unquoted"}, {"foreground": "ff0000","token": "source.ocaml entity.name.type.module"}, {"foreground": "ff0000","background": "83000033","token": "source.ocaml support.other.module"}, {"foreground": "00fff8","token": "entity.name.type.variant"}, {"foreground": "00ff79","token": "source.ocaml entity.name.tag"}, {"foreground": "00ff79","token": "source.ocaml meta.record.definition"}, {"foreground": "ffffff","fontStyle": "bold","token": "punctuation.separator.parameters"}, {"foreground": "4c4c4c","background": "33333333","token": "meta.brace.pipe"}, {"foreground": "666666","fontStyle": "bold","token": "meta.brace.erb"}, {"foreground": "666666","fontStyle": "bold","token": "source.ruby.embedded.source.brace"}, {"foreground": "666666","fontStyle": "bold","token": "punctuation.section.dictionary"}, {"foreground": "666666","fontStyle": "bold","token": "punctuation.terminator.dictionary"}, {"foreground": "666666","fontStyle": "bold","token": "punctuation.separator.object"}, {"foreground": "666666","fontStyle": "bold","token": "punctuation.separator.statement"}, {"foreground": "666666","fontStyle": "bold","token": "punctuation.separator.key-value.css"}, {"foreground": "999999","fontStyle": "bold","token": "punctuation.section.scope.curly"}, {"foreground": "999999","fontStyle": "bold","token": "punctuation.section.scope"}, {"foreground": "0c823b","fontStyle": "bold","token": "punctuation.separator.objects"}, {"foreground": "0c823b","fontStyle": "bold","token": "meta.group.braces.curly meta.delimiter.object.comma"}, {"foreground": "0c823b","fontStyle": "bold","token": "punctuation.separator.key-value -meta.tag"}, {"foreground": "0c823b","fontStyle": "bold","token": "source.ocaml punctuation.separator.match-definition"}, {"foreground": "800043","token": "punctuation.separator.parameters.function.js"}, {"foreground": "800043","token": "punctuation.definition.function"}, {"foreground": "800043","token": "punctuation.separator.function-return"}, {"foreground": "800043","token": "punctuation.separator.function-definition"}, {"foreground": "800043","token": "punctuation.definition.arguments"}, {"foreground": "800043","token": "punctuation.separator.arguments"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "meta.group.braces.square punctuation.section.scope"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "meta.group.braces.square meta.delimiter.object.comma"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "meta.brace.square"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "punctuation.separator.array"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "punctuation.section.array"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "punctuation.definition.array"}, {"foreground": "7f5e40","background": "803d001a","fontStyle": "bold","token": "punctuation.definition.constant.range"}, {"background": "803d001a","token": "meta.structure.array -punctuation.definition.array"}, {"background": "803d001a","token": "meta.definition.range -punctuation.definition.constant.range"}, {"background": "00000080","token": "meta.brace.curly meta.group.css"}, {"foreground": "666666","background": "00000080","token": "meta.source.embedded"}, {"foreground": "666666","background": "00000080","token": "entity.other.django.tagbraces"}, {"background": "00000080","token": "source.ruby meta.even-tab"}, {"background": "00000080","token": "source.ruby meta.even-tab.group2"}, {"background": "00000080","token": "source.ruby meta.even-tab.group4"}, {"background": "00000080","token": "source.ruby meta.even-tab.group6"}, {"background": "00000080","token": "source.ruby meta.even-tab.group8"}, {"background": "00000080","token": "source.ruby meta.even-tab.group10"}, {"background": "00000080","token": "source.ruby meta.even-tab.group12"}, {"foreground": "666666","token": "meta.block.slate"}, {"foreground": "cccccc","token": "meta.block.content.slate"}, {"background": "0a0a0a","token": "meta.odd-tab.group1"}, {"background": "0a0a0a","token": "meta.group.braces"}, {"background": "0a0a0a","token": "meta.block.slate"}, {"background": "0a0a0a","token": "text.xml.strict meta.tag"}, {"background": "0a0a0a","token": "meta.paren-group"}, {"background": "0a0a0a","token": "meta.section"}, {"background": "0e0e0e","token": "meta.even-tab.group2"}, {"background": "0e0e0e","token": "meta.group.braces meta.group.braces"}, {"background": "0e0e0e","token": "meta.block.slate meta.block.slate"}, {"background": "0e0e0e","token": "text.xml.strict meta.tag meta.tag"}, {"background": "0e0e0e","token": "meta.group.braces meta.group.braces"}, {"background": "0e0e0e","token": "meta.paren-group meta.paren-group"}, {"background": "0e0e0e","token": "meta.section meta.section"}, {"background": "111111","token": "meta.odd-tab.group3"}, {"background": "111111","token": "meta.group.braces meta.group.braces meta.group.braces"}, {"background": "111111","token": "meta.block.slate meta.block.slate meta.block.slate"}, {"background": "111111","token": "text.xml.strict meta.tag meta.tag meta.tag"}, {"background": "111111","token": "meta.group.braces meta.group.braces meta.group.braces"}, {"background": "111111","token": "meta.paren-group meta.paren-group meta.paren-group"}, {"background": "111111","token": "meta.section meta.section meta.section"}, {"background": "151515","token": "meta.even-tab.group4"}, {"background": "151515","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "151515","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "151515","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag"}, {"background": "151515","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "151515","token": "meta.paren-group meta.paren-group meta.paren-group meta.paren-group"}, {"background": "151515","token": "meta.section meta.section meta.section meta.section"}, {"background": "191919","token": "meta.odd-tab.group5"}, {"background": "191919","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "191919","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "191919","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "191919","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "191919","token": "meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group"}, {"background": "191919","token": "meta.section meta.section meta.section meta.section meta.section"}, {"background": "1c1c1c","token": "meta.even-tab.group6"}, {"background": "1c1c1c","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1c1c1c","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "1c1c1c","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "1c1c1c","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1c1c1c","token": "meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group"}, {"background": "1c1c1c","token": "meta.section meta.section meta.section meta.section meta.section meta.section"}, {"background": "1f1f1f","token": "meta.odd-tab.group7"}, {"background": "1f1f1f","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1f1f1f","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "1f1f1f","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "1f1f1f","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1f1f1f","token": "meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group"}, {"background": "1f1f1f","token": "meta.section meta.section meta.section meta.section meta.section meta.section meta.section"}, {"background": "212121","token": "meta.even-tab.group8"}, {"background": "212121","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "212121","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "212121","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "212121","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "212121","token": "meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group"}, {"background": "212121","token": "meta.section meta.section meta.section meta.section meta.section meta.section meta.section meta.section"}, {"background": "242424","token": "meta.odd-tab.group9"}, {"background": "242424","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "242424","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "242424","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "242424","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "242424","token": "meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group meta.paren-group"}, {"background": "242424","token": "meta.section meta.section meta.section meta.section meta.section meta.section meta.section meta.section meta.section"}, {"background": "1f1f1f","token": "meta.even-tab.group10"}, {"background": "151515","token": "meta.odd-tab.group11"}, {"foreground": "1b95e2","token": "meta.property.vendor.microsoft.trident.4"}, {"foreground": "1b95e2","token": "meta.property.vendor.microsoft.trident.4 support.type.property-name"}, {"foreground": "1b95e2","token": "meta.property.vendor.microsoft.trident.4 punctuation.terminator.rule"}, {"foreground": "f5c034","token": "meta.property.vendor.microsoft.trident.5"}, {"foreground": "f5c034","token": "meta.property.vendor.microsoft.trident.5 support.type.property-name"}, {"foreground": "f5c034","token": "meta.property.vendor.microsoft.trident.5 punctuation.separator.key-value"}, {"foreground": "f5c034","token": "meta.property.vendor.microsoft.trident.5 punctuation.terminator.rule"} ], "colors": { "editor.foreground": "#EEEEEE", "editor.background": "#0D0D0DFA", "editor.selectionBackground": "#0010B499", "editor.lineHighlightBackground": "#00008033", "editorCursor.foreground": "#3333FF", "editorWhitespace.foreground": "#CCCCCC1A", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let brillianceDull = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "050505FA","token": ""}, {"foreground": "000000","background": "ffffff","fontStyle": "bold","token": "meta.thomas_aylott"}, {"foreground": "555555","background": "ffffff","fontStyle": "underline","token": "meta.subtlegradient"}, {"foreground": "e6e6e6","background": "ffffff","token": "meta.subtlegradient"}, {"foreground": "d2d1ab","background": "803d0033","token": "string -meta.tag -meta.doctype -string.regexp -string.literal -string.interpolated -string.quoted.literal -string.unquoted"}, {"foreground": "d2d1ab","background": "803d0033","token": "variable.parameter.misc.css"}, {"foreground": "d2d1ab","background": "803d0033","token": "text string source string"}, {"foreground": "d2d1ab","background": "803d0033","token": "string.unquoted string"}, {"foreground": "d2d1ab","background": "803d0033","token": "string.regexp string"}, {"foreground": "533f2c","token": "punctuation.definition.string -meta.tag"}, {"foreground": "fff80033","token": "string.regexp punctuation.definition.string"}, {"foreground": "fff80033","token": "string.quoted.literal punctuation.definition.string"}, {"foreground": "fff80033","token": "string.quoted.double.ruby.mod punctuation.definition.string"}, {"foreground": "a6a458","background": "43800033","token": "string.quoted.literal"}, {"foreground": "a6a458","background": "43800033","token": "string.quoted.double.ruby.mod"}, {"foreground": "d2beab","token": "string.unquoted -string.unquoted.embedded"}, {"foreground": "d2beab","token": "string.quoted.double.multiline"}, {"foreground": "d2beab","token": "meta.scope.heredoc"}, {"foreground": "d2d1ab","background": "1a1a1a","token": "string.interpolated"}, {"foreground": "a6a458","background": "43800033","token": "string.regexp"}, {"background": "43800033","token": "string.regexp.group"}, {"foreground": "ffffff66","background": "43800033","token": "string.regexp.group string.regexp.group"}, {"foreground": "ffffff66","background": "43800033","token": "string.regexp.group string.regexp.group string.regexp.group"}, {"foreground": "ffffff66","background": "43800033","token": "string.regexp.group string.regexp.group string.regexp.group string.regexp.group"}, {"foreground": "80a659","background": "43800033","token": "string.regexp.character-class"}, {"foreground": "56a5a4","background": "43800033","token": "string.regexp.arbitrary-repitition"}, {"foreground": "a75980","token": "source.regexp keyword.operator"}, {"foreground": "ffffff","fontStyle": "italic","token": "string.regexp comment"}, {"background": "0086ff33","token": "meta.group.assertion.regexp"}, {"foreground": "5780a6","token": "meta.assertion"}, {"foreground": "5780a6","token": "meta.group.assertion keyword.control.group.regexp"}, {"foreground": "95a658","token": "constant.numeric"}, {"foreground": "80a659","token": "constant.character"}, {"foreground": "59a559","token": "constant.language"}, {"foreground": "59a559","token": "keyword.other.unit"}, {"foreground": "59a559","token": "constant.other.java"}, {"foreground": "59a559","token": "constant.other.unit"}, {"foreground": "59a559","background": "04800033","token": "constant.language.pseudo-variable"}, {"foreground": "57a57d","token": "constant.other"}, {"foreground": "57a57d","token": "constant.block"}, {"foreground": "56a5a4","token": "support.constant"}, {"foreground": "56a5a4","token": "constant.name"}, {"foreground": "5e6b6b","token": "variable.language"}, {"foreground": "5e6b6b","token": "variable.other.readwrite.global.pre-defined"}, {"foreground": "56a5a4","token": "variable.other.constant"}, {"foreground": "56a5a4","background": "00807c33","token": "support.variable"}, {"foreground": "2b5252","background": "00438033","token": "variable.other.readwrite.global"}, {"foreground": "5780a6","token": "variable.other"}, {"foreground": "5780a6","token": "variable.js"}, {"foreground": "5780a6","background": "0007ff33","token": "variable.other.readwrite.class"}, {"foreground": "555f69","token": "variable.other.readwrite.instance"}, {"foreground": "555f69","token": "variable.other.php"}, {"foreground": "555f69","token": "variable.other.normal"}, {"foreground": "00000080","token": "punctuation.definition -punctuation.definition.comment"}, {"foreground": "00000080","token": "punctuation.separator.variable"}, {"foreground": "a77d58","token": "storage -storage.modifier"}, {"background": "803d0033","token": "other.preprocessor"}, {"background": "803d0033","token": "entity.name.preprocessor"}, {"foreground": "666666","token": "variable.language.this.js"}, {"foreground": "533f2c","token": "storage.modifier"}, {"foreground": "a7595a","token": "entity.name.class"}, {"foreground": "a7595a","token": "entity.name.type.class"}, {"foreground": "a7595a","token": "entity.name.type.module"}, {"foreground": "532d2d","background": "29161780","token": "meta.class -meta.class.instance"}, {"foreground": "532d2d","background": "29161780","token": "declaration.class"}, {"foreground": "532d2d","background": "29161780","token": "meta.definition.class"}, {"foreground": "532d2d","background": "29161780","token": "declaration.module"}, {"foreground": "a7595a","background": "80000433","token": "support.type"}, {"foreground": "a7595a","background": "80000433","token": "support.class"}, {"foreground": "a7595a","token": "entity.name.instance"}, {"background": "80004333","token": "meta.class.instance.constructor"}, {"foreground": "a75980","background": "80000433","token": "entity.other.inherited-class"}, {"foreground": "a75980","background": "80000433","token": "entity.name.module"}, {"foreground": "a75980","token": "object.property.function"}, {"foreground": "a75980","token": "meta.definition.method"}, {"foreground": "532d40","background": "80004333","token": "meta.function -(meta.tell-block)"}, {"foreground": "532d40","background": "80004333","token": "meta.property.function"}, {"foreground": "532d40","background": "80004333","token": "declaration.function"}, {"foreground": "a75980","token": "entity.name.function"}, {"foreground": "a75980","token": "entity.name.preprocessor"}, {"foreground": "a459a5","token": "keyword"}, {"foreground": "a459a5","background": "3c008033","token": "keyword.control"}, {"foreground": "8d809d","token": "keyword.other.special-method"}, {"foreground": "8d809d","token": "meta.function-call entity.name.function -(meta.function-call meta.function)"}, {"foreground": "8d809d","token": "support.function - variable"}, {"foreground": "634683","token": "support.function - variable"}, {"foreground": "7979b7","fontStyle": "bold","token": "keyword.operator"}, {"foreground": "7979b7","fontStyle": "bold","token": "declaration.function.operator"}, {"foreground": "7979b7","fontStyle": "bold","token": "meta.preprocessor.c.include"}, {"foreground": "9899c8","token": "keyword.operator.comparison"}, {"foreground": "abacd2","background": "3c008033","token": "variable.parameter -variable.parameter.misc.css"}, {"foreground": "abacd2","background": "3c008033","token": "meta.definition.method meta.definition.param-list"}, {"foreground": "abacd2","background": "3c008033","token": "meta.function.method.with-arguments variable.parameter.function"}, {"foreground": "cdcdcd","background": "333333","token": "meta.doctype"}, {"foreground": "cdcdcd","background": "333333","token": "meta.tag.sgml-declaration.doctype"}, {"foreground": "cdcdcd","background": "333333","token": "meta.tag.sgml.doctype"}, {"foreground": "333333","token": "meta.tag"}, {"foreground": "666666","background": "333333bf","token": "meta.tag.structure"}, {"foreground": "666666","background": "333333bf","token": "meta.tag.segment"}, {"foreground": "4c4c4c","background": "4c4c4c33","token": "meta.tag.block"}, {"foreground": "4c4c4c","background": "4c4c4c33","token": "meta.tag.xml"}, {"foreground": "4c4c4c","background": "4c4c4c33","token": "meta.tag.key"}, {"foreground": "a77d58","background": "803d0033","token": "meta.tag.inline"}, {"background": "803d0033","token": "meta.tag.inline source"}, {"foreground": "a7595a","background": "80000433","token": "meta.tag.other"}, {"foreground": "a7595a","background": "80000433","token": "entity.name.tag.style"}, {"foreground": "a7595a","background": "80000433","token": "source entity.other.attribute-name -text.html.basic.embedded"}, {"foreground": "a7595a","background": "80000433","token": "entity.name.tag.script"}, {"foreground": "a7595a","background": "80000433","token": "meta.tag.block.script"}, {"foreground": "5780a6","background": "00438033","token": "meta.tag.form"}, {"foreground": "5780a6","background": "00438033","token": "meta.tag.block.form"}, {"foreground": "a459a5","background": "3c008033","token": "meta.tag.meta"}, {"background": "121212","token": "meta.section.html.head"}, {"background": "0043801a","token": "meta.section.html.form"}, {"foreground": "666666","token": "meta.tag.xml"}, {"foreground": "ffffff4d","token": "entity.name.tag"}, {"foreground": "ffffff33","token": "entity.other.attribute-name"}, {"foreground": "ffffff33","token": "meta.tag punctuation.definition.string"}, {"foreground": "ffffff66","token": "meta.tag string -source -punctuation"}, {"foreground": "ffffff66","token": "text source text meta.tag string -punctuation"}, {"foreground": "a6a458","background": "33333333","token": "markup markup -(markup meta.paragraph.list)"}, {"foreground": "000000","background": "ffffff","token": "markup.hr"}, {"foreground": "666666","background": "33333380","token": "markup.heading"}, {"fontStyle": "bold","token": "markup.bold"}, {"fontStyle": "italic","token": "markup.italic"}, {"fontStyle": "underline","token": "markup.underline"}, {"foreground": "5780a6","token": "meta.reference"}, {"foreground": "5780a6","token": "markup.underline.link"}, {"foreground": "56a5a4","background": "00438033","token": "entity.name.reference"}, {"foreground": "56a5a4","fontStyle": "underline","token": "meta.reference.list markup.underline.link"}, {"foreground": "56a5a4","fontStyle": "underline","token": "text.html.textile markup.underline.link"}, {"foreground": "999999","background": "000000","token": "markup.raw.block"}, {"background": "ffffff1a","token": "markup.quote"}, {"foreground": "666666","background": "00000080","token": "meta.selector"}, {"foreground": "575aa6","background": "00048033","token": "meta.attribute-match.css"}, {"foreground": "7c58a5","token": "entity.other.attribute-name.pseudo-class"}, {"foreground": "7c58a5","token": "entity.other.attribute-name.tag.pseudo-class"}, {"foreground": "a459a5","token": "meta.selector entity.other.attribute-name.class"}, {"foreground": "a75980","token": "meta.selector entity.other.attribute-name.id"}, {"foreground": "a7595a","token": "meta.selector entity.name.tag"}, {"foreground": "a77d58","fontStyle": "bold","token": "entity.name.tag.wildcard"}, {"foreground": "a77d58","fontStyle": "bold","token": "entity.other.attribute-name.universal"}, {"foreground": "333333","fontStyle": "bold","token": "meta.scope.property-list"}, {"foreground": "999999","token": "meta.property-name"}, {"foreground": "ffffff","background": "000000","token": "support.type.property-name"}, {"foreground": "999999","background": "0d0d0d","token": "meta.property-value"}, {"background": "000000","token": "text.latex markup.raw"}, {"foreground": "bdabd1","token": "text.latex support.function -support.function.textit -support.function.emph"}, {"foreground": "ffffffbf","token": "text.latex support.function.section"}, {"foreground": "000000","background": "ffffff","token": "text.latex entity.name.section -meta.group -keyword.operator.braces"}, {"background": "00000080","token": "text.latex keyword.operator.delimiter"}, {"foreground": "999999","token": "text.latex keyword.operator.brackets"}, {"foreground": "666666","token": "text.latex keyword.operator.braces"}, {"foreground": "0008ff4d","background": "00048033","token": "meta.footnote"}, {"background": "ffffff0d","token": "text.latex meta.label.reference"}, {"foreground": "a7595a","background": "180d0c","token": "text.latex keyword.control.ref"}, {"foreground": "d2beab","background": "291616","token": "text.latex variable.parameter.label.reference"}, {"foreground": "a75980","background": "180d12","token": "text.latex keyword.control.cite"}, {"foreground": "e8d5de","background": "29161f","token": "variable.parameter.cite"}, {"foreground": "ffffff80","token": "text.latex variable.parameter.label"}, {"foreground": "33333333","token": "text.latex meta.group.braces"}, {"foreground": "33333333","background": "00000080","token": "text.latex meta.environment.list"}, {"foreground": "33333333","background": "00000080","token": "text.latex meta.environment.list meta.environment.list"}, {"foreground": "33333333","background": "000000","token": "text.latex meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "33333333","token": "text.latex meta.environment.list meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "33333333","token": "text.latex meta.environment.list meta.environment.list meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "33333333","token": "text.latex meta.environment.list meta.environment.list meta.environment.list meta.environment.list meta.environment.list meta.environment.list"}, {"foreground": "000000","background": "cdcdcd","token": "text.latex meta.end-document"}, {"foreground": "000000","background": "cdcdcd","token": "text.latex meta.begin-document"}, {"foreground": "000000","background": "cdcdcd","token": "meta.end-document.latex support.function"}, {"foreground": "000000","background": "cdcdcd","token": "meta.end-document.latex variable.parameter"}, {"foreground": "000000","background": "cdcdcd","token": "meta.begin-document.latex support.function"}, {"foreground": "000000","background": "cdcdcd","token": "meta.begin-document.latex variable.parameter"}, {"foreground": "596b61","background": "45815d33","token": "meta.brace.erb.return-value"}, {"background": "66666633","token": "source.ruby.rails.embedded.return-value.one-line"}, {"foreground": "56a5a4","background": "00fff81a","token": "punctuation.section.embedded -(source string source punctuation.section.embedded)"}, {"foreground": "56a5a4","background": "00fff81a","token": "meta.brace.erb.html"}, {"background": "00fff81a","token": "source.ruby.rails.embedded.one-line"}, {"foreground": "555f69","token": "source string source punctuation.section.embedded"}, {"background": "000000","token": "source"}, {"background": "000000","token": "meta.brace.erb"}, {"foreground": "ffffff","background": "33333380","token": "source string source"}, {"foreground": "999999","background": "00000099","token": "source string.interpolated source"}, {"background": "3333331a","token": "source.java.embedded"}, {"foreground": "ffffff","token": "text -text.xml.strict"}, {"foreground": "cccccc","background": "000000","token": "text source"}, {"foreground": "cccccc","background": "000000","token": "meta.scope.django.template"}, {"foreground": "999999","token": "text string source"}, {"foreground": "333333","token": "meta.syntax"}, {"foreground": "211211","background": "a7595a","fontStyle": "bold","token": "invalid"}, {"foreground": "8f8fc3","background": "0000ff1a","fontStyle": "italic","token": "0comment"}, {"foreground": "0000ff1a","fontStyle": "bold","token": "comment punctuation"}, {"foreground": "333333","token": "comment"}, {"foreground": "262626","background": "8080800d","fontStyle": "bold italic","token": "comment punctuation"}, {"fontStyle": "italic","token": "text comment.block -source"}, {"foreground": "81bb9e","background": "15281f","token": "markup.inserted"}, {"foreground": "bc839f","background": "400021","token": "markup.deleted"}, {"foreground": "c3c38f","background": "533f2c","token": "markup.changed"}, {"foreground": "ffffff","background": "000000","token": "text.subversion-commit meta.scope.changed-files"}, {"foreground": "ffffff","background": "000000","token": "text.subversion-commit meta.scope.changed-files.svn meta.diff.separator"}, {"foreground": "000000","background": "ffffff","token": "text.subversion-commit"}, {"foreground": "ffffff","background": "ffffff03","fontStyle": "bold","token": "punctuation.terminator"}, {"foreground": "ffffff","background": "ffffff03","fontStyle": "bold","token": "meta.delimiter"}, {"foreground": "ffffff","background": "ffffff03","fontStyle": "bold","token": "punctuation.separator.method"}, {"background": "000000bf","token": "punctuation.terminator.statement"}, {"background": "000000bf","token": "meta.delimiter.statement.js"}, {"background": "00000040","token": "meta.delimiter.object.js"}, {"foreground": "533f2c","fontStyle": "bold","token": "string.quoted.single.brace"}, {"foreground": "533f2c","fontStyle": "bold","token": "string.quoted.double.brace"}, {"background": "ffffff","token": "text.blog -(text.blog text)"}, {"foreground": "666666","background": "ffffff","token": "meta.headers.blog"}, {"foreground": "192b2a","background": "00fff81a","token": "meta.headers.blog keyword.other.blog"}, {"foreground": "533f2c","background": "ffff551a","token": "meta.headers.blog string.unquoted.blog"}, {"foreground": "4c4c4c","background": "33333333","token": "meta.brace.pipe"}, {"foreground": "4c4c4c","fontStyle": "bold","token": "meta.brace.erb"}, {"foreground": "4c4c4c","fontStyle": "bold","token": "source.ruby.embedded.source.brace"}, {"foreground": "4c4c4c","fontStyle": "bold","token": "punctuation.section.dictionary"}, {"foreground": "4c4c4c","fontStyle": "bold","token": "punctuation.terminator.dictionary"}, {"foreground": "4c4c4c","fontStyle": "bold","token": "punctuation.separator.object"}, {"foreground": "ffffff","fontStyle": "bold","token": "meta.group.braces.curly punctuation.section.scope"}, {"foreground": "ffffff","fontStyle": "bold","token": "meta.brace.curly"}, {"foreground": "345743","fontStyle": "bold","token": "punctuation.separator.objects"}, {"foreground": "345743","fontStyle": "bold","token": "meta.group.braces.curly meta.delimiter.object.comma"}, {"foreground": "345743","fontStyle": "bold","token": "punctuation.separator.key-value -meta.tag"}, {"foreground": "695f55","background": "803d001a","fontStyle": "bold","token": "meta.group.braces.square punctuation.section.scope"}, {"foreground": "695f55","background": "803d001a","fontStyle": "bold","token": "meta.group.braces.square meta.delimiter.object.comma"}, {"foreground": "695f55","background": "803d001a","fontStyle": "bold","token": "meta.brace.square"}, {"foreground": "695f55","background": "803d001a","fontStyle": "bold","token": "punctuation.separator.array"}, {"foreground": "695f55","background": "803d001a","fontStyle": "bold","token": "punctuation.section.array"}, {"foreground": "cdcdcd","background": "00000080","token": "meta.brace.curly meta.group"}, {"foreground": "532d40","fontStyle": "bold","token": "meta.group.braces.round punctuation.section.scope"}, {"foreground": "532d40","fontStyle": "bold","token": "meta.group.braces.round meta.delimiter.object.comma"}, {"foreground": "532d40","fontStyle": "bold","token": "meta.brace.round"}, {"foreground": "abacd2","background": "3c008033","token": "punctuation.section.function"}, {"foreground": "abacd2","background": "3c008033","token": "meta.brace.curly.function"}, {"foreground": "abacd2","background": "3c008033","token": "meta.function-call punctuation.section.scope.ruby"}, {"foreground": "666666","background": "00000080","token": "meta.source.embedded"}, {"foreground": "666666","background": "00000080","token": "entity.other.django.tagbraces"}, {"background": "0a0a0a","token": "meta.odd-tab.group1"}, {"background": "0a0a0a","token": "meta.group.braces"}, {"background": "0a0a0a","token": "meta.block.slate"}, {"background": "0a0a0a","token": "text.xml.strict meta.tag"}, {"background": "0a0a0a","token": "meta.tell-block meta.tell-block"}, {"background": "0e0e0e","token": "meta.even-tab.group2"}, {"background": "0e0e0e","token": "meta.group.braces meta.group.braces"}, {"background": "0e0e0e","token": "meta.block.slate meta.block.slate"}, {"background": "0e0e0e","token": "text.xml.strict meta.tag meta.tag"}, {"background": "0e0e0e","token": "meta.group.braces meta.group.braces"}, {"background": "0e0e0e","token": "meta.tell-block meta.tell-block"}, {"background": "111111","token": "meta.odd-tab.group3"}, {"background": "111111","token": "meta.group.braces meta.group.braces meta.group.braces"}, {"background": "111111","token": "meta.block.slate meta.block.slate meta.block.slate"}, {"background": "111111","token": "text.xml.strict meta.tag meta.tag meta.tag"}, {"background": "111111","token": "meta.group.braces meta.group.braces meta.group.braces"}, {"background": "111111","token": "meta.tell-block meta.tell-block meta.tell-block"}, {"background": "151515","token": "meta.even-tab.group4"}, {"background": "151515","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "151515","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "151515","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag"}, {"background": "151515","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "151515","token": "meta.tell-block meta.tell-block meta.tell-block meta.tell-block"}, {"background": "191919","token": "meta.odd-tab.group5"}, {"background": "191919","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "191919","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "191919","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "191919","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "191919","token": "meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block"}, {"background": "1c1c1c","token": "meta.even-tab.group6"}, {"background": "1c1c1c","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1c1c1c","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "1c1c1c","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "1c1c1c","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1c1c1c","token": "meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block"}, {"background": "1f1f1f","token": "meta.odd-tab.group7"}, {"background": "1f1f1f","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1f1f1f","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "1f1f1f","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "1f1f1f","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "1f1f1f","token": "meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block"}, {"background": "212121","token": "meta.even-tab.group8"}, {"background": "212121","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "212121","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "212121","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "212121","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "212121","token": "meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block"}, {"background": "242424","token": "meta.odd-tab.group11"}, {"background": "242424","token": "meta.odd-tab.group10"}, {"background": "242424","token": "meta.odd-tab.group9"}, {"background": "242424","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "242424","token": "meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate meta.block.slate"}, {"background": "242424","token": "text.xml.strict meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag meta.tag"}, {"background": "242424","token": "meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces meta.group.braces"}, {"background": "242424","token": "meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block meta.tell-block"}, {"foreground": "666666","token": "meta.block.slate"}, {"foreground": "cdcdcd","token": "meta.block.content.slate"} ], "colors": { "editor.foreground": "#CDCDCD", "editor.background": "#050505FA", "editor.selectionBackground": "#2E2EE64D", "editor.lineHighlightBackground": "#0000801A", "editorCursor.foreground": "#7979B7", "editorWhitespace.foreground": "#CDCDCD1A", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let chromeDevTools = { "base": "vs", "inherit": true, "rules": [ {"background": "FFFFFF","token": ""}, {"foreground": "c41a16","token": "string"}, {"foreground": "1c00cf","token": "constant.numeric"}, {"foreground": "aa0d91","token": "keyword"}, {"foreground": "000000","token": "keyword.operator"}, {"foreground": "aa0d91","token": "constant.language"}, {"foreground": "990000","token": "support.class.exception"}, {"foreground": "000000","token": "entity.name.function"}, {"fontStyle": "bold underline","token": "entity.name.type"}, {"fontStyle": "italic","token": "variable.parameter"}, {"foreground": "007400","token": "comment"}, {"foreground": "ff0000","token": "invalid"}, {"background": "e71a1100","token": "invalid.deprecated.trailing-whitespace"}, {"foreground": "000000","background": "fafafafc","token": "text source"}, {"foreground": "aa0d91","token": "meta.tag"}, {"foreground": "aa0d91","token": "declaration.tag"}, {"foreground": "000000","fontStyle": "bold","token": "support"}, {"foreground": "aa0d91","token": "storage"}, {"fontStyle": "bold underline","token": "entity.name.section"}, {"foreground": "000000","fontStyle": "bold","token": "entity.name.function.frame"}, {"foreground": "333333","token": "meta.tag.preprocessor.xml"}, {"foreground": "994500","fontStyle": "italic","token": "entity.other.attribute-name"}, {"foreground": "881280","token": "entity.name.tag"} ], "colors": { "editor.foreground": "#000000", "editor.background": "#FFFFFF", "editor.selectionBackground": "#BAD6FD", "editor.lineHighlightBackground": "#0000001A", "editorCursor.foreground": "#000000", "editorWhitespace.foreground": "#B3B3B3F4", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let cloudsMidnight = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "191919","token": ""}, {"foreground": "3c403b","token": "comment"}, {"foreground": "5d90cd","token": "string"}, {"foreground": "46a609","token": "constant.numeric"}, {"foreground": "39946a","token": "constant.language"}, {"foreground": "927c5d","token": "keyword"}, {"foreground": "927c5d","token": "support.constant.property-value"}, {"foreground": "927c5d","token": "constant.other.color"}, {"foreground": "366f1a","token": "keyword.other.unit"}, {"foreground": "a46763","token": "entity.other.attribute-name.html"}, {"foreground": "4b4b4b","token": "keyword.operator"}, {"foreground": "e92e2e","token": "storage"}, {"foreground": "858585","token": "entity.other.inherited-class"}, {"foreground": "606060","token": "entity.name.tag"}, {"foreground": "a165ac","token": "constant.character.entity"}, {"foreground": "a165ac","token": "support.class.js"}, {"foreground": "606060","token": "entity.other.attribute-name"}, {"foreground": "e92e2e","token": "meta.selector.css"}, {"foreground": "e92e2e","token": "entity.name.tag.css"}, {"foreground": "e92e2e","token": "entity.other.attribute-name.id.css"}, {"foreground": "e92e2e","token": "entity.other.attribute-name.class.css"}, {"foreground": "616161","token": "meta.property-name.css"}, {"foreground": "e92e2e","token": "support.function"}, {"foreground": "ffffff","background": "e92e2e","token": "invalid"}, {"foreground": "e92e2e","token": "punctuation.section.embedded"}, {"foreground": "606060","token": "punctuation.definition.tag"}, {"foreground": "a165ac","token": "constant.other.color.rgb-value.css"}, {"foreground": "a165ac","token": "support.constant.property-value.css"} ], "colors": { "editor.foreground": "#929292", "editor.background": "#191919", "editor.selectionBackground": "#000000", "editor.lineHighlightBackground": "#D7D7D708", "editorCursor.foreground": "#7DA5DC", "editorWhitespace.foreground": "#BFBFBF", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let clouds = { "base": "vs", "inherit": true, "rules": [ {"background": "FFFFFF","token": ""}, {"foreground": "bcc8ba","token": "comment"}, {"foreground": "5d90cd","token": "string"}, {"foreground": "46a609","token": "constant.numeric"}, {"foreground": "39946a","token": "constant.language"}, {"foreground": "af956f","token": "keyword"}, {"foreground": "af956f","token": "support.constant.property-value"}, {"foreground": "af956f","token": "constant.other.color"}, {"foreground": "96dc5f","token": "keyword.other.unit"}, {"foreground": "484848","token": "keyword.operator"}, {"foreground": "c52727","token": "storage"}, {"foreground": "858585","token": "entity.other.inherited-class"}, {"foreground": "606060","token": "entity.name.tag"}, {"foreground": "bf78cc","token": "constant.character.entity"}, {"foreground": "bf78cc","token": "support.class.js"}, {"foreground": "606060","token": "entity.other.attribute-name"}, {"foreground": "c52727","token": "meta.selector.css"}, {"foreground": "c52727","token": "entity.name.tag.css"}, {"foreground": "c52727","token": "entity.other.attribute-name.id.css"}, {"foreground": "c52727","token": "entity.other.attribute-name.class.css"}, {"foreground": "484848","token": "meta.property-name.css"}, {"foreground": "c52727","token": "support.function"}, {"background": "ff002a","token": "invalid"}, {"foreground": "c52727","token": "punctuation.section.embedded"}, {"foreground": "606060","token": "punctuation.definition.tag"}, {"foreground": "bf78cc","token": "constant.other.color.rgb-value.css"}, {"foreground": "bf78cc","token": "support.constant.property-value.css"} ], "colors": { "editor.foreground": "#000000", "editor.background": "#FFFFFF", "editor.selectionBackground": "#BDD5FC", "editor.lineHighlightBackground": "#FFFBD1", "editorCursor.foreground": "#000000", "editorWhitespace.foreground": "#BFBFBF", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let cobalt = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "002240","token": ""}, {"foreground": "e1efff","token": "punctuation - (punctuation.definition.string || punctuation.definition.comment)"}, {"foreground": "ff628c","token": "constant"}, {"foreground": "ffdd00","token": "entity"}, {"foreground": "ff9d00","token": "keyword"}, {"foreground": "ffee80","token": "storage"}, {"foreground": "3ad900","token": "string -string.unquoted.old-plist -string.unquoted.heredoc"}, {"foreground": "3ad900","token": "string.unquoted.heredoc string"}, {"foreground": "0088ff","fontStyle": "italic","token": "comment"}, {"foreground": "80ffbb","token": "support"}, {"foreground": "cccccc","token": "variable"}, {"foreground": "ff80e1","token": "variable.language"}, {"foreground": "ffee80","token": "meta.function-call"}, {"foreground": "f8f8f8","background": "800f00","token": "invalid"}, {"foreground": "ffffff","background": "223545","token": "text source"}, {"foreground": "ffffff","background": "223545","token": "string.unquoted.heredoc"}, {"foreground": "ffffff","background": "223545","token": "source source"}, {"foreground": "80fcff","fontStyle": "italic","token": "entity.other.inherited-class"}, {"foreground": "9eff80","token": "string.quoted source"}, {"foreground": "80ff82","token": "string constant"}, {"foreground": "80ffc2","token": "string.regexp"}, {"foreground": "edef7d","token": "string variable"}, {"foreground": "ffb054","token": "support.function"}, {"foreground": "eb939a","token": "support.constant"}, {"foreground": "ff1e00","token": "support.type.exception"}, {"foreground": "8996a8","token": "meta.preprocessor.c"}, {"foreground": "afc4db","token": "meta.preprocessor.c keyword"}, {"foreground": "73817d","token": "meta.sgml.html meta.doctype"}, {"foreground": "73817d","token": "meta.sgml.html meta.doctype entity"}, {"foreground": "73817d","token": "meta.sgml.html meta.doctype string"}, {"foreground": "73817d","token": "meta.xml-processing"}, {"foreground": "73817d","token": "meta.xml-processing entity"}, {"foreground": "73817d","token": "meta.xml-processing string"}, {"foreground": "9effff","token": "meta.tag"}, {"foreground": "9effff","token": "meta.tag entity"}, {"foreground": "9effff","token": "meta.selector.css entity.name.tag"}, {"foreground": "ffb454","token": "meta.selector.css entity.other.attribute-name.id"}, {"foreground": "5fe461","token": "meta.selector.css entity.other.attribute-name.class"}, {"foreground": "9df39f","token": "support.type.property-name.css"}, {"foreground": "f6f080","token": "meta.property-group support.constant.property-value.css"}, {"foreground": "f6f080","token": "meta.property-value support.constant.property-value.css"}, {"foreground": "f6aa11","token": "meta.preprocessor.at-rule keyword.control.at-rule"}, {"foreground": "edf080","token": "meta.property-value support.constant.named-color.css"}, {"foreground": "edf080","token": "meta.property-value constant"}, {"foreground": "eb939a","token": "meta.constructor.argument.css"}, {"foreground": "f8f8f8","background": "000e1a","token": "meta.diff"}, {"foreground": "f8f8f8","background": "000e1a","token": "meta.diff.header"}, {"foreground": "f8f8f8","background": "4c0900","token": "markup.deleted"}, {"foreground": "f8f8f8","background": "806f00","token": "markup.changed"}, {"foreground": "f8f8f8","background": "154f00","token": "markup.inserted"}, {"background": "8fddf630","token": "markup.raw"}, {"background": "004480","token": "markup.quote"}, {"background": "130d26","token": "markup.list"}, {"foreground": "c1afff","fontStyle": "bold","token": "markup.bold"}, {"foreground": "b8ffd9","fontStyle": "italic","token": "markup.italic"}, {"foreground": "c8e4fd","background": "001221","fontStyle": "bold","token": "markup.heading"} ], "colors": { "editor.foreground": "#FFFFFF", "editor.background": "#002240", "editor.selectionBackground": "#B36539BF", "editor.lineHighlightBackground": "#00000059", "editorCursor.foreground": "#FFFFFF", "editorWhitespace.foreground": "#FFFFFF26", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let dracula = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "282a36","token": ""}, {"foreground": "6272a4","token": "comment"}, {"foreground": "f1fa8c","token": "string"}, {"foreground": "bd93f9","token": "constant.numeric"}, {"foreground": "bd93f9","token": "constant.language"}, {"foreground": "bd93f9","token": "constant.character"}, {"foreground": "bd93f9","token": "constant.other"}, {"foreground": "ffb86c","token": "variable.other.readwrite.instance"}, {"foreground": "ff79c6","token": "constant.character.escaped"}, {"foreground": "ff79c6","token": "constant.character.escape"}, {"foreground": "ff79c6","token": "string source"}, {"foreground": "ff79c6","token": "string source.ruby"}, {"foreground": "ff79c6","token": "keyword"}, {"foreground": "ff79c6","token": "storage"}, {"foreground": "8be9fd","fontStyle": "italic","token": "storage.type"}, {"foreground": "50fa7b","fontStyle": "underline","token": "entity.name.class"}, {"foreground": "50fa7b","fontStyle": "italic underline","token": "entity.other.inherited-class"}, {"foreground": "50fa7b","token": "entity.name.function"}, {"foreground": "ffb86c","fontStyle": "italic","token": "variable.parameter"}, {"foreground": "ff79c6","token": "entity.name.tag"}, {"foreground": "50fa7b","token": "entity.other.attribute-name"}, {"foreground": "8be9fd","token": "support.function"}, {"foreground": "6be5fd","token": "support.constant"}, {"foreground": "66d9ef","fontStyle": " italic","token": "support.type"}, {"foreground": "66d9ef","fontStyle": " italic","token": "support.class"}, {"foreground": "f8f8f0","background": "ff79c6","token": "invalid"}, {"foreground": "f8f8f0","background": "bd93f9","token": "invalid.deprecated"}, {"foreground": "cfcfc2","token": "meta.structure.dictionary.json string.quoted.double.json"}, {"foreground": "6272a4","token": "meta.diff"}, {"foreground": "6272a4","token": "meta.diff.header"}, {"foreground": "ff79c6","token": "markup.deleted"}, {"foreground": "50fa7b","token": "markup.inserted"}, {"foreground": "e6db74","token": "markup.changed"}, {"foreground": "bd93f9","token": "constant.numeric.line-number.find-in-files - match"}, {"foreground": "e6db74","token": "entity.name.filename"}, {"foreground": "f83333","token": "message.error"}, {"foreground": "eeeeee","token": "punctuation.definition.string.begin.json - meta.structure.dictionary.value.json"}, {"foreground": "eeeeee","token": "punctuation.definition.string.end.json - meta.structure.dictionary.value.json"}, {"foreground": "8be9fd","token": "meta.structure.dictionary.json string.quoted.double.json"}, {"foreground": "f1fa8c","token": "meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "50fa7b","token": "meta meta meta meta meta meta meta.structure.dictionary.value string"}, {"foreground": "ffb86c","token": "meta meta meta meta meta meta.structure.dictionary.value string"}, {"foreground": "ff79c6","token": "meta meta meta meta meta.structure.dictionary.value string"}, {"foreground": "bd93f9","token": "meta meta meta meta.structure.dictionary.value string"}, {"foreground": "50fa7b","token": "meta meta meta.structure.dictionary.value string"}, {"foreground": "ffb86c","token": "meta meta.structure.dictionary.value string"} ], "colors": { "editor.foreground": "#f8f8f2", "editor.background": "#282a36", "editor.selectionBackground": "#44475a", "editor.lineHighlightBackground": "#44475a", "editorCursor.foreground": "#f8f8f0", "editorWhitespace.foreground": "#3B3A32", "editorIndentGuide.activeBackground": "#9D550FB0", "editor.selectionHighlightBorder": "#222218", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let dreamweaver = { "base": "vs", "inherit": true, "rules": [ {"background": "FFFFFF","token": ""}, {"foreground": "000000","token": "text"}, {"foreground": "ee000b","token": "constant.numeric - source.css"}, {"foreground": "9a9a9a","token": "comment"}, {"foreground": "00359e","token": "text.html meta.tag"}, {"foreground": "001eff","token": "text.html.basic meta.tag string.quoted - source"}, {"foreground": "000000","fontStyle": "bold","token": "text.html.basic constant.character.entity.html"}, {"foreground": "106800","token": "text.html meta.tag.a - string"}, {"foreground": "6d232e","token": "text.html meta.tag.img - string"}, {"foreground": "ff9700","token": "text.html meta.tag.form - string"}, {"foreground": "009079","token": "text.html meta.tag.table - string"}, {"foreground": "842b44","token": "source.js.embedded.html punctuation.definition.tag - source.php"}, {"foreground": "842b44","token": "source.js.embedded.html entity.name.tag.script"}, {"foreground": "842b44","token": "source.js.embedded entity.other.attribute-name - source.js string"}, {"foreground": "9a9a9a","token": "source.js comment - source.php"}, {"foreground": "000000","token": "source.js meta.function - source.php"}, {"foreground": "24c696","token": "source.js meta.class - source.php"}, {"foreground": "24c696","token": "source.js support.function - source.php"}, {"foreground": "0035ff","token": "source.js string - source.php"}, {"foreground": "0035ff","token": "source.js keyword.operator"}, {"foreground": "7e00b7","token": "source.js support.class"}, {"foreground": "000000","fontStyle": "bold","token": "source.js storage"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js storage - storage.type.function - source.php"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js constant - source.php"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js keyword - source.php"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js variable.language"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js meta.brace"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js punctuation.definition.parameters.begin"}, {"foreground": "05208c","fontStyle": "bold","token": "source.js punctuation.definition.parameters.end"}, {"foreground": "106800","token": "source.js string.regexp"}, {"foreground": "106800","token": "source.js string.regexp constant"}, {"foreground": "8d00b7","token": "source.css.embedded.html punctuation.definition.tag"}, {"foreground": "8d00b7","token": "source.css.embedded.html entity.name.tag.style"}, {"foreground": "8d00b7","token": "source.css.embedded entity.other.attribute-name - meta.selector"}, {"foreground": "009c7f","fontStyle": "bold","token": "source.css meta.at-rule.import.css"}, {"foreground": "ee000b","fontStyle": "bold","token": "source.css keyword.other.important"}, {"foreground": "430303","fontStyle": "bold","token": "source.css meta.at-rule.media"}, {"foreground": "106800","token": "source.css string"}, {"foreground": "da29ff","token": "source.css meta.selector"}, {"foreground": "da29ff","token": "source.css meta.property-list"}, {"foreground": "da29ff","token": "source.css meta.at-rule"}, {"foreground": "da29ff","fontStyle": "bold","token": "source.css punctuation.separator - source.php"}, {"foreground": "da29ff","fontStyle": "bold","token": "source.css punctuation.terminator - source.php"}, {"foreground": "05208c","token": "source.css meta.property-name"}, {"foreground": "0035ff","token": "source.css meta.property-value"}, {"foreground": "ee000b","fontStyle": "bold","token": "source.php punctuation.section.embedded.begin"}, {"foreground": "ee000b","fontStyle": "bold","token": "source.php punctuation.section.embedded.end"}, {"foreground": "000000","token": "source.php - punctuation.section"}, {"foreground": "000000","token": "source.php variable"}, {"foreground": "000000","token": "source.php meta.function.arguments"}, {"foreground": "05208c","token": "source.php punctuation - string - variable - meta.function"}, {"foreground": "24bf96","token": "source.php storage.type"}, {"foreground": "009714","token": "source.php keyword - comment"}, {"foreground": "009714","token": "source.php storage.type.class"}, {"foreground": "009714","token": "source.php storage.type.interface"}, {"foreground": "009714","token": "source.php storage.modifier"}, {"foreground": "009714","token": "source.php constant.language"}, {"foreground": "0035ff","token": "source.php support"}, {"foreground": "0035ff","token": "source.php storage"}, {"foreground": "0035ff","token": "source.php keyword.operator"}, {"foreground": "0035ff","token": "source.php storage.type.function"}, {"foreground": "0092f2","token": "source.php variable.other.global"}, {"foreground": "551d02","token": "source.php support.constant"}, {"foreground": "551d02","token": "source.php constant.language.php"}, {"foreground": "e20000","token": "source.php string"}, {"foreground": "e20000","token": "source.php string keyword.operator"}, {"foreground": "ff6200","token": "source.php string.quoted.double variable"}, {"foreground": "ff9404","token": "source.php comment"}, {"foreground": "ee000b","background": "efff8a","fontStyle": "bold","token": "invalid"} ], "colors": { "editor.foreground": "#000000", "editor.background": "#FFFFFF", "editor.selectionBackground": "#5EA0FF", "editor.lineHighlightBackground": "#00000012", "editorCursor.foreground": "#000000", "editorWhitespace.foreground": "#BFBFBF", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let espressoLibre = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "2A211C","token": ""}, {"foreground": "0066ff","fontStyle": "italic","token": "comment"}, {"foreground": "43a8ed","fontStyle": "bold","token": "keyword"}, {"foreground": "43a8ed","fontStyle": "bold","token": "storage"}, {"foreground": "44aa43","token": "constant.numeric"}, {"foreground": "c5656b","fontStyle": "bold","token": "constant"}, {"foreground": "585cf6","fontStyle": "bold","token": "constant.language"}, {"foreground": "318495","token": "variable.language"}, {"foreground": "318495","token": "variable.other"}, {"foreground": "049b0a","token": "string"}, {"foreground": "2fe420","token": "constant.character.escape"}, {"foreground": "2fe420","token": "string source"}, {"foreground": "1a921c","token": "meta.preprocessor"}, {"foreground": "9aff87","fontStyle": "bold","token": "keyword.control.import"}, {"foreground": "ff9358","fontStyle": "bold","token": "entity.name.function"}, {"foreground": "ff9358","fontStyle": "bold","token": "keyword.other.name-of-parameter.objc"}, {"fontStyle": "underline","token": "entity.name.type"}, {"fontStyle": "italic","token": "entity.other.inherited-class"}, {"fontStyle": "italic","token": "variable.parameter"}, {"foreground": "8b8e9c","token": "storage.type.method"}, {"fontStyle": "italic","token": "meta.section entity.name.section"}, {"fontStyle": "italic","token": "declaration.section entity.name.section"}, {"foreground": "7290d9","fontStyle": "bold","token": "support.function"}, {"foreground": "6d79de","fontStyle": "bold","token": "support.class"}, {"foreground": "6d79de","fontStyle": "bold","token": "support.type"}, {"foreground": "00af0e","fontStyle": "bold","token": "support.constant"}, {"foreground": "2f5fe0","fontStyle": "bold","token": "support.variable"}, {"foreground": "687687","token": "keyword.operator.js"}, {"foreground": "ffffff","background": "990000","token": "invalid"}, {"background": "ffd0d0","token": "invalid.deprecated.trailing-whitespace"}, {"background": "f5aa7730","token": "text source"}, {"background": "f5aa7730","token": "string.unquoted"}, {"foreground": "8f7e65","token": "meta.tag.preprocessor.xml"}, {"foreground": "888888","token": "meta.tag.sgml.doctype"}, {"fontStyle": "italic","token": "string.quoted.docinfo.doctype.DTD"}, {"foreground": "43a8ed","token": "meta.tag"}, {"foreground": "43a8ed","token": "declaration.tag"}, {"fontStyle": "bold","token": "entity.name.tag"}, {"fontStyle": "italic","token": "entity.other.attribute-name"} ], "colors": { "editor.foreground": "#BDAE9D", "editor.background": "#2A211C", "editor.selectionBackground": "#C3DCFF", "editor.lineHighlightBackground": "#3A312C", "editorCursor.foreground": "#889AFF", "editorWhitespace.foreground": "#BFBFBF", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let githubDark = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "24292e","token": ""}, {"foreground": "959da5","token": "comment"}, {"foreground": "959da5","token": "punctuation.definition.comment"}, {"foreground": "959da5","token": "string.comment"}, {"foreground": "c8e1ff","token": "constant"}, {"foreground": "c8e1ff","token": "entity.name.constant"}, {"foreground": "c8e1ff","token": "variable.other.constant"}, {"foreground": "c8e1ff","token": "variable.language"}, {"foreground": "b392f0","token": "entity"}, {"foreground": "b392f0","token": "entity.name"}, {"foreground": "f6f8fa","token": "variable.parameter.function"}, {"foreground": "7bcc72","token": "entity.name.tag"}, {"foreground": "ea4a5a","token": "keyword"}, {"foreground": "ea4a5a","token": "storage"}, {"foreground": "ea4a5a","token": "storage.type"}, {"foreground": "f6f8fa","token": "storage.modifier.package"}, {"foreground": "f6f8fa","token": "storage.modifier.import"}, {"foreground": "f6f8fa","token": "storage.type.java"}, {"foreground": "79b8ff","token": "string"}, {"foreground": "79b8ff","token": "punctuation.definition.string"}, {"foreground": "79b8ff","token": "string punctuation.section.embedded source"}, {"foreground": "c8e1ff","token": "support"}, {"foreground": "c8e1ff","token": "meta.property-name"}, {"foreground": "fb8532","token": "variable"}, {"foreground": "f6f8fa","token": "variable.other"}, {"foreground": "d73a49","fontStyle": "bold italic underline","token": "invalid.broken"}, {"foreground": "d73a49","fontStyle": "bold italic underline","token": "invalid.deprecated"}, {"foreground": "fafbfc","background": "d73a49","fontStyle": "italic underline","token": "invalid.illegal"}, {"foreground": "fafbfc","background": "d73a49","fontStyle": "italic underline","token": "carriage-return"}, {"foreground": "d73a49","fontStyle": "bold italic underline","token": "invalid.unimplemented"}, {"foreground": "d73a49","token": "message.error"}, {"foreground": "f6f8fa","token": "string source"}, {"foreground": "c8e1ff","token": "string variable"}, {"foreground": "79b8ff","token": "source.regexp"}, {"foreground": "79b8ff","token": "string.regexp"}, {"foreground": "79b8ff","token": "string.regexp.character-class"}, {"foreground": "79b8ff","token": "string.regexp constant.character.escape"}, {"foreground": "79b8ff","token": "string.regexp source.ruby.embedded"}, {"foreground": "79b8ff","token": "string.regexp string.regexp.arbitrary-repitition"}, {"foreground": "7bcc72","fontStyle": "bold","token": "string.regexp constant.character.escape"}, {"foreground": "c8e1ff","token": "support.constant"}, {"foreground": "c8e1ff","token": "support.variable"}, {"foreground": "c8e1ff","token": "meta.module-reference"}, {"foreground": "fb8532","token": "markup.list"}, {"foreground": "0366d6","fontStyle": "bold","token": "markup.heading"}, {"foreground": "0366d6","fontStyle": "bold","token": "markup.heading entity.name"}, {"foreground": "c8e1ff","token": "markup.quote"}, {"foreground": "f6f8fa","fontStyle": "italic","token": "markup.italic"}, {"foreground": "f6f8fa","fontStyle": "bold","token": "markup.bold"}, {"foreground": "c8e1ff","token": "markup.raw"}, {"foreground": "b31d28","background": "ffeef0","token": "markup.deleted"}, {"foreground": "b31d28","background": "ffeef0","token": "meta.diff.header.from-file"}, {"foreground": "b31d28","background": "ffeef0","token": "punctuation.definition.deleted"}, {"foreground": "176f2c","background": "f0fff4","token": "markup.inserted"}, {"foreground": "176f2c","background": "f0fff4","token": "meta.diff.header.to-file"}, {"foreground": "176f2c","background": "f0fff4","token": "punctuation.definition.inserted"}, {"foreground": "b08800","background": "fffdef","token": "markup.changed"}, {"foreground": "b08800","background": "fffdef","token": "punctuation.definition.changed"}, {"foreground": "2f363d","background": "959da5","token": "markup.ignored"}, {"foreground": "2f363d","background": "959da5","token": "markup.untracked"}, {"foreground": "b392f0","fontStyle": "bold","token": "meta.diff.range"}, {"foreground": "c8e1ff","token": "meta.diff.header"}, {"foreground": "0366d6","fontStyle": "bold","token": "meta.separator"}, {"foreground": "0366d6","token": "meta.output"}, {"foreground": "ffeef0","token": "brackethighlighter.tag"}, {"foreground": "ffeef0","token": "brackethighlighter.curly"}, {"foreground": "ffeef0","token": "brackethighlighter.round"}, {"foreground": "ffeef0","token": "brackethighlighter.square"}, {"foreground": "ffeef0","token": "brackethighlighter.angle"}, {"foreground": "ffeef0","token": "brackethighlighter.quote"}, {"foreground": "d73a49","token": "brackethighlighter.unmatched"}, {"foreground": "d73a49","token": "sublimelinter.mark.error"}, {"foreground": "fb8532","token": "sublimelinter.mark.warning"}, {"foreground": "6a737d","token": "sublimelinter.gutter-mark"}, {"foreground": "79b8ff","fontStyle": "underline","token": "constant.other.reference.link"}, {"foreground": "79b8ff","fontStyle": "underline","token": "string.other.link"} ], "colors": { "editor.foreground": "#c9d1d9", "editor.background": "#0d1117", "editor.selectionBackground": "#4c2889", "editor.inactiveSelectionBackground": "#444d56", "editor.lineHighlightBackground": "#444d56", "editorCursor.foreground": "#c9d1d9", "editorWhitespace.foreground": "#6a737d", "editorIndentGuide.background": "#6a737d", "editorIndentGuide.activeBackground": "#f6f8fa", "editor.selectionHighlightBorder": "#444d56", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let githubLight = { "base": "vs", "inherit": true, "rules": [ {"background": "ffffff","token": ""}, {"foreground": "6a737d","token": "comment"}, {"foreground": "6a737d","token": "punctuation.definition.comment"}, {"foreground": "6a737d","token": "string.comment"}, {"foreground": "005cc5","token": "constant"}, {"foreground": "005cc5","token": "entity.name.constant"}, {"foreground": "005cc5","token": "variable.other.constant"}, {"foreground": "005cc5","token": "variable.language"}, {"foreground": "6f42c1","token": "entity"}, {"foreground": "6f42c1","token": "entity.name"}, {"foreground": "24292e","token": "variable.parameter.function"}, {"foreground": "22863a","token": "entity.name.tag"}, {"foreground": "d73a49","token": "keyword"}, {"foreground": "d73a49","token": "storage"}, {"foreground": "d73a49","token": "storage.type"}, {"foreground": "24292e","token": "storage.modifier.package"}, {"foreground": "24292e","token": "storage.modifier.import"}, {"foreground": "24292e","token": "storage.type.java"}, {"foreground": "032f62","token": "string"}, {"foreground": "032f62","token": "punctuation.definition.string"}, {"foreground": "032f62","token": "string punctuation.section.embedded source"}, {"foreground": "005cc5","token": "support"}, {"foreground": "005cc5","token": "meta.property-name"}, {"foreground": "e36209","token": "variable"}, {"foreground": "24292e","token": "variable.other"}, {"foreground": "b31d28","fontStyle": "bold italic underline","token": "invalid.broken"}, {"foreground": "b31d28","fontStyle": "bold italic underline","token": "invalid.deprecated"}, {"foreground": "fafbfc","background": "b31d28","fontStyle": "italic underline","token": "invalid.illegal"}, {"foreground": "fafbfc","background": "d73a49","fontStyle": "italic underline","token": "carriage-return"}, {"foreground": "b31d28","fontStyle": "bold italic underline","token": "invalid.unimplemented"}, {"foreground": "b31d28","token": "message.error"}, {"foreground": "24292e","token": "string source"}, {"foreground": "005cc5","token": "string variable"}, {"foreground": "032f62","token": "source.regexp"}, {"foreground": "032f62","token": "string.regexp"}, {"foreground": "032f62","token": "string.regexp.character-class"}, {"foreground": "032f62","token": "string.regexp constant.character.escape"}, {"foreground": "032f62","token": "string.regexp source.ruby.embedded"}, {"foreground": "032f62","token": "string.regexp string.regexp.arbitrary-repitition"}, {"foreground": "22863a","fontStyle": "bold","token": "string.regexp constant.character.escape"}, {"foreground": "005cc5","token": "support.constant"}, {"foreground": "005cc5","token": "support.variable"}, {"foreground": "005cc5","token": "meta.module-reference"}, {"foreground": "735c0f","token": "markup.list"}, {"foreground": "005cc5","fontStyle": "bold","token": "markup.heading"}, {"foreground": "005cc5","fontStyle": "bold","token": "markup.heading entity.name"}, {"foreground": "22863a","token": "markup.quote"}, {"foreground": "24292e","fontStyle": "italic","token": "markup.italic"}, {"foreground": "24292e","fontStyle": "bold","token": "markup.bold"}, {"foreground": "005cc5","token": "markup.raw"}, {"foreground": "b31d28","background": "ffeef0","token": "markup.deleted"}, {"foreground": "b31d28","background": "ffeef0","token": "meta.diff.header.from-file"}, {"foreground": "b31d28","background": "ffeef0","token": "punctuation.definition.deleted"}, {"foreground": "22863a","background": "f0fff4","token": "markup.inserted"}, {"foreground": "22863a","background": "f0fff4","token": "meta.diff.header.to-file"}, {"foreground": "22863a","background": "f0fff4","token": "punctuation.definition.inserted"}, {"foreground": "e36209","background": "ffebda","token": "markup.changed"}, {"foreground": "e36209","background": "ffebda","token": "punctuation.definition.changed"}, {"foreground": "f6f8fa","background": "005cc5","token": "markup.ignored"}, {"foreground": "f6f8fa","background": "005cc5","token": "markup.untracked"}, {"foreground": "6f42c1","fontStyle": "bold","token": "meta.diff.range"}, {"foreground": "005cc5","token": "meta.diff.header"}, {"foreground": "005cc5","fontStyle": "bold","token": "meta.separator"}, {"foreground": "005cc5","token": "meta.output"}, {"foreground": "586069","token": "brackethighlighter.tag"}, {"foreground": "586069","token": "brackethighlighter.curly"}, {"foreground": "586069","token": "brackethighlighter.round"}, {"foreground": "586069","token": "brackethighlighter.square"}, {"foreground": "586069","token": "brackethighlighter.angle"}, {"foreground": "586069","token": "brackethighlighter.quote"}, {"foreground": "b31d28","token": "brackethighlighter.unmatched"}, {"foreground": "b31d28","token": "sublimelinter.mark.error"}, {"foreground": "e36209","token": "sublimelinter.mark.warning"}, {"foreground": "959da5","token": "sublimelinter.gutter-mark"}, {"foreground": "032f62","fontStyle": "underline","token": "constant.other.reference.link"}, {"foreground": "032f62","fontStyle": "underline","token": "string.other.link"} ], "colors": { "editor.foreground": "#24292e", "editor.background": "#ffffff", "editor.selectionBackground": "#c8c8fa", "editor.inactiveSelectionBackground": "#fafbfc", "editor.lineHighlightBackground": "#fafbfc", "editorCursor.foreground": "#24292e", "editorWhitespace.foreground": "#959da5", "editorIndentGuide.background": "#959da5", "editorIndentGuide.activeBackground": "#24292e", "editor.selectionHighlightBorder": "#fafbfc", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let github = { "base": "vs", "inherit": true, "rules": [ {"background": "F8F8FF","token": ""}, {"foreground": "999988","fontStyle": "italic","token": "comment"}, {"foreground": "999999","fontStyle": "bold","token": "comment.block.preprocessor"}, {"foreground": "999999","fontStyle": "bold italic","token": "comment.documentation"}, {"foreground": "999999","fontStyle": "bold italic","token": "comment.block.documentation"}, {"foreground": "a61717","background": "e3d2d2","token": "invalid.illegal"}, {"fontStyle": "bold","token": "keyword"}, {"fontStyle": "bold","token": "storage"}, {"fontStyle": "bold","token": "keyword.operator"}, {"fontStyle": "bold","token": "constant.language"}, {"fontStyle": "bold","token": "support.constant"}, {"foreground": "445588","fontStyle": "bold","token": "storage.type"}, {"foreground": "445588","fontStyle": "bold","token": "support.type"}, {"foreground": "008080","token": "entity.other.attribute-name"}, {"foreground": "0086b3","token": "variable.other"}, {"foreground": "999999","token": "variable.language"}, {"foreground": "445588","fontStyle": "bold","token": "entity.name.type"}, {"foreground": "445588","fontStyle": "bold","token": "entity.other.inherited-class"}, {"foreground": "445588","fontStyle": "bold","token": "support.class"}, {"foreground": "008080","token": "variable.other.constant"}, {"foreground": "800080","token": "constant.character.entity"}, {"foreground": "990000","token": "entity.name.exception"}, {"foreground": "990000","token": "entity.name.function"}, {"foreground": "990000","token": "support.function"}, {"foreground": "990000","token": "keyword.other.name-of-parameter"}, {"foreground": "555555","token": "entity.name.section"}, {"foreground": "000080","token": "entity.name.tag"}, {"foreground": "008080","token": "variable.parameter"}, {"foreground": "008080","token": "support.variable"}, {"foreground": "009999","token": "constant.numeric"}, {"foreground": "009999","token": "constant.other"}, {"foreground": "dd1144","token": "string - string source"}, {"foreground": "dd1144","token": "constant.character"}, {"foreground": "009926","token": "string.regexp"}, {"foreground": "990073","token": "constant.other.symbol"}, {"fontStyle": "bold","token": "punctuation"}, {"foreground": "000000","background": "ffdddd","token": "markup.deleted"}, {"fontStyle": "italic","token": "markup.italic"}, {"foreground": "aa0000","token": "markup.error"}, {"foreground": "999999","token": "markup.heading.1"}, {"foreground": "000000","background": "ddffdd","token": "markup.inserted"}, {"foreground": "888888","token": "markup.output"}, {"foreground": "888888","token": "markup.raw"}, {"foreground": "555555","token": "markup.prompt"}, {"fontStyle": "bold","token": "markup.bold"}, {"foreground": "aaaaaa","token": "markup.heading"}, {"foreground": "aa0000","token": "markup.traceback"}, {"fontStyle": "underline","token": "markup.underline"}, {"foreground": "999999","background": "eaf2f5","token": "meta.diff.range"}, {"foreground": "999999","background": "eaf2f5","token": "meta.diff.index"}, {"foreground": "999999","background": "eaf2f5","token": "meta.separator"}, {"foreground": "999999","background": "ffdddd","token": "meta.diff.header.from-file"}, {"foreground": "999999","background": "ddffdd","token": "meta.diff.header.to-file"}, {"foreground": "4183c4","token": "meta.link"} ], "colors": { "editor.foreground": "#000000", "editor.background": "#F8F8FF", "editor.selectionBackground": "#B4D5FE", "editor.lineHighlightBackground": "#FFFEEB", "editorCursor.foreground": "#666666", "editorWhitespace.foreground": "#BBBBBB", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let merbivoreSoft = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "161616","token": ""}, {"foreground": "ad2ea4","fontStyle": "italic","token": "comment"}, {"foreground": "fc6f09","token": "keyword"}, {"foreground": "fc6f09","token": "storage"}, {"foreground": "fc83ff","token": "entity.other.inherited-class"}, {"foreground": "58c554","token": "constant.numeric"}, {"foreground": "1edafb","token": "constant"}, {"foreground": "8dff0a","token": "constant.library"}, {"foreground": "fc6f09","token": "support.function"}, {"foreground": "fdc251","token": "constant.language"}, {"foreground": "8dff0a","token": "string"}, {"foreground": "1edafb","token": "support.type"}, {"foreground": "8dff0a","token": "support.constant"}, {"foreground": "fc6f09","token": "meta.tag"}, {"foreground": "fc6f09","token": "declaration.tag"}, {"foreground": "fc6f09","token": "entity.name.tag"}, {"foreground": "ffff89","token": "entity.other.attribute-name"}, {"foreground": "ffffff","background": "990000","token": "invalid"}, {"foreground": "519f50","token": "constant.character.escaped"}, {"foreground": "519f50","token": "constant.character.escape"}, {"foreground": "519f50","token": "string source"}, {"foreground": "519f50","token": "string source.ruby"}, {"foreground": "e6e1dc","background": "144212","token": "markup.inserted"}, {"foreground": "e6e1dc","background": "660000","token": "markup.deleted"}, {"background": "2f33ab","token": "meta.diff.header"}, {"background": "2f33ab","token": "meta.separator.diff"}, {"background": "2f33ab","token": "meta.diff.index"}, {"background": "2f33ab","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#E6E1DC", "editor.background": "#161616", "editor.selectionBackground": "#454545", "editor.lineHighlightBackground": "#333435", "editorCursor.foreground": "#FFFFFF", "editorWhitespace.foreground": "#404040", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let monokai = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "272822","token": ""}, {"foreground": "75715e","token": "comment"}, {"foreground": "e6db74","token": "string"}, {"foreground": "ae81ff","token": "constant.numeric"}, {"foreground": "ae81ff","token": "constant.language"}, {"foreground": "ae81ff","token": "constant.character"}, {"foreground": "ae81ff","token": "constant.other"}, {"foreground": "f92672","token": "keyword"}, {"foreground": "f92672","token": "storage"}, {"foreground": "66d9ef","fontStyle": "italic","token": "storage.type"}, {"foreground": "a6e22e","fontStyle": "underline","token": "entity.name.class"}, {"foreground": "a6e22e","fontStyle": "italic underline","token": "entity.other.inherited-class"}, {"foreground": "a6e22e","token": "entity.name.function"}, {"foreground": "fd971f","fontStyle": "italic","token": "variable.parameter"}, {"foreground": "f92672","token": "entity.name.tag"}, {"foreground": "a6e22e","token": "entity.other.attribute-name"}, {"foreground": "66d9ef","token": "support.function"}, {"foreground": "66d9ef","token": "support.constant"}, {"foreground": "66d9ef","fontStyle": "italic","token": "support.type"}, {"foreground": "66d9ef","fontStyle": "italic","token": "support.class"}, {"foreground": "f8f8f0","background": "f92672","token": "invalid"}, {"foreground": "f8f8f0","background": "ae81ff","token": "invalid.deprecated"}, {"foreground": "cfcfc2","token": "meta.structure.dictionary.json string.quoted.double.json"}, {"foreground": "75715e","token": "meta.diff"}, {"foreground": "75715e","token": "meta.diff.header"}, {"foreground": "f92672","token": "markup.deleted"}, {"foreground": "a6e22e","token": "markup.inserted"}, {"foreground": "e6db74","token": "markup.changed"}, {"foreground": "ae81ffa0","token": "constant.numeric.line-number.find-in-files - match"}, {"foreground": "e6db74","token": "entity.name.filename.find-in-files"} ], "colors": { "editor.foreground": "#F8F8F2", "editor.background": "#272822", "editor.selectionBackground": "#49483E", "editor.lineHighlightBackground": "#3E3D32", "editorCursor.foreground": "#F8F8F0", "editorWhitespace.foreground": "#3B3A32", "editorIndentGuide.activeBackground": "#9D550FB0", "editor.selectionHighlightBorder": "#222218", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let nightOwl = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "011627","token": ""}, {"foreground": "637777","token": "comment"}, {"foreground": "addb67","token": "string"}, {"foreground": "ecc48d","token": "vstring.quoted"}, {"foreground": "ecc48d","token": "variable.other.readwrite.js"}, {"foreground": "5ca7e4","token": "string.regexp"}, {"foreground": "5ca7e4","token": "string.regexp keyword.other"}, {"foreground": "5f7e97","token": "meta.function punctuation.separator.comma"}, {"foreground": "f78c6c","token": "constant.numeric"}, {"foreground": "f78c6c","token": "constant.character.numeric"}, {"foreground": "addb67","token": "variable"}, {"foreground": "c792ea","token": "keyword"}, {"foreground": "c792ea","token": "punctuation.accessor"}, {"foreground": "c792ea","token": "storage"}, {"foreground": "c792ea","token": "meta.var.expr"}, {"foreground": "c792ea","token": "meta.class meta.method.declaration meta.var.expr storage.type.jsm"}, {"foreground": "c792ea","token": "storage.type.property.js"}, {"foreground": "c792ea","token": "storage.type.property.ts"}, {"foreground": "c792ea","token": "storage.type.property.tsx"}, {"foreground": "82aaff","token": "storage.type"}, {"foreground": "ffcb8b","token": "entity.name.class"}, {"foreground": "ffcb8b","token": "meta.class entity.name.type.class"}, {"foreground": "addb67","token": "entity.other.inherited-class"}, {"foreground": "82aaff","token": "entity.name.function"}, {"foreground": "addb67","token": "punctuation.definition.variable"}, {"foreground": "d3423e","token": "punctuation.section.embedded"}, {"foreground": "d6deeb","token": "punctuation.terminator.expression"}, {"foreground": "d6deeb","token": "punctuation.definition.arguments"}, {"foreground": "d6deeb","token": "punctuation.definition.array"}, {"foreground": "d6deeb","token": "punctuation.section.array"}, {"foreground": "d6deeb","token": "meta.array"}, {"foreground": "d9f5dd","token": "punctuation.definition.list.begin"}, {"foreground": "d9f5dd","token": "punctuation.definition.list.end"}, {"foreground": "d9f5dd","token": "punctuation.separator.arguments"}, {"foreground": "d9f5dd","token": "punctuation.definition.list"}, {"foreground": "d3423e","token": "string.template meta.template.expression"}, {"foreground": "d6deeb","token": "string.template punctuation.definition.string"}, {"foreground": "c792ea","fontStyle": "italic","token": "italic"}, {"foreground": "addb67","fontStyle": "bold","token": "bold"}, {"foreground": "82aaff","token": "constant.language"}, {"foreground": "82aaff","token": "punctuation.definition.constant"}, {"foreground": "82aaff","token": "variable.other.constant"}, {"foreground": "7fdbca","token": "support.function.construct"}, {"foreground": "7fdbca","token": "keyword.other.new"}, {"foreground": "82aaff","token": "constant.character"}, {"foreground": "82aaff","token": "constant.other"}, {"foreground": "f78c6c","token": "constant.character.escape"}, {"foreground": "addb67","token": "entity.other.inherited-class"}, {"foreground": "d7dbe0","token": "variable.parameter"}, {"foreground": "7fdbca","token": "entity.name.tag"}, {"foreground": "cc2996","token": "punctuation.definition.tag.html"}, {"foreground": "cc2996","token": "punctuation.definition.tag.begin"}, {"foreground": "cc2996","token": "punctuation.definition.tag.end"}, {"foreground": "addb67","token": "entity.other.attribute-name"}, {"foreground": "addb67","token": "entity.name.tag.custom"}, {"foreground": "82aaff","token": "support.function"}, {"foreground": "82aaff","token": "support.constant"}, {"foreground": "7fdbca","token": "upport.constant.meta.property-value"}, {"foreground": "addb67","token": "support.type"}, {"foreground": "addb67","token": "support.class"}, {"foreground": "addb67","token": "support.variable.dom"}, {"foreground": "7fdbca","token": "support.constant"}, {"foreground": "7fdbca","token": "keyword.other.special-method"}, {"foreground": "7fdbca","token": "keyword.other.new"}, {"foreground": "7fdbca","token": "keyword.other.debugger"}, {"foreground": "7fdbca","token": "keyword.control"}, {"foreground": "c792ea","token": "keyword.operator.comparison"}, {"foreground": "c792ea","token": "keyword.control.flow.js"}, {"foreground": "c792ea","token": "keyword.control.flow.ts"}, {"foreground": "c792ea","token": "keyword.control.flow.tsx"}, {"foreground": "c792ea","token": "keyword.control.ruby"}, {"foreground": "c792ea","token": "keyword.control.module.ruby"}, {"foreground": "c792ea","token": "keyword.control.class.ruby"}, {"foreground": "c792ea","token": "keyword.control.def.ruby"}, {"foreground": "c792ea","token": "keyword.control.loop.js"}, {"foreground": "c792ea","token": "keyword.control.loop.ts"}, {"foreground": "c792ea","token": "keyword.control.import.js"}, {"foreground": "c792ea","token": "keyword.control.import.ts"}, {"foreground": "c792ea","token": "keyword.control.import.tsx"}, {"foreground": "c792ea","token": "keyword.control.from.js"}, {"foreground": "c792ea","token": "keyword.control.from.ts"}, {"foreground": "c792ea","token": "keyword.control.from.tsx"}, {"foreground": "ffffff","background": "ff2c83","token": "invalid"}, {"foreground": "ffffff","background": "d3423e","token": "invalid.deprecated"}, {"foreground": "7fdbca","token": "keyword.operator"}, {"foreground": "c792ea","token": "keyword.operator.relational"}, {"foreground": "c792ea","token": "keyword.operator.assignement"}, {"foreground": "c792ea","token": "keyword.operator.arithmetic"}, {"foreground": "c792ea","token": "keyword.operator.bitwise"}, {"foreground": "c792ea","token": "keyword.operator.increment"}, {"foreground": "c792ea","token": "keyword.operator.ternary"}, {"foreground": "637777","token": "comment.line.double-slash"}, {"foreground": "cdebf7","token": "object"}, {"foreground": "ff5874","token": "constant.language.null"}, {"foreground": "d6deeb","token": "meta.brace"}, {"foreground": "c792ea","token": "meta.delimiter.period"}, {"foreground": "d9f5dd","token": "punctuation.definition.string"}, {"foreground": "ff5874","token": "constant.language.boolean"}, {"foreground": "ffffff","token": "object.comma"}, {"foreground": "7fdbca","token": "variable.parameter.function"}, {"foreground": "80cbc4","token": "support.type.vendor.property-name"}, {"foreground": "80cbc4","token": "support.constant.vendor.property-value"}, {"foreground": "80cbc4","token": "support.type.property-name"}, {"foreground": "80cbc4","token": "meta.property-list entity.name.tag"}, {"foreground": "57eaf1","token": "meta.property-list entity.name.tag.reference"}, {"foreground": "f78c6c","token": "constant.other.color.rgb-value punctuation.definition.constant"}, {"foreground": "ffeb95","token": "constant.other.color"}, {"foreground": "ffeb95","token": "keyword.other.unit"}, {"foreground": "c792ea","token": "meta.selector"}, {"foreground": "fad430","token": "entity.other.attribute-name.id"}, {"foreground": "80cbc4","token": "meta.property-name"}, {"foreground": "c792ea","token": "entity.name.tag.doctype"}, {"foreground": "c792ea","token": "meta.tag.sgml.doctype"}, {"foreground": "d9f5dd","token": "punctuation.definition.parameters"}, {"foreground": "ecc48d","token": "string.quoted"}, {"foreground": "ecc48d","token": "string.quoted.double"}, {"foreground": "ecc48d","token": "string.quoted.single"}, {"foreground": "addb67","token": "support.constant.math"}, {"foreground": "addb67","token": "support.type.property-name.json"}, {"foreground": "addb67","token": "support.constant.json"}, {"foreground": "c789d6","token": "meta.structure.dictionary.value.json string.quoted.double"}, {"foreground": "80cbc4","token": "string.quoted.double.json punctuation.definition.string.json"}, {"foreground": "ff5874","token": "meta.structure.dictionary.json meta.structure.dictionary.value constant.language"}, {"foreground": "d6deeb","token": "variable.other.ruby"}, {"foreground": "ecc48d","token": "entity.name.type.class.ruby"}, {"foreground": "ecc48d","token": "keyword.control.class.ruby"}, {"foreground": "ecc48d","token": "meta.class.ruby"}, {"foreground": "7fdbca","token": "constant.language.symbol.hashkey.ruby"}, {"foreground": "e0eddd","background": "a57706","fontStyle": "italic","token": "meta.diff"}, {"foreground": "e0eddd","background": "a57706","fontStyle": "italic","token": "meta.diff.header"}, {"foreground": "ef535090","fontStyle": "italic","token": "markup.deleted"}, {"foreground": "a2bffc","fontStyle": "italic","token": "markup.changed"}, {"foreground": "a2bffc","fontStyle": "italic","token": "meta.diff.header.git"}, {"foreground": "a2bffc","fontStyle": "italic","token": "meta.diff.header.from-file"}, {"foreground": "a2bffc","fontStyle": "italic","token": "meta.diff.header.to-file"}, {"foreground": "219186","background": "eae3ca","token": "markup.inserted"}, {"foreground": "d3201f","token": "other.package.exclude"}, {"foreground": "d3201f","token": "other.remove"}, {"foreground": "269186","token": "other.add"}, {"foreground": "ff5874","token": "constant.language.python"}, {"foreground": "82aaff","token": "variable.parameter.function.python"}, {"foreground": "82aaff","token": "meta.function-call.arguments.python"}, {"foreground": "b2ccd6","token": "meta.function-call.python"}, {"foreground": "b2ccd6","token": "meta.function-call.generic.python"}, {"foreground": "d6deeb","token": "punctuation.python"}, {"foreground": "addb67","token": "entity.name.function.decorator.python"}, {"foreground": "8eace3","token": "source.python variable.language.special"}, {"foreground": "82b1ff","token": "markup.heading.markdown"}, {"foreground": "c792ea","fontStyle": "italic","token": "markup.italic.markdown"}, {"foreground": "addb67","fontStyle": "bold","token": "markup.bold.markdown"}, {"foreground": "697098","token": "markup.quote.markdown"}, {"foreground": "80cbc4","token": "markup.inline.raw.markdown"}, {"foreground": "ff869a","token": "markup.underline.link.markdown"}, {"foreground": "ff869a","token": "markup.underline.link.image.markdown"}, {"foreground": "d6deeb","token": "string.other.link.title.markdown"}, {"foreground": "d6deeb","token": "string.other.link.description.markdown"}, {"foreground": "82b1ff","token": "punctuation.definition.string.markdown"}, {"foreground": "82b1ff","token": "punctuation.definition.string.begin.markdown"}, {"foreground": "82b1ff","token": "punctuation.definition.string.end.markdown"}, {"foreground": "82b1ff","token": "meta.link.inline.markdown punctuation.definition.string"}, {"foreground": "7fdbca","token": "punctuation.definition.metadata.markdown"}, {"foreground": "82b1ff","token": "beginning.punctuation.definition.list.markdown"} ], "colors": { "editor.foreground": "#d6deeb", "editor.background": "#011627", "editor.selectionBackground": "#5f7e9779", "editor.lineHighlightBackground": "#010E17", "editorCursor.foreground": "#80a4c2", "editorWhitespace.foreground": "#2e2040", "editorIndentGuide.background": "#5e81ce52", "editor.selectionHighlightBorder": "#122d42", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let nord = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "2E3440","token": ""}, {"foreground": "616e88","token": "comment"}, {"foreground": "a3be8c","token": "string"}, {"foreground": "b48ead","token": "constant.numeric"}, {"foreground": "81a1c1","token": "constant.language"}, {"foreground": "81a1c1","token": "keyword"}, {"foreground": "81a1c1","token": "storage"}, {"foreground": "81a1c1","token": "storage.type"}, {"foreground": "8fbcbb","token": "entity.name.class"}, {"foreground": "8fbcbb","fontStyle": " bold","token": "entity.other.inherited-class"}, {"foreground": "88c0d0","token": "entity.name.function"}, {"foreground": "81a1c1","token": "entity.name.tag"}, {"foreground": "8fbcbb","token": "entity.other.attribute-name"}, {"foreground": "88c0d0","token": "support.function"}, {"foreground": "f8f8f0","background": "f92672","token": "invalid"}, {"foreground": "f8f8f0","background": "ae81ff","token": "invalid.deprecated"}, {"foreground": "b48ead","token": "constant.color.other.rgb-value"}, {"foreground": "ebcb8b","token": "constant.character.escape"}, {"foreground": "8fbcbb","token": "variable.other.constant"} ], "colors": { "editor.foreground": "#D8DEE9", "editor.background": "#2E3440", "editor.selectionBackground": "#434C5ECC", "editor.lineHighlightBackground": "#3B4252", "editorCursor.foreground": "#D8DEE9", "editorWhitespace.foreground": "#434C5ECC", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let oceanicNext = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "1B2B34","token": ""}, {"foreground": "65737e","token": "comment"}, {"foreground": "65737e","token": "punctuation.definition.comment"}, {"foreground": "cdd3de","token": "variable"}, {"foreground": "c594c5","token": "keyword"}, {"foreground": "c594c5","token": "storage.type"}, {"foreground": "c594c5","token": "storage.modifier"}, {"foreground": "5fb3b3","token": "keyword.operator"}, {"foreground": "5fb3b3","token": "constant.other.color"}, {"foreground": "5fb3b3","token": "punctuation"}, {"foreground": "5fb3b3","token": "meta.tag"}, {"foreground": "5fb3b3","token": "punctuation.definition.tag"}, {"foreground": "5fb3b3","token": "punctuation.separator.inheritance.php"}, {"foreground": "5fb3b3","token": "punctuation.definition.tag.html"}, {"foreground": "5fb3b3","token": "punctuation.definition.tag.begin.html"}, {"foreground": "5fb3b3","token": "punctuation.definition.tag.end.html"}, {"foreground": "5fb3b3","token": "punctuation.section.embedded"}, {"foreground": "5fb3b3","token": "keyword.other.template"}, {"foreground": "5fb3b3","token": "keyword.other.substitution"}, {"foreground": "eb606b","token": "entity.name.tag"}, {"foreground": "eb606b","token": "meta.tag.sgml"}, {"foreground": "eb606b","token": "markup.deleted.git_gutter"}, {"foreground": "6699cc","token": "entity.name.function"}, {"foreground": "6699cc","token": "meta.function-call"}, {"foreground": "6699cc","token": "variable.function"}, {"foreground": "6699cc","token": "support.function"}, {"foreground": "6699cc","token": "keyword.other.special-method"}, {"foreground": "6699cc","token": "meta.block-level"}, {"foreground": "f2777a","token": "support.other.variable"}, {"foreground": "f2777a","token": "string.other.link"}, {"foreground": "f99157","token": "constant.numeric"}, {"foreground": "f99157","token": "constant.language"}, {"foreground": "f99157","token": "support.constant"}, {"foreground": "f99157","token": "constant.character"}, {"foreground": "f99157","token": "variable.parameter"}, {"foreground": "f99157","token": "keyword.other.unit"}, {"foreground": "99c794","fontStyle": "normal","token": "string"}, {"foreground": "99c794","fontStyle": "normal","token": "constant.other.symbol"}, {"foreground": "99c794","fontStyle": "normal","token": "constant.other.key"}, {"foreground": "99c794","fontStyle": "normal","token": "entity.other.inherited-class"}, {"foreground": "99c794","fontStyle": "normal","token": "markup.heading"}, {"foreground": "99c794","fontStyle": "normal","token": "markup.inserted.git_gutter"}, {"foreground": "99c794","fontStyle": "normal","token": "meta.group.braces.curly constant.other.object.key.js string.unquoted.label.js"}, {"foreground": "fac863","token": "entity.name.class"}, {"foreground": "fac863","token": "entity.name.type.class"}, {"foreground": "fac863","token": "support.type"}, {"foreground": "fac863","token": "support.class"}, {"foreground": "fac863","token": "support.orther.namespace.use.php"}, {"foreground": "fac863","token": "meta.use.php"}, {"foreground": "fac863","token": "support.other.namespace.php"}, {"foreground": "fac863","token": "markup.changed.git_gutter"}, {"foreground": "ec5f67","token": "entity.name.module.js"}, {"foreground": "ec5f67","token": "variable.import.parameter.js"}, {"foreground": "ec5f67","token": "variable.other.class.js"}, {"foreground": "ec5f67","fontStyle": "italic","token": "variable.language"}, {"foreground": "cdd3de","token": "meta.group.braces.curly.js constant.other.object.key.js string.unquoted.label.js"}, {"foreground": "d8dee9","token": "meta.class-method.js entity.name.function.js"}, {"foreground": "d8dee9","token": "variable.function.constructor"}, {"foreground": "d8dee9","token": "meta.class.js meta.class.property.js meta.method.js string.unquoted.js entity.name.function.js"}, {"foreground": "bb80b3","token": "entity.other.attribute-name"}, {"foreground": "99c794","token": "markup.inserted"}, {"foreground": "ec5f67","token": "markup.deleted"}, {"foreground": "bb80b3","token": "markup.changed"}, {"foreground": "5fb3b3","token": "string.regexp"}, {"foreground": "5fb3b3","token": "constant.character.escape"}, {"fontStyle": "underline","token": "*url*"}, {"fontStyle": "underline","token": "*link*"}, {"fontStyle": "underline","token": "*uri*"}, {"foreground": "ab7967","token": "constant.numeric.line-number.find-in-files - match"}, {"foreground": "99c794","token": "entity.name.filename.find-in-files"}, {"foreground": "6699cc","fontStyle": "italic","token": "tag.decorator.js entity.name.tag.js"}, {"foreground": "6699cc","fontStyle": "italic","token": "tag.decorator.js punctuation.definition.tag.js"}, {"foreground": "ec5f67","fontStyle": "italic","token": "source.js constant.other.object.key.js string.unquoted.label.js"}, {"foreground": "fac863","token": "source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "fac863","token": "source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "c594c5","token": "source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "c594c5","token": "source.json meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "d8dee9","token": "source.json meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "d8dee9","token": "source.json meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "6699cc","token": "source.json meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "6699cc","token": "source.json meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "ab7967","token": "source.json meta meta meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "ab7967","token": "source.json meta meta meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "ec5f67","token": "source.json meta meta meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "ec5f67","token": "source.json meta meta meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "f99157","token": "source.json meta meta meta meta.structure.dictionary.json string.quoted.double.json - meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "f99157","token": "source.json meta meta meta meta.structure.dictionary.json punctuation.definition.string - meta meta meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "fac863","token": "source.json meta meta.structure.dictionary.json string.quoted.double.json - meta meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "fac863","token": "source.json meta meta.structure.dictionary.json punctuation.definition.string - meta meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"}, {"foreground": "c594c5","token": "source.json meta.structure.dictionary.json string.quoted.double.json - meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json"}, {"foreground": "c594c5","token": "source.json meta.structure.dictionary.json punctuation.definition.string - meta.structure.dictionary.json meta.structure.dictionary.value.json punctuation.definition.string"} ], "colors": { "editor.foreground": "#CDD3DE", "editor.background": "#1B2B34", "editor.selectionBackground": "#4f5b66", "editor.lineHighlightBackground": "#65737e55", "editorCursor.foreground": "#c0c5ce", "editorWhitespace.foreground": "#65737e", "editorIndentGuide.background": "#65737F", "editorIndentGuide.activeBackground": "#FBC95A", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let pastelsOnDark = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "211E1E","token": ""}, {"foreground": "555555","token": "comment"}, {"foreground": "555555","token": "comment.block"}, {"foreground": "ad9361","token": "string"}, {"foreground": "cccccc","token": "constant.numeric"}, {"foreground": "a1a1ff","token": "keyword"}, {"foreground": "2f006e","token": "meta.preprocessor"}, {"fontStyle": "bold","token": "keyword.control.import"}, {"foreground": "a1a1ff","token": "support.function"}, {"foreground": "0000ff","token": "declaration.function function-result"}, {"fontStyle": "bold","token": "declaration.function function-name"}, {"fontStyle": "bold","token": "declaration.function argument-name"}, {"foreground": "0000ff","token": "declaration.function function-arg-type"}, {"fontStyle": "italic","token": "declaration.function function-argument"}, {"fontStyle": "underline","token": "declaration.class class-name"}, {"fontStyle": "italic underline","token": "declaration.class class-inheritance"}, {"foreground": "fff9f9","background": "ff0000","fontStyle": "bold","token": "invalid"}, {"background": "ffd0d0","token": "invalid.deprecated.trailing-whitespace"}, {"fontStyle": "italic","token": "declaration.section section-name"}, {"foreground": "c10006","token": "string.interpolation"}, {"foreground": "666666","token": "string.regexp"}, {"foreground": "c1c144","token": "variable"}, {"foreground": "6782d3","token": "constant"}, {"foreground": "afa472","token": "constant.character"}, {"foreground": "de8e30","fontStyle": "bold","token": "constant.language"}, {"fontStyle": "underline","token": "embedded"}, {"foreground": "858ef4","token": "keyword.markup.element-name"}, {"foreground": "9b456f","token": "keyword.markup.attribute-name"}, {"foreground": "9b456f","token": "meta.attribute-with-value"}, {"foreground": "c82255","fontStyle": "bold","token": "keyword.exception"}, {"foreground": "47b8d6","token": "keyword.operator"}, {"foreground": "6969fa","fontStyle": "bold","token": "keyword.control"}, {"foreground": "68685b","token": "meta.tag.preprocessor.xml"}, {"foreground": "888888","token": "meta.tag.sgml.doctype"}, {"fontStyle": "italic","token": "string.quoted.docinfo.doctype.DTD"}, {"foreground": "909090","token": "comment.other.server-side-include.xhtml"}, {"foreground": "909090","token": "comment.other.server-side-include.html"}, {"foreground": "858ef4","token": "text.html declaration.tag"}, {"foreground": "858ef4","token": "text.html meta.tag"}, {"foreground": "858ef4","token": "text.html entity.name.tag.xhtml"}, {"foreground": "9b456f","token": "keyword.markup.attribute-name"}, {"foreground": "777777","token": "keyword.other.phpdoc.php"}, {"foreground": "c82255","token": "keyword.other.include.php"}, {"foreground": "de8e20","fontStyle": "bold","token": "support.constant.core.php"}, {"foreground": "de8e10","fontStyle": "bold","token": "support.constant.std.php"}, {"foreground": "b72e1d","token": "variable.other.global.php"}, {"foreground": "00ff00","token": "variable.other.global.safer.php"}, {"foreground": "bfa36d","token": "string.quoted.single.php"}, {"foreground": "6969fa","token": "keyword.storage.php"}, {"foreground": "ad9361","token": "string.quoted.double.php"}, {"foreground": "ec9e00","token": "entity.other.attribute-name.id.css"}, {"foreground": "b8cd06","fontStyle": "bold","token": "entity.name.tag.css"}, {"foreground": "edca06","token": "entity.other.attribute-name.class.css"}, {"foreground": "2e759c","token": "entity.other.attribute-name.pseudo-class.css"}, {"foreground": "ffffff","background": "ff0000","token": "invalid.bad-comma.css"}, {"foreground": "9b2e4d","token": "support.constant.property-value.css"}, {"foreground": "e1c96b","token": "support.type.property-name.css"}, {"foreground": "666633","token": "constant.other.rgb-value.css"}, {"foreground": "666633","token": "support.constant.font-name.css"}, {"foreground": "7171f3","token": "support.constant.tm-language-def"}, {"foreground": "7171f3","token": "support.constant.name.tm-language-def"}, {"foreground": "6969fa","token": "keyword.other.unit.css"} ], "colors": { "editor.foreground": "#DADADA", "editor.background": "#211E1E", "editor.selectionBackground": "#73597E80", "editor.lineHighlightBackground": "#353030", "editorCursor.foreground": "#FFFFFF", "editorWhitespace.foreground": "#4F4D4D", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let sunburst = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "000000","token": ""}, {"foreground": "aeaeae","fontStyle": "italic","token": "comment"}, {"foreground": "3387cc","token": "constant"}, {"foreground": "89bdff","token": "entity"}, {"foreground": "e28964","token": "keyword"}, {"foreground": "99cf50","token": "storage"}, {"foreground": "65b042","token": "string"}, {"foreground": "9b859d","token": "support"}, {"foreground": "3e87e3","token": "variable"}, {"foreground": "fd5ff1","fontStyle": "italic underline","token": "invalid.deprecated"}, {"foreground": "fd5ff1","background": "562d56bf","token": "invalid.illegal"}, {"background": "b1b3ba08","token": "text source"}, {"foreground": "9b5c2e","fontStyle": "italic","token": "entity.other.inherited-class"}, {"foreground": "daefa3","token": "string.quoted source"}, {"foreground": "ddf2a4","token": "string constant"}, {"foreground": "e9c062","token": "string.regexp"}, {"foreground": "cf7d34","token": "string.regexp constant.character.escape"}, {"foreground": "cf7d34","token": "string.regexp source.ruby.embedded"}, {"foreground": "cf7d34","token": "string.regexp string.regexp.arbitrary-repitition"}, {"foreground": "8a9a95","token": "string variable"}, {"foreground": "dad085","token": "support.function"}, {"foreground": "cf6a4c","token": "support.constant"}, {"foreground": "8996a8","token": "meta.preprocessor.c"}, {"foreground": "afc4db","token": "meta.preprocessor.c keyword"}, {"fontStyle": "underline","token": "entity.name.type"}, {"foreground": "676767","fontStyle": "italic","token": "meta.cast"}, {"foreground": "494949","token": "meta.sgml.html meta.doctype"}, {"foreground": "494949","token": "meta.sgml.html meta.doctype entity"}, {"foreground": "494949","token": "meta.sgml.html meta.doctype string"}, {"foreground": "494949","token": "meta.xml-processing"}, {"foreground": "494949","token": "meta.xml-processing entity"}, {"foreground": "494949","token": "meta.xml-processing string"}, {"foreground": "89bdff","token": "meta.tag"}, {"foreground": "89bdff","token": "meta.tag entity"}, {"foreground": "e0c589","token": "source entity.name.tag"}, {"foreground": "e0c589","token": "source entity.other.attribute-name"}, {"foreground": "e0c589","token": "meta.tag.inline"}, {"foreground": "e0c589","token": "meta.tag.inline entity"}, {"foreground": "e18964","token": "entity.name.tag.namespace"}, {"foreground": "e18964","token": "entity.other.attribute-name.namespace"}, {"foreground": "cda869","token": "meta.selector.css entity.name.tag"}, {"foreground": "8f9d6a","token": "meta.selector.css entity.other.attribute-name.tag.pseudo-class"}, {"foreground": "8b98ab","token": "meta.selector.css entity.other.attribute-name.id"}, {"foreground": "9b703f","token": "meta.selector.css entity.other.attribute-name.class"}, {"foreground": "c5af75","token": "support.type.property-name.css"}, {"foreground": "f9ee98","token": "meta.property-group support.constant.property-value.css"}, {"foreground": "f9ee98","token": "meta.property-value support.constant.property-value.css"}, {"foreground": "8693a5","token": "meta.preprocessor.at-rule keyword.control.at-rule"}, {"foreground": "dd7b3b","token": "meta.property-value support.constant.named-color.css"}, {"foreground": "dd7b3b","token": "meta.property-value constant"}, {"foreground": "8f9d6a","token": "meta.constructor.argument.css"}, {"foreground": "f8f8f8","background": "0e2231","fontStyle": "italic","token": "meta.diff"}, {"foreground": "f8f8f8","background": "0e2231","fontStyle": "italic","token": "meta.diff.header"}, {"foreground": "f8f8f8","background": "420e09","token": "markup.deleted"}, {"foreground": "f8f8f8","background": "4a410d","token": "markup.changed"}, {"foreground": "f8f8f8","background": "253b22","token": "markup.inserted"}, {"foreground": "e9c062","fontStyle": "italic","token": "markup.italic"}, {"foreground": "e9c062","fontStyle": "bold","token": "markup.bold"}, {"foreground": "e18964","fontStyle": "underline","token": "markup.underline"}, {"foreground": "e1d4b9","background": "fee09c12","fontStyle": "italic","token": "markup.quote"}, {"foreground": "fedcc5","background": "632d04","token": "markup.heading"}, {"foreground": "fedcc5","background": "632d04","token": "markup.heading entity"}, {"foreground": "e1d4b9","token": "markup.list"}, {"foreground": "578bb3","background": "b1b3ba08","token": "markup.raw"}, {"foreground": "f67b37","fontStyle": "italic","token": "markup comment"}, {"foreground": "60a633","background": "242424","token": "meta.separator"}, {"background": "eeeeee29","token": "meta.line.entry.logfile"}, {"background": "eeeeee29","token": "meta.line.exit.logfile"}, {"background": "751012","token": "meta.line.error.logfile"} ], "colors": { "editor.foreground": "#F8F8F8", "editor.background": "#000000", "editor.selectionBackground": "#DDF0FF33", "editor.lineHighlightBackground": "#FFFFFF0D", "editorCursor.foreground": "#A7A7A7", "editorWhitespace.foreground": "#CAE2FB3D", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let tomorrowNightBlue = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "002451","token": ""}, {"foreground": "7285b7","token": "comment"}, {"foreground": "ffffff","token": "keyword.operator.class"}, {"foreground": "ffffff","token": "keyword.operator"}, {"foreground": "ffffff","token": "constant.other"}, {"foreground": "ffffff","token": "source.php.embedded.line"}, {"foreground": "ff9da4","token": "variable"}, {"foreground": "ff9da4","token": "support.other.variable"}, {"foreground": "ff9da4","token": "string.other.link"}, {"foreground": "ff9da4","token": "string.regexp"}, {"foreground": "ff9da4","token": "entity.name.tag"}, {"foreground": "ff9da4","token": "entity.other.attribute-name"}, {"foreground": "ff9da4","token": "meta.tag"}, {"foreground": "ff9da4","token": "declaration.tag"}, {"foreground": "ff9da4","token": "markup.deleted.git_gutter"}, {"foreground": "ffc58f","token": "constant.numeric"}, {"foreground": "ffc58f","token": "constant.language"}, {"foreground": "ffc58f","token": "support.constant"}, {"foreground": "ffc58f","token": "constant.character"}, {"foreground": "ffc58f","token": "variable.parameter"}, {"foreground": "ffc58f","token": "punctuation.section.embedded"}, {"foreground": "ffc58f","token": "keyword.other.unit"}, {"foreground": "ffeead","token": "entity.name.class"}, {"foreground": "ffeead","token": "entity.name.type.class"}, {"foreground": "ffeead","token": "support.type"}, {"foreground": "ffeead","token": "support.class"}, {"foreground": "d1f1a9","token": "string"}, {"foreground": "d1f1a9","token": "constant.other.symbol"}, {"foreground": "d1f1a9","token": "entity.other.inherited-class"}, {"foreground": "d1f1a9","token": "markup.heading"}, {"foreground": "d1f1a9","token": "markup.inserted.git_gutter"}, {"foreground": "99ffff","token": "keyword.operator"}, {"foreground": "99ffff","token": "constant.other.color"}, {"foreground": "bbdaff","token": "entity.name.function"}, {"foreground": "bbdaff","token": "meta.function-call"}, {"foreground": "bbdaff","token": "support.function"}, {"foreground": "bbdaff","token": "keyword.other.special-method"}, {"foreground": "bbdaff","token": "meta.block-level"}, {"foreground": "bbdaff","token": "markup.changed.git_gutter"}, {"foreground": "ebbbff","token": "keyword"}, {"foreground": "ebbbff","token": "storage"}, {"foreground": "ebbbff","token": "storage.type"}, {"foreground": "ebbbff","token": "entity.name.tag.css"}, {"foreground": "ffffff","background": "f99da5","token": "invalid"}, {"foreground": "ffffff","background": "bbdafe","token": "meta.separator"}, {"foreground": "ffffff","background": "ebbbff","token": "invalid.deprecated"}, {"foreground": "ffffff","token": "markup.inserted.diff"}, {"foreground": "ffffff","token": "markup.deleted.diff"}, {"foreground": "ffffff","token": "meta.diff.header.to-file"}, {"foreground": "ffffff","token": "meta.diff.header.from-file"}, {"foreground": "718c00","token": "markup.inserted.diff"}, {"foreground": "718c00","token": "meta.diff.header.to-file"}, {"foreground": "c82829","token": "markup.deleted.diff"}, {"foreground": "c82829","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.to-file"}, {"foreground": "3e999f","fontStyle": "italic","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#FFFFFF", "editor.background": "#002451", "editor.selectionBackground": "#003F8E", "editor.lineHighlightBackground": "#00346E", "editorCursor.foreground": "#FFFFFF", "editorWhitespace.foreground": "#404F7D", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let tomorrowNightBright = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "000000","token": ""}, {"foreground": "969896","token": "comment"}, {"foreground": "eeeeee","token": "keyword.operator.class"}, {"foreground": "eeeeee","token": "constant.other"}, {"foreground": "eeeeee","token": "source.php.embedded.line"}, {"foreground": "d54e53","token": "variable"}, {"foreground": "d54e53","token": "support.other.variable"}, {"foreground": "d54e53","token": "string.other.link"}, {"foreground": "d54e53","token": "string.regexp"}, {"foreground": "d54e53","token": "entity.name.tag"}, {"foreground": "d54e53","token": "entity.other.attribute-name"}, {"foreground": "d54e53","token": "meta.tag"}, {"foreground": "d54e53","token": "declaration.tag"}, {"foreground": "d54e53","token": "markup.deleted.git_gutter"}, {"foreground": "e78c45","token": "constant.numeric"}, {"foreground": "e78c45","token": "constant.language"}, {"foreground": "e78c45","token": "support.constant"}, {"foreground": "e78c45","token": "constant.character"}, {"foreground": "e78c45","token": "variable.parameter"}, {"foreground": "e78c45","token": "punctuation.section.embedded"}, {"foreground": "e78c45","token": "keyword.other.unit"}, {"foreground": "e7c547","token": "entity.name.class"}, {"foreground": "e7c547","token": "entity.name.type.class"}, {"foreground": "e7c547","token": "support.type"}, {"foreground": "e7c547","token": "support.class"}, {"foreground": "b9ca4a","token": "string"}, {"foreground": "b9ca4a","token": "constant.other.symbol"}, {"foreground": "b9ca4a","token": "entity.other.inherited-class"}, {"foreground": "b9ca4a","token": "markup.heading"}, {"foreground": "b9ca4a","token": "markup.inserted.git_gutter"}, {"foreground": "70c0b1","token": "keyword.operator"}, {"foreground": "70c0b1","token": "constant.other.color"}, {"foreground": "7aa6da","token": "entity.name.function"}, {"foreground": "7aa6da","token": "meta.function-call"}, {"foreground": "7aa6da","token": "support.function"}, {"foreground": "7aa6da","token": "keyword.other.special-method"}, {"foreground": "7aa6da","token": "meta.block-level"}, {"foreground": "7aa6da","token": "markup.changed.git_gutter"}, {"foreground": "c397d8","token": "keyword"}, {"foreground": "c397d8","token": "storage"}, {"foreground": "c397d8","token": "storage.type"}, {"foreground": "c397d8","token": "entity.name.tag.css"}, {"foreground": "ced2cf","background": "df5f5f","token": "invalid"}, {"foreground": "ced2cf","background": "82a3bf","token": "meta.separator"}, {"foreground": "ced2cf","background": "b798bf","token": "invalid.deprecated"}, {"foreground": "ffffff","token": "markup.inserted.diff"}, {"foreground": "ffffff","token": "markup.deleted.diff"}, {"foreground": "ffffff","token": "meta.diff.header.to-file"}, {"foreground": "ffffff","token": "meta.diff.header.from-file"}, {"foreground": "718c00","token": "markup.inserted.diff"}, {"foreground": "718c00","token": "meta.diff.header.to-file"}, {"foreground": "c82829","token": "markup.deleted.diff"}, {"foreground": "c82829","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.to-file"}, {"foreground": "3e999f","fontStyle": "italic","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#DEDEDE", "editor.background": "#000000", "editor.selectionBackground": "#424242", "editor.lineHighlightBackground": "#2A2A2A", "editorCursor.foreground": "#9F9F9F", "editorWhitespace.foreground": "#343434", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let tomorrowNightEighties = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "2D2D2D","token": ""}, {"foreground": "999999","token": "comment"}, {"foreground": "cccccc","token": "keyword.operator.class"}, {"foreground": "cccccc","token": "constant.other"}, {"foreground": "cccccc","token": "source.php.embedded.line"}, {"foreground": "f2777a","token": "variable"}, {"foreground": "f2777a","token": "support.other.variable"}, {"foreground": "f2777a","token": "string.other.link"}, {"foreground": "f2777a","token": "entity.name.tag"}, {"foreground": "f2777a","token": "entity.other.attribute-name"}, {"foreground": "f2777a","token": "meta.tag"}, {"foreground": "f2777a","token": "declaration.tag"}, {"foreground": "f2777a","token": "markup.deleted.git_gutter"}, {"foreground": "f99157","token": "constant.numeric"}, {"foreground": "f99157","token": "constant.language"}, {"foreground": "f99157","token": "support.constant"}, {"foreground": "f99157","token": "constant.character"}, {"foreground": "f99157","token": "variable.parameter"}, {"foreground": "f99157","token": "punctuation.section.embedded"}, {"foreground": "f99157","token": "keyword.other.unit"}, {"foreground": "ffcc66","token": "entity.name.class"}, {"foreground": "ffcc66","token": "entity.name.type.class"}, {"foreground": "ffcc66","token": "support.type"}, {"foreground": "ffcc66","token": "support.class"}, {"foreground": "99cc99","token": "string"}, {"foreground": "99cc99","token": "constant.other.symbol"}, {"foreground": "99cc99","token": "entity.other.inherited-class"}, {"foreground": "99cc99","token": "markup.heading"}, {"foreground": "99cc99","token": "markup.inserted.git_gutter"}, {"foreground": "66cccc","token": "keyword.operator"}, {"foreground": "66cccc","token": "constant.other.color"}, {"foreground": "6699cc","token": "entity.name.function"}, {"foreground": "6699cc","token": "meta.function-call"}, {"foreground": "6699cc","token": "support.function"}, {"foreground": "6699cc","token": "keyword.other.special-method"}, {"foreground": "6699cc","token": "meta.block-level"}, {"foreground": "6699cc","token": "markup.changed.git_gutter"}, {"foreground": "cc99cc","token": "keyword"}, {"foreground": "cc99cc","token": "storage"}, {"foreground": "cc99cc","token": "storage.type"}, {"foreground": "cc99cc","token": "entity.name.tag.css"}, {"foreground": "cdcdcd","background": "f2777a","token": "invalid"}, {"foreground": "cdcdcd","background": "99cccc","token": "meta.separator"}, {"foreground": "cdcdcd","background": "cc99cc","token": "invalid.deprecated"}, {"foreground": "ffffff","token": "markup.inserted.diff"}, {"foreground": "ffffff","token": "markup.deleted.diff"}, {"foreground": "ffffff","token": "meta.diff.header.to-file"}, {"foreground": "ffffff","token": "meta.diff.header.from-file"}, {"foreground": "718c00","token": "markup.inserted.diff"}, {"foreground": "718c00","token": "meta.diff.header.to-file"}, {"foreground": "c82829","token": "markup.deleted.diff"}, {"foreground": "c82829","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.to-file"}, {"foreground": "3e999f","fontStyle": "italic","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#CCCCCC", "editor.background": "#2D2D2D", "editor.selectionBackground": "#515151", "editor.lineHighlightBackground": "#393939", "editorCursor.foreground": "#CCCCCC", "editorWhitespace.foreground": "#6A6A6A", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let tomorrowNight = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "1D1F21","token": ""}, {"foreground": "969896","token": "comment"}, {"foreground": "ced1cf","token": "keyword.operator.class"}, {"foreground": "ced1cf","token": "constant.other"}, {"foreground": "ced1cf","token": "source.php.embedded.line"}, {"foreground": "cc6666","token": "variable"}, {"foreground": "cc6666","token": "support.other.variable"}, {"foreground": "cc6666","token": "string.other.link"}, {"foreground": "cc6666","token": "string.regexp"}, {"foreground": "cc6666","token": "entity.name.tag"}, {"foreground": "cc6666","token": "entity.other.attribute-name"}, {"foreground": "cc6666","token": "meta.tag"}, {"foreground": "cc6666","token": "declaration.tag"}, {"foreground": "cc6666","token": "markup.deleted.git_gutter"}, {"foreground": "de935f","token": "constant.numeric"}, {"foreground": "de935f","token": "constant.language"}, {"foreground": "de935f","token": "support.constant"}, {"foreground": "de935f","token": "constant.character"}, {"foreground": "de935f","token": "variable.parameter"}, {"foreground": "de935f","token": "punctuation.section.embedded"}, {"foreground": "de935f","token": "keyword.other.unit"}, {"foreground": "f0c674","token": "entity.name.class"}, {"foreground": "f0c674","token": "entity.name.type.class"}, {"foreground": "f0c674","token": "support.type"}, {"foreground": "f0c674","token": "support.class"}, {"foreground": "b5bd68","token": "string"}, {"foreground": "b5bd68","token": "constant.other.symbol"}, {"foreground": "b5bd68","token": "entity.other.inherited-class"}, {"foreground": "b5bd68","token": "markup.heading"}, {"foreground": "b5bd68","token": "markup.inserted.git_gutter"}, {"foreground": "8abeb7","token": "keyword.operator"}, {"foreground": "8abeb7","token": "constant.other.color"}, {"foreground": "81a2be","token": "entity.name.function"}, {"foreground": "81a2be","token": "meta.function-call"}, {"foreground": "81a2be","token": "support.function"}, {"foreground": "81a2be","token": "keyword.other.special-method"}, {"foreground": "81a2be","token": "meta.block-level"}, {"foreground": "81a2be","token": "markup.changed.git_gutter"}, {"foreground": "b294bb","token": "keyword"}, {"foreground": "b294bb","token": "storage"}, {"foreground": "b294bb","token": "storage.type"}, {"foreground": "b294bb","token": "entity.name.tag.css"}, {"foreground": "ced2cf","background": "df5f5f","token": "invalid"}, {"foreground": "ced2cf","background": "82a3bf","token": "meta.separator"}, {"foreground": "ced2cf","background": "b798bf","token": "invalid.deprecated"}, {"foreground": "ffffff","token": "markup.inserted.diff"}, {"foreground": "ffffff","token": "markup.deleted.diff"}, {"foreground": "ffffff","token": "meta.diff.header.to-file"}, {"foreground": "ffffff","token": "meta.diff.header.from-file"}, {"foreground": "718c00","token": "markup.inserted.diff"}, {"foreground": "718c00","token": "meta.diff.header.to-file"}, {"foreground": "c82829","token": "markup.deleted.diff"}, {"foreground": "c82829","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.to-file"}, {"foreground": "3e999f","fontStyle": "italic","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#C5C8C6", "editor.background": "#1D1F21", "editor.selectionBackground": "#3074B8", "editor.lineHighlightBackground": "#282A2E", "editorCursor.foreground": "#AEAFAD", "editorWhitespace.foreground": "#4B4E55", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let tomorrow = { "base": "vs", "inherit": true, "rules": [ {"background": "FFFFFF","token": ""}, {"foreground": "8e908c","token": "comment"}, {"foreground": "666969","token": "keyword.operator.class"}, {"foreground": "666969","token": "constant.other"}, {"foreground": "666969","token": "source.php.embedded.line"}, {"foreground": "c82829","token": "variable"}, {"foreground": "c82829","token": "support.other.variable"}, {"foreground": "c82829","token": "string.other.link"}, {"foreground": "c82829","token": "string.regexp"}, {"foreground": "c82829","token": "entity.name.tag"}, {"foreground": "c82829","token": "entity.other.attribute-name"}, {"foreground": "c82829","token": "meta.tag"}, {"foreground": "c82829","token": "declaration.tag"}, {"foreground": "c82829","token": "markup.deleted.git_gutter"}, {"foreground": "f5871f","token": "constant.numeric"}, {"foreground": "f5871f","token": "constant.language"}, {"foreground": "f5871f","token": "support.constant"}, {"foreground": "f5871f","token": "constant.character"}, {"foreground": "f5871f","token": "variable.parameter"}, {"foreground": "f5871f","token": "punctuation.section.embedded"}, {"foreground": "f5871f","token": "keyword.other.unit"}, {"foreground": "c99e00","token": "entity.name.class"}, {"foreground": "c99e00","token": "entity.name.type.class"}, {"foreground": "c99e00","token": "support.type"}, {"foreground": "c99e00","token": "support.class"}, {"foreground": "718c00","token": "string"}, {"foreground": "718c00","token": "constant.other.symbol"}, {"foreground": "718c00","token": "entity.other.inherited-class"}, {"foreground": "718c00","token": "markup.heading"}, {"foreground": "718c00","token": "markup.inserted.git_gutter"}, {"foreground": "3e999f","token": "keyword.operator"}, {"foreground": "3e999f","token": "constant.other.color"}, {"foreground": "4271ae","token": "entity.name.function"}, {"foreground": "4271ae","token": "meta.function-call"}, {"foreground": "4271ae","token": "support.function"}, {"foreground": "4271ae","token": "keyword.other.special-method"}, {"foreground": "4271ae","token": "meta.block-level"}, {"foreground": "4271ae","token": "markup.changed.git_gutter"}, {"foreground": "8959a8","token": "keyword"}, {"foreground": "8959a8","token": "storage"}, {"foreground": "8959a8","token": "storage.type"}, {"foreground": "ffffff","background": "c82829","token": "invalid"}, {"foreground": "ffffff","background": "4271ae","token": "meta.separator"}, {"foreground": "ffffff","background": "8959a8","token": "invalid.deprecated"}, {"foreground": "ffffff","token": "markup.inserted.diff"}, {"foreground": "ffffff","token": "markup.deleted.diff"}, {"foreground": "ffffff","token": "meta.diff.header.to-file"}, {"foreground": "ffffff","token": "meta.diff.header.from-file"}, {"background": "718c00","token": "markup.inserted.diff"}, {"background": "718c00","token": "meta.diff.header.to-file"}, {"background": "c82829","token": "markup.deleted.diff"}, {"background": "c82829","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.from-file"}, {"foreground": "ffffff","background": "4271ae","token": "meta.diff.header.to-file"}, {"foreground": "3e999f","fontStyle": "italic","token": "meta.diff.range"} ], "colors": { "editor.foreground": "#4D4D4C", "editor.background": "#FFFFFF", "editor.selectionBackground": "#D6D6D6", "editor.lineHighlightBackground": "#EFEFEF", "editorCursor.foreground": "#AEAFAD", "editorWhitespace.foreground": "#D1D1D1", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData let twilight = { "base": "vs-dark", "inherit": true, "rules": [ {"background": "141414","token": ""}, {"foreground": "5f5a60","fontStyle": "italic","token": "comment"}, {"foreground": "cf6a4c","token": "constant"}, {"foreground": "9b703f","token": "entity"}, {"foreground": "cda869","token": "keyword"}, {"foreground": "f9ee98","token": "storage"}, {"foreground": "8f9d6a","token": "string"}, {"foreground": "9b859d","token": "support"}, {"foreground": "7587a6","token": "variable"}, {"foreground": "d2a8a1","fontStyle": "italic underline","token": "invalid.deprecated"}, {"foreground": "f8f8f8","background": "562d56bf","token": "invalid.illegal"}, {"background": "b0b3ba14","token": "text source"}, {"background": "b1b3ba21","token": "text.html.ruby source"}, {"foreground": "9b5c2e","fontStyle": "italic","token": "entity.other.inherited-class"}, {"foreground": "daefa3","token": "string source"}, {"foreground": "ddf2a4","token": "string constant"}, {"foreground": "e9c062","token": "string.regexp"}, {"foreground": "cf7d34","token": "string.regexp constant.character.escape"}, {"foreground": "cf7d34","token": "string.regexp source.ruby.embedded"}, {"foreground": "cf7d34","token": "string.regexp string.regexp.arbitrary-repitition"}, {"foreground": "8a9a95","token": "string variable"}, {"foreground": "dad085","token": "support.function"}, {"foreground": "cf6a4c","token": "support.constant"}, {"foreground": "8996a8","token": "meta.preprocessor.c"}, {"foreground": "afc4db","token": "meta.preprocessor.c keyword"}, {"foreground": "494949","token": "meta.tag.sgml.doctype"}, {"foreground": "494949","token": "meta.tag.sgml.doctype entity"}, {"foreground": "494949","token": "meta.tag.sgml.doctype string"}, {"foreground": "494949","token": "meta.tag.preprocessor.xml"}, {"foreground": "494949","token": "meta.tag.preprocessor.xml entity"}, {"foreground": "494949","token": "meta.tag.preprocessor.xml string"}, {"foreground": "ac885b","token": "declaration.tag"}, {"foreground": "ac885b","token": "declaration.tag entity"}, {"foreground": "ac885b","token": "meta.tag"}, {"foreground": "ac885b","token": "meta.tag entity"}, {"foreground": "e0c589","token": "declaration.tag.inline"}, {"foreground": "e0c589","token": "declaration.tag.inline entity"}, {"foreground": "e0c589","token": "source entity.name.tag"}, {"foreground": "e0c589","token": "source entity.other.attribute-name"}, {"foreground": "e0c589","token": "meta.tag.inline"}, {"foreground": "e0c589","token": "meta.tag.inline entity"}, {"foreground": "cda869","token": "meta.selector.css entity.name.tag"}, {"foreground": "8f9d6a","token": "meta.selector.css entity.other.attribute-name.tag.pseudo-class"}, {"foreground": "8b98ab","token": "meta.selector.css entity.other.attribute-name.id"}, {"foreground": "9b703f","token": "meta.selector.css entity.other.attribute-name.class"}, {"foreground": "c5af75","token": "support.type.property-name.css"}, {"foreground": "f9ee98","token": "meta.property-group support.constant.property-value.css"}, {"foreground": "f9ee98","token": "meta.property-value support.constant.property-value.css"}, {"foreground": "8693a5","token": "meta.preprocessor.at-rule keyword.control.at-rule"}, {"foreground": "ca7840","token": "meta.property-value support.constant.named-color.css"}, {"foreground": "ca7840","token": "meta.property-value constant"}, {"foreground": "8f9d6a","token": "meta.constructor.argument.css"}, {"foreground": "f8f8f8","background": "0e2231","fontStyle": "italic","token": "meta.diff"}, {"foreground": "f8f8f8","background": "0e2231","fontStyle": "italic","token": "meta.diff.header"}, {"foreground": "f8f8f8","background": "0e2231","fontStyle": "italic","token": "meta.separator"}, {"foreground": "f8f8f8","background": "420e09","token": "markup.deleted"}, {"foreground": "f8f8f8","background": "4a410d","token": "markup.changed"}, {"foreground": "f8f8f8","background": "253b22","token": "markup.inserted"}, {"foreground": "f9ee98","token": "markup.list"}, {"foreground": "cf6a4c","token": "markup.heading"} ], "colors": { "editor.foreground": "#F8F8F8", "editor.background": "#141414", "editor.selectionBackground": "#DDF0FF33", "editor.lineHighlightBackground": "#FFFFFF08", "editorCursor.foreground": "#A7A7A7", "editorWhitespace.foreground": "#FFFFFF40", 'diffEditor.insertedTextBackground': '#2ea04320', 'diffEditor.insertedLineBackground': '#2ea04326', 'diffEditor.removedTextBackground': '#f8514920', 'diffEditor.removedLineBackground': '#f8514920', 'diffEditor.insertedTextBorder': '#2ea04300', 'diffEditor.removedTextBorder': '#f8514900', } } as Monaco.editor.IStandaloneThemeData export const editorThemes: Record = { "all-hallows-eve": allHallowsEve, "amy": amy, "birds-of-paradise": birdsOfParadise, "blackboard": blackboard, "brilliance-black": brillianceBlack, "brilliance-dull": brillianceDull, "chrome-dev-tools": chromeDevTools, "clouds-midnight": cloudsMidnight, "clouds": clouds, "cobalt": cobalt, "dracula": dracula, "dreamweaver": dreamweaver, "espresso-libre": espressoLibre, "github-dark": githubDark, "github-light": githubLight, "github": github, "merbivore-soft": merbivoreSoft, "monokai": monokai, "night-owl": nightOwl, "nord": nord, "oceanic-next": oceanicNext, "pastels-on-dark": pastelsOnDark, "sunburst": sunburst, "tomorrow-night-blue": tomorrowNightBlue, "tomorrow-night-bright": tomorrowNightBright, "tomorrow-night-eighties": tomorrowNightEighties, "tomorrow-night": tomorrowNight, "tomorrow": tomorrow, "twilight": twilight, } ================================================ FILE: www/app/Event.ts ================================================ /// import EventDispatcher from './dispatcher/EventDispatcher'; import * as Csrf from './Csrf'; let connected = false; const pendingEvents: Record = {}; function connect(): void { let url = ''; let location = window.location; if (location.protocol === 'https:') { url += 'wss'; } else { url += 'ws'; } url += '://' + location.host + '/event?csrf_token=' + Csrf.token; let socket = new WebSocket(url); socket.addEventListener('close', () => { setTimeout(() => { connect(); }, 500); }); socket.addEventListener('message', (evt) => { const eventData = JSON.parse(evt.data).data; const eventId = JSON.stringify(eventData); if (pendingEvents[eventId]) { return; } pendingEvents[eventId] = eventData; setTimeout(() => { if (pendingEvents[eventId]) { console.log(eventData); EventDispatcher.dispatch(eventData); delete pendingEvents[eventId]; } }, 300); }); } export function init() { if (connected) { return; } connected = true; connect(); } ================================================ FILE: www/app/EventEmitter.ts ================================================ /// import * as Events from 'events'; export default class EventEmitter extends Events.EventEmitter { emitDefer(event: string | symbol, ...args: any[]): void { setTimeout((): void => { this.emit(event, ...args); }); } } ================================================ FILE: www/app/License.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Alert from './Alert'; import * as Csrf from './Csrf'; export let oracle = false; export function save(): Promise { return new Promise((resolve, reject): void => { SuperAgent .put('/license') .send({ oracle: oracle, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save license state'); reject(err); return; } resolve(); }); }); } export function setOracle(state: boolean): void { oracle = state; } ================================================ FILE: www/app/Loader.ts ================================================ /// import Dispatcher from './dispatcher/Dispatcher'; import * as LoadingTypes from './types/LoadingTypes'; import * as MiscUtils from './utils/MiscUtils'; export default class Loader { _id: string; constructor() { this._id = MiscUtils.uuid(); } loading(): Loader { Dispatcher.dispatch({ type: LoadingTypes.ADD, data: { id: this._id, }, }); return this; } done(): Loader { Dispatcher.dispatch({ type: LoadingTypes.DONE, data: { id: this._id, }, }); return this; } } ================================================ FILE: www/app/References.d.ts ================================================ declare module '@novnc/novnc' { export default class RFB { constructor(target: HTMLDivElement, url: string, options?: any); [key:string]: any; } } ================================================ FILE: www/app/Router.ts ================================================ /// import * as CompletionActions from './actions/CompletionActions'; import * as UserActions from './actions/UserActions'; import * as SessionActions from './actions/SessionActions'; import * as AuditActions from './actions/AuditActions'; import * as NodeActions from './actions/NodeActions'; import * as PolicyActions from './actions/PolicyActions'; import * as CertificateActions from './actions/CertificateActions'; import * as SecretActions from './actions/SecretActions'; import * as OrganizationActions from './actions/OrganizationActions'; import * as DatacenterActions from './actions/DatacenterActions'; import * as AlertActions from './actions/AlertActions'; import * as ZoneActions from './actions/ZoneActions'; import * as ShapeActions from './actions/ShapeActions'; import * as BlockActions from './actions/BlockActions'; import * as VpcActions from './actions/VpcActions'; import * as DomainActions from './actions/DomainActions'; import * as PlanActions from './actions/PlanActions'; import * as BalancerActions from './actions/BalancerActions'; import * as StorageActions from './actions/StorageActions'; import * as ImageActions from './actions/ImageActions'; import * as PoolActions from './actions/PoolActions'; import * as DiskActions from './actions/DiskActions'; import * as InstanceActions from './actions/InstanceActions'; import * as PodActions from './actions/PodActions'; import * as FirewallActions from './actions/FirewallActions'; import * as AuthorityActions from './actions/AuthorityActions'; import * as LogActions from './actions/LogActions'; import * as SettingsActions from './actions/SettingsActions'; import * as SubscriptionActions from './actions/SubscriptionActions'; export function setLocation(location: string) { window.location.hash = location let evt = new Event("router_update") window.dispatchEvent(evt) } export function reload() { let evt = new Event("router_update") window.dispatchEvent(evt) } export function refresh(callback?: () => void) { let pathname = window.location.hash.replace(/^#/, ''); CompletionActions.sync(); if (pathname === '/users') { UserActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname.startsWith('/user/')) { UserActions.reload().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); SessionActions.reload().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); AuditActions.reload().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/nodes') { NodeActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/policies') { SettingsActions.sync(); PolicyActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/certificates') { CertificateActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/secrets') { SecretActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/organizations') { OrganizationActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/datacenters') { DatacenterActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/zones') { ZoneActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/shapes') { ShapeActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/blocks') { BlockActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/vpcs') { VpcActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/domains') { DomainActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/plans') { PlanActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/balancers') { BalancerActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/storages') { StorageActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/images') { ImageActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/pools') { PoolActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/disks') { DiskActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/instances') { InstanceActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/pods') { PodActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/firewalls') { FirewallActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/authorities') { AuthorityActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/alerts') { AlertActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/logs') { LogActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/settings') { SettingsActions.sync().then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else if (pathname === '/subscription') { SubscriptionActions.sync(true).then((): void => { if (callback) { callback() } }).catch((): void => { if (callback) { callback() } }); } else { console.log(`Failed to match refresh ${pathname}`) this.setState({ ...this.state, disabled: false, }); } } ================================================ FILE: www/app/Styles.tsx ================================================ /// import * as React from 'react'; import * as Theme from './Theme'; import * as Blueprint from '@blueprintjs/core'; interface Colors { white: string; black: string; blue1: string; blue2: string; blue3: string; blue4: string; blue5: string; darkGray1: string; darkGray2: string; darkGray3: string; darkGray4: string; darkGray5: string; forest1: string; forest2: string; forest3: string; forest4: string; forest5: string; gold1: string; gold2: string; gold3: string; gold4: string; gold5: string; gray1: string; gray2: string; gray3: string; gray4: string; gray5: string; green1: string; green2: string; green3: string; green4: string; green5: string; indigo1: string; indigo2: string; indigo3: string; indigo4: string; indigo5: string; lightGray1: string; lightGray2: string; lightGray3: string; lightGray4: string; lightGray5: string; lime1: string; lime2: string; lime3: string; lime4: string; lime5: string; orange1: string; orange2: string; orange3: string; orange4: string; orange5: string; red1: string; red2: string; red3: string; red4: string; red5: string; rose1: string; rose2: string; rose3: string; rose4: string; rose5: string; sepia1: string; sepia2: string; sepia3: string; sepia4: string; sepia5: string; turquoise1: string; turquoise2: string; turquoise3: string; turquoise4: string; turquoise5: string; vermilion1: string; vermilion2: string; vermilion3: string; vermilion4: string; vermilion5: string; violet1: string; violet2: string; violet3: string; violet4: string; violet5: string; } interface Styles { colors: Colors; } export const colors = { white: Blueprint.Colors.WHITE, black: Blueprint.Colors.BLACK, blue1: Blueprint.Colors.BLUE1, blue2: Blueprint.Colors.BLUE2, blue3: Blueprint.Colors.BLUE3, blue4: Blueprint.Colors.BLUE4, blue5: Blueprint.Colors.BLUE5, darkGray1: Blueprint.Colors.DARK_GRAY1, darkGray2: Blueprint.Colors.DARK_GRAY2, darkGray3: Blueprint.Colors.DARK_GRAY3, darkGray4: Blueprint.Colors.DARK_GRAY4, darkGray5: Blueprint.Colors.DARK_GRAY5, forest1: Blueprint.Colors.FOREST1, forest2: Blueprint.Colors.FOREST2, forest3: Blueprint.Colors.FOREST3, forest4: Blueprint.Colors.FOREST4, forest5: Blueprint.Colors.FOREST5, gold1: Blueprint.Colors.GOLD1, gold2: Blueprint.Colors.GOLD2, gold3: Blueprint.Colors.GOLD3, gold4: Blueprint.Colors.GOLD4, gold5: Blueprint.Colors.GOLD5, gray1: Blueprint.Colors.GRAY1, gray2: Blueprint.Colors.GRAY2, gray3: Blueprint.Colors.GRAY3, gray4: Blueprint.Colors.GRAY4, gray5: Blueprint.Colors.GRAY5, green1: Blueprint.Colors.GREEN1, green2: Blueprint.Colors.GREEN2, green3: Blueprint.Colors.GREEN3, green4: Blueprint.Colors.GREEN4, green5: Blueprint.Colors.GREEN5, indigo1: Blueprint.Colors.INDIGO1, indigo2: Blueprint.Colors.INDIGO2, indigo3: Blueprint.Colors.INDIGO3, indigo4: Blueprint.Colors.INDIGO4, indigo5: Blueprint.Colors.INDIGO5, lightGray1: Blueprint.Colors.LIGHT_GRAY1, lightGray2: Blueprint.Colors.LIGHT_GRAY2, lightGray3: Blueprint.Colors.LIGHT_GRAY3, lightGray4: Blueprint.Colors.LIGHT_GRAY4, lightGray5: Blueprint.Colors.LIGHT_GRAY5, lime1: Blueprint.Colors.LIME1, lime2: Blueprint.Colors.LIME2, lime3: Blueprint.Colors.LIME3, lime4: Blueprint.Colors.LIME4, lime5: Blueprint.Colors.LIME5, orange1: Blueprint.Colors.ORANGE1, orange2: Blueprint.Colors.ORANGE2, orange3: Blueprint.Colors.ORANGE3, orange4: Blueprint.Colors.ORANGE4, orange5: Blueprint.Colors.ORANGE5, red1: Blueprint.Colors.RED1, red2: Blueprint.Colors.RED2, red3: Blueprint.Colors.RED3, red4: Blueprint.Colors.RED4, red5: Blueprint.Colors.RED5, rose1: Blueprint.Colors.ROSE1, rose2: Blueprint.Colors.ROSE2, rose3: Blueprint.Colors.ROSE3, rose4: Blueprint.Colors.ROSE4, rose5: Blueprint.Colors.ROSE5, sepia1: Blueprint.Colors.SEPIA1, sepia2: Blueprint.Colors.SEPIA2, sepia3: Blueprint.Colors.SEPIA3, sepia4: Blueprint.Colors.SEPIA4, sepia5: Blueprint.Colors.SEPIA5, turquoise1: Blueprint.Colors.TURQUOISE1, turquoise2: Blueprint.Colors.TURQUOISE2, turquoise3: Blueprint.Colors.TURQUOISE3, turquoise4: Blueprint.Colors.TURQUOISE4, turquoise5: Blueprint.Colors.TURQUOISE5, vermilion1: Blueprint.Colors.VERMILION1, vermilion2: Blueprint.Colors.VERMILION2, vermilion3: Blueprint.Colors.VERMILION3, vermilion4: Blueprint.Colors.VERMILION4, vermilion5: Blueprint.Colors.VERMILION5, violet1: Blueprint.Colors.VIOLET1, violet2: Blueprint.Colors.VIOLET2, violet3: Blueprint.Colors.VIOLET3, violet4: Blueprint.Colors.VIOLET4, violet5: Blueprint.Colors.VIOLET5, }; export const fixedHeight = 1000 export const headerShift = (): string => { if (Theme.theme === "dark") { return "rgba(0, 0, 0, 0.13)" } return "rgba(0, 0, 0, 0.04)" } ================================================ FILE: www/app/Theme.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Alert from './Alert'; import * as Csrf from './Csrf'; import * as MiscUtils from './utils/MiscUtils'; import * as EditorThemes from './EditorThemes'; import * as Monaco from "monaco-editor" export interface Callback { (): void; } let callbacks: Set = new Set(); export let theme = 'dark'; export let themeVer = 5; let editorThemeName = ''; export const monospaceSize = "12px" export const monospaceFont = "Consolas, Menlo, 'Roboto Mono', 'DejaVu Sans Mono'" export const monospaceWeight = "500" export function save(): Promise { return new Promise((resolve, reject): void => { SuperAgent .put('/theme') .send({ theme: theme + `-${themeVer}`, editor_theme: editorThemeName, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save theme'); reject(err); return; } resolve(); }); }); } export function themeVer3(): void { const blueprintTheme3 = document.getElementById( "blueprint3-theme") as HTMLLinkElement const blueprintTheme5 = document.getElementById( "blueprint5-theme") as HTMLLinkElement blueprintTheme3.disabled = false; blueprintTheme5.disabled = true; if (theme === "dark") { document.body.className = 'bp3-theme bp5-dark'; document.documentElement.className = 'dark3-scroll bp5-focus-disabled'; } else { document.body.className = 'bp3-theme'; document.documentElement.className = 'bp5-focus-disabled'; } themeVer = 3; } export function themeVer5(): void { const blueprintTheme3 = document.getElementById( "blueprint3-theme") as HTMLLinkElement const blueprintTheme5 = document.getElementById( "blueprint5-theme") as HTMLLinkElement blueprintTheme3.disabled = true; blueprintTheme5.disabled = false; if (theme === "dark") { document.body.className = 'bp5-dark'; document.documentElement.className = 'dark5-scroll bp5-focus-disabled'; } else { document.body.className = ''; document.documentElement.className = 'bp5-focus-disabled'; } themeVer = 5; } export function light(): void { theme = 'light'; if (themeVer === 3) { document.body.className = 'bp3-theme'; document.documentElement.className = 'bp5-focus-disabled'; } else { document.body.className = ''; document.documentElement.className = 'bp5-focus-disabled'; } callbacks.forEach((callback: Callback): void => { callback(); }); } export function dark(): void { theme = 'dark'; if (themeVer === 3) { document.body.className = 'bp3-theme bp5-dark'; document.documentElement.className = 'dark3-scroll bp5-focus-disabled'; } else { document.body.className = 'bp5-dark'; document.documentElement.className = 'dark5-scroll bp5-focus-disabled'; } callbacks.forEach((callback: Callback): void => { callback(); }); } export function toggle(ver3: boolean): void { if (theme === "dark") { light(); if (ver3) { themeVer3(); } else { themeVer5(); } } else if (theme === "light") { dark(); if (ver3) { themeVer3(); } else { themeVer5(); } } } export function getEditorTheme(): string { if (!editorThemeName) { if (theme === "light") { return "github-light"; } else { return "github-dark"; } } return editorThemeName } export function setEditorTheme(name: string) { editorThemeName = name callbacks.forEach((callback: Callback): void => { callback(); }); } export function addChangeListener(callback: Callback): void { callbacks.add(callback); } export function removeChangeListener(callback: () => void): void { callbacks.delete(callback); } export let editorThemeNames: Record = {} for (let themeName in EditorThemes.editorThemes) { let editorTheme = EditorThemes.editorThemes[themeName] Monaco.editor.defineTheme(themeName, editorTheme) let formattedThemeName = MiscUtils.titleCase( themeName.replaceAll("-", " ")) editorThemeNames[themeName] = formattedThemeName } ================================================ FILE: www/app/actions/AlertActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import AlertsStore from '../stores/AlertsStore'; import * as AlertTypes from '../types/AlertTypes'; import * as MiscUtils from '../utils/MiscUtils'; import CompletionStore from "../stores/CompletionStore"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/alert') .query({ ...AlertsStore.filter, page: AlertsStore.page, page_count: AlertsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load alerts'); reject(err); return; } Dispatcher.dispatch({ type: AlertTypes.SYNC, data: { alerts: res.body.alerts, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: AlertTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: AlertTypes.Filter): Promise { Dispatcher.dispatch({ type: AlertTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(alert: AlertTypes.Alert): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/alert/' + alert.id) .send(alert) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save alert'); reject(err); return; } resolve(); }); }); } export function create(alert: AlertTypes.Alert): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/alert') .send(alert) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create alert'); reject(err); return; } resolve(); }); }); } export function remove(alertId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/alert/' + alertId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (err) { Alert.errorRes(res, 'Failed to delete alerts'); reject(err); return; } resolve(); }); }); } export function removeMulti(alertIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/alert') .send(alertIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete alerts'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: AlertTypes.AlertDispatch) => { switch (action.type) { case AlertTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/AuditActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Constants from '../Constants'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as AuditTypes from '../types/AuditTypes'; import * as MiscUtils from '../utils/MiscUtils'; import AuditsStore from '../stores/AuditsStore'; let syncId: string; export function load(userId: string): Promise { if (!userId) { return Promise.resolve(); } let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/audit/' + userId) .query({ page: AuditsStore.page, page_count: AuditsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load audits'); reject(err); return; } Dispatcher.dispatch({ type: AuditTypes.SYNC, data: { userId: userId, audits: res.body.audits, count: res.body.count, }, }); resolve(); }); }); } export function reload(): Promise { return load(AuditsStore.userId); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: AuditTypes.TRAVERSE, data: { page: page, }, }); return reload(); } EventDispatcher.register((action: AuditTypes.AuditDispatch) => { switch (action.type) { case AuditTypes.CHANGE: if (!Constants.user) { reload(); } break; } }); ================================================ FILE: www/app/actions/AuthorityActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as AuthorityTypes from '../types/AuthorityTypes'; import AuthoritiesStore from '../stores/AuthoritiesStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; let syncNamesId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/authority') .query({ ...AuthoritiesStore.filter, page: AuthoritiesStore.page, page_count: AuthoritiesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load authorities'); reject(err); return; } Dispatcher.dispatch({ type: AuthorityTypes.SYNC, data: { authorities: res.body.authorities, count: res.body.count, }, }); resolve(); }); }); } export function syncNames(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/authority') .query({ names: true, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load authority names'); reject(err); return; } Dispatcher.dispatch({ type: AuthorityTypes.SYNC_NAMES, data: { authorities: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: AuthorityTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: AuthorityTypes.Filter): Promise { Dispatcher.dispatch({ type: AuthorityTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(authority: AuthorityTypes.Authority): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/authority/' + authority.id) .send(authority) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save authority'); reject(err); return; } resolve(); }); }); } export function create(authority: AuthorityTypes.Authority): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/authority') .send(authority) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create authority'); reject(err); return; } resolve(); }); }); } export function remove(authorityId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/authority/' + authorityId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete authority'); reject(err); return; } resolve(); }); }); } export function removeMulti(authorityIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/authority') .send(authorityIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete authorities'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: AuthorityTypes.AuthorityDispatch) => { switch (action.type) { case AuthorityTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/BalancerActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as BalancerTypes from '../types/BalancerTypes'; import BalancersStore from '../stores/BalancersStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/balancer') .query({ ...BalancersStore.filter, page: BalancersStore.page, page_count: BalancersStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load balancers'); reject(err); return; } Dispatcher.dispatch({ type: BalancerTypes.SYNC, data: { balancers: res.body.balancers, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: BalancerTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: BalancerTypes.Filter): Promise { Dispatcher.dispatch({ type: BalancerTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(balancer: BalancerTypes.Balancer): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/balancer/' + balancer.id) .send(balancer) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save balancer'); reject(err); return; } resolve(); }); }); } export function create(balancer: BalancerTypes.Balancer): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/balancer') .send(balancer) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create balancer'); reject(err); return; } resolve(); }); }); } export function remove(balancerId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/balancer/' + balancerId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete balancer'); reject(err); return; } resolve(); }); }); } export function removeMulti(balancerIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/balancer') .send(balancerIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete balancers'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: BalancerTypes.BalancerDispatch) => { switch (action.type) { case BalancerTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/BlockActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as BlockTypes from '../types/BlockTypes'; import BlocksStore from '../stores/BlocksStore'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/block') .query({ ...BlocksStore.filter, page: BlocksStore.page, page_count: BlocksStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load blocks'); reject(err); return; } Dispatcher.dispatch({ type: BlockTypes.SYNC, data: { blocks: res.body.blocks, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: BlockTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: BlockTypes.Filter): Promise { Dispatcher.dispatch({ type: BlockTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(block: BlockTypes.Block): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/block/' + block.id) .send(block) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save block'); reject(err); return; } resolve(); }); }); } export function create(block: BlockTypes.Block): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/block') .send(block) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create block'); reject(err); return; } resolve(); }); }); } export function remove(blockId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/block/' + blockId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete blocks'); reject(err); return; } resolve(); }); }); } export function removeMulti(blockIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/block') .send(blockIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete blocks'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: BlockTypes.BlockDispatch) => { switch (action.type) { case BlockTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/CertificateActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as CertificateTypes from '../types/CertificateTypes'; import CertificatesStore from '../stores/CertificatesStore'; import * as MiscUtils from '../utils/MiscUtils'; import CompletionStore from "../stores/CompletionStore"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/certificate') .query({ ...CertificatesStore.filter, page: CertificatesStore.page, page_count: CertificatesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load certificates'); reject(err); return; } Dispatcher.dispatch({ type: CertificateTypes.SYNC, data: { certificates: res.body.certificates, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: CertificateTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: CertificateTypes.Filter): Promise { Dispatcher.dispatch({ type: CertificateTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(cert: CertificateTypes.Certificate): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/certificate/' + cert.id) .send(cert) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save certificate'); reject(err); return; } resolve(); }); }); } export function create(cert: CertificateTypes.Certificate): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/certificate') .send(cert) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create certificate'); reject(err); return; } resolve(); }); }); } export function remove(certId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/certificate/' + certId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete certificates'); reject(err); return; } resolve(); }); }); } export function removeMulti(certificateIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/certificate') .send(certificateIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete certificates'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: CertificateTypes.CertificateDispatch) => { switch (action.type) { case CertificateTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/CompletionActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Constants from '../Constants'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as GlobalTypes from '../types/GlobalTypes'; import * as CompletionTypes from '../types/CompletionTypes'; import CompletionStore from '../stores/CompletionStore'; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; let lastSyncTime: number | null = null; let syncInProgress: boolean = false; export function sync(): Promise { if (syncInProgress) { return Promise.resolve(); } syncInProgress = true; let curSyncId = MiscUtils.uuid(); syncId = curSyncId; return new Promise((resolve, reject): void => { try { SuperAgent .get('/completion') .query({ ...CompletionStore.filter, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization || "") .end((err: any, res: SuperAgent.Response): void => { syncInProgress = false; if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load completion data'); reject(err); return; } Dispatcher.dispatch({ type: CompletionTypes.SYNC, data: { completion: res.body, }, }); lastSyncTime = Date.now(); resolve(); }); } catch (e) { syncInProgress = false; reject(e); } }); } export function lastSync(): number | null { return lastSyncTime; } export function filter(filt: CompletionTypes.Filter): Promise { Dispatcher.dispatch({ type: CompletionTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function setUserOrganization(userOrg: string): void { Dispatcher.dispatch({ type: GlobalTypes.RESET, data: { organization: userOrg, }, }); Dispatcher.dispatch({ type: GlobalTypes.RELOAD, data: { organization: userOrg, }, }); } EventDispatcher.register((action: CompletionTypes.CompletionDispatch) => { switch (action.type) { case CompletionTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/DatacenterActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as DatacenterTypes from '../types/DatacenterTypes'; import DatacentersStore from '../stores/DatacentersStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; let syncNamesId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/datacenter') .query({ ...DatacentersStore.filter, page: DatacentersStore.page, page_count: DatacentersStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load datacenters'); reject(err); return; } Dispatcher.dispatch({ type: DatacenterTypes.SYNC, data: { datacenters: res.body.datacenters, count: res.body.count, }, }); resolve(); }); }); } export function syncNames(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/datacenter') .query({ names: true, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load datacenter names'); reject(err); return; } Dispatcher.dispatch({ type: DatacenterTypes.SYNC_NAMES, data: { secrets: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: DatacenterTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: DatacenterTypes.Filter): Promise { Dispatcher.dispatch({ type: DatacenterTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(datacenter: DatacenterTypes.Datacenter): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/datacenter/' + datacenter.id) .send(datacenter) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save datacenter'); reject(err); return; } resolve(); }); }); } export function create(datacenter: DatacenterTypes.Datacenter): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/datacenter') .send(datacenter) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create datacenter'); reject(err); return; } resolve(); }); }); } export function remove(datacenterId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/datacenter/' + datacenterId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete datacenters'); reject(err); return; } resolve(); }); }); } export function removeMulti(datacenterIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/datacenter') .send(datacenterIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete datacenters'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: DatacenterTypes.DatacenterDispatch) => { switch (action.type) { case DatacenterTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/DeviceActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as DeviceTypes from '../types/DeviceTypes'; import * as MiscUtils from '../utils/MiscUtils'; import DevicesStore from '../stores/DevicesStore'; import * as PolicyTypes from "../types/PolicyTypes"; let syncId: string; export function load(userId: string): Promise { if (!userId) { return Promise.resolve(); } let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/device/' + userId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load devices'); reject(err); return; } Dispatcher.dispatch({ type: DeviceTypes.SYNC, data: { userId: userId, devices: res.body, }, }); resolve(); }); }); } export function reload(): Promise { return load(DevicesStore.userId); } export function create(device: DeviceTypes.Device): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/device') .send(device) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create device'); reject(err); return; } resolve(); }); }); } export function testAlert(deviceId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/device/' + deviceId + '/alert') .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to send test alert'); reject(err); return; } resolve(); }); }); } export function commit(device: DeviceTypes.Device): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/device/' + device.id) .send(device) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save device'); reject(err); return; } resolve(); }); }); } export function remove(deviceId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/device/' + deviceId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete device'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: DeviceTypes.DeviceDispatch) => { switch (action.type) { case DeviceTypes.CHANGE: reload(); break; } }); ================================================ FILE: www/app/actions/DiskActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as DiskTypes from '../types/DiskTypes'; import DisksStore from '../stores/DisksStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/disk') .query({ ...DisksStore.filter, page: DisksStore.page, page_count: DisksStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load disks'); reject(err); return; } Dispatcher.dispatch({ type: DiskTypes.SYNC, data: { disks: res.body.disks, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: DiskTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: DiskTypes.Filter): Promise { Dispatcher.dispatch({ type: DiskTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(disk: DiskTypes.Disk): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/disk/' + disk.id) .send(disk) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save disk'); reject(err); return; } resolve(); }); }); } export function create(disk: DiskTypes.Disk): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/disk') .send(disk) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create disk'); reject(err); return; } resolve(); }); }); } export function remove(diskId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/disk/' + diskId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete disk'); reject(err); return; } resolve(); }); }); } export function removeMulti(diskIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/disk') .send(diskIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete disks'); reject(err); return; } resolve(); }); }); } export function forceRemoveMulti(diskIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/disk') .query({ force: true, }) .send(diskIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete disks'); reject(err); return; } resolve(); }); }); } export function updateMulti(diskIds: string[], action: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/disk') .send({ "ids": diskIds, "action": action, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to update disks'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: DiskTypes.DiskDispatch) => { switch (action.type) { case DiskTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/DomainActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as DomainTypes from '../types/DomainTypes'; import DomainsStore from '../stores/DomainsStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; let syncNamesId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/domain') .query({ ...DomainsStore.filter, page: DomainsStore.page, page_count: DomainsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load domains'); reject(err); return; } Dispatcher.dispatch({ type: DomainTypes.SYNC, data: { domains: res.body.domains, count: res.body.count, }, }); resolve(); }); }); } export function syncName(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/domain') .query({ names: true, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load domain names'); reject(err); return; } Dispatcher.dispatch({ type: DomainTypes.SYNC_NAME, data: { domains: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: DomainTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: DomainTypes.Filter): Promise { Dispatcher.dispatch({ type: DomainTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(domain: DomainTypes.Domain): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/domain/' + domain.id) .send(domain) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save domain'); reject(err); return; } resolve(); }); }); } export function create(domain: DomainTypes.Domain): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/domain') .send(domain) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create domain'); reject(err); return; } resolve(); }); }); } export function remove(domainId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/domain/' + domainId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete domain'); reject(err); return; } resolve(); }); }); } export function removeMulti(domainIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/domain') .send(domainIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete domains'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: DomainTypes.DomainDispatch) => { switch (action.type) { case DomainTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/FirewallActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as FirewallTypes from '../types/FirewallTypes'; import FirewallsStore from '../stores/FirewallsStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/firewall') .query({ ...FirewallsStore.filter, page: FirewallsStore.page, page_count: FirewallsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load firewalls'); reject(err); return; } Dispatcher.dispatch({ type: FirewallTypes.SYNC, data: { firewalls: res.body.firewalls, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: FirewallTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: FirewallTypes.Filter): Promise { Dispatcher.dispatch({ type: FirewallTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(firewall: FirewallTypes.Firewall): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/firewall/' + firewall.id) .send(firewall) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save firewall'); reject(err); return; } resolve(); }); }); } export function create(firewall: FirewallTypes.Firewall): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/firewall') .send(firewall) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create firewall'); reject(err); return; } resolve(); }); }); } export function remove(firewallId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/firewall/' + firewallId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete firewall'); reject(err); return; } resolve(); }); }); } export function removeMulti(firewallIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/firewall') .send(firewallIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete firewalls'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: FirewallTypes.FirewallDispatch) => { switch (action.type) { case FirewallTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/ImageActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as ImageTypes from '../types/ImageTypes'; import ImagesStore from '../stores/ImagesStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/image') .query({ ...ImagesStore.filter, page: ImagesStore.page, page_count: ImagesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load images'); reject(err); return; } Dispatcher.dispatch({ type: ImageTypes.SYNC, data: { images: res.body.images, count: res.body.count, }, }); resolve(); }); }); } export function syncDatacenter(datacenter: string): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; if (!datacenter) { Dispatcher.dispatch({ type: ImageTypes.SYNC_DATACENTER, data: { images: [], }, }); return Promise.resolve(); } let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/image') .query({ datacenter: datacenter, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load images names'); reject(err); return; } Dispatcher.dispatch({ type: ImageTypes.SYNC_DATACENTER, data: { images: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: ImageTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: ImageTypes.Filter): Promise { Dispatcher.dispatch({ type: ImageTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(image: ImageTypes.Image): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/image/' + image.id) .send(image) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save image'); reject(err); return; } resolve(); }); }); } export function create(image: ImageTypes.Image): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/image') .send(image) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create image'); reject(err); return; } resolve(); }); }); } export function remove(imageId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/image/' + imageId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete image'); reject(err); return; } resolve(); }); }); } export function removeMulti(imageIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/image') .send(imageIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete images'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: ImageTypes.ImageDispatch) => { switch (action.type) { case ImageTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/InstanceActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as InstanceTypes from '../types/InstanceTypes'; import InstancesStore from '../stores/InstancesStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/instance') .query({ ...InstancesStore.filter, page: InstancesStore.page, page_count: InstancesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load instances'); reject(err); return; } Dispatcher.dispatch({ type: InstanceTypes.SYNC, data: { instances: res.body.instances, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: InstanceTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: InstanceTypes.Filter): Promise { Dispatcher.dispatch({ type: InstanceTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(instance: InstanceTypes.Instance): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/instance/' + instance.id) .send(instance) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save instance'); reject(err); return; } resolve(); }); }); } export function create(instance: InstanceTypes.Instance): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/instance') .send(instance) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create instance'); reject(err); return; } resolve(); }); }); } export function remove(instanceId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/instance/' + instanceId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete instance'); reject(err); return; } resolve(); }); }); } export function removeMulti(instanceIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/instance') .send(instanceIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete instances'); reject(err); return; } resolve(); }); }); } export function forceRemoveMulti(instanceIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/instance') .query({ force: true, }) .send(instanceIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to force delete instances'); reject(err); return; } resolve(); }); }); } export function updateMulti(instanceIds: string[], action: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/instance') .send({ "ids": instanceIds, "action": action, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to update instances'); reject(err); return; } resolve(); }); }); } export function syncNode(node: string, pool: string): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let scope: string; let query: {[key: string]: string}; if (node) { scope = node; query = { node_names: node, }; } else { scope = pool; query = { pool_names: pool, }; } if (!scope) { return Promise.resolve(); } let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/instance') .query(query) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load instance names'); reject(err); return; } Dispatcher.dispatch({ type: InstanceTypes.SYNC_NODE, data: { scope: scope, instances: res.body, }, }); resolve(); }); }); } EventDispatcher.register((action: InstanceTypes.InstanceDispatch) => { switch (action.type) { case InstanceTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/LogActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Constants from '../Constants'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as LogTypes from '../types/LogTypes'; import LogsStore from '../stores/LogsStore'; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/log') .query({ ...LogsStore.filter, page: LogsStore.page, page_count: LogsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load logs'); reject(err); return; } Dispatcher.dispatch({ type: LogTypes.SYNC, data: { logs: res.body.logs, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: LogTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: LogTypes.Filter): Promise { Dispatcher.dispatch({ type: LogTypes.FILTER, data: { filter: filt, }, }); return sync(); } EventDispatcher.register((action: LogTypes.LogDispatch) => { switch (action.type) { case LogTypes.CHANGE: if (!Constants.user && window.location.hash.indexOf('/logs') !== -1) { sync(); } break; } }); ================================================ FILE: www/app/actions/NodeActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as NodeTypes from '../types/NodeTypes'; import NodesStore from '../stores/NodesStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; let syncZonesId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/node') .query({ ...NodesStore.filter, page: NodesStore.page, page_count: NodesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load nodes'); reject(err); return; } Dispatcher.dispatch({ type: NodeTypes.SYNC, data: { nodes: res.body.nodes, count: res.body.count, }, }); resolve(); }); }); } export function syncZone(zone: string): Promise { let curSyncId = MiscUtils.uuid(); syncZonesId = curSyncId; if (!zone) { Dispatcher.dispatch({ type: NodeTypes.SYNC_ZONE, data: { nodes: [], }, }); return Promise.resolve(); } let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/node') .query({ names: true, zone: zone, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncZonesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load nodes names'); reject(err); return; } Dispatcher.dispatch({ type: NodeTypes.SYNC_ZONE, data: { nodes: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: NodeTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: NodeTypes.Filter): Promise { Dispatcher.dispatch({ type: NodeTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(node: NodeTypes.Node): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/node/' + node.id) .send(node) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save node'); reject(err); return; } resolve(); }); }); } export function operation(nodeId: string, operation: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/node/' + nodeId + '/' + operation) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to update node'); reject(err); return; } resolve(); }); }); } export function init(nodeId: string, data: NodeTypes.NodeInit): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/node/' + nodeId + '/init') .send(data) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to update node'); reject(err); return; } resolve(); }); }); } export function create(node: NodeTypes.Node): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/node') .send(node) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create node'); reject(err); return; } resolve(); }); }); } export function remove(nodeId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/node/' + nodeId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete nodes'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: NodeTypes.NodeDispatch) => { switch (action.type) { case NodeTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/OrganizationActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as OrganizationTypes from '../types/OrganizationTypes'; import OrganizationsStore from '../stores/OrganizationsStore'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/organization') .query({ ...OrganizationsStore.filter, page: OrganizationsStore.page, page_count: OrganizationsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load organizations'); reject(err); return; } Dispatcher.dispatch({ type: OrganizationTypes.SYNC, data: { organizations: res.body.organizations, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: OrganizationTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: OrganizationTypes.Filter): Promise { Dispatcher.dispatch({ type: OrganizationTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(org: OrganizationTypes.Organization): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/organization/' + org.id) .send(org) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save organization'); reject(err); return; } resolve(); }); }); } export function create(org: OrganizationTypes.Organization): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/organization') .send(org) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create organization'); reject(err); return; } resolve(); }); }); } export function remove(orgId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/organization/' + orgId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete organizations'); reject(err); return; } resolve(); }); }); } export function removeMulti(organizationIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/organization') .send(organizationIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete organizations'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: OrganizationTypes.OrganizationDispatch) => { switch (action.type) { case OrganizationTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/PlanActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as PlanTypes from '../types/PlanTypes'; import PlansStore from '../stores/PlansStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; let syncNamesId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/plan') .query({ ...PlansStore.filter, page: PlansStore.page, page_count: PlansStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load plans'); reject(err); return; } Dispatcher.dispatch({ type: PlanTypes.SYNC, data: { plans: res.body.plans, count: res.body.count, }, }); resolve(); }); }); } export function syncName(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/plan') .query({ names: true, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load plan names'); reject(err); return; } Dispatcher.dispatch({ type: PlanTypes.SYNC_NAME, data: { plans: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: PlanTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: PlanTypes.Filter): Promise { Dispatcher.dispatch({ type: PlanTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(plan: PlanTypes.Plan): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/plan/' + plan.id) .send(plan) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save plan'); reject(err); return; } resolve(); }); }); } export function create(plan: PlanTypes.Plan): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/plan') .send(plan) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create plan'); reject(err); return; } resolve(); }); }); } export function remove(planId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/plan/' + planId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete plan'); reject(err); return; } resolve(); }); }); } export function removeMulti(planIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/plan') .send(planIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete plans'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: PlanTypes.PlanDispatch) => { switch (action.type) { case PlanTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/PodActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as PodTypes from '../types/PodTypes'; import CompletionStore from "../stores/CompletionStore"; import PodsStore from '../stores/PodsStore'; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; let syncUnitId: string; let lastPodId: string; let lastUnitId: string let dataSyncReqs: {[key: string]: SuperAgent.Request} = {}; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/pod') .query({ ...PodsStore.filter, page: PodsStore.page, page_count: PodsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load pods'); reject(err); return; } Dispatcher.dispatch({ type: PodTypes.SYNC, data: { pods: res.body.pods, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: PodTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: PodTypes.Filter): Promise { Dispatcher.dispatch({ type: PodTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(pod: PodTypes.Pod): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/pod/' + pod.id) .send(pod) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save pod'); reject(err); return; } resolve(); }); }); } export function commitDeploy(pod: PodTypes.Pod, resync?: boolean): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/pod/' + pod.id + "/deploy") .send(pod) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save pod'); reject(err); return; } if (resync) { sync(true) } resolve(); }); }); } export function commitDrafts(pod: PodTypes.Pod, resync?: boolean): Promise { return new Promise((resolve, reject): void => { SuperAgent .put('/pod/' + pod.id + "/drafts") .send(pod) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save pod'); reject(err); return; } if (resync) { sync(true) } resolve(); }); }); } export function create(pod: PodTypes.Pod): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/pod') .send(pod) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create pod'); reject(err); return; } resolve(); }); }); } export function remove(podId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/pod/' + podId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete pod'); reject(err); return; } resolve(); }); }); } export function removeMulti(podIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/pod') .send(podIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete pods'); reject(err); return; } resolve(); }); }); } export function syncUnit(podId?: string, unitId?: string): Promise { if (!podId) { podId = lastPodId } else { lastPodId = podId } if (!unitId) { unitId = lastUnitId } else { lastUnitId = unitId } if (!podId || !unitId) { return Promise.resolve(); } let curSyncId = MiscUtils.uuid(); syncUnitId = curSyncId; return new Promise((resolve, reject): void => { SuperAgent .get('/pod/' + podId + "/unit/" + unitId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncUnitId || (res && res.status === 404)) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load pod unit'); reject(err); return; } Dispatcher.dispatch({ type: PodTypes.SYNC_UNIT, data: { unit: res.body, }, }); resolve(); }); }); } export function deployUnit(podId: string, unitId: string, specId: string, count: number): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/pod/' + podId + "/unit/" + unitId + "/deployment") .send({ count: count, spec: specId, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create deployments'); reject(err); return; } resolve(); }); }); } export function updateMultiUnitAction(podId: string, unitId: string, deploymentIds: string[], action: string, commit?: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/pod/' + podId + "/unit/" + unitId + "/deployment") .query({ action: action, commit: commit, }) .send(deploymentIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to modify deployments'); reject(err); return; } resolve(); }); }); } export function commitDeployment(deply: PodTypes.Deployment): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/pod/' + deply.pod + "/unit/" + deply.unit + "/deployment/" + deply.id) .send(deply) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save deployment'); reject(err); return; } resolve(); }); }); } export function log(deply: PodTypes.Deployment, resource: string, noLoading?: boolean): Promise { let curDataSyncId = MiscUtils.uuid(); let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { let req = SuperAgent.get('/pod/' + deply.pod + "/unit/" + deply.unit + "/deployment/" + deply.id + "/log") .query({ resource: resource, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .on('abort', () => { if (loader) { loader.done(); } resolve(null); }); dataSyncReqs[curDataSyncId] = req; req.end((err: any, res: SuperAgent.Response): void => { delete dataSyncReqs[curDataSyncId]; if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(null); return; } if (err) { Alert.errorRes(res, 'Failed to load check log'); reject(err); return; } resolve(res.body); }); }); } export function syncSpecs(podId: string, unitId: string, page: number, noLoading?: boolean): Promise { let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get("/pod/" + podId + "/unit/" + unitId + "/spec") .query({ page: page, page_count: 100, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(null); return; } if (err) { Alert.errorRes(res, 'Failed to load unit commits'); reject(err); return; } res.body.unit = unitId res.body.page = page res.body.page_count = 100 resolve(res.body as PodTypes.CommitData); }); }); } export function spec(podId: string, unitId: string, specId: string): Promise { return new Promise((resolve, reject): void => { SuperAgent .get("/pod/" + podId + "/unit/" + unitId + "/spec/" + specId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(null); return; } if (err) { Alert.errorRes(res, 'Failed to load unit commits'); reject(err); return; } resolve(res.body as PodTypes.Commit); }); }); } export function dataCancel(): void { for (let [key, val] of Object.entries(dataSyncReqs)) { val.abort(); } } EventDispatcher.register((action: PodTypes.PodDispatch) => { switch (action.type) { case PodTypes.CHANGE: sync(); syncUnit(); break; } }); ================================================ FILE: www/app/actions/PolicyActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as PolicyTypes from '../types/PolicyTypes'; import PoliciesStore from '../stores/PoliciesStore'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/policy') .query({ ...PoliciesStore.filter, page: PoliciesStore.page, page_count: PoliciesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load policies'); reject(err); return; } Dispatcher.dispatch({ type: PolicyTypes.SYNC, data: { policies: res.body.policies, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: PolicyTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: PolicyTypes.Filter): Promise { Dispatcher.dispatch({ type: PolicyTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(policy: PolicyTypes.Policy): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/policy/' + policy.id) .send(policy) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save policy'); reject(err); return; } resolve(); }); }); } export function create(policy: PolicyTypes.Policy): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/policy') .send(policy) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create policy'); reject(err); return; } resolve(); }); }); } export function remove(policyId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/policy/' + policyId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete policies'); reject(err); return; } resolve(); }); }); } export function removeMulti(policyIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/policy') .send(policyIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete policies'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: PolicyTypes.PolicyDispatch) => { switch (action.type) { case PolicyTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/PoolActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as PoolTypes from '../types/PoolTypes'; import PoolsStore from '../stores/PoolsStore'; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/pool') .query({ ...PoolsStore.filter, page: PoolsStore.page, page_count: PoolsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load pools'); reject(err); return; } Dispatcher.dispatch({ type: PoolTypes.SYNC, data: { pools: res.body.pools, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: PoolTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: PoolTypes.Filter): Promise { Dispatcher.dispatch({ type: PoolTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(pool: PoolTypes.Pool): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/pool/' + pool.id) .send(pool) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save pool'); reject(err); return; } resolve(); }); }); } export function create(pool: PoolTypes.Pool): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/pool') .send(pool) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create pool'); reject(err); return; } resolve(); }); }); } export function remove(poolId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/pool/' + poolId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete pool'); reject(err); return; } resolve(); }); }); } export function removeMulti(poolIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/pool') .send(poolIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete pools'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: PoolTypes.PoolDispatch) => { switch (action.type) { case PoolTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/RelationsActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import * as RelationTypes from '../types/RelationTypes'; import CompletionStore from "../stores/CompletionStore"; export function load(kind: string, id: string): Promise { return new Promise((resolve, reject): void => { SuperAgent .get("/relations/" + kind + "/" + id) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (res && res.status === 401) { window.location.href = '/login'; resolve(null); return; } if (err) { Alert.errorRes(res, 'Failed to load resource overview'); reject(err); return; } resolve(res.body as RelationTypes.Relation); }); }); } ================================================ FILE: www/app/actions/SecretActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as SecretTypes from '../types/SecretTypes'; import SecretsStore from '../stores/SecretsStore'; import * as MiscUtils from '../utils/MiscUtils'; import CompletionStore from "../stores/CompletionStore"; let syncId: string; let syncNamesId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/secret') .query({ ...SecretsStore.filter, page: SecretsStore.page, page_count: SecretsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load secrets'); reject(err); return; } Dispatcher.dispatch({ type: SecretTypes.SYNC, data: { secrets: res.body.secrets, count: res.body.count, }, }); resolve(); }); }); } export function syncNames(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/secret') .query({ names: true, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load secret names'); reject(err); return; } Dispatcher.dispatch({ type: SecretTypes.SYNC_NAMES, data: { secrets: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: SecretTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: SecretTypes.Filter): Promise { Dispatcher.dispatch({ type: SecretTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(secr: SecretTypes.Secret): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/secret/' + secr.id) .send(secr) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save secret'); reject(err); return; } resolve(); }); }); } export function create(secr: SecretTypes.Secret): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/secret') .send(secr) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create secret'); reject(err); return; } resolve(); }); }); } export function remove(secrId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/secret/' + secrId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete secrets'); reject(err); return; } resolve(); }); }); } export function removeMulti(secretIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/secret') .send(secretIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete secrets'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: SecretTypes.SecretDispatch) => { switch (action.type) { case SecretTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/SessionActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import * as Constants from "../Constants"; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as SessionTypes from '../types/SessionTypes'; import * as MiscUtils from '../utils/MiscUtils'; import SessionsStore from '../stores/SessionsStore'; let syncId: string; export function _load(userId: string): Promise { if (!userId) { return Promise.resolve(); } let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/session/' + userId) .query({ show_removed: SessionsStore.showRemoved, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load sessions'); reject(err); return; } Dispatcher.dispatch({ type: SessionTypes.SYNC, data: { userId: userId, sessions: res.body, }, }); resolve(); }); }); } export function load(userId: string): Promise { Dispatcher.dispatch({ type: SessionTypes.SHOW_REMOVED, data: { showRemoved: false, }, }); return _load(userId); } export function reload(): Promise { return _load(SessionsStore.userId); } export function showRemoved(state: boolean): Promise { Dispatcher.dispatch({ type: SessionTypes.SHOW_REMOVED, data: { showRemoved: state, }, }); return reload(); } export function remove(sessionId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/session/' + sessionId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete session'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: SessionTypes.SessionDispatch) => { switch (action.type) { case SessionTypes.CHANGE: if (!Constants.user) { reload(); } break; } }); ================================================ FILE: www/app/actions/SettingsActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as SettingsTypes from '../types/SettingsTypes'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/settings') .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to sync builds'); reject(err); return; } Dispatcher.dispatch({ type: SettingsTypes.SYNC, data: res.body, }); resolve(); }); }); } export function commit( settings: SettingsTypes.Settings): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/settings') .send(settings) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to commit settings'); reject(err); return; } Dispatcher.dispatch({ type: SettingsTypes.SYNC, data: res.body, }); resolve(); }); }); } EventDispatcher.register((action: SettingsTypes.SettingsDispatch) => { switch (action.type) { case SettingsTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/ShapeActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as ShapeTypes from '../types/ShapeTypes'; import ShapesStore from '../stores/ShapesStore'; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/shape') .query({ ...ShapesStore.filter, page: ShapesStore.page, page_count: ShapesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load shapes'); reject(err); return; } Dispatcher.dispatch({ type: ShapeTypes.SYNC, data: { shapes: res.body.shapes, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: ShapeTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: ShapeTypes.Filter): Promise { Dispatcher.dispatch({ type: ShapeTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(shape: ShapeTypes.Shape): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/shape/' + shape.id) .send(shape) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save shape'); reject(err); return; } resolve(); }); }); } export function create(shape: ShapeTypes.Shape): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/shape') .send(shape) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create shape'); reject(err); return; } resolve(); }); }); } export function remove(shapeId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/shape/' + shapeId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete shape'); reject(err); return; } resolve(); }); }); } export function removeMulti(shapeIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/shape') .send(shapeIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete shapes'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: ShapeTypes.ShapeDispatch) => { switch (action.type) { case ShapeTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/StorageActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as StorageTypes from '../types/StorageTypes'; import StoragesStore from '../stores/StoragesStore'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/storage') .query({ ...StoragesStore.filter, page: StoragesStore.page, page_count: StoragesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load storages'); reject(err); return; } Dispatcher.dispatch({ type: StorageTypes.SYNC, data: { storages: res.body.storages, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: StorageTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: StorageTypes.Filter): Promise { Dispatcher.dispatch({ type: StorageTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(storage: StorageTypes.Storage): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/storage/' + storage.id) .send(storage) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save storage'); reject(err); return; } resolve(); }); }); } export function create(storage: StorageTypes.Storage): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/storage') .send(storage) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create storage'); reject(err); return; } resolve(); }); }); } export function remove(storageId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/storage/' + storageId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete storages'); reject(err); return; } resolve(); }); }); } export function removeMulti(storageIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/storage') .send(storageIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete storages'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: StorageTypes.StorageDispatch) => { switch (action.type) { case StorageTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/SubscriptionActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as SubscriptionTypes from '../types/SubscriptionTypes'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function sync(update: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/subscription' + (update ? '/update' : '')) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to sync subscription'); reject(err); Dispatcher.dispatch({ type: SubscriptionTypes.SYNC, data: {}, }); return; } Dispatcher.dispatch({ type: SubscriptionTypes.SYNC, data: res.body, }); resolve(); }); }); } export function activate(license: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/subscription') .send({ license: license, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to activate subscription'); reject(err); return; } Dispatcher.dispatch({ type: SubscriptionTypes.SYNC, data: res.body, }); resolve(); }); }); } export function cancel(key: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('https://app.pritunl.com/subscription') .send({ key: key, }) .set('Accept', 'application/json') .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to cancel subscription'); reject(err); return; } resolve(); sync(true); }); }); } EventDispatcher.register((action: SubscriptionTypes.SubscriptionDispatch) => { switch (action.type) { case SubscriptionTypes.CHANGE: if (!Constants.user) { sync(false); } break; } }); ================================================ FILE: www/app/actions/UserActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as UserTypes from '../types/UserTypes'; import UserStore from '../stores/UserStore'; import UsersStore from '../stores/UsersStore'; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; export function load(userId: string): Promise { if (!userId) { let user: UserTypes.User = { id: null, type: 'local', roles: [], permissions: [], }; Dispatcher.dispatch({ type: UserTypes.LOAD, data: { user: user, }, }); return Promise.resolve(); } let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/user/' + userId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load user'); reject(err); return; } Dispatcher.dispatch({ type: UserTypes.LOAD, data: { user: res.body, }, }); resolve(); }); }); } export function reload(): Promise { return load(UserStore.user ? UserStore.user.id : null); } export function unload(): void { Dispatcher.dispatch({ type: UserTypes.UNLOAD, }); } export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/user') .query({ ...UsersStore.filter, page: UsersStore.page, page_count: UsersStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load users'); reject(err); return; } Dispatcher.dispatch({ type: UserTypes.SYNC, data: { users: res.body.users, count: res.body.count, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: UserTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: UserTypes.Filter): Promise { Dispatcher.dispatch({ type: UserTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(user: UserTypes.User): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/user/' + user.id) .send(user) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save user'); reject(err); return; } Dispatcher.dispatch({ type: UserTypes.LOAD, data: { user: res.body, }, }); resolve(); }); }); } export function create(user: UserTypes.User): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/user') .send(user) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create user'); reject(err); return; } resolve(); }); }); } export function remove(userIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/user') .send(userIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return } if (err) { Alert.errorRes(res, 'Failed to delete users'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: UserTypes.UserDispatch) => { switch (action.type) { case UserTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/actions/VpcActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as VpcTypes from '../types/VpcTypes'; import VpcsStore from '../stores/VpcsStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; let syncId: string; let syncNamesId: string; export function sync(noLoading?: boolean): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader: Loader; if (!noLoading) { loader = new Loader().loading(); } return new Promise((resolve, reject): void => { SuperAgent .get('/vpc') .query({ ...VpcsStore.filter, page: VpcsStore.page, page_count: VpcsStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { if (loader) { loader.done(); } if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load vpcs'); reject(err); return; } Dispatcher.dispatch({ type: VpcTypes.SYNC, data: { vpcs: res.body.vpcs, count: res.body.count, }, }); resolve(); }); }); } export function syncNames(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/vpc') .query({ names: "true", }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load vpcs names'); reject(err); return; } Dispatcher.dispatch({ type: VpcTypes.SYNC_NAMES, data: { vpcs: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: VpcTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: VpcTypes.Filter): Promise { Dispatcher.dispatch({ type: VpcTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(vpc: VpcTypes.Vpc): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/vpc/' + vpc.id) .send(vpc) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save vpc'); reject(err); return; } resolve(); }); }); } export function create(vpc: VpcTypes.Vpc): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/vpc') .send(vpc) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create vpc'); reject(err); return; } resolve(); }); }); } export function remove(vpcId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/vpc/' + vpcId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete vpc'); reject(err); return; } resolve(); }); }); } export function removeMulti(vpcIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/vpc') .send(vpcIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete vpcs'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: VpcTypes.VpcDispatch) => { switch (action.type) { case VpcTypes.CHANGE: sync(); break; } }); ================================================ FILE: www/app/actions/ZoneActions.ts ================================================ /// import * as SuperAgent from 'superagent'; import Dispatcher from '../dispatcher/Dispatcher'; import EventDispatcher from '../dispatcher/EventDispatcher'; import * as Alert from '../Alert'; import * as Csrf from '../Csrf'; import Loader from '../Loader'; import * as ZoneTypes from '../types/ZoneTypes'; import ZonesStore from '../stores/ZonesStore'; import CompletionStore from "../stores/CompletionStore"; import * as MiscUtils from '../utils/MiscUtils'; import * as Constants from "../Constants"; let syncId: string; let syncNamesId: string; export function sync(): Promise { let curSyncId = MiscUtils.uuid(); syncId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/zone') .query({ ...ZonesStore.filter, page: ZonesStore.page, page_count: ZonesStore.pageCount, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load zones'); reject(err); return; } Dispatcher.dispatch({ type: ZoneTypes.SYNC, data: { zones: res.body.zones, count: res.body.count, }, }); resolve(); }); }); } export function syncNames(): Promise { let curSyncId = MiscUtils.uuid(); syncNamesId = curSyncId; let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .get('/zone') .query({ names: true, }) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (curSyncId !== syncNamesId) { resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to load zone names'); reject(err); return; } Dispatcher.dispatch({ type: ZoneTypes.SYNC_NAMES, data: { secrets: res.body, }, }); resolve(); }); }); } export function traverse(page: number): Promise { Dispatcher.dispatch({ type: ZoneTypes.TRAVERSE, data: { page: page, }, }); return sync(); } export function filter(filt: ZoneTypes.Filter): Promise { Dispatcher.dispatch({ type: ZoneTypes.FILTER, data: { filter: filt, }, }); return sync(); } export function commit(zone: ZoneTypes.Zone): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .put('/zone/' + zone.id) .send(zone) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to save zone'); reject(err); return; } resolve(); }); }); } export function create(zone: ZoneTypes.Zone): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .post('/zone') .send(zone) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to create zone'); reject(err); return; } resolve(); }); }); } export function remove(zoneId: string): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/zone/' + zoneId) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .set('Organization', CompletionStore.userOrganization) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete zones'); reject(err); return; } resolve(); }); }); } export function removeMulti(zoneIds: string[]): Promise { let loader = new Loader().loading(); return new Promise((resolve, reject): void => { SuperAgent .delete('/zone') .send(zoneIds) .set('Accept', 'application/json') .set('Csrf-Token', Csrf.token) .end((err: any, res: SuperAgent.Response): void => { loader.done(); if (res && res.status === 401) { window.location.href = '/login'; resolve(); return; } if (err) { Alert.errorRes(res, 'Failed to delete zones'); reject(err); return; } resolve(); }); }); } EventDispatcher.register((action: ZoneTypes.ZoneDispatch) => { switch (action.type) { case ZoneTypes.CHANGE: if (!Constants.user) { sync(); } break; } }); ================================================ FILE: www/app/completion/Cache.ts ================================================ /// import Dispatcher from '../dispatcher/Dispatcher' import EventEmitter from "../EventEmitter" import * as CompletionTypes from "../types/CompletionTypes" import * as GlobalTypes from "../types/GlobalTypes" export interface Kind { name: string label: string title: string } export interface Resource { id: string name: string label?: string info: ResourceInfo[] tags?: Tag[] } export interface ResourceInfo { label: string value: string | number } export interface Tag { name: string label: string } export interface Dispatch { type: string } class CompletionCache extends EventEmitter { _kindMap: Record = {} _kinds: Kind[] = [] _resourceMap: Record> = {} _resources: Record = {} _token = Dispatcher.register((this._callback).bind(this)) constructor() { super() } get kinds(): Kind[] { return this._kinds } kind(name: string): Kind { const i = this._kindMap[name] if (i === undefined) { return null } return this._kinds[i] } resource(kindName: string, name: string): Resource { const kindResourceMap = this._resourceMap[kindName] if (!kindResourceMap) { return null } const i = kindResourceMap[name] if (i === undefined) { return null } return this._resources[kindName][i] } resources(kind: string): Resource[] { return (this._resources[kind] || []) } addChangeListener(callback: () => void): void { this.on(GlobalTypes.CHANGE, callback) } removeChangeListener(callback: () => void): void { this.removeListener(GlobalTypes.CHANGE, callback) } _reset(): void { this._kinds = [] this._kindMap = {} this._resources = {} this._resourceMap = {} } _callback(action: Dispatch): void { switch (action.type) { case GlobalTypes.RESET: this._reset() break } } update(resources: CompletionTypes.Completion): void { this._kinds = [] this._resources = {} let resourceList: Resource[] let subResourceList: Resource[] this._kinds.push({ name: "organization", label: "Organization", title: "**Organization**", }) resourceList = [] for (let item of (resources.organizations ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["organization"] = resourceList this._kinds.push({ name: "domain", label: "Domain", title: "**Domain**", }) resourceList = [] for (let item of (resources.domains ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["domain"] = resourceList this._kinds.push({ name: "vpc", label: "VPC", title: "**VPC**", }) this._kinds.push({ name: "subnet", label: "Subnet", title: "**Subnet**", }) resourceList = [] subResourceList = [] for (let item of (resources.vpcs ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) for (let subItem of (item.subnets || [])) { subResourceList.push({ id: subItem.id, name: subItem.name, label: "[" + item.name + "] " + subItem.name, info: [ { label: "**Name**", value: subItem.name, }, { label: "**VPC**", value: item.name, }, ], }) } } this._resources["vpc"] = resourceList this._resources["subnet"] = subResourceList this._kinds.push({ name: "datacenter", label: "Datacenter", title: "**Datacenter**", }) resourceList = [] for (let item of (resources.datacenters ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["datacenter"] = resourceList this._kinds.push({ name: "node", label: "Node", title: "**Node**", }) resourceList = [] for (let item of (resources.nodes ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["node"] = resourceList this._kinds.push({ name: "pool", label: "Pool", title: "**Pool**", }) resourceList = [] for (let item of (resources.pools ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["pool"] = resourceList this._kinds.push({ name: "zone", label: "Zone", title: "**Zone**", }) resourceList = [] for (let item of (resources.zones ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["zone"] = resourceList this._kinds.push({ name: "shape", label: "Shapes", title: "**Shapes**", }) resourceList = [] for (let item of (resources.shapes ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["shape"] = resourceList this._kinds.push({ name: "image", label: "Image", title: "**Image**", }) resourceList = [] for (let item of (resources.images ||[])) { let tags: Tag[] = [] for (let tag of (item.tags || [])) { tags.push({ name: tag, label: tag, }) } resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], tags: tags, }) } this._resources["image"] = resourceList this._kinds.push({ name: "build", label: "Build", title: "**Build**", }) resourceList = [] for (let item of (resources.builds ||[])) { let tags: Tag[] = [] for (let tag of (item.tags || [])) { tags.push({ name: tag.tag, label: tag.tag, }) } resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], tags: tags, }) } this._resources["build"] = resourceList this._kinds.push({ name: "instance", label: "Instance", title: "**Instance**", }) resourceList = [] for (let item of (resources.instances ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, { label: "**Memory**", value: item.memory, }, { label: "**Processors**", value: item.processors, }, ], }) } this._resources["instance"] = resourceList this._kinds.push({ name: "plan", label: "Plan", title: "**Plan**", }) resourceList = [] for (let item of (resources.plans ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["plan"] = resourceList this._kinds.push({ name: "certificate", label: "Certificate", title: "**Certificate**", }) resourceList = [] for (let item of (resources.certificates ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["certificate"] = resourceList this._kinds.push({ name: "secret", label: "Secret", title: "**Secret**", }) resourceList = [] for (let item of (resources.secrets ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["secret"] = resourceList this._kinds.push({ name: "pod", label: "Pod", title: "**Pod**", }) resourceList = [] for (let item of (resources.pods ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["pod"] = resourceList this._kinds.push({ name: "unit", label: "Unit", title: "**Unit**", }) resourceList = [] for (let item of (resources.units ||[])) { resourceList.push({ id: item.id, name: item.name, info: [ { label: "**Name**", value: item.name, }, ], }) } this._resources["unit"] = resourceList this._kindMap = {} for (let i = 0; i < this._kinds.length; i++) { this._kindMap[this._kinds[i].name] = i } this._resourceMap = {} Object.entries(this._resources).forEach(([kindName, resources]) => { let kindResourceMap: Record = {} for (let i = 0; i < resources.length; i++) { kindResourceMap[resources[i].name] = i } this._resourceMap[kindName] = kindResourceMap }) } } export default new CompletionCache() ================================================ FILE: www/app/completion/Engine.ts ================================================ /// import * as Monaco from "monaco-editor" import * as MonacoEditor from "@monaco-editor/react" import * as MonacoYaml from "monaco-yaml" import CompletionCache from "./Cache" import * as Types from "./Types" let registered = false export type Match = Monaco.languages.ProviderResult< Monaco.languages.CompletionList> const noMatch: Match = { suggestions: [] } export enum CompletionItemKind { Method = 0, Function = 1, Constructor = 2, Field = 3, Variable = 4, Class = 5, Struct = 6, Interface = 7, Module = 8, Property = 9, Event = 10, Operator = 11, Unit = 12, Value = 13, Constant = 14, Enum = 15, EnumMember = 16, Keyword = 17, Text = 18, Color = 19, File = 20, Reference = 21, Customcolor = 22, Folder = 23, TypeParameter = 24, User = 25, Issue = 26, Snippet = 27 } export enum CompletionItemInsertTextRule { None = 0, /** * Adjust whitespace/indentation of multiline insert texts to * match the current line indentation. */ KeepWhitespace = 1, /** * `insertText` is a snippet. */ InsertAsSnippet = 4 } export function handleBeforeMount( monaco: MonacoEditor.Monaco): void { MonacoYaml.configureMonacoYaml(monaco, { enableSchemaRequest: false, schemas: [ { fileMatch: ["instance.yaml"], schema: { type: "object", properties: { name: { type: "string", description: "Instance name", }, kind: { type: "string", enum: ["instance"], description: "Resource kind", }, count: { type: "integer", description: "Number of instances", }, plan: { type: "string", description: "Instance plan", }, zone: { type: "string", description: "Availability zone", }, node: { type: "string", description: "Specific node for the instance", }, shape: { type: "string", description: "Instance shape specification", }, vpc: { type: "string", description: "VPC identifier", }, subnet: { type: "string", description: "Subnet identifier", }, roles: { type: "array", items: { type: "string", }, description: "List of roles assigned to the instance", }, processors: { type: "integer", description: "Number of processors allocated", }, memory: { type: "integer", description: "Memory allocated in MB", }, uefi: { type: "boolean", description: "Enable UEFI boot", }, secureBoot: { type: "boolean", description: "Enable secure boot", }, cloudType: { type: "string", description: "Cloud provider type", }, tpm: { type: "boolean", description: "Enable Trusted Platform Module", }, vnc: { type: "boolean", description: "Enable VNC access", }, deleteProtection: { type: "boolean", description: "Enable deletion protection", }, skipSourceDestCheck: { type: "boolean", description: "Skip source/destination check", }, gui: { type: "boolean", description: "Desktop GUI", }, hostAddress: { type: "boolean", description: "Allocate host address", }, publicAddress: { type: "boolean", description: "Allocate public IPv4 address", }, publicAddress6: { type: "boolean", description: "Allocate public IPv6 address", }, dhcpServer: { type: "boolean", description: "Enable DHCP server", }, image: { type: "string", description: "Image identifier", }, mounts: { type: "array", items: { type: "object", properties: { name: { type: "string", description: "Mount name", }, type: { type: "string", enum: ["host_path", "disk"], description: "Mount type", }, path: { type: "string", description: "Mount path", }, hostPath: { type: "string", description: "Host mount path", }, disks: { type: "array", items: { type: "string", }, description: "List of disk identifiers", }, }, required: ["path"], description: "Disk mount configuration", }, description: "Disk mounts", }, nodePorts: { type: "array", items: { type: "object", properties: { protocol: { type: "string", enum: ["tcp", "udp"], description: "Network protocol", }, externalPort: { type: "integer", minimum: 1, maximum: 65535, description: "External port number", }, internalPort: { type: "integer", minimum: 1, maximum: 65535, description: "Internal port number", }, }, required: ["protocol", "externalPort", "internalPort"], description: "Node port mapping", }, description: "Node port configurations", }, certificates: { type: "array", items: { type: "string", }, description: "List of certificate identifiers", }, secrets: { type: "array", items: { type: "string", }, description: "List of secret identifiers", }, pods: { type: "array", items: { type: "string", }, description: "List of pod identifiers", }, diskSize: { type: "integer", description: "Size of disk in GB", }, }, required: ["name", "kind", "zone", "vpc", "subnet", "image"], description: "Instance configuration", }, uri: "https://todo.pritunl.com/instance-schema.json", }, { fileMatch: ["domain.yaml"], schema: { type: "object", properties: { name: { type: "string", description: "Domain name identifier", }, kind: { type: "string", enum: ["domain"], description: "Resource kind", }, records: { type: "array", items: { type: "object", properties: { name: { type: "string", description: "Record name (subdomain or label)", }, domain: { type: "string", description: "Domain name for this record", }, type: { type: "string", enum: [ "host", "private", "private6", "public", "public6", "cloud_public", "cloud_public6", "cloud_private", ], description: "Record type", }, }, required: ["name", "domain", "type"], description: "DNS record configuration", }, description: "List of DNS records", }, }, required: ["name", "kind", "records"], description: "Domain and DNS records configuration", }, uri: "https://todo.pritunl.com/domain-schema.json", }, { fileMatch: ["firewall.yaml"], schema: { type: "object", required: ["name", "kind", "ingress"], properties: { name: { type: "string", description: "The name of the firewall rule", }, kind: { type: "string", enum: ["firewall"], description: "Resource kind", }, ingress: { type: "array", description: "Ingress rules for the firewall", items: { type: "object", required: ["protocol", "port", "source"], properties: { protocol: { type: "string", enum: ["all", "icmp", "tcp", "udp", "multicast", "broadcast"], description: "The protocol for this rule", }, port: { type: ["number", "string"], minimum: 1, maximum: 65535, description: "Port number or range " + "(e.g. \"80\" or \"80-443\")", }, source: { type: "array", description: "Source addresses or networks", items: { type: "string", }, }, }, }, }, }, }, uri: "https://todo.pritunl.com/firewall-schema.json", }, ], }); } export function handleAfterMount( editor: Monaco.editor.IStandaloneCodeEditor, monaco: MonacoEditor.Monaco): void { if (registered) { return } registered = true monaco.languages.registerHoverProvider("markdown", { provideHover: (model, position, token) => { const lineContent = model.getLineContent(position.lineNumber) const match = lineContent.match( /\+\/([a-zA-Z0-9-]*)\/([a-zA-Z0-9-]*)/) if (!match) { return null } let kindName = match[1] let resourceName = match[2] let kind = CompletionCache.kind(kindName) let resource = CompletionCache.resource(kindName, resourceName) if (kind && resource) { let contents = [ {value: kind.title}, ] let data: string[] = [] for (let item of resource.info) { data.push(item.label + ": " + item.value) } contents.push({ value: data.join(" \n"), }) return { range: { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: match.index + 1, endColumn: match.index + kindName.length + resourceName.length + 5, }, contents: contents, } } return null } }) editor.updateOptions({ suggest: { preview: true, } }); monaco.languages.setLanguageConfiguration("markdown", { wordPattern: /[^+\/:]+/g, }); monaco.languages.registerCompletionItemProvider("markdown", { triggerCharacters: ["+", "/", ":"], provideCompletionItems: (model, position) => { const textBeforeCursor = model.getValueInRange({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: position.column, }) const selectorWithTagMatch = textBeforeCursor.match( /\+\/([a-zA-Z0-9-]*)\/([a-zA-Z0-9-]*):([a-zA-Z0-9-]*)\/$/); const selectorDirectMatch = textBeforeCursor.match( /\+\/([a-zA-Z0-9-]*)\/([a-zA-Z0-9-]*)\/$/); if (selectorWithTagMatch || selectorDirectMatch) { const match = selectorWithTagMatch || selectorDirectMatch; const kindName = match[1]; const resourceName = match[2]; let selectorKey = ""; switch (kindName) { case "instance": selectorKey = "instance"; break; case "vpc": selectorKey = "vpc"; break; case "subnet": selectorKey = "subnet"; break; case "certificate": selectorKey = "certificate"; break; case "secret": selectorKey = "secret"; break; case "unit": selectorKey = "unit"; break; default: return noMatch; } const selectors = Types.Selectors[selectorKey]; if (!selectors) { return noMatch; } const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: position.column, endColumn: position.column, } let suggestions: Monaco.languages.CompletionItem[] = []; for (const [key, info] of Object.entries(selectors)) { suggestions.push({ label: key, kind: CompletionItemKind.Value, insertText: key, insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, documentation: info.tooltip, detail: info.tooltip, range: range, }) } return { suggestions: suggestions, } } const tagMatch = textBeforeCursor.match( /\+\/([a-zA-Z0-9-]*)\/([a-zA-Z0-9-]*):$/); if (tagMatch) { let kindName = tagMatch[1] let resourceName = tagMatch[2] let resource = CompletionCache.resource(kindName, resourceName) if (!resource || !resource.tags) { return noMatch } const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: position.column, endColumn: position.column, } let suggestions: Monaco.languages.CompletionItem[] = [] for (const tag of resource.tags) { suggestions.push({ label: tag.name, kind: CompletionItemKind.Field, insertText: tag.name, insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, documentation: "Tag", range: range, }) } return { suggestions: suggestions, } } const resourceMatch = textBeforeCursor.match(/\+\/([a-zA-Z0-9-]*)\/$/) if (resourceMatch) { let kind = CompletionCache.kind(resourceMatch[1]) if (!kind) { return noMatch } const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: position.column, endColumn: position.column, } let suggestions: Monaco.languages.CompletionItem[] = [] CompletionCache.resources(kind.name).forEach((resource, index) => { suggestions.push({ label: resource.label || resource.name, kind: CompletionItemKind.Property, insertText: resource.name, insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, documentation: kind.title, range: range, sortText: index.toString().padStart(3, "0"), }) }) return { suggestions: suggestions, } } const kindMatch = textBeforeCursor.match(/\+\/$/) if (kindMatch) { const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: position.column, endColumn: position.column, } let suggestions: Monaco.languages.CompletionItem[] = [] for (const kind of CompletionCache.kinds) { suggestions.push({ label: kind.name, kind: CompletionItemKind.Class, insertText: kind.name, insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, documentation: kind.title, range: range, }) } return { suggestions: suggestions, } } return noMatch }, }) } ================================================ FILE: www/app/completion/Types.ts ================================================ /// import * as CompletionTypes from "../types/CompletionTypes"; import * as OrganizationTypes from "../types/OrganizationTypes"; import * as DomainTypes from "../types/DomainTypes"; import * as VpcTypes from "../types/VpcTypes"; import * as DatacenterTypes from "../types/DatacenterTypes"; import * as NodeTypes from "../types/NodeTypes"; import * as PoolTypes from "../types/PoolTypes"; import * as ZoneTypes from "../types/ZoneTypes"; import * as ShapeTypes from "../types/ShapeTypes"; import * as ImageTypes from "../types/ImageTypes"; import * as InstanceTypes from "../types/InstanceTypes"; import * as PlanTypes from "../types/PlanTypes"; import * as CertificateTypes from "../types/CertificateTypes"; import * as SecretTypes from "../types/SecretTypes"; import * as PodTypes from "../types/PodTypes"; export interface Resources { organizations: OrganizationTypes.OrganizationsRo; domains: DomainTypes.DomainsRo; vpcs: VpcTypes.VpcsRo; subnets: VpcTypes.Subnet[]; datacenters: DatacenterTypes.DatacentersRo; nodes: NodeTypes.NodesRo; pools: PoolTypes.PoolsRo; zones: ZoneTypes.ZonesRo; shapes: ShapeTypes.ShapesRo; images: ImageTypes.ImagesRo; builds: CompletionTypes.Build[]; instances: InstanceTypes.InstancesRo; plans: PlanTypes.PlansRo; certificates: CertificateTypes.CertificatesRo; secrets: SecretTypes.SecretsRo; pods: PodTypes.PodsRo; units: PodTypes.UnitsRo; } export interface Kind { name: string label: string title: string } export interface Resource { id: string name: string info: ResourceInfo[] } export interface ResourceInfo { label: string value: string | number } export interface Dispatch { type: string } export type SelectorInfo = { label: string; tooltip: string; } export const Selectors: Record> = { "instance": { "id": { label: "ID", tooltip: "Unique identifier of the instance" }, "organization": { label: "Organization", tooltip: "Organization the instance belongs to" }, "zone": { label: "Zone", tooltip: "Availability zone where the instance is deployed" }, "vpc": { label: "VPC", tooltip: "Virtual Private Cloud network the instance is connected to" }, "subnet": { label: "Subnet", tooltip: "Subnet within the VPC where the instance resides" }, "cloud_subnet": { label: "Cloud Subnet", tooltip: "Cloud Cloud subnet configuration" }, "cloud_vnic": { label: "Cloud VNIC", tooltip: "Cloud virtual network interface" }, "image": { label: "Image", tooltip: "Base image used for the instance" }, "state": { label: "State", tooltip: "Current operational state of the instance" }, "uefi": { label: "UEFI", tooltip: "Unified Extensible Firmware Interface status" }, "secure_boot": { label: "Secure Boot", tooltip: "Status of secure boot feature" }, "tpm": { label: "TPM", tooltip: "Trusted Platform Module status" }, "dhcp_server": { label: "DHCP Server", tooltip: "Dynamic Host Configuration Protocol server status" }, "cloud_type": { label: "Cloud Type", tooltip: "Type of cloud infrastructure being used" }, "delete_protection": { label: "Delete Protection", tooltip: "Status of deletion protection feature" }, "skip_source_dest_check": { label: "Skip Source/Dest Check", tooltip: "Status of source/destination checking" }, "qemu_version": { label: "QEMU Version", tooltip: "Version of QEMU virtualization software" }, "public_ips": { label: "Public IPs", tooltip: "List of public IPv4 addresses" }, "public_ips6": { label: "Public IPv6", tooltip: "List of public IPv6 addresses" }, "private_ips": { label: "Private IPs", tooltip: "List of private IPv4 addresses" }, "private_ips6": { label: "Private IPv6", tooltip: "List of private IPv6 addresses" }, "gateway_ips": { label: "Gateway IPs", tooltip: "IPv4 gateway addresses" }, "gateway_ips6": { label: "Gateway IPv6", tooltip: "IPv6 gateway addresses" }, "cloud_private_ips": { label: "Cloud Private IPs", tooltip: "Cloud private IP addresses" }, "cloud_public_ips": { label: "Cloud Public IPs", tooltip: "Cloud public IP addresses" }, "host_ips": { label: "Host IPs", tooltip: "IP addresses of the host machine" }, "network_namespace": { label: "Network Namespace", tooltip: "Network namespace configuration" }, "no_public_address": { label: "No Public Address", tooltip: "Indicates if public IPv4 addressing is disabled" }, "no_public_address6": { label: "No Public IPv6", tooltip: "Indicates if public IPv6 addressing is disabled" }, "no_host_address": { label: "No Host Address", tooltip: "Indicates if host addressing is disabled" }, "node": { label: "Node", tooltip: "Physical or virtual node where the instance runs" }, "shape": { label: "Shape", tooltip: "Instance type and size configuration" }, "name": { label: "Name", tooltip: "Display name of the instance" }, "root_enabled": { label: "Root Enabled", tooltip: "Status of root access" }, "memory": { label: "Memory", tooltip: "Allocated RAM" }, "processors": { label: "Processors", tooltip: "Number of allocated CPU cores" }, "roles": { label: "Roles", tooltip: "Access roles assigned to the instance" }, "vnc": { label: "VNC", tooltip: "Virtual Network Computing status" }, "spice": { label: "SPICE", tooltip: "Simple Protocol for Independent Computing Environments status" }, "gui": { label: "GUI", tooltip: "Graphical User Interface status" }, "deployment": { label: "Deployment", tooltip: "Deployment configuration details" } }, "vpc": { "id": { label: "ID", tooltip: "Unique identifier of the VPC" }, "name": { label: "Name", tooltip: "Display name of the VPC" }, "vpc_id": { label: "VPC ID", tooltip: "Cloud provider's VPC identifier" }, "network": { label: "Network", tooltip: "IPv4 network configuration" }, "network6": { label: "Network IPv6", tooltip: "IPv6 network configuration" } }, "subnet": { "id": { label: "ID", tooltip: "Unique identifier of the subnet" }, "name": { label: "Name", tooltip: "Display name of the subnet" }, "network": { label: "Network", tooltip: "Network address range of the subnet" } }, "certificate": { "id": { label: "ID", tooltip: "Unique identifier of the certificate" }, "name": { label: "Name", tooltip: "Display name of the certificate" }, "type": { label: "Type", tooltip: "Type of certificate" }, "key": { label: "Key", tooltip: "Certificate key information" }, "certificate": { label: "Certificate", tooltip: "Certificate content" } }, "secret": { "id": { label: "ID", tooltip: "Unique identifier of the secret" }, "name": { label: "Name", tooltip: "Display name of the secret" }, "type": { label: "Type", tooltip: "Type of secret" }, "key": { label: "Key", tooltip: "Secret key identifier" }, "value": { label: "Value", tooltip: "Protected secret value" }, "region": { label: "Region", tooltip: "Region where the secret is stored" }, "public_key": { label: "Public Key", tooltip: "Public key component" }, "private_key": { label: "Private Key", tooltip: "Private key component" } }, "unit": { "id": { label: "ID", tooltip: "Unique identifier of the unit" }, "name": { label: "Name", tooltip: "Display name of the unit" }, "kind": { label: "Kind", tooltip: "Type of unit" }, "count": { label: "Count", tooltip: "Number of instances in the unit" }, "public_ips": { label: "Public IPs", tooltip: "List of public IPv4 addresses" }, "public_ips6": { label: "Public IPv6", tooltip: "List of public IPv6 addresses" }, "healthy_public_ips": { label: "Healthy Public IPs", tooltip: "List of healthy public IPv4 addresses" }, "healthy_public_ips6": { label: "Healthy Public IPv6", tooltip: "List of healthy public IPv6 addresses" }, "unhealthy_public_ips": { label: "Unhealthy Public IPs", tooltip: "List of unhealthy public IPv4 addresses" }, "unhealthy_public_ips6": { label: "Unhealthy Public IPv6", tooltip: "List of unhealthy public IPv6 addresses" }, "private_ips": { label: "Private IPs", tooltip: "List of private IPv4 addresses" }, "private_ips6": { label: "Private IPv6", tooltip: "List of private IPv6 addresses" }, "healthy_private_ips": { label: "Healthy Private IPs", tooltip: "List of healthy private IPv4 addresses" }, "healthy_private_ips6": { label: "Healthy Private IPv6", tooltip: "List of healthy private IPv6 addresses" }, "unhealthy_private_ips": { label: "Unhealthy Private IPs", tooltip: "List of unhealthy private IPv4 addresses" }, "unhealthy_private_ips6": { label: "Unhealthy Private IPv6", tooltip: "List of unhealthy private IPv6 addresses" }, "cloud_private_ips": { label: "Cloud Private IPs", tooltip: "List of cloud private IP addresses" }, "cloud_public_ips": { label: "Cloud Public IPs", tooltip: "List of cloud public IP addresses" }, "healthy_cloud_public_ips": { label: "Healthy Cloud Public IPs", tooltip: "List of healthy cloud public IP addresses" }, "healthy_cloud_private_ips": { label: "Healthy Cloud Private IPs", tooltip: "List of healthy cloud private IP addresses" }, "unhealthy_cloud_public_ips": { label: "Unhealthy Cloud Public IPs", tooltip: "List of unhealthy cloud public IP addresses" }, "unhealthy_cloud_private_ips": { label: "Unhealthy Cloud Private IPs", tooltip: "List of unhealthy cloud private IP addresses" } } }; ================================================ FILE: www/app/components/AdvisoryDialog.tsx ================================================ /// import * as React from "react"; import * as Blueprint from "@blueprintjs/core"; import * as InstanceTypes from "../types/InstanceTypes"; import * as MiscUtils from "../utils/MiscUtils"; interface CveDetail { cve: string; detail: InstanceTypes.Advisory; } interface UpdateEntry { update: InstanceTypes.Update; cves: CveDetail[]; importantCves: CveDetail[]; worstScore: number; worstSeverity: string; link?: string; } interface State { open: boolean; showLowSeverity: boolean; expanded: {[key: string]: boolean}; expandedCves: {[advisory: string]: boolean}; } interface Props { updates: InstanceTypes.Update[]; } const css = { dialog: { width: "90%", maxWidth: "720px", } as React.CSSProperties, body: { padding: "12px 16px", maxHeight: "70vh", overflow: "auto", } as React.CSSProperties, header: { margin: "0 0 10px 0", fontWeight: 600, } as React.CSSProperties, count: { marginLeft: "6px", opacity: 0.7, } as React.CSSProperties, section: { display: "flex", alignItems: "center", margin: "14px 0 8px 0", fontWeight: 600, } as React.CSSProperties, updateCard: { padding: "12px", marginBottom: "12px", } as React.CSSProperties, cveCard: { padding: "10px", marginTop: "8px", background: "rgba(138, 155, 168, 0.06)", borderRadius: "3px", } as React.CSSProperties, headerRow: { alignItems: "center", marginBottom: "8px", gap: "8px", } as React.CSSProperties, headerTag: { paddingTop: "3px", paddingBottom: "3px", marginRight: "6px", } as React.CSSProperties, title: { fontFamily: "monospace", fontSize: "14px", fontWeight: 600, } as React.CSSProperties, cveTitle: { fontFamily: "monospace", fontSize: "13px", fontWeight: 600, } as React.CSSProperties, tagRow: { marginBottom: "8px", gap: "6px", } as React.CSSProperties, tag: { paddingTop: "3px", paddingBottom: "3px", marginRight: "6px", marginBottom: "4px", } as React.CSSProperties, packages: { fontSize: "11px", color: "var(--bp5-text-color-muted, #5f6b7c)", marginBottom: "6px", wordBreak: "break-all", } as React.CSSProperties, packageName: { fontFamily: "monospace", fontSize: "14px", fontWeight: 600, padding: "3px 8px", background: "rgba(138, 155, 168, 0.15)", borderRadius: "3px", wordBreak: "break-all", } as React.CSSProperties, packageList: { fontSize: "11px", fontFamily: "monospace", color: "var(--bp5-text-color-muted, #5f6b7c)", marginBottom: "8px", padding: "6px 8px", background: "rgba(138, 155, 168, 0.08)", borderRadius: "3px", wordBreak: "break-all", whiteSpace: "pre-wrap", } as React.CSSProperties, description: { fontSize: "12px", whiteSpace: "pre-wrap", wordBreak: "break-word", padding: "6px 8px", background: "rgba(138, 155, 168, 0.1)", borderRadius: "3px", } as React.CSSProperties, descriptionLimited: { display: "-webkit-box", WebkitLineClamp: 6, WebkitBoxOrient: "vertical", overflow: "hidden", } as React.CSSProperties, descriptionToggle: { marginTop: "4px", padding: "0", minHeight: "0", fontSize: "11px", } as React.CSSProperties, hiddenToggle: { marginTop: "8px", padding: "2px 6px", minHeight: "0", fontSize: "11px", } as React.CSSProperties, } export default class AdvisoryDialog extends React.Component { constructor(props: any, context: any) { super(props, context); this.state = { open: false, showLowSeverity: false, expanded: {}, expandedCves: {}, } } rpmName(pkg: string): string { if (!pkg) { return pkg } let s = pkg let lastDot = s.lastIndexOf('.') if (lastDot > 0) { s = s.slice(0, lastDot) } let lastDash = s.lastIndexOf('-') if (lastDash > 0) { s = s.slice(0, lastDash) } lastDash = s.lastIndexOf('-') if (lastDash > 0) { s = s.slice(0, lastDash) } return s } advisoryLink(advisoryRaw: string): string { let advisory = (advisoryRaw || "").replace(/[^a-zA-Z0-9:-]/g, '') if (advisory.startsWith('ALSA') || advisory.startsWith('RLSA') || advisory.startsWith('RHSA')) { return `https://access.redhat.com/errata/RH${advisory.slice(2)}` } else if (advisory.startsWith('ELSA')) { return `https://linux.oracle.com/errata/${advisory}.html` } else if (advisory.startsWith('FEDORA')) { return `https://bodhi.fedoraproject.org/updates/${advisory}` } return "" } severityIntent(severity: string): Blueprint.Intent { switch ((severity || "").toLowerCase()) { case "critical": return Blueprint.Intent.DANGER; case "high": return Blueprint.Intent.WARNING; case "medium": return Blueprint.Intent.PRIMARY; default: return Blueprint.Intent.NONE; } } isImportantCve(detail: InstanceTypes.Advisory): boolean { if (!detail) { return false; } if (detail.severity === "critical") { return true; } if (detail.vector === "network" && (detail.severity === "high" || (detail.score || 0) >= 7)) { return true; } return false; } cveSortScore(detail: InstanceTypes.Advisory): number { let s = detail?.score || 0; if (detail?.vector === "network") s += 100; if (detail?.severity === "critical") s += 50; if (detail?.privileges === "none") s += 10; if (detail?.interaction === "none") s += 5; return s; } buildEntries(): UpdateEntry[] { let updates = this.props.updates; if (!updates) { return []; } let entries: UpdateEntry[] = []; for (let update of updates) { let cves = update.cves || []; let details = update.details || []; let pairs: CveDetail[] = []; let seen = new Set(); for (let i = 0; i < cves.length; i++) { let cve = cves[i]; let detail = details[i]; if (!cve || !detail || seen.has(cve)) { continue; } seen.add(cve); pairs.push({cve: cve, detail: detail}); } pairs.sort((a, b) => this.cveSortScore(b.detail) - this.cveSortScore(a.detail)); let importantCves = pairs.filter( (p): boolean => this.isImportantCve(p.detail)); let worstScore = 0; let worstSeverity = ""; let severityRank: {[key: string]: number} = { "critical": 4, "high": 3, "medium": 2, "low": 1, }; let worstRank = 0; for (let p of pairs) { let score = this.cveSortScore(p.detail); if (score > worstScore) { worstScore = score; } let sev = (p.detail.severity || "").toLowerCase(); let rank = severityRank[sev] || 0; if (rank > worstRank) { worstRank = rank; worstSeverity = sev; } } entries.push({ update: update, cves: pairs, importantCves: importantCves, worstScore: worstScore, worstSeverity: worstSeverity, link: this.advisoryLink(update.advisory || ""), }); } return entries; } buttonIntent(entries: UpdateEntry[]): string { if (entries.length === 0) { return ""; } let hasHigh = false; for (let entry of entries) { if (entry.worstSeverity === "critical") { return "bp5-intent-danger"; } if (entry.worstSeverity === "high") { hasHigh = true; } } return hasHigh ? "bp5-intent-warning" : "bp5-intent-primary"; } renderCveCard(entry: UpdateEntry, pair: CveDetail): JSX.Element { let d = pair.detail; let key = (entry.update.advisory || "") + "|" + pair.cve; let nvdUrl = `https://access.redhat.com/security/cve/${pair.cve}`; let tags: JSX.Element[] = []; if (d.vector === "network") { tags.push(Network); } else if (d.vector === "adjacent") { tags.push(Adjacent); } else if (d.vector === "local") { tags.push(Local); } else if (d.vector === "physical") { tags.push(Physical); } if (d.privileges === "none") { tags.push(Unauthenticated); } else if (d.privileges === "low") { tags.push(Low Privileged); } else if (d.privileges === "high") { tags.push(High Privileged); } if (d.interaction === "none") { tags.push(No Interaction); } else if (d.interaction === "required") { tags.push(User Interaction); } if (d.scope === "changed") { tags.push(Scope Changed); } let sevIntent = this.severityIntent(d.severity || ""); let sevLabel = MiscUtils.capitalize(d.severity || "Unknown"); let scoreLabel = d.score ? ` ${d.score.toFixed(1)}` : ""; return
{sevLabel}{scoreLabel} {pair.cve}
{tags.length > 0 &&
{tags}
} {d.description && this.renderDescription(key, d.description)}
; } renderDescription(key: string, text: string): JSX.Element { let expanded = !!this.state.expanded[key]; let style = expanded ? css.description : { ...css.description, ...css.descriptionLimited, }; return <>
{text}
; } renderUpdateCard(entry: UpdateEntry): JSX.Element { let update = entry.update; let sevIntent = this.severityIntent(entry.worstSeverity); let sevLabel = entry.worstSeverity ? MiscUtils.capitalize(entry.worstSeverity) : "Unknown"; let advisoryKey = update.advisory || ""; let cvesExpanded = !!this.state.expandedCves[advisoryKey]; let cvesToShow = cvesExpanded ? entry.cves : entry.importantCves; let hiddenCount = entry.cves.length - entry.importantCves.length; let packages = update.packages || []; let primaryName = packages.length > 0 ? this.rpmName(packages[0]) : ""; let hasFullVersionInfo = packages.length > 1 || (packages.length === 1 && packages[0] !== primaryName); let detailsKey = advisoryKey + "|details"; let detailsExpanded = !!this.state.expanded[detailsKey]; let hasDescription = !!update.description; let showDetailsToggle = hasDescription || hasFullVersionInfo; let descriptionStyle = detailsExpanded ? css.description : { ...css.description, ...css.descriptionLimited, }; return
{sevLabel} {primaryName && {primaryName} } {entry.link ? {update.advisory} : {update.advisory} }
{hasDescription &&
{update.description}
} {detailsExpanded && hasFullVersionInfo &&
{packages.join("\n")}
} {showDetailsToggle && } {cvesToShow.map((p): JSX.Element => this.renderCveCard(entry, p))} {hiddenCount > 0 && }
; } renderBody(entries: UpdateEntry[]): JSX.Element { if (entries.length === 0) { return
No security advisories
No outstanding security advisories reported by the guest agent.
; } entries.sort((a, b) => b.worstScore - a.worstScore); let important: UpdateEntry[] = []; let other: UpdateEntry[] = []; for (let entry of entries) { if (entry.importantCves.length > 0) { important.push(entry); } else { other.push(entry); } } return
{important.length > 0 ? <>
High Risk ({important.length})
{important.map((e): JSX.Element => this.renderUpdateCard(e))} :
No remotely exploitable or critical advisories.
} {other.length > 0 ? <> {this.state.showLowSeverity ?
{other.map((e): JSX.Element => this.renderUpdateCard(e))}
: null} : null}
; } render(): JSX.Element { let entries = this.buildEntries(); let dialog: JSX.Element if (this.state.open) { dialog = Security Advisories ({entries.length} advisor{entries.length === 1 ? "y" : "ies"}) } style={css.dialog} isOpen={this.state.open} usePortal={true} portalContainer={document.body} onClose={(): void => { this.setState({ ...this.state, open: false, }) }} > {this.renderBody(entries)}
} return
{dialog}
} } ================================================ FILE: www/app/components/Alert.tsx ================================================ /// import * as React from 'react'; import * as AlertTypes from '../types/AlertTypes'; import * as AuthorityTypes from '../types/AuthorityTypes'; import * as OrganizationTypes from "../types/OrganizationTypes"; import AlertDetailed from './AlertDetailed'; interface Props { alert: AlertTypes.AlertRo; organizations: OrganizationTypes.OrganizationsRo; authorities: AuthorityTypes.AuthoritiesRo; selected: boolean; onSelect: (shift: boolean) => void; open: boolean; onOpen: () => void; } const css = { card: { display: 'table-row', width: '100%', padding: 0, boxShadow: 'none', cursor: 'pointer', } as React.CSSProperties, cardOpen: { display: 'table-row', width: '100%', padding: 0, boxShadow: 'none', position: 'relative', } as React.CSSProperties, select: { margin: '2px 0 0 0', paddingTop: '3px', minHeight: '18px', } as React.CSSProperties, name: { verticalAlign: 'top', display: 'table-cell', padding: '8px', } as React.CSSProperties, nameSpan: { margin: '1px 5px 0 0', } as React.CSSProperties, item: { verticalAlign: 'top', display: 'table-cell', padding: '9px', whiteSpace: 'nowrap', } as React.CSSProperties, roles: { verticalAlign: 'top', display: 'table-cell', padding: '0 8px 8px 8px', } as React.CSSProperties, icon: { marginRight: '3px', } as React.CSSProperties, tag: { margin: '8px 5px 0 5px', minHeight: '20px', } as React.CSSProperties, bars: { verticalAlign: 'top', display: 'table-cell', padding: '8px', width: '30px', } as React.CSSProperties, bar: { height: '6px', marginBottom: '1px', } as React.CSSProperties, barLast: { height: '6px', } as React.CSSProperties, }; export default class Alert extends React.Component { render(): JSX.Element { let alert = this.props.alert; if (this.props.open) { return
{ this.props.onOpen(); }} />
; } let cardStyle = { ...css.card, }; let roles: JSX.Element[] = []; for (let role of alert.roles) { roles.push(
{role}
, ); } return
{ let target = evt.target as HTMLElement; if (target.className.indexOf('open-ignore') !== -1) { return; } this.props.onOpen(); }} >
{alert.name}
{roles}
; } } ================================================ FILE: www/app/components/AlertDetailed.tsx ================================================ /// import * as React from 'react'; import * as AlertTypes from '../types/AlertTypes'; import * as AuthorityTypes from "../types/AuthorityTypes"; import * as OrganizationTypes from "../types/OrganizationTypes"; import * as AlertActions from '../actions/AlertActions'; import * as PageInfos from './PageInfo'; import PageInput from './PageInput'; import PageSave from './PageSave'; import PageInfo from './PageInfo'; import PageTextArea from './PageTextArea'; import ConfirmButton from './ConfirmButton'; import PageInputButton from './PageInputButton'; import Help from './Help'; import PageSwitch from "./PageSwitch"; import PageSelect from "./PageSelect"; import * as Constants from "../Constants"; interface Props { alert: AlertTypes.AlertRo; authorities: AuthorityTypes.AuthoritiesRo; organizations: OrganizationTypes.OrganizationsRo; selected: boolean; onSelect: (shift: boolean) => void; onClose: () => void; } interface State { disabled: boolean; changed: boolean; message: string; addRole: string; addIgnore: string; alert: AlertTypes.Alert; } const css = { card: { position: 'relative', padding: '48px 10px 0 10px', width: '100%', } as React.CSSProperties, remove: { position: 'absolute', top: '5px', right: '5px', } as React.CSSProperties, item: { margin: '9px 5px 0 5px', minHeight: '20px', } as React.CSSProperties, itemsLabel: { display: 'block', } as React.CSSProperties, itemsAdd: { margin: '8px 0 15px 0', } as React.CSSProperties, group: { flex: 1, minWidth: '250px', margin: '0 10px', } as React.CSSProperties, controlButton: { marginRight: '10px', } as React.CSSProperties, save: { paddingBottom: '10px', } as React.CSSProperties, button: { height: '30px', } as React.CSSProperties, buttons: { cursor: 'pointer', position: 'absolute', top: 0, left: 0, right: 0, padding: '4px', height: '39px', } as React.CSSProperties, label: { width: '100%', maxWidth: '280px', } as React.CSSProperties, status: { margin: '6px 0 0 1px', } as React.CSSProperties, icon: { marginRight: '3px', } as React.CSSProperties, inputGroup: { width: '100%', } as React.CSSProperties, protocol: { flex: '0 1 auto', } as React.CSSProperties, port: { flex: '1', } as React.CSSProperties, select: { margin: '7px 0px 0px 6px', paddingTop: '3px', } as React.CSSProperties, header: { fontSize: '20px', marginTop: '-10px', paddingBottom: '2px', marginBottom: '10px', borderBottomStyle: 'solid', } as React.CSSProperties, heading: { margin: '19px 0 0 0', } as React.CSSProperties, alertsButtons: { marginTop: '8px', } as React.CSSProperties, alertsAdd: { margin: '8px 0 0 8px', } as React.CSSProperties, }; export default class AlertDetailed extends React.Component { constructor(props: any, context: any) { super(props, context); this.state = { disabled: false, changed: false, message: '', addRole: '', addIgnore: '', alert: null, }; } set(name: string, val: any): void { let alert: any; if (this.state.changed) { alert = { ...this.state.alert, }; } else { alert = { ...this.props.alert, }; } alert[name] = val; this.setState({ ...this.state, changed: true, alert: alert, }); } onSave = (): void => { this.setState({ ...this.state, disabled: true, }); AlertActions.commit(this.state.alert).then((): void => { this.setState({ ...this.state, message: 'Your changes have been saved', changed: false, disabled: false, }); setTimeout((): void => { if (!this.state.changed) { this.setState({ ...this.state, alert: null, changed: false, }); } }, 1000); setTimeout((): void => { if (!this.state.changed) { this.setState({ ...this.state, message: '', }); } }, 3000); }).catch((): void => { this.setState({ ...this.state, message: '', disabled: false, }); }); } onDelete = (): void => { this.setState({ ...this.state, disabled: true, }); AlertActions.remove(this.props.alert.id).then((): void => { this.setState({ ...this.state, disabled: false, }); }).catch((): void => { this.setState({ ...this.state, disabled: false, }); }); } onAddRole = (): void => { let alert: AlertTypes.Alert; if (this.state.changed) { alert = { ...this.state.alert, }; } else { alert = { ...this.props.alert, }; } let roles = [ ...alert.roles, ]; if (!this.state.addRole) { return; } if (roles.indexOf(this.state.addRole) === -1) { roles.push(this.state.addRole); } roles.sort(); alert.roles = roles; this.setState({ ...this.state, changed: true, message: '', addRole: '', alert: alert, }); } onRemoveRole(role: string): void { let alert: AlertTypes.Alert; if (this.state.changed) { alert = { ...this.state.alert, }; } else { alert = { ...this.props.alert, }; } let roles = [ ...alert.roles, ]; let i = roles.indexOf(role); if (i === -1) { return; } roles.splice(i, 1); alert.roles = roles; this.setState({ ...this.state, changed: true, message: '', addRole: '', alert: alert, }); } onAddIgnore = (): void => { let alert: AlertTypes.Alert; if (this.state.changed) { alert = { ...this.state.alert, }; } else { alert = { ...this.props.alert, }; } let ignores = [ ...(alert.ignores || []), ]; if (!this.state.addIgnore) { return; } if (ignores.indexOf(this.state.addIgnore) === -1) { ignores.push(this.state.addIgnore); } ignores.sort(); alert.ignores = ignores; this.setState({ ...this.state, changed: true, message: '', addIgnore: '', alert: alert, }); } onRemoveIgnore(ignore: string): void { let alert: AlertTypes.Alert; if (this.state.changed) { alert = { ...this.state.alert, }; } else { alert = { ...this.props.alert, }; } let ignores = [ ...(alert.ignores || []), ]; let i = ignores.indexOf(ignore); if (i === -1) { return; } ignores.splice(i, 1); alert.ignores = ignores; this.setState({ ...this.state, changed: true, message: '', addIgnore: '', alert: alert, }); } render(): JSX.Element { let alert: AlertTypes.Alert = this.state.alert || this.props.alert; let hasOrganizations = !!this.props.organizations.length; let organizationsSelect: JSX.Element[] = []; if (this.props.organizations && this.props.organizations.length) { organizationsSelect.push( ); for (let organization of this.props.organizations) { organizationsSelect.push( , ); } } if (!hasOrganizations) { organizationsSelect.push( ); } let fields: PageInfos.Field[] = [ { label: 'ID', value: this.props.alert.id || 'None', }, ]; let roles: JSX.Element[] = []; for (let role of alert.roles) { roles.push(
{role}
, ); } let ignores: JSX.Element[] = []; for (let ignore of (alert.ignores || [])) { ignores.push(
{ignore}
, ); } let valueInt = false; let valueStr = false; let valueLabel = ''; let valueHelp = ''; let ignoreShow = false; let ignoreLabel = ''; let ignoreTitle = ''; let ignoreHelp = ''; switch (alert.resource) { case "system_cpu_level": valueInt = true; valueLabel = 'Usage Threshold'; valueHelp = 'Maximum percent CPU usage as integer ' + 'before alert is triggered.'; break; case "system_memory_level": valueInt = true; valueLabel = 'Usage Threshold'; valueHelp = 'Maximum percent memory usage as integer ' + 'before alert is triggered.'; break; case "system_swap_level": valueInt = true; valueLabel = 'Usage Threshold'; valueHelp = 'Maximum percent swap usage as integer ' + 'before alert is triggered.'; break; case "system_hugepages_level": valueInt = true; valueLabel = 'Usage Threshold'; valueHelp = 'Maximum percent hugepages usage as integer ' + 'before alert is triggered.'; break; case "disk_usage_level": valueInt = true; valueLabel = 'Usage Threshold'; valueHelp = 'Maximum percent disk space usage as integer ' + 'before alert is triggered.'; break; case "kmsg_keyword": valueStr = true; valueLabel = 'Dmesg Keyword Match'; valueHelp = 'Case insensitive dmesg match string to trigger alert.'; break; } return
{ if (evt.target instanceof HTMLElement && evt.target.className.indexOf('tab-close') !== -1) { this.props.onClose(); } }} >
{ this.set('name', val); }} /> { this.set('comment', val); }} /> { this.setState({ ...this.state, addRole: val, }); }} onSubmit={this.onAddRole} /> { this.set('resource', val); }} >